API reference

Documentation

Everything you need to integrate The Beauty API: 180,000 products, 22,000 ingredient records, and 180,000 hosted product images behind a uniform JSON interface.

For coding agents

Paste this into your CLAUDE.md, AGENTS.md or editor rules and your agent calls the API correctly without reading the docs. Also at https://thebeautyapi.com/agent.md.

Show the 22 lines ▾
# The Beauty API: instructions for coding agents
Base URL: https://api.thebeautyapi.com/v1 (JSON over HTTPS)
Auth: send the header x-api-key: $BEAUTY_API_KEY on every /v1 request. Keep the key in an environment variable, never in code or in this file.
Reference: https://thebeautyapi.com/docs.md | Spec: https://api.thebeautyapi.com/openapi.json | Keys: https://thebeautyapi.com/dashboard

## Endpoints
- Health: GET /health (no key)
- Search products: GET /v1/products/search?q=&page=
- Filter products: GET /v1/products?category=&brand=&origin=&fragrance_free=&alcohol_free=&paraben_free=&sulfate_free=&silicone_free=&has_ingredient=&excludes_ingredient=&page=
- Get a product: GET /v1/products/{id}
- Batch products: POST /v1/products/batch (body {"ids": [uuid, ...]}, max 50)
- Search ingredients: GET /v1/ingredients/search?q=&page=
- Get an ingredient: GET /v1/ingredients/{id}

## Rules
1. Find a product with search, then fetch the full record with GET /v1/products/{id}. Search and filter return slim records (id, brand, name, category, image_url); detail and batch return the full record with the ingredient list in on-label order.
2. Every word in q must appear in the product's brand or name, in any order. Send the brand plus the distinctive part of the name ("CeraVe Moisturizing Cream"), never a full retail title. Case, accents and punctuation are ignored; the last word may be partial.
3. When pagination.total is 0 the response carries a no_match object: drop the word no_match.hint names and retry. An empty result does not mean the brand is missing.
4. Pages hold 20 results; page runs 1 to 51 and anything higher returns 400. Read pagination.total and pagination.has_more; above 1,020 matches, narrow with filters instead of paging.
5. Batch takes up to 50 ids and is billed per item (50 ids = 50 calls); unknown ids come back as null in the same positions.
6. Errors are JSON {statusCode, error, message} and message says what to change. 403 is a missing or revoked key. 429 is the rate limit; no rate-limit headers are sent, so back off exponentially.
7. image_url can be null.
On this page

Overview

The Beauty API is a REST API for a normalized catalog of beauty products and ingredients: 180,000 products, 22,000 ingredient records, and 4.8 million product–ingredient links, refreshed once every two months. All endpoints accept and return JSON over HTTPS, and responses are compressed (brotli or gzip) when your client sends Accept-Encoding.

Base URL
https://api.thebeautyapi.com/v1

The API describes itself. A request to the root of the API (or to /v1) returns a discovery document listing every endpoint with its method, URL and summary, plus links to this reference and to the OpenAPI 3.1 spec, which is served live at /openapi.json. Neither needs an API key, so agents and SDK generators can start from the base URL alone.

Discoverycurl
curl https://api.thebeautyapi.com/              # endpoints, docs_url, openapi_url, authentication
curl https://api.thebeautyapi.com/openapi.json  # OpenAPI 3.1 document

This reference is also published as one plain markdown file at https://thebeautyapi.com/docs.md, with an index at https://thebeautyapi.com/llms.txt, so a coding agent can read the whole page in a single request instead of parsing this HTML.

Authentication

Every request under /v1 requires an API key in the x-api-key header. Keys are issued from your dashboard, with up to 2 active keys per account and create, reveal, and revoke available at any time. A missing, invalid, or revoked key returns 403. Only /health, the discovery document and /openapi.json are open.

Authenticated requestcurl
curl "https://api.thebeautyapi.com/v1/products/search?q=retinol&page=1" \
  -H "x-api-key: YOUR_API_KEY"

Treat API keys as secrets. Call the API from your backend and never embed a key in client-side or mobile code: anyone who can read your key can spend your quota.

Quickstart

Set your key in the environment, then search the catalog from the language of your choice.

Example
curl "https://api.thebeautyapi.com/v1/products/search?q=niacinamide&page=1" \
  -H "x-api-key: $BEAUTY_API_KEY"

Endpoints

Products

Find a product by name, filter the catalog by attributes, then read the full record with its ingredient list. Barcode lookup and batch reads return the same full record.

GET/v1/products/search

Search products

Search by brand and product name. Matching is strict: every word in q must appear in the product's brand or name, in any order. Case, accents and punctuation are ignored (punctuation splits words: Dr.Jart+ is searched as dr jart), and the last word may be partial (cera finds CeraVe), so it suits autocomplete. Returns slim records; follow up with the product endpoint for the full payload.

ParameterInTypeRequiredDescription
qquerystringYesSearch text, 1–200 characters. Use the brand plus the distinctive part of the name, not the full retail title.
pagequeryintegerNoPage number, 1–51. Optional; defaults to 1. 20 results per page.
Request
curl "https://api.thebeautyapi.com/v1/products/search?q=niacinamide&page=1" \
  -H "x-api-key: $BEAUTY_API_KEY"
200 OKjson
{
  "results": [
    {
      "id": "06d9476a-397b-5f61-abc8-640b30a9ae23",
      "brand": "The Ordinary",
      "name": "Niacinamide 10% + Zinc 1%",
      "category": "skincare",
      "image_url": "https://images.thebeautyapi.com/06d9476a-397b-5f61-abc8-640b30a9ae23.jpg"
    }
  ],
  "pagination": { "page": 1, "has_more": true, "total": 1571 }
}
QueryResult
q=CeraVe Moisturizing Cream✓CeraVe Moisturizing Cream and its variants (Baby, Intensive, Psoriasis, For Dry To Very Dry Skin…)
q=CeraVe Moisturizing Cream Daily Face and Body Moisturizer for Dry Skin✗No results: "Daily", "Face", "Body" and "Moisturizer" come from the retail listing, not the stored name. The response's no_match.hint names the least specific word to drop, and no_match.terms shows each word's own match count.
q=Laneige sleeping mask✓Every LANEIGE sleeping mask (Water, Lip, Cica, Time Freeze…)

Notes:

  1. 01An empty results array means no product has all of those words. It does not mean the brand is missing.
  2. 02When total is 0 the response carries no_match: hint names the word to drop, and terms gives each word's own match count. Drop the word that matches nothing (a typo, a receipt line number) or the least specific one (serum, cream, SPF) and retry.
  3. 03Send brand + short name from the start, never a full listing title.
  4. 04Words split on anything that is not a letter or digit: Oil-Free searches as oil and free, 10% as 10. Only the first 10 words count.
  5. 05A query with no letters or digits returns 400.
GET/v1/products

Filter products

Discover products by category, brand, origin, formulation flags, and ingredient composition. All filters combine with AND. Returns the same slim records as search.

ParameterInTypeRequiredDescription
categoryquerystringNoExact, case-sensitive match on the stored category: skincare, suncare, makeup, haircare, body/bath, fragrance or other. Any other value returns an empty page with no_match, not an error.
brandquerystringNoBrand name. Case, accents, punctuation and spaces are ignored: supergoop finds Supergoop!, sk ii finds SK-II. Misspellings are not corrected; use search to find the stored brand string.
originquerystringNoExact, case-sensitive match on the brand's country of origin as stored, e.g. USA, South Korea, France, United Kingdom. Unknown for ~53% of products, which no origin value matches.
fragrance_freequerybooleanNoFormulation flags. Also: alcohol_free, paraben_free, sulfate_free, silicone_free.
has_ingredientqueryuuidNoOnly products containing this ingredient. Ingredient IDs come from the ingredient endpoints.
excludes_ingredientqueryuuid listNoOnly products containing none of these ingredients. Comma-separated, up to 10.
pagequeryintegerNoPage number, 1–51. Optional; defaults to 1. 20 results per page.
Request
curl "https://api.thebeautyapi.com/v1/products?category=skincare&fragrance_free=true&has_ingredient=a1526dea-37d1-5904-9106-be583378339b&page=1" \
  -H "x-api-key: $BEAUTY_API_KEY"
200 OKjson
{
  "results": [
    {
      "id": "06d9476a-397b-5f61-abc8-640b30a9ae23",
      "brand": "The Ordinary",
      "name": "Niacinamide 10% + Zinc 1%",
      "category": "skincare",
      "image_url": "https://images.thebeautyapi.com/06d9476a-397b-5f61-abc8-640b30a9ae23.jpg"
    }
  ],
  "pagination": { "page": 1, "has_more": true, "total": 11501 }
}

Notes:

  1. 01An unknown ingredient ID returns an empty page, not an error.
  2. 02When total is 0, no_match.hint says why: the brand has no stored spelling (with a pointer to search), the other filters exclude every product of that brand, or the ingredient ID is unknown.
GET/v1/products/{id}

Get a product

The complete record: brand, category, origin, formulation flags, hosted image URL, and the full ingredient list in exact on-label order, each ingredient with its normalized name, functional classes, safety ratings, and CAS/EC/IUPAC/Ph. Eur. identifiers.

ParameterInTypeRequiredDescription
idpathuuidYesProduct ID.
Request
curl https://api.thebeautyapi.com/v1/products/06d9476a-397b-5f61-abc8-640b30a9ae23 \
  -H "x-api-key: $BEAUTY_API_KEY"
200 OK (trimmed)json
{
  "id": "06d9476a-397b-5f61-abc8-640b30a9ae23",
  "brand": "The Ordinary",
  "name": "Niacinamide 10% + Zinc 1%",
  "image_url": "https://images.thebeautyapi.com/06d9476a-397b-5f61-abc8-640b30a9ae23.jpg",
  "category": "skincare",
  "origin": "Canada",
  "contains_fragrance": false,
  "contains_drying_alcohol": false,
  "contains_parabens": false,
  "contains_sulfates": false,
  "contains_silicones": false,
  "ingredients": [
    /* … position 0 … */
    {
      "position": 1,
      "label_name": "Niacinamide 10.0%",
      "concentration": 10.0,
      "id": "a1526dea-37d1-5904-9106-be583378339b",
      "name": "Niacinamide",
      "rating": "direct actives",
      "irritancy": null,
      "comedogenicity": null,
      "functions": ["smoothing"],
      "cas_number": "98-92-0",
      "ec_number": "202-713-4",
      "ph_eur_name": "Nicotinamidum",
      "iupac_name": "3-Pyridinecarboxamide",
      "other_names": "vitamin B3, nicotinamide",
      "category": null
    },
    /* … positions 2–10 … */
  ]
}

Notes:

  1. 01concentration is present only where the manufacturer discloses it publicly. It is never estimated.
  2. 02Barcodes are never in this payload, on any plan. Barcode-enabled plans resolve a GTIN to a product with Barcode lookup.
GET/v1/products/barcode/{barcode}

Barcode lookup

Resolve a scanned or catalog barcode (UPC-A, EAN-13, GTIN-8/14) to its product and return the same complete record as Get a product. Unit codes, multipack codes and sibling-variant codes (shade/scent/size of the same line) all resolve, and any zero-padded GTIN form of the same code (UPC-A, EAN-13, GTIN-14) is treated as identical. Barcode-enabled plans only.

ParameterInTypeRequiredDescription
barcodepathstringYes8–14 digits, no spaces or dashes (^[0-9]{8,14}$).
Request
curl https://api.thebeautyapi.com/v1/products/barcode/769915195941 \
  -H "x-api-key: $BEAUTY_API_KEY"
200 OK (same shape as Get a product)json
{
  "id": "06d9476a-397b-5f61-abc8-640b30a9ae23",
  "brand": "The Ordinary",
  "name": "Niacinamide 10% + Zinc 1%",
  "image_url": "https://images.thebeautyapi.com/06d9476a-397b-5f61-abc8-640b30a9ae23.jpg",
  "category": "skincare",
  "origin": "Canada",
  "contains_fragrance": false,
  "contains_drying_alcohol": false,
  "contains_parabens": false,
  "contains_sulfates": false,
  "contains_silicones": false,
  "ingredients": [ /* … as in Get a product … */ ]
}

Notes:

  1. 01The response never lists the product's barcodes. The API resolves a code to a product; it does not enumerate codes. The full GTIN list ships only with the licensed dataset.
  2. 02403 when your plan has no barcode lookup (the message reads “Your current plan does not include Barcode Lookup access.”). 404 when nothing matches. 400 for malformed input.
  3. 03Bills 1 item.
  4. 04Roughly 46% of the catalog carries a code today; coverage grows with each refresh.
POST/v1/products/batch

Batch products

Fetch up to 50 complete product records in one request. The response array has the same length and order as the ids you send; unknown IDs come back as null.

ParameterInTypeRequiredDescription
idsbodyuuid[]Yes1–50 product IDs.
Request
curl -X POST https://api.thebeautyapi.com/v1/products/batch \
  -H "x-api-key: $BEAUTY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"ids": ["06d9476a-397b-5f61-abc8-640b30a9ae23", "00000000-0000-0000-0000-000000000000"]}'
200 OK (trimmed)json
[
  {
    "id": "06d9476a-397b-5f61-abc8-640b30a9ae23",
    "brand": "The Ordinary",
    "name": "Niacinamide 10% + Zinc 1%",
    "ingredients": [ … ]
  },
  null
]

Notes:

  1. 01Send JSON with Content-Type: application/json. A missing or unrecognised content type returns 415.
  2. 02A body that is not valid JSON, or ids missing, empty, over 50 or not UUIDs, returns 400.
  3. 03Billed per item: a batch of 50 counts as 50 calls. The X-Item-Count response header carries the billable count.
  4. 04Rate limits and the 100-request trial allowance count requests, so a batch is one request against those. Trial calls never incur overage.

Endpoints

Ingredients

Search the ingredient dictionary by INCI name or synonym, then read an ingredient's full record: functional classes, irritancy and comedogenicity ratings, and chemical identifiers. Use the IDs with the product filters.

GET/v1/ingredients/search

Search ingredients

Search the 22,000-record ingredient dictionary by canonical INCI name and known synonyms. Use the returned IDs with the product filters or the ingredient endpoint.

ParameterInTypeRequiredDescription
qquerystringYesSearch text, 1–200 characters.
pagequeryintegerNoPage number, 1–51. Optional; defaults to 1. 20 results per page.
Request
curl "https://api.thebeautyapi.com/v1/ingredients/search?q=niacinamide&page=1" \
  -H "x-api-key: $BEAUTY_API_KEY"
200 OKjson
{
  "results": [
    {
      "id": "a1526dea-37d1-5904-9106-be583378339b",
      "name": "Niacinamide",
      "other_names": "vitamin B3, nicotinamide",
      "rating": "direct actives"
    }
  ],
  "pagination": { "page": 1, "has_more": false, "total": 6 }
}
GET/v1/ingredients/{id}

Get an ingredient

An ingredient's full scientific record: normalized name, synonyms, functional classes, irritancy and comedogenicity ratings, and CAS, EC, IUPAC, and Ph. Eur. identifiers.

ParameterInTypeRequiredDescription
idpathuuidYesIngredient ID.
Request
curl https://api.thebeautyapi.com/v1/ingredients/a1526dea-37d1-5904-9106-be583378339b \
  -H "x-api-key: $BEAUTY_API_KEY"
200 OKjson
{
  "id": "a1526dea-37d1-5904-9106-be583378339b",
  "name": "Niacinamide",
  "rating": "direct actives",
  "irritancy": null,
  "comedogenicity": null,
  "functions": ["smoothing"],
  "cas_number": "98-92-0",
  "ec_number": "202-713-4",
  "ph_eur_name": "Nicotinamidum",
  "iupac_name": "3-Pyridinecarboxamide",
  "other_names": "vitamin B3, nicotinamide",
  "category": null
}

Endpoints

Reference

Service health, and how every endpoint behaves: paging, errors, plan limits, images, barcodes and versioning.

GET/health

Health

Liveness check plus the size and build date of the catalogue currently being served. built_at (YYYY-MM-DD, UTC) is the freshness signal: the API is refreshed on its own cadence, independently of the downloadable dataset (see the changelog). Needs no API key, like the discovery document at the API root and the spec at /openapi.json.

No authentication required.

Request
curl https://api.thebeautyapi.com/health
200 OKjson
{
  "status": "ok",
  "products": 179166,
  "ingredients": 22471,
  "built_at": "2026-09-11"
}

Pagination

List endpoints return 20 results per page. Pass page (1–51) and read the pagination object on the response; iterate while has_more is true. total is the number of matches across all pages, so a script can tell one exact hit from an ambiguous set before it reads the rows. A page beyond 51 is rejected with 400, so a total above 1,020 means narrow the query with filters instead of paging deeper.

Pagination objectjson
"pagination": { "page": 2, "has_more": true, "total": 47 }

Errors

Errors use conventional HTTP status codes with a uniform JSON body:

Error bodyjson
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "No product with id 00000000-0000-4000-8000-000000000000"
}

Branch on statusCode; the message is written for a person reading a log and says what to change. A parameter the endpoint does not accept, for example, comes back with the accepted list and the endpoint to use instead:

Validation errorjson
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Unknown query parameters 'name', 'limit'. GET /v1/products accepts: category, brand, origin, fragrance_free, alcohol_free, paraben_free, sulfate_free, silicone_free, has_ingredient, excludes_ingredient, page. To find products by name, use GET /v1/products/search?q=<brand and product name>. Pages are fixed at 20 results; use page=2, page=3, … for more."
}
StatusMeaning
400The request could never match anything: a query parameter the endpoint does not accept, a malformed ID or barcode, a page above 51, empty or over-long search text, or an invalid batch body. The message names the field and, for an unknown parameter, lists the ones the endpoint accepts. Fix the request; do not retry as-is.
402Trial expired or payment required. Resolve billing on the dashboard before retrying.
403Missing, invalid, or disabled API key, or a plan without barcode lookup calling the barcode endpoint.
404Well-formed, but nothing matches: no product or ingredient with that ID, no product with that barcode, or a path the API does not serve. The message echoes the ID or barcode.
413Request body over 1 MB. Only the batch endpoint reads a body, and 50 UUIDs are under 3 KB, so a valid batch never reaches this.
415Batch request sent without a Content-Type header, or with one the API cannot parse. Send Content-Type: application/json.
429Trial request allowance exhausted (use Convert to paid on the dashboard), rate limit exceeded, or the monthly request ceiling reached. Paid plans have an abuse ceiling of several times the included calls (included calls never block; overage is billed). Gateway ceilings reset on the 1st (UTC); paid allowances reset with your billing month. The trial allowance never resets. Successful trial requests include X-Trial-Requests-Remaining. Back off for rate limits; use the dashboard for an exhausted trial.
500Internal error on our side. Safe to retry.
503Access verification is temporarily unavailable. Retry shortly with backoff.
504The origin did not answer in time. Safe to retry with backoff.

Rate limits & quotas

New accounts receive a seven-day trial with 100 requests total across all keys. At the cap, requests return 429 until you convert to paid or the trial ends. Trial requests never incur overage. Paid monthly fees are collected upfront; usage above the included allowance is billed at the end of your billing month at $0.02 per call. Paid batch requests meter per item, while each batch consumes one trial request. Paid allowances reset at the start of each billing month.

PlanPriceIncluded calls / monthRate limit
Starter$40/month10,00010 req/sec, burst 20
Starter + Barcode$60/month10,00010 req/sec, burst 20
Pro$200/month100,00025 req/sec, burst 50
Pro + Barcode$300/month100,00025 req/sec, burst 50

Requests above the rate limit return 429. No rate-limit headers are sent, so handle 429 with exponential backoff.

Product images

Every product record carries an image_url pointing to standardized, white-background product photography served from images.thebeautyapi.com. 180,000 images are hosted for you. Display them directly in your product UI; there is nothing to download or re-host. A small number of products have no photograph, in which case image_url is null.

Barcodes

Barcodes are a lookup key, not a field. On barcode-enabled plans, GET /v1/products/barcode/{barcode} resolves a UPC-A / EAN-13 / GTIN-8/14 to its product and returns the standard product record. No response on any plan lists a product’s codes: detail, batch, search and filter payloads are identical with or without barcode access. Unit codes, multipack codes and sibling-variant codes (shade, scent or size of the same line) all resolve; a UPC-A and its zero-padded EAN-13 form are the same code. About 46% of the catalog carries at least one code today. If you need the codes themselves, they ship as a gtin14 list per product in the licensed dataset.

Versioning

The API is versioned in the path. Fields may be added to responses at any time and are non-breaking: parse what you need and ignore the rest. Breaking changes would ship under a new version path with advance notice to all subscribers.

Data refreshes and API changes are recorded in the changelog.

Try it in Postman

The fastest way to see every endpoint working before you write code: import our ready-made Postman collection, paste your API key into one variable, and send each request above. The collection also includes a folder of requests that are meant to fail, so you can see exactly what a 400, 403 and 404 look like.

Read the guide: How to Test The Beauty API in Postman

Ready to integrate?

Start a seven-day trial with 100 requests. Then plans from $40/month with 10,000 calls included.