Products API

Live, filterable access to the BeautyFeeds product catalog, plus self-service credit-metered access for expanding what your organization can pull. See Plans & Features for what's included per plan.

Scope: this API covers product data only. Reviews are not available via API — they're purchased and downloaded manually from the Global Data page, not through any endpoint below.

Want to try it instead of just reading?

The Playground sends real requests with your real API key and shows the real response — no code required. Sign in to open the Playground →

Authentication

Every request needs your organization's API key in a header:

X-API-KEY: your_api_key_here

Find your key on your dashboard. Requests without a valid key, or from an inactive subscription, return 401 Unauthorized.

GET/api/v1/beauty_products

Returns products your organization currently has access to. Always live at query time — every request reads current data straight from the database, never a stale HTTP-level cache. The underlying catalog itself is refreshed on a monthly cycle by default (weekly or daily on request) — this endpoint just guarantees you always see the latest refresh, not an older cached response.

Query parameters

ParamTypeDescription
brand_namestringFilter by brand (partial match). Comma-separated for an OR match, e.g. Fenty,Sephora
categorystringFilter by category (partial match). Comma-separated for OR
site_namestringFilter by retailer, e.g. sephora.com. Comma-separated for OR
countrystringFilter by country code, e.g. us. Comma-separated for OR
uniq_idsstringComma-separated exact product IDs — selects specific products instead of a whole brand/category
updated_withinstringlast_7_days, last_30_days, or last_1_year — only products re-crawled within that window. Plan-gated; not included on every plan.
pageintegerDefault 1
per_pageintegerCapped at your plan's per-page limit (default 50)

Example response

A real response, from an org with a large assigned catalog and no active filter (so matched_in_catalog/expansion_notice are null — they only populate when a filter is given):

{ "meta": { "total_results": 27294, "total_pages": 546, "current_page": 166, "per_page": 50, "status": "success", "data_source": "live", "assigned_urls_count": 27294, "matched_in_catalog": null, "next_page_url": "https://api.beautyfeeds.io/api/v1/beauty_products?page=167&per_page=50", "previous_page_url": "https://api.beautyfeeds.io/api/v1/beauty_products?page=165&per_page=50", "renewal_notice": "These 27294 assigned products remain free to access until your subscription renews on 2026-09-16. At renewal, continued access is re-charged from your new billing cycle's credits — check /api/v1/beauty_products/stats beforehand to preview the cost.", "expansion_notice": null, "ingredients_data_notice": "Ingredient/INCI fields (ingredients, ingredients_formatted, inci_format, raw_ingredients) are extracted and normalized automatically from retailer pages. They are not manually verified and may not be 100% accurate — confirm against the original source before relying on them for formulation, compliance, or labeling decisions." }, "data": [ { "uniq_id": "4d8d2fa92f8559b1", "site_name": "amazon.com", "country": "us", { ...rest of the row, see "Which fields you get back" below ... } } ] }

next_page_url / previous_page_url are null at either end of the result set, so you can branch on presence instead of computing bounds yourself. matched_in_catalog is the full-catalog count for the given filter (not just what you're entitled to) — null when no filter is given. expansion_notice is also null unless the catalog has more matches than you're currently assigned. If either shows a gap, use assign to unlock the rest.

On ingredient/INCI data: it's automatically parsed and normalized, not manually verified — ingredients_data_notice repeats this on every response as a standing reminder, not just here. Treat it as a strong starting point for formulation research, not a certified source.

Which fields you get back

data rows only include the fields your plan grants — see Plans & Features for the exact list per plan. A Starter-tier response, for example, won't include ingredients or description even if you don't filter for them; an Enterprise plan (or any plan with no field restriction configured) gets every field. This applies to the row shape only — meta is always complete regardless of plan.

List-shaped fields (ingredients, ingredients_formatted, variants, additional_images, breadcrumbs, gtin_list, ean_list, highlights, other_information) come back as real JSON arrays/objects, not JSON-encoded strings — no need to JSON.parse them again on your end.

GET/api/v1/beauty_products/stats

Free preview — see what a filter would grant and what it would cost, without spending anything or changing your access. Requires at least one filter (brand_name, category, site_name, country, or precise uniq_ids — a comma-separated list to select specific products instead of a whole brand/category).

# request GET /api/v1/beauty_products/stats?brand_name=fenty # response { "meta": { "status": "success" }, "matched_total": 10000, "already_assigned": 5000, "new_available": 5000, "credits_required": 5000, "credit_balance": 15000 }

POST/api/v1/beauty_products/assign

Grants access to newly-matched products and charges credits for exactly what's new — products you already have access to are never re-charged. If your balance can't cover everything matched, you're granted as many as you can afford rather than the call failing outright.

# request POST /api/v1/beauty_products/assign?brand_name=fenty # or select specific products instead of a whole brand POST /api/v1/beauty_products/assign?uniq_ids=abc123,def456 # response { "meta": { "status": "success" }, "newly_assigned": 5000, "requested_new": 5000, "total_owned_for_filter": 10000, "credits_used": 5000, "credit_balance": 15000, "notice": null }

Once assigned, those products are immediately included in GET /api/v1/beauty_products — no separate step needed.

notice is null on a clean assign, and explains itself otherwise — two independent reasons it can be non-null: your credit balance couldn't cover everything matched (assigned as many as affordable), or a small number of matched products couldn't be resolved due to a data sync issue and weren't charged for. Either way, credits_used/newly_assigned always reflect what actually happened, not what was requested.

POST/api/v1/beauty_products/reset

Releases access to products matching the given filter (or specific uniq_ids). Credits are not refunded — see how credits work.

# request POST /api/v1/beauty_products/reset?brand_name=fenty # response { "meta": { "status": "success" }, "removed": 10000, "credits_refunded": 0, "credit_balance": 15000 }

How credits & renewal work

  • Credits are spent once per product, the moment it's newly granted — downloading it or querying it via the API afterward is free, no matter how many times, for the rest of the current billing cycle.
  • reset stops future access to a product but never refunds the credit already spent this cycle.
  • Access isn't permanent across billing cycles: the product catalog itself is re-crawled every cycle, so continued access to your currently-assigned products is re-affirmed and re-charged from your new cycle's credit pool at renewal. You'll see this cost ahead of time on your dashboard and by email before it happens.
  • Credits are separate from your plan's request-rate limit — credits govern how many distinct products you can access; the request limit governs how many API calls you can make per cycle.

GET/api/v1/reviews/stats

Note the different host — this endpoint is served by app.beautyfeeds.io (this app), not api.beautyfeeds.io. Reviews live in a separate datastore from products, so they're not part of the Products API above.

Analytics and pricing only — review count and estimated cost for products you already have Products API access to. Does not return the review records themselves; those are still purchased and downloaded from Global Data. Same X-API-KEY auth and filters (brand_name, category, site_name, country, uniq_ids) as the Products API — at least one is required.

# request GET https://app.beautyfeeds.io/api/v1/reviews/stats?brand_name=fenty # response { "meta": { "status": "success" }, "matched_products": 5000, "reviews_count": 128000, "price_per_record_usd": 0.0025, "estimated_cost_usd": 320.0, "purchase_url": "https://app.beautyfeeds.io/client/global_data", "note": "Analytics and pricing only — reviews are purchased and downloaded from the Global Data page, not delivered by this endpoint." }

Errors

StatusMeaning
401Missing/invalid X-API-KEY, or an inactive subscription
403Your plan doesn't include API access, or doesn't include a filter you passed (e.g. updated_within on a plan that doesn't have it)
422stats/assign/reset called with no filter
429Plan request limit or rate limit exceeded
404Unmatched path or method
500Something went wrong on our end — every error response is a clean, minimal JSON object; internals are never exposed in the response body

Rate limits

Two independent limits apply: your plan's request quota (per billing cycle, resets on renewal), and a general abuse backstop of 300 requests/minute per IP. Normal usage — including paging through a large result set quickly — stays well under the backstop; it exists to catch scripted abuse, not to slow down real usage.