{"openapi":"3.1.0","info":{"title":"The Beauty API","description":"Normalized skincare & cosmetics data: the full product catalogue and ingredient dictionary with dermatological safety data, in exact on-label order. GET /health reports the release and size of the catalogue being served.","version":"1.0.0"},"components":{"schemas":{},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Your API key, from the developer dashboard at https://thebeautyapi.com/dashboard."}}},"paths":{"/health":{"get":{"summary":"Liveness + dataset build info","tags":["meta"],"description":"No API key needed. Reports the catalogue the origin is currently serving: its size and when the snapshot was built. Everything here is read from the served database, never from configuration. The API is refreshed independently of the downloadable dataset (see https://thebeautyapi.com/changelog); built_at is the freshness signal.","responses":{"200":{"description":"The service is up and serving this catalogue.","content":{"application/json":{"schema":{"description":"The service is up and serving this catalogue.","type":"object","required":["status","products","ingredients","built_at"],"properties":{"status":{"type":"string","enum":["ok"]},"products":{"description":"Products in the served catalogue.","type":"integer"},"ingredients":{"description":"Ingredients in the served catalogue.","type":"integer"},"built_at":{"description":"Date the served database was built from the source catalogue, YYYY-MM-DD (UTC); \"unknown\" if the build did not record it.","type":"string"}}}}}}},"security":[]}},"/v1/products/search":{"get":{"summary":"Search products by brand and name (autocomplete-friendly)","tags":["products"],"description":"Full-text search over brand and product name only (not ingredients or category). Results are ranked by relevance. When nothing matches, `no_match.terms` reports how many products each word matches on its own.","parameters":[{"schema":{"minLength":1,"maxLength":200,"type":"string"},"in":"query","name":"q","required":true,"description":"Brand and/or product name. Split into words on anything that is not a letter or digit; every word must match (AND) and the last word matches as a prefix, so typing ahead works. Only the first 10 words are used, letter case and accents are ignored and search operators are treated as plain words. A value with no letters or digits is a 400."},{"schema":{"minimum":1,"maximum":51,"default":1,"type":"integer"},"in":"query","name":"page","required":false,"description":"Page number, 1 to 51. Optional, defaults to 1. Pages are fixed at 20 results; there is no page-size parameter."}],"responses":{"200":{"description":"One page of matching products.","content":{"application/json":{"schema":{"description":"One page of matching products.","type":"object","required":["results","pagination"],"properties":{"results":{"description":"Up to 20 products.","type":"array","items":{"type":"object","required":["id","brand","name","category","image_url"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Product UUID; use it with GET /v1/products/{id} and POST /v1/products/batch.","type":"string"},"brand":{"anyOf":[{"description":"Brand name as stored.","type":"string"},{"type":"null"}]},"name":{"anyOf":[{"description":"Product name as stored, without the brand.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["skincare","suncare","makeup","haircare","body/bath","fragrance","other"],"description":"One of skincare, suncare, makeup, haircare, body/bath, fragrance or other. null (unclassified) for ~2.5% of products."},{"type":"null"}]},"image_url":{"anyOf":[{"description":"Hosted product image URL (JPEG). null when the catalogue has no image.","type":"string"},{"type":"null"}]}}}},"pagination":{"type":"object","required":["page","has_more","total"],"properties":{"page":{"description":"The page that was served.","type":"integer"},"has_more":{"description":"true when rows exist beyond this page. Page 51 is the last page the API serves, so has_more can be true there while page 52 is a 400.","type":"boolean"},"total":{"description":"Number of matching rows across all pages. Pages stop at 51 (1,020 rows), so total can exceed what is reachable by paging; narrow the query instead.","type":"integer"}}},"no_match":{"description":"Only present when total is 0.","type":"object","required":["hint"],"properties":{"hint":{"description":"Why nothing matched and what to change, in plain words.","type":"string"},"terms":{"description":"Search endpoints only (GET /v1/products/search, GET /v1/ingredients/search): one entry per searched word, so the caller can see which word killed the match. GET /v1/products sends hint alone.","type":"array","items":{"type":"object","required":["term","matches"],"properties":{"term":{"description":"The searched word, lower-cased.","type":"string"},"matches":{"description":"Rows this word matches on its own.","type":"integer"}}}}}}}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: q missing, empty, over 200 characters or without any letters or digits; page not an integer from 1 to 51; a parameter given twice, or a query parameter this endpoint does not have (the message lists the accepted ones). The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: q missing, empty, over 200 characters or without any letters or digits; page not an integer from 1 to 51; a parameter given twice, or a query parameter this endpoint does not have (the message lists the accepted ones). The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"invalidKey":{"value":{"statusCode":403,"error":"Forbidden","message":"Invalid or missing API key"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}},"/v1/products":{"get":{"summary":"Filter and discover products","tags":["products"],"description":"All filters combine with AND; with no filters it pages through the whole catalogue. `has_ingredient` / `excludes_ingredient` take ingredient UUIDs (from the ingredient endpoints); `excludes_ingredient` accepts up to 10, comma-separated. Results come in a stable catalogue order, not ranked. When nothing matches, `no_match.hint` says which filter to change.","parameters":[{"schema":{"minLength":1,"maxLength":100,"type":"string"},"in":"query","name":"category","required":false,"description":"Exact, case-sensitive match on the stored category: skincare, suncare, makeup, haircare, body/bath, fragrance or other. Any other value matches nothing (an empty page with no_match, not an error)."},{"schema":{"minLength":1,"maxLength":200,"type":"string"},"in":"query","name":"brand","required":false,"description":"Brand name. Case, accents, punctuation and spaces are ignored: supergoop finds Supergoop!, sk ii finds SK-II, caudalie finds Caudalíe. Misspellings are not corrected; use /products/search to discover the stored brand string. A brand not in the catalogue is an empty page with no_match; a value with no letters or digits is a 400."},{"schema":{"minLength":1,"maxLength":100,"type":"string"},"in":"query","name":"origin","required":false,"description":"Exact, 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."},{"schema":{"type":"boolean"},"in":"query","name":"fragrance_free","required":false,"description":"true keeps only products whose contains_fragrance is false (no fragrance). false, or omitting the parameter, applies no filter. Only the literal words true and false are accepted."},{"schema":{"type":"boolean"},"in":"query","name":"alcohol_free","required":false,"description":"true keeps only products whose contains_drying_alcohol is false (no drying alcohol). false, or omitting the parameter, applies no filter. Only the literal words true and false are accepted."},{"schema":{"type":"boolean"},"in":"query","name":"paraben_free","required":false,"description":"true keeps only products whose contains_parabens is false (no parabens). false, or omitting the parameter, applies no filter. Only the literal words true and false are accepted."},{"schema":{"type":"boolean"},"in":"query","name":"sulfate_free","required":false,"description":"true keeps only products whose contains_sulfates is false (no sulfates). false, or omitting the parameter, applies no filter. Only the literal words true and false are accepted."},{"schema":{"type":"boolean"},"in":"query","name":"silicone_free","required":false,"description":"true keeps only products whose contains_silicones is false (no silicones). false, or omitting the parameter, applies no filter. Only the literal words true and false are accepted."},{"schema":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"in":"query","name":"has_ingredient","required":false,"description":"Ingredient UUID (from GET /v1/ingredients/search); keeps products whose ingredient list contains it. Letter case is ignored. An unknown UUID is an empty page with no_match, not an error."},{"schema":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(,[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}){0,9}$","type":"string"},"in":"query","name":"excludes_ingredient","required":false,"description":"1 to 10 ingredient UUIDs, comma-separated with no spaces; keeps products containing none of them. Letter case is ignored. UUIDs not in the catalogue are ignored; more than 10 is a 400."},{"schema":{"minimum":1,"maximum":51,"default":1,"type":"integer"},"in":"query","name":"page","required":false,"description":"Page number, 1 to 51. Optional, defaults to 1. Pages are fixed at 20 results; there is no page-size parameter."}],"responses":{"200":{"description":"One page of matching products.","content":{"application/json":{"schema":{"description":"One page of matching products.","type":"object","required":["results","pagination"],"properties":{"results":{"description":"Up to 20 products.","type":"array","items":{"type":"object","required":["id","brand","name","category","image_url"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Product UUID; use it with GET /v1/products/{id} and POST /v1/products/batch.","type":"string"},"brand":{"anyOf":[{"description":"Brand name as stored.","type":"string"},{"type":"null"}]},"name":{"anyOf":[{"description":"Product name as stored, without the brand.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["skincare","suncare","makeup","haircare","body/bath","fragrance","other"],"description":"One of skincare, suncare, makeup, haircare, body/bath, fragrance or other. null (unclassified) for ~2.5% of products."},{"type":"null"}]},"image_url":{"anyOf":[{"description":"Hosted product image URL (JPEG). null when the catalogue has no image.","type":"string"},{"type":"null"}]}}}},"pagination":{"type":"object","required":["page","has_more","total"],"properties":{"page":{"description":"The page that was served.","type":"integer"},"has_more":{"description":"true when rows exist beyond this page. Page 51 is the last page the API serves, so has_more can be true there while page 52 is a 400.","type":"boolean"},"total":{"description":"Number of matching rows across all pages. Pages stop at 51 (1,020 rows), so total can exceed what is reachable by paging; narrow the query instead.","type":"integer"}}},"no_match":{"description":"Only present when total is 0.","type":"object","required":["hint"],"properties":{"hint":{"description":"Why nothing matched and what to change, in plain words.","type":"string"},"terms":{"description":"Search endpoints only (GET /v1/products/search, GET /v1/ingredients/search): one entry per searched word, so the caller can see which word killed the match. GET /v1/products sends hint alone.","type":"array","items":{"type":"object","required":["term","matches"],"properties":{"term":{"description":"The searched word, lower-cased.","type":"string"},"matches":{"description":"Rows this word matches on its own.","type":"integer"}}}}}}}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: a filter value of the wrong type (the *_free filters take only true or false), has_ingredient not a UUID, excludes_ingredient not 1 to 10 comma-separated UUIDs, brand without letters or digits, page not an integer from 1 to 51, a parameter given twice, or a query parameter this endpoint does not have (the message lists the accepted ones). The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: a filter value of the wrong type (the *_free filters take only true or false), has_ingredient not a UUID, excludes_ingredient not 1 to 10 comma-separated UUIDs, brand without letters or digits, page not an integer from 1 to 51, a parameter given twice, or a query parameter this endpoint does not have (the message lists the accepted ones). The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"invalidKey":{"value":{"statusCode":403,"error":"Forbidden","message":"Invalid or missing API key"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}},"/v1/products/{id}":{"get":{"summary":"Get a product with its full ingredient list (fat payload)","tags":["products"],"parameters":[{"schema":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"in":"path","name":"id","required":true,"description":"The public UUID from a list, search or batch response. Letter case is ignored."}],"responses":{"200":{"description":"The full product payload.","content":{"application/json":{"schema":{"description":"The full product payload.","type":"object","required":["id","brand","name","image_url","category","origin","contains_fragrance","contains_drying_alcohol","contains_parabens","contains_sulfates","contains_silicones","ingredients"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Product UUID; use it with GET /v1/products/{id} and POST /v1/products/batch.","type":"string"},"brand":{"anyOf":[{"description":"Brand name as stored.","type":"string"},{"type":"null"}]},"name":{"anyOf":[{"description":"Product name as stored, without the brand.","type":"string"},{"type":"null"}]},"image_url":{"anyOf":[{"description":"Hosted product image URL (JPEG). null when the catalogue has no image.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["skincare","suncare","makeup","haircare","body/bath","fragrance","other"],"description":"One of skincare, suncare, makeup, haircare, body/bath, fragrance or other. null (unclassified) for ~2.5% of products."},{"type":"null"}]},"origin":{"anyOf":[{"description":"Country of the brand's headquarters (best effort), e.g. USA, South Korea. null (unknown) for ~53% of products.","type":"string"},{"type":"null"}]},"contains_fragrance":{"description":"true when at least one ingredient is tagged fragrance. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_drying_alcohol":{"description":"true when at least one ingredient is tagged drying_alcohol. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_parabens":{"description":"true when at least one ingredient is tagged parabens. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_sulfates":{"description":"true when at least one ingredient is tagged sulfates. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_silicones":{"description":"true when at least one ingredient is tagged silicones. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"ingredients":{"description":"The full ingredient list in on-label order (sorted by position). Empty when the catalogue has no ingredient list for the product.","type":"array","items":{"type":"object","required":["position","label_name","concentration","id","name","rating","irritancy","comedogenicity","functions","cas_number","ec_number","ph_eur_name","iupac_name","other_names","category"],"properties":{"position":{"description":"Label-order index on this product, from 0: the on-pack INCI order, which by regulation is descending by concentration for ingredients at 1% or more.","type":"integer"},"label_name":{"description":"The ingredient text exactly as printed on this product, which can differ from the canonical name.","type":"string"},"concentration":{"anyOf":[{"description":"Declared percent concentration (% w/w) when the brand published it. null for ~99.7% of entries.","type":"number"},{"type":"null"}]},"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Ingredient UUID; use it with GET /v1/ingredients/{id}, has_ingredient and excludes_ingredient.","type":"string"},"name":{"description":"Canonical ingredient name. One row per chemical, however products print it (see label_name on a product).","type":"string"},"rating":{"anyOf":[{"type":"string","enum":["direct actives","supporting actives","sensitizers"],"description":"Editorial rating bucket: direct actives, supporting actives or sensitizers. null (unrated) for ~97.5% of ingredients. Not medical or regulatory guidance."},{"type":"null"}]},"irritancy":{"anyOf":[{"description":"Irritancy on a 0 to 5 scale (higher = more irritating), from published assays on the undiluted ingredient. A string because values come as points and ranges: \"0\", \"3\", \"0-3\", \"1-2\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"comedogenicity":{"anyOf":[{"description":"Pore-clogging potential on a 0 to 5 scale (higher = more comedogenic), same string/range encoding as irritancy: \"0\", \"2\", \"3-5\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"functions":{"description":"What the ingredient does in a formula, from a controlled vocabulary (solvent, humectant, antioxidant, preservative, emollient, skin conditioning, ...). Empty for ~51% of ingredients.","type":"array","items":{"type":"string"}},"cas_number":{"anyOf":[{"description":"CAS Registry Number.","type":"string"},{"type":"null"}]},"ec_number":{"anyOf":[{"description":"European Community (EC/EINECS) number.","type":"string"},{"type":"null"}]},"ph_eur_name":{"anyOf":[{"description":"European Pharmacopoeia name.","type":"string"},{"type":"null"}]},"iupac_name":{"anyOf":[{"description":"IUPAC systematic name.","type":"string"},{"type":"null"}]},"other_names":{"anyOf":[{"description":"Synonyms and alternative names, comma-separated in one string.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["fragrance","drying_alcohol","parabens","sulfates","silicones"],"description":"The \"free-from\" tag this ingredient triggers, if any: fragrance, drying_alcohol, parabens, sulfates or silicones. null for ~97.5% of ingredients. Drives the product-level contains_* flags."},{"type":"null"}]}}}}}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: id is not a UUID (a barcode sent here is pointed at the barcode endpoint). The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: id is not a UUID (a barcode sent here is pointed at the barcode endpoint). The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"invalidKey":{"value":{"statusCode":403,"error":"Forbidden","message":"Invalid or missing API key"}}}}}},"404":{"description":"No product has this id. The message echoes the id.","content":{"application/json":{"schema":{"description":"No product has this id. The message echoes the id.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}},"/v1/products/barcode/{barcode}":{"get":{"summary":"Look up a product by barcode (GTIN/EAN/UPC)","tags":["products"],"description":"Returns the full product payload for the primary product of this barcode (unit, multipack and sibling-variant codes all resolve). Any zero-padded GTIN form of a stored code is treated as equivalent. Requires a barcode-enabled plan; other plans receive 403. The response is the standard product payload: the API resolves barcodes to products but never lists a product's barcodes.","parameters":[{"schema":{"pattern":"^[0-9]{8,14}$","type":"string"},"in":"path","name":"barcode","required":true,"description":"GTIN/EAN/UPC, 8-14 digits, no separators. Leading zeros are optional: every zero-padded form of a code resolves to the same product."}],"responses":{"200":{"description":"The full product payload.","content":{"application/json":{"schema":{"description":"The full product payload.","type":"object","required":["id","brand","name","image_url","category","origin","contains_fragrance","contains_drying_alcohol","contains_parabens","contains_sulfates","contains_silicones","ingredients"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Product UUID; use it with GET /v1/products/{id} and POST /v1/products/batch.","type":"string"},"brand":{"anyOf":[{"description":"Brand name as stored.","type":"string"},{"type":"null"}]},"name":{"anyOf":[{"description":"Product name as stored, without the brand.","type":"string"},{"type":"null"}]},"image_url":{"anyOf":[{"description":"Hosted product image URL (JPEG). null when the catalogue has no image.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["skincare","suncare","makeup","haircare","body/bath","fragrance","other"],"description":"One of skincare, suncare, makeup, haircare, body/bath, fragrance or other. null (unclassified) for ~2.5% of products."},{"type":"null"}]},"origin":{"anyOf":[{"description":"Country of the brand's headquarters (best effort), e.g. USA, South Korea. null (unknown) for ~53% of products.","type":"string"},{"type":"null"}]},"contains_fragrance":{"description":"true when at least one ingredient is tagged fragrance. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_drying_alcohol":{"description":"true when at least one ingredient is tagged drying_alcohol. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_parabens":{"description":"true when at least one ingredient is tagged parabens. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_sulfates":{"description":"true when at least one ingredient is tagged sulfates. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_silicones":{"description":"true when at least one ingredient is tagged silicones. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"ingredients":{"description":"The full ingredient list in on-label order (sorted by position). Empty when the catalogue has no ingredient list for the product.","type":"array","items":{"type":"object","required":["position","label_name","concentration","id","name","rating","irritancy","comedogenicity","functions","cas_number","ec_number","ph_eur_name","iupac_name","other_names","category"],"properties":{"position":{"description":"Label-order index on this product, from 0: the on-pack INCI order, which by regulation is descending by concentration for ingredients at 1% or more.","type":"integer"},"label_name":{"description":"The ingredient text exactly as printed on this product, which can differ from the canonical name.","type":"string"},"concentration":{"anyOf":[{"description":"Declared percent concentration (% w/w) when the brand published it. null for ~99.7% of entries.","type":"number"},{"type":"null"}]},"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Ingredient UUID; use it with GET /v1/ingredients/{id}, has_ingredient and excludes_ingredient.","type":"string"},"name":{"description":"Canonical ingredient name. One row per chemical, however products print it (see label_name on a product).","type":"string"},"rating":{"anyOf":[{"type":"string","enum":["direct actives","supporting actives","sensitizers"],"description":"Editorial rating bucket: direct actives, supporting actives or sensitizers. null (unrated) for ~97.5% of ingredients. Not medical or regulatory guidance."},{"type":"null"}]},"irritancy":{"anyOf":[{"description":"Irritancy on a 0 to 5 scale (higher = more irritating), from published assays on the undiluted ingredient. A string because values come as points and ranges: \"0\", \"3\", \"0-3\", \"1-2\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"comedogenicity":{"anyOf":[{"description":"Pore-clogging potential on a 0 to 5 scale (higher = more comedogenic), same string/range encoding as irritancy: \"0\", \"2\", \"3-5\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"functions":{"description":"What the ingredient does in a formula, from a controlled vocabulary (solvent, humectant, antioxidant, preservative, emollient, skin conditioning, ...). Empty for ~51% of ingredients.","type":"array","items":{"type":"string"}},"cas_number":{"anyOf":[{"description":"CAS Registry Number.","type":"string"},{"type":"null"}]},"ec_number":{"anyOf":[{"description":"European Community (EC/EINECS) number.","type":"string"},{"type":"null"}]},"ph_eur_name":{"anyOf":[{"description":"European Pharmacopoeia name.","type":"string"},{"type":"null"}]},"iupac_name":{"anyOf":[{"description":"IUPAC systematic name.","type":"string"},{"type":"null"}]},"other_names":{"anyOf":[{"description":"Synonyms and alternative names, comma-separated in one string.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["fragrance","drying_alcohol","parabens","sulfates","silicones"],"description":"The \"free-from\" tag this ingredient triggers, if any: fragrance, drying_alcohol, parabens, sulfates or silicones. null for ~97.5% of ingredients. Drives the product-level contains_* flags."},{"type":"null"}]}}}}}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: barcode is not 8 to 14 digits, or has spaces or separators. The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: barcode is not 8 to 14 digits, or has spaces or separators. The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key; or, on this endpoint: The key's plan does not include Barcode Lookup.","content":{"application/json":{"schema":{"description":"The key's plan does not include Barcode Lookup.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"404":{"description":"No product carries this barcode in any GTIN form. The message echoes the barcode.","content":{"application/json":{"schema":{"description":"No product carries this barcode in any GTIN form. The message echoes the barcode.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}},"/v1/products/batch":{"post":{"summary":"Fetch up to 50 products in one request","tags":["products"],"description":"Returns an array the same length and order as `ids`; unknown UUIDs come back as null. Metered per item — the `X-Item-Count` response header carries the billable item count. The body must be JSON sent with `Content-Type: application/json`; a missing or unrecognised content type is a 415.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"minItems":1,"maxItems":50,"description":"1 to 50 product UUIDs. Letter case is ignored. Duplicates are allowed and each occurrence is returned, and billed, separately.","type":"array","items":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"}}}}}}},"responses":{"200":{"description":"One entry per requested id, in the same order; null where no product has that id.","content":{"application/json":{"schema":{"description":"One entry per requested id, in the same order; null where no product has that id.","type":"array","items":{"anyOf":[{"description":"The full product payload.","type":"object","required":["id","brand","name","image_url","category","origin","contains_fragrance","contains_drying_alcohol","contains_parabens","contains_sulfates","contains_silicones","ingredients"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Product UUID; use it with GET /v1/products/{id} and POST /v1/products/batch.","type":"string"},"brand":{"anyOf":[{"description":"Brand name as stored.","type":"string"},{"type":"null"}]},"name":{"anyOf":[{"description":"Product name as stored, without the brand.","type":"string"},{"type":"null"}]},"image_url":{"anyOf":[{"description":"Hosted product image URL (JPEG). null when the catalogue has no image.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["skincare","suncare","makeup","haircare","body/bath","fragrance","other"],"description":"One of skincare, suncare, makeup, haircare, body/bath, fragrance or other. null (unclassified) for ~2.5% of products."},{"type":"null"}]},"origin":{"anyOf":[{"description":"Country of the brand's headquarters (best effort), e.g. USA, South Korea. null (unknown) for ~53% of products.","type":"string"},{"type":"null"}]},"contains_fragrance":{"description":"true when at least one ingredient is tagged fragrance. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_drying_alcohol":{"description":"true when at least one ingredient is tagged drying_alcohol. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_parabens":{"description":"true when at least one ingredient is tagged parabens. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_sulfates":{"description":"true when at least one ingredient is tagged sulfates. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"contains_silicones":{"description":"true when at least one ingredient is tagged silicones. Derived from the ingredient list by rules, not lab analysis.","type":"boolean"},"ingredients":{"description":"The full ingredient list in on-label order (sorted by position). Empty when the catalogue has no ingredient list for the product.","type":"array","items":{"type":"object","required":["position","label_name","concentration","id","name","rating","irritancy","comedogenicity","functions","cas_number","ec_number","ph_eur_name","iupac_name","other_names","category"],"properties":{"position":{"description":"Label-order index on this product, from 0: the on-pack INCI order, which by regulation is descending by concentration for ingredients at 1% or more.","type":"integer"},"label_name":{"description":"The ingredient text exactly as printed on this product, which can differ from the canonical name.","type":"string"},"concentration":{"anyOf":[{"description":"Declared percent concentration (% w/w) when the brand published it. null for ~99.7% of entries.","type":"number"},{"type":"null"}]},"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Ingredient UUID; use it with GET /v1/ingredients/{id}, has_ingredient and excludes_ingredient.","type":"string"},"name":{"description":"Canonical ingredient name. One row per chemical, however products print it (see label_name on a product).","type":"string"},"rating":{"anyOf":[{"type":"string","enum":["direct actives","supporting actives","sensitizers"],"description":"Editorial rating bucket: direct actives, supporting actives or sensitizers. null (unrated) for ~97.5% of ingredients. Not medical or regulatory guidance."},{"type":"null"}]},"irritancy":{"anyOf":[{"description":"Irritancy on a 0 to 5 scale (higher = more irritating), from published assays on the undiluted ingredient. A string because values come as points and ranges: \"0\", \"3\", \"0-3\", \"1-2\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"comedogenicity":{"anyOf":[{"description":"Pore-clogging potential on a 0 to 5 scale (higher = more comedogenic), same string/range encoding as irritancy: \"0\", \"2\", \"3-5\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"functions":{"description":"What the ingredient does in a formula, from a controlled vocabulary (solvent, humectant, antioxidant, preservative, emollient, skin conditioning, ...). Empty for ~51% of ingredients.","type":"array","items":{"type":"string"}},"cas_number":{"anyOf":[{"description":"CAS Registry Number.","type":"string"},{"type":"null"}]},"ec_number":{"anyOf":[{"description":"European Community (EC/EINECS) number.","type":"string"},{"type":"null"}]},"ph_eur_name":{"anyOf":[{"description":"European Pharmacopoeia name.","type":"string"},{"type":"null"}]},"iupac_name":{"anyOf":[{"description":"IUPAC systematic name.","type":"string"},{"type":"null"}]},"other_names":{"anyOf":[{"description":"Synonyms and alternative names, comma-separated in one string.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["fragrance","drying_alcohol","parabens","sulfates","silicones"],"description":"The \"free-from\" tag this ingredient triggers, if any: fragrance, drying_alcohol, parabens, sulfates or silicones. null for ~97.5% of ingredients. Drives the product-level contains_* flags."},{"type":"null"}]}}}}}},{"type":"null"}]}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}},"X-Item-Count":{"description":"Billable item count for this request (= ids.length)","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: body not valid JSON, empty, or not an object; ids missing, empty, over 50 items, or containing a value that is not a UUID (the message names the index, e.g. ids[3]). The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: body not valid JSON, empty, or not an object; ids missing, empty, over 50 items, or containing a value that is not a UUID (the message names the index, e.g. ids[3]). The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"invalidKey":{"value":{"statusCode":403,"error":"Forbidden","message":"Invalid or missing API key"}}}}}},"415":{"description":"The request carried no Content-Type, or one this endpoint cannot parse. Send Content-Type: application/json.","content":{"application/json":{"schema":{"description":"The request carried no Content-Type, or one this endpoint cannot parse. Send Content-Type: application/json.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}},"/v1/ingredients/search":{"get":{"summary":"Search ingredients by canonical name and synonyms","tags":["ingredients"],"description":"Full-text search over the canonical name and `other_names`. Ingredients whose name starts with the query come first (shortest first), then by relevance with the name weighted above synonyms. When nothing matches, `no_match.terms` reports how many ingredients each word matches on its own.","parameters":[{"schema":{"minLength":1,"maxLength":200,"type":"string"},"in":"query","name":"q","required":true,"description":"Ingredient name or synonym. Split into words on anything that is not a letter or digit; every word must match (AND) and the last word matches as a prefix, so typing ahead works. Only the first 10 words are used, letter case and accents are ignored and search operators are treated as plain words. A value with no letters or digits is a 400."},{"schema":{"minimum":1,"maximum":51,"default":1,"type":"integer"},"in":"query","name":"page","required":false,"description":"Page number, 1 to 51. Optional, defaults to 1. Pages are fixed at 20 results; there is no page-size parameter."}],"responses":{"200":{"description":"One page of matching ingredients.","content":{"application/json":{"schema":{"description":"One page of matching ingredients.","type":"object","required":["results","pagination"],"properties":{"results":{"description":"Up to 20 ingredients. Ingredients whose canonical name starts with the query come first (shortest name first), then by relevance with the name weighted above synonyms.","type":"array","items":{"type":"object","required":["id","name","other_names","rating"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Ingredient UUID; use it with GET /v1/ingredients/{id}, has_ingredient and excludes_ingredient.","type":"string"},"name":{"description":"Canonical ingredient name. One row per chemical, however products print it (see label_name on a product).","type":"string"},"other_names":{"anyOf":[{"description":"Synonyms and alternative names, comma-separated in one string.","type":"string"},{"type":"null"}]},"rating":{"anyOf":[{"type":"string","enum":["direct actives","supporting actives","sensitizers"],"description":"Editorial rating bucket: direct actives, supporting actives or sensitizers. null (unrated) for ~97.5% of ingredients. Not medical or regulatory guidance."},{"type":"null"}]}}}},"pagination":{"type":"object","required":["page","has_more","total"],"properties":{"page":{"description":"The page that was served.","type":"integer"},"has_more":{"description":"true when rows exist beyond this page. Page 51 is the last page the API serves, so has_more can be true there while page 52 is a 400.","type":"boolean"},"total":{"description":"Number of matching rows across all pages. Pages stop at 51 (1,020 rows), so total can exceed what is reachable by paging; narrow the query instead.","type":"integer"}}},"no_match":{"description":"Only present when total is 0.","type":"object","required":["hint"],"properties":{"hint":{"description":"Why nothing matched and what to change, in plain words.","type":"string"},"terms":{"description":"Search endpoints only (GET /v1/products/search, GET /v1/ingredients/search): one entry per searched word, so the caller can see which word killed the match. GET /v1/products sends hint alone.","type":"array","items":{"type":"object","required":["term","matches"],"properties":{"term":{"description":"The searched word, lower-cased.","type":"string"},"matches":{"description":"Rows this word matches on its own.","type":"integer"}}}}}}}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: q missing, empty, over 200 characters or without any letters or digits; page not an integer from 1 to 51; a parameter given twice, or a query parameter this endpoint does not have (the message lists the accepted ones). The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: q missing, empty, over 200 characters or without any letters or digits; page not an integer from 1 to 51; a parameter given twice, or a query parameter this endpoint does not have (the message lists the accepted ones). The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"invalidKey":{"value":{"statusCode":403,"error":"Forbidden","message":"Invalid or missing API key"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}},"/v1/ingredients/{id}":{"get":{"summary":"Get an ingredient's full scientific record","tags":["ingredients"],"parameters":[{"schema":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","type":"string"},"in":"path","name":"id","required":true,"description":"The public UUID from a list, search or batch response. Letter case is ignored."}],"responses":{"200":{"description":"The ingredient's full record.","content":{"application/json":{"schema":{"description":"The ingredient's full record.","type":"object","required":["id","name","rating","irritancy","comedogenicity","functions","cas_number","ec_number","ph_eur_name","iupac_name","other_names","category"],"properties":{"id":{"pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Ingredient UUID.","type":"string"},"name":{"description":"Canonical ingredient name. One row per chemical, however products print it (see label_name on a product).","type":"string"},"rating":{"anyOf":[{"type":"string","enum":["direct actives","supporting actives","sensitizers"],"description":"Editorial rating bucket: direct actives, supporting actives or sensitizers. null (unrated) for ~97.5% of ingredients. Not medical or regulatory guidance."},{"type":"null"}]},"irritancy":{"anyOf":[{"description":"Irritancy on a 0 to 5 scale (higher = more irritating), from published assays on the undiluted ingredient. A string because values come as points and ranges: \"0\", \"3\", \"0-3\", \"1-2\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"comedogenicity":{"anyOf":[{"description":"Pore-clogging potential on a 0 to 5 scale (higher = more comedogenic), same string/range encoding as irritancy: \"0\", \"2\", \"3-5\". null (not rated) for ~99% of ingredients.","type":"string"},{"type":"null"}]},"functions":{"description":"What the ingredient does in a formula, from a controlled vocabulary (solvent, humectant, antioxidant, preservative, emollient, skin conditioning, ...). Empty for ~51% of ingredients.","type":"array","items":{"type":"string"}},"cas_number":{"anyOf":[{"description":"CAS Registry Number.","type":"string"},{"type":"null"}]},"ec_number":{"anyOf":[{"description":"European Community (EC/EINECS) number.","type":"string"},{"type":"null"}]},"ph_eur_name":{"anyOf":[{"description":"European Pharmacopoeia name.","type":"string"},{"type":"null"}]},"iupac_name":{"anyOf":[{"description":"IUPAC systematic name.","type":"string"},{"type":"null"}]},"other_names":{"anyOf":[{"description":"Synonyms and alternative names, comma-separated in one string.","type":"string"},{"type":"null"}]},"category":{"anyOf":[{"type":"string","enum":["fragrance","drying_alcohol","parabens","sulfates","silicones"],"description":"The \"free-from\" tag this ingredient triggers, if any: fragrance, drying_alcohol, parabens, sulfates or silicones. null for ~97.5% of ingredients. Drives the product-level contains_* flags."},{"type":"null"}]}}}}},"headers":{"X-Trial-Requests-Remaining":{"description":"Present on successful trial requests; remaining account-wide requests out of 100 total.","schema":{"type":"integer"}}}},"400":{"description":"Malformed request: id is not a UUID. The message names the field. Fix the request; do not retry as-is.","content":{"application/json":{"schema":{"description":"Malformed request: id is not a UUID. The message names the field. Fix the request; do not retry as-is.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"402":{"description":"Trial expired or payment required. Resolve billing on the dashboard.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"payment":{"value":{"statusCode":402,"error":"Payment Required","message":"Your trial has ended or payment is required. Open https://thebeautyapi.com/dashboard to continue."}}}}}},"403":{"description":"Invalid, missing or disabled API key.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"invalidKey":{"value":{"statusCode":403,"error":"Forbidden","message":"Invalid or missing API key"}}}}}},"404":{"description":"No ingredient has this id. The message echoes the id.","content":{"application/json":{"schema":{"description":"No ingredient has this id. The message echoes the id.","type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"description":"The HTTP status code, repeated in the body.","type":"integer"},"error":{"description":"The HTTP reason phrase, e.g. Bad Request, Not Found.","type":"string"},"message":{"description":"What went wrong, naming the field or id. For an unknown query parameter it lists the parameters the endpoint accepts.","type":"string"}}}}}},"429":{"description":"Trial allowance exhausted, rate limit exceeded, or the monthly request ceiling for this API key reached (the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls; the included calls themselves never block, overage is billed). No rate-limit headers are sent; back off and retry with exponential delay.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"trial":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Your 100 trial requests have been used. Convert to paid at https://thebeautyapi.com/dashboard, or wait for your scheduled trial conversion."}},"rateLimit":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Rate limit exceeded; retry with backoff"}},"quota":{"value":{"statusCode":429,"error":"Too Many Requests","message":"Monthly request ceiling reached for this API key: the Sandbox hard cap, or on paid plans the abuse ceiling of several times the included calls. Ceilings reset on the 1st (UTC); paid allowances reset with your billing month."}}}}}},"503":{"description":"Access verification temporarily unavailable; retry shortly.","content":{"application/json":{"schema":{"type":"object","required":["statusCode","error","message"],"properties":{"statusCode":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"}}},"examples":{"unavailable":{"value":{"statusCode":503,"error":"Service Unavailable","message":"Unable to verify API access. Please retry shortly."}}}}}}}}}},"servers":[{"url":"https://api.thebeautyapi.com"}],"tags":[{"name":"products","description":"Product search, discovery and detail"},{"name":"ingredients","description":"The ingredient dictionary"},{"name":"meta","description":"Service health"}],"security":[{"ApiKeyAuth":[]}],"externalDocs":{"description":"API documentation","url":"https://thebeautyapi.com/docs"}}