Products & Ingredients 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: the Products API (below) covers product data only. Ingredients API access — catalog search by ingredient, or a product's full ingredient breakdown — is a separate plan and subscription, documented in its own section further down this page. Reviews are not available via API at all — they're purchased and downloaded manually from the Global Data page.
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:
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
| Param | Type | Description |
|---|---|---|
brand_name | string | Filter by brand (partial match). Comma-separated for an OR match, e.g. Fenty,Sephora |
category | string | Filter by category (partial match). Comma-separated for OR |
site_name | string | Filter by retailer, e.g. sephora.com. Comma-separated for OR |
country | string | Filter by country code, e.g. us. Comma-separated for OR |
uniq_ids | string | Comma-separated exact product IDs — selects specific products instead of a whole brand/category |
updated_within | string | last_7_days, last_30_days, or last_1_year — only products re-crawled within that window. Plan-gated; not included on every plan. |
page | integer | Default 1 |
per_page | integer | Capped 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):
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).
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.
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.
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.
resetstops 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.
GET/api/v1/ingredient_analysis
Served by api.beautyfeeds.io (same host as the Products API above), but gated by its own
separate plan and subscription — not included with a Products API plan, and vice versa.
See Plans for pricing tiers.
Two independent query modes on one endpoint — pass exactly one of ingredient or
uniq_id (both or neither returns 400). This endpoint always returns a fixed,
minimal set of product identifiers regardless of plan (name, url, brand, source, uniq_id, sku, gtin, upc,
position) — full product data is the separate Products API's job.
Mode 1 — search by ingredient
Comma-separate multiple ingredients for an OR match — resolved against both canonical INCI names and known
synonyms. Terms that don't resolve at all are reported back in unresolved_terms, not silently
dropped — but a term that does resolve and simply has zero catalog matches won't show up there
either (it did resolve); check meta.ingredients instead, which lists every requested ingredient
with its own catalog stats (matched_products, catalog-wide frequency_pct, typical
avg_position, and any EU/US restrictions) regardless of whether it matched anything.
A trailing (Common Name) is also tried stripped before giving up — the standard US label
format is the INCI name with the common name in parentheses right in the declaration (e.g.
Cocos Nucifera (Coconut) Water), so pasting terms straight off a real product label resolves
the same as passing the bare INCI name. This is the one structured exception to "name known ingredients
directly" — free-text prose (disclaimers, shade-label prefixes, marketing copy) still isn't parsed here;
that's what the portal's paste-a-list tool is for.
One row per product in data — matched_ingredients lists every
requested ingredient that product contains, each with its own position.
match_percentage is what fraction of your requested ingredients this product has.
On restriction text: restrictions is copied verbatim from the EU CosIng
database — some entries carry a generic-sounding cross-reference (e.g. "ban substances that deplete the
ozone layer") on ingredients with no obvious connection to it. That's a known quirk of the source data
itself, not a data error on our end — we don't edit or filter restriction text.
On concerns: short categorical labels derived entirely from our own
regulatory sources — California Prop 65 (parsed into the actual listed concern, e.g. "Cancer" or
"Developmental"), FDA restricted-use color additives, EU CosIng/Annex restrictions, and the EU's declarable
fragrance allergen list. This is not sourced from EWG Skin Deep or any other third-party
hazard-scoring database — every label traces back to a specific regulatory restriction already shown in
restrictions, or to the concerns array on ?uniq_id= ingredient rows.
Plan-gated fields: meta.ingredients[].function/frequency_pct/avg_position and
restrictions only appear on Starter and above — Free gets inci_name/matched_products
only. brand_insights is null below Pro. The total number of matching products
returned is also capped by plan (Free: 3, Starter: 5, Pro: 10, Enterprise: 50) — data itself
(per-product fields) is otherwise identical across every tier. See Plans for
the full breakdown.
Mode 2 — full breakdown for a known product
meta.ingredient_analysis_available: false (not a 404) means the product exists but
hasn't been through ingredient extraction yet — routine given the catalog's ongoing extraction backlog, not an error.
Shared usage with Ingredient Analysis: this API and the portal's Ingredient Analysis paste-a-list tool draw from the same monthly request budget — one subscription covers both, and using either one counts against the same limit.
Errors
| Status | Meaning |
|---|---|
400 | /ingredient_analysis called with both or neither of ingredient/uniq_id |
401 | Missing/invalid X-API-KEY, or an inactive subscription for the API being called — Products and Ingredients are gated by separate subscriptions, see Ingredients API |
403 | Your 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) |
422 | stats/assign/reset called with no filter |
429 | Plan request limit or rate limit exceeded |
404 | Unmatched path or method |
500 | Something 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.