CosIng · API · Developers

How to query the CosIng database programmatically

8 min
  • The CosIng database API has public endpoints you can query with a free API key you get with your email: ingredient identity search and per-Annex counts. The regulatory watch feed needs no key at all.
  • The free key returns identity only — INCI name, CAS, EC number, chemical name, functions and a link to the public fiche. Annex entries, limits and conditions are on the paid plans.
  • One gotcha will bite you in the first five minutes: search defaults to regulated ingredients only. regulated_only=false unlocks the full INCI inventory.

1. What you can query

The CosIng database is the European Commission's inventory of cosmetic ingredients: INCI names, CAS and EC numbers, functions, and the restrictions defined in Regulation 1223/2009 Annexes II–VI. The official portal serves humans; if you are building software, you want the same data over HTTP with predictable JSON.

That is what the CosIng database API provides. The base URL for all public endpoints is:

https://api.bdapi.app/api/public

The /cosing/* endpoints take a free API key. Get one with your email — it arrives in your inbox — then export it once and every example below works as written:

export BDAPI_API_KEY="your-key-here"

Send it in the X-API-Key header (or as Authorization: Bearer <key>). The free quota is 500 requests per day per email, and per-IP rate limits apply on top — we will get to both at the end, because you should design around them from the start. Since 12 October 2026, calls without a key return 401 api_key_required. The ingredient search on bdapi.app stays free and needs no key.

What the free key covers:

Free keyPaid plans
Identity search: INCI name, CAS, EC number, chemical name, functions, link to the public ficheAnnex entries, reference numbers, maximum concentrations, conditions, warnings
Per-Annex counts (/cosing/stats)Annex, CMR, allergen and recently-prohibited listings, full ingredient and substance detail

2. Search by INCI name, CAS or EC number

The workhorse endpoint. It matches INCI names, CAS numbers and EC numbers:

curl "https://api.bdapi.app/api/public/cosing/search?q=phenoxyethanol&limit=1" \
  -H "X-API-Key: $BDAPI_API_KEY"
{
  "rows": [{
    "result_type": "ingredient",
    "inci_name": "PHENOXYETHANOL",
    "cas_no": "122-99-6",
    "ec_no": "204-589-7",
    "chemical_name": "2-Phenoxyethanol",
    "function_names": ["ANTIMICROBIAL", "PRESERVATIVE"],
    "slug": "phenoxyethanol",
    "has_fiche": true,
    "matched_via": "inci",
    "fiche_url": "https://bdapi.app/en/cosing-database/ingredient/phenoxyethanol/"
  }],
  "total": 1
}

Response trimmed for readability. Three fields matter more than the rest:

  • matched_via — why the row matched: inci (exact INCI name), cas (exact CAS number), vernacular or alias (a common name or another alias, reported in matched_text), or partial (a substring match, the lowest confidence). Use it to decide whether a match is good enough to accept without a human looking at it.
  • slug — the stable identifier of the ingredient on bdapi.app.
  • fiche_url — the link to the public fiche (next section), or null when the ingredient has none.

Searching by CAS number works with the same parameter: ?q=122-99-6 finds the same row, with matched_via: "cas". If you would rather browse than write curl commands, the CosIng ingredient search page runs the same kind of lookup from a UI.

3. The gotcha: regulated_only defaults to true

By default, the search covers regulated ingredients: entries whose Annex II–VI record is linked in the relational layer. Some inventory entries, retinol among them, are keyed under a separate substance record, so a strict regulated-only search for them finds nothing. When that happens the endpoint retries once without the filter and marks the response with "fallback": "not_regulated" — so check for that key before you treat a row as a regulated ingredient. To search the full INCI inventory from the start, opt out of the default filter:

curl "https://api.bdapi.app/api/public/cosing/search?q=retinol&regulated_only=false&limit=1" \
  -H "X-API-Key: $BDAPI_API_KEY"
{
  "rows": [{
    "inci_name": "RETINOL",
    "cas_no": "68-26-8 / 11103-57-4",
    "ec_no": "200-683-7 / 234-328-2",
    "chemical_name": "Retinol; vitamin A",
    "slug": "retinol"
  }],
  "total": 8
}

Rule of thumb: compliance-oriented lookups → default; inventory-wide search boxes → regulated_only=false. If retinol's regulatory status is what you are after, the retinol restriction under Regulation 2024/996 has its own write-up.

4. Link to the public fiche

Every row with has_fiche: true carries a fiche_url pointing at the ingredient's page on bdapi.app, for example phenoxyethanol. The page shows the Annex entries, reference numbers, limits and conditions for a person to read, so an internal tool can resolve an INCI list to identities with the API and send the reviewer to the fiche for the regulatory detail. Substances use the same pattern under /en/cosing-database/substance/<slug>/.

5. What needs a paid plan

The routes that return regulatory data are on the paid plans: ingredient and substance detail by slug, the slug listings, the Annex listings (/cosing/annex/:code/ingredients), CMR substances, fragrance allergens, microplastics, recently prohibited substances and the function listings. With the free key they answer:

{
  "error": "paid_plan_required",
  "message": "Regulatory data (annex entries, limits, conditions) is available on the paid plans. ...",
  "upgrade_url": "https://bdapi.app/en/contact/?origin=public-api-quota"
}

with status 403. A 403 does not count against your daily quota. If Annex semantics are new to you, how to read the CosIng Annexes covers what each one governs, and CMR substances in cosmetics covers the CMR mechanics.

6. Live counts and dataset freshness

The stats endpoint returns per-Annex counts and inventory totals — useful for dashboards and for sanity-checking your own replica:

curl "https://api.bdapi.app/api/public/cosing/stats" \
  -H "X-API-Key: $BDAPI_API_KEY"
{
  "annexes": [
    { "code": "II",  "substance_count": 1758, "cas_count": 2073 },
    { "code": "III", "substance_count": 381,  "cas_count": 481 },
    { "code": "IV",  "substance_count": 154,  "cas_count": 199 },
    { "code": "V",   "substance_count": 58,   "cas_count": 134 },
    { "code": "VI",  "substance_count": 34,   "cas_count": 39 }
  ],
  "regulated_total": 2385,
  "inci_inventory_total": 33621
}

There is also a regulatory watch feed — the latest events detected across DG SANTE, SCCS, EUR-Lex and Safety Gate, each with its official source URL:

curl "https://api.bdapi.app/api/public/watch/latest?limit=5"

7. Quota and rate limits: design around them, not into them

Two limits apply, and they are independent.

The daily quota belongs to your email: 500 requests per UTC day, counted across every key issued to that address. Each keyed response tells you where you stand:

X-BDAPI-Quota-Limit: 500
X-BDAPI-Quota-Remaining: 487

When the quota runs out you get a 429 with "error": "daily_quota_exceeded" until the next UTC day. A missing key, once keys are required, returns 401 with api_key_required; a wrong or revoked one returns 401 with invalid_api_key. Both bodies include a signup_url, so your error handling can point a user straight at the fix. A paid-plan route returns 403 with paid_plan_required and an upgrade_url.

The rate limits are per IP, per minute, per route:

EndpointLimit
/cosing/search60 req/min
/cosing/stats30 req/min
/watch/latest60 req/min

Three practical consequences:

  1. Respect the cache headers. Every response carries Cache-Control tuned to how often the underlying data changes. A polite client that caches will rarely see a limit.
  2. Tell the two 429s apart. A rate-limit 429 includes retryAfter in seconds: back off for that long, do not hammer. A daily_quota_exceeded 429 will not clear by retrying — stop until the next UTC day.
  3. Do not retry a 403. paid_plan_required is a property of the route, not a transient error: retrying returns the same answer.

8. From prototype to production

Everything above is read-only identity lookup over HTTP — enough for prototypes, internal tools and dashboards that need to resolve ingredient names and numbers. Checking products against the Annexes needs the regulatory data, and production compliance software has a further constraint: ingredient validation should not depend on a network call to anyone, including us.

That is what the paid plans of the CosIng database API are for, including incremental SQL patches that replicate the full dataset into your own PostgreSQL, verified with SHA-256 before apply, so your product validates offline. If you are evaluating that jump, how to choose a CosIng data source gives you the criteria — and CosIng API vs manual XLS downloads compares it against the do-it-yourself route.

Explore the CosIng database API →

Endpoint reference with documented rate limits, verified examples, and the paid plans when your prototype needs the regulatory data.

This article was written with the assistance of artificial intelligence and was reviewed and verified by a person against the official sources (EUR-Lex, CosIng, European Commission). More on our use of AI

← Back to the blog