# Is this cosmetic ingredient allowed in the EU? > Prohibited, restricted or annex-authorised under Regulation 1223/2009 — one call returns every Annex entry that covers a substance, with the label wording served word for word. Published 2026-08-10 · HTML version: https://invoket.com/blog/is-this-cosmetic-ingredient-allowed-in-the-eu --- "Can we use this ingredient?" is not a yes/no question in EU cosmetics law. A substance can be **prohibited** under Annex II and simultaneously **restricted** for one narrow professional use under Annex III; it can be authorised as a **preservative** at one concentration and restricted as a plain ingredient at another. The obligations stack, and the entry that governs depends on the consolidated version of the Regulation in force on the date you ask. [`GET /label/cosmetic/ingredient`](/docs/api/label-cosmetic-ingredient) returns **every Annex entry covering the substance**, each with its published maximum concentration, conditions of use and **mandatory label wording served word for word** — resolved from a local store, with no network call and no secret at request time. ## The version in force right now was written in January 2026 Regulation (EC) No 1223/2009 is not a fixed text. It is amended several times a year, and the amendment that governs today is recent enough to sit past most model cutoffs: - **Commission Regulation (EU) 2026/78** of **12 January 2026**, on the use in cosmetic products of certain substances classified as **carcinogenic, mutagenic or toxic for reproduction** — published in the OJ on 13 January 2026, and **applicable from 1 May 2026**. It amends Annexes II, III, IV and V, adding Annex II entries **1752 to 1766** and Annex III entries **379** and **380**. That is the version the endpoint serves: `version.consolidated_ref` reads `02009R1223 — EN — 01.05.2026`. And it changes real answers. **Acetone oxime** (CAS `127-06-0`) was an ordinary ingredient until 30 April 2026 and is a prohibited substance from 1 May: ```json { "verdict": "prohibited", "substance": { "chemical_name": "Acetone oxime", "cas_numbers": ["127-06-0"], "ec_numbers": ["204-820-1"], "resolved_from": "annexes" }, "entries": [ { "annex": "II", "entry_number": "1754", "status": "prohibited", "conditions": [], "amending_act": "COMMISSION REGULATION (EU) 2026/78 of 12 January 2026", "amending_celex": "32026R0078", "amending_oj_date": "2026-01-13", "applicable_from": "2026-05-01" } ] } ``` Silver is the other shape of the same amendment — not banned outright but pinned to two product types at a single level, under a brand-new Annex III entry: ```json { "annex": "III", "entry_number": "379", "status": "restricted", "conditions": [ { "product_type_or_body_part": "(a) Toothpaste\n(b) Mouthwash", "max_concentration": "For (a) and (b): 0,05 %" } ], "amending_act": "COMMISSION REGULATION (EU) 2026/78 of 12 January 2026", "amending_celex": "32026R0078" } ``` `amending_celex` is what makes the answer checkable: an agent can walk from a served restriction straight back to the act on EUR-Lex that founds it. ## Only the Annexes are authoritative — CosIng is informative **CosIng**, the European Commission's cosmetic-ingredient database, is a convenient index and a legally non-binding one. Only the **Annexes to the Regulation** carry legal force. The service keeps the two apart and tells you which one spoke, in `substance.resolved_from`: - **`"annexes"`** — the identity came from the authoritative source itself; - **`"cosing_index"`** — CosIng recognised the substance, which is what turns an unknown identifier into an honest dated `not_listed` instead of a 404; - **`source_divergence: true`** with a `divergences[]` array — a recorded gap between CosIng and an Annex applies here. **The Annex prevails, and the gap is reported rather than silently resolved.** Tools that stop at CosIng are answering from the informative layer and calling it compliance. The distinction is a field, not a footnote, precisely so an agent can branch on it. ## One substance, several Annexes, all of them binding Salicylic acid is the case that breaks single-verdict thinking. It is restricted under **Annex III entry 98** *and* authorised as a preservative under **Annex V entry 3**, and both apply: ```json { "verdict": "restricted", "substance": { "inci_name": "Salicylic acid", "cas_numbers": ["69-72-7"], "ec_numbers": ["200-712-3"], "resolved_from": "annexes" }, "entries": [ { "annex": "III", "entry_number": "98", "status": "restricted", "conditions": [ { "product_type_or_body_part": "(a) Rinse-off hair products\n(b) Other products except body lotion, eye shadow, mascara, eyeliner, lipstick, roll-on deodorant\n(c) Body lotion, eye shadow, mascara, eyeliner, lipstick, roll-on deodorant", "max_concentration": "(a) 3,0 %\n(b) 2,0 %\n(c) 0,5 %", "other_conditions": "(a) (b) (c)\nNot to be used in preparations for children under 3 years of age. …\nThese levels are inclusive of any use of salicylic acid.", "required_label_wording": "(a) (b) (c)\nNot to be used for children under 3 years of age (10)" } ], "amending_act": "COMMISSION REGULATION (EU) 2021/850 of 26 May 2021" }, { "annex": "V", "entry_number": "3", "status": "authorised_preservative", "…": "…" } ], "note": "This substance is covered by 2 entries (III/98, V/3). The top-level verdict reflects the most restrictive one; each entry carries its own status and conditions, which all apply cumulatively." } ``` The top-level `verdict` is the **most restrictive** status found. It is a routing signal, not the whole answer — an agent that reads `verdict` and stops loses half the obligations. `note` says so in words, on every multi-entry substance. Titanium dioxide behaves the same way (Annex III entry 321 **and** Annex VI entry 27 as a UV filter), with the Annex VI conditions explicitly cross-referencing the Annex III entry. Hydroquinone is the sharpest version: **prohibited** under Annex II entry 1339, with a single surviving Annex III exception for artificial nail systems, professional use only. ## The label wording is the deliverable, and it is served verbatim For a restricted or authorised entry, `required_label_wording` is what has to be printed. It comes back exactly as published — dashes, line breaks and all: ```json "required_label_wording": "— For professional use only\n— Avoid skin contact\n— Read directions for use carefully" ``` Nothing is converted, rounded, reformulated or translated. `"3,0 %"` keeps its decimal comma; `"0,02 % (after mixing for use)"` keeps its parenthetical, because the parenthetical is the condition; a multi-part restriction keeps its `(a)` `(b)` `(c)` structure, because that structure is what binds each concentration to its product type. Reformatting regulatory text is how meaning gets lost, so the service refuses to do it. ## Key on the identifier when you have one Four keys are accepted — `cas`, `ec`, `inci`, `name` — exactly one per call, and `cas` and `ec` are **check-digit validated before any lookup**, the same discipline an IBAN gets from mod-97. There is **no fuzzy matching** on names: an approximate INCI name is a miss, not a guess. That last rule has a practical consequence worth wiring in. Annex entries are frequently **grouped**: Annex V entry 12 covers "4-Hydroxybenzoic acid and its Methyl- and Ethyl-esters, and their salts" and publishes nine INCI names in a single field. And new CMR entries arrive under their chemical name, with no INCI column at all. So the number is the join that always holds: - `cas=99-76-3` → **`authorised_preservative`**, Annex V entry 12, resolved from the Annexes; - `cas=127-06-0` → **`prohibited`**, Annex II entry 1754. When your input is free text from a product page, resolve it to a CAS or EC number first and key on that. `inci` and `name` are exact-match lookups into name spaces that the Annexes and CosIng populate differently. ## `not_listed` is a billed 200, and it is not an authorisation A resolved identity that the Annexes do not carry is a **successful answer** — 200, billed, with an empty `entries` and a dated statement of what the absence does and does not mean: ```json { "verdict": "not_listed", "substance": { "inci_name": "GLYCERIN", "cas_numbers": ["56-81-5"], "resolved_from": "cosing_index" }, "entries": [], "note": "Not listed in Annexes II to VI of Regulation (EC) No 1223/2009 as in force on 2026-08-10 (consolidated version 02009R1223 — EN — 01.05.2026 of the Regulation). This verdict is dated and is not an authorisation: it means no specific prohibition or restriction entry applies to this substance at that date." } ``` This is the x402 golden rule applied to a register: the agent asked whether the Annexes carry the substance, and "no, at this date" is the answer it paid for — never a 404. The unresolved case is the one that *is* a `404`: an identity unknown to **both** the Annexes and the CosIng index. There is nothing to answer about, and nothing is billed. The distinction between the two is the whole point. **Absence from Annexes II to VI is not an authorisation.** It means no EU-wide prohibition or restriction entry applies; the safety assessment of the ingredient and of the finished product stays with the responsible person placing it on the market. That sentence ships in the `limits` array of every single response, alongside three others: this is not a safety assessment, values are served exactly as published, and CosIng is informative only. ## `product_type` filters, and counts what it removed Passing `product_type` keeps the conditions whose published scope covers that product type, **plus** the unbounded ones. It is a reading aid, and it is built so it can never let an agent believe it saw everything. Salicylic acid with `product_type=Toothpaste` is the instructive case. The Annex III entry comes back with **zero** conditions and `"conditions_omitted_by_product_type": 1` — its conditions are all scoped to product types that are not toothpaste. The Annex V preservative entry keeps its conditions, because they carry no product-type scope of their own. The entry does not disappear, and the count of what was trimmed travels with it. ## What the date window covers `date` serves the consolidated version in force at that date. Two boundaries matter: - **Backwards, there is a floor.** A date before the oldest ingested consolidated version is a `400` that names the bound: `'date' predates the earliest consolidated version of Regulation (EC) No 1223/2009 in store (2026-05-01)`. The endpoint serves the version in force and the ones that follow — it is **not a historical archive** of every amendment since 2009. Extrapolating a verdict backwards would be inventing law. - **Forwards, it stops at what is published.** When an amendment is published and ingested but not yet applicable, the affected entry carries an `upcoming` block — `change`, `applicable_from`, `consolidated_ref` — so a formulation being planned for a future launch can be checked against the version that will govern it. Amendments that do not exist yet are not predicted. ## Screening a whole INCI list in one settlement A formulation is not one ingredient. [`POST /label/cosmetic/ingredient/batch`](/docs/api/label-cosmetic-ingredient-batch) takes up to **200** items — mixing `cas`, `ec`, `inci` and `name` — evaluated at **one** date, settled as a single x402 payment and priced per unit. Each result stays at its position, including per-item failures (`INVALID_INPUT` for a bad check digit, `NOT_FOUND` for an unresolved identity), so nothing has to be re-aligned by hand. The `summary` is always fully serialised, zeros included: ```json { "prohibited": 0, "restricted": 5, "authorised_colorant": 0, "authorised_preservative": 2, "authorised_uv_filter": 4, "not_listed": 4, "error": 1 } ``` `"prohibited": 0` is information — *no prohibited substance in this list* — and an agent that had to infer it from a missing key would be inferring from silence. Two deliberate non-behaviours: the input **order is preserved but never interpreted** (an INCI list is conventionally written in descending concentration order, and exploiting that convention would be inventing concentrations), and there is no de-duplication. **This is not a formulation compliance verdict**: no concentration in the finished product is computed, and nothing is concluded about the product as a whole. ## Where it sits in the x402 loop No account, no key, no contract review precedes the first call: 1. **Discover** the endpoint in the live catalog, call it, receive the `402` challenge. 2. **Pay** — sign the chosen rail and replay the request. 3. **Read** — `verdict`, every entry with its Annex, status, concentrations and label wording, `version.consolidated_ref`, and the `as_of` that dates all of it. 4. **Branch** — clear the ingredient, print the mandatory wording, or stop the formulation and hand a human the CELEX reference. The [Quickstart](/docs/quickstart) walks the full discover → `402` → pay → replay cycle with runnable snippets. Price and accepted rails are served live by the [catalog](https://api.invoket.com/catalog), never pinned in an article. ## Freshness, and why staleness is never a 5xx Answers come from a **snapshot** of the consolidated Regulation published on EUR-Lex, dated by the version in force at the queried date rather than by the ingestion date — the regulatory version *is* the answer. A missed refresh keeps serving the last good snapshot with `freshness.as_of` telling the truth about it. A dataset never ingested at all is a `503 DATA_UNAVAILABLE`, because there is no last good snapshot to serve. ## Chain it with the rest of the label family The same dated-verdict discipline covers the neighbouring regimes: - [`GET /label/chem/classify`](/docs/api/label-chem-classify) — harmonised CLP classification of a substance, versioned by ATP. The CMR classifications it serves are what feed the cosmetics Omnibus amendments in the first place: a substance classified CMR under CLP is the input to the next Annex II entry. - [`GET /label/food/additive`](/docs/api/label-food-additive) — whether an additive is authorised in a given food category, and at which level. Both answer from a dated publication rather than from recall — the only property that makes any of them safe to put behind an autonomous agent. For the full field reference and error codes, see the [`GET /label/cosmetic/ingredient` documentation](/docs/api/label-cosmetic-ingredient); for how agents discover and call Invoket endpoints, see [For agents](/docs/for-agents).