GET /label/chem/classify
Resolves a substance identifier into its harmonised classification and labelling under the CLP Regulation, Annex VI (ECHA): the hazard classes with their H-statements, GHS pictograms, signal word, concentration limits and M-factors — exactly as published, and versioned by ATP (the entry in force at the queried date, plus any published-but-not-yet-applicable change). The answer is served from a local store built from the official Annex VI dataset, so a lookup resolves in milliseconds with no network call and no secret at request time.
Use it when an agent needs a grounded, dated, sourced verdict before shipping an
EU label, a safety data sheet or a formulation — “is this substance
harmonised under CLP, and what does its label say?”, “does the classification
change under the next ATP?”. This is the differentiator: Annex VI moves by an
annual ATP that lands after model cutoffs (the 22nd ATP applies 2026-05-01,
the 23rd is published and applies 2027-02-01), so an LLM answers from a stale or
invented classification where this endpoint answers from the version in force
at the date you ask. Lookup is by CAS, EC or exact name only — there is
no fuzzy search in v1. See the live /catalog for the
authoritative endpoint listing and price.
This endpoint returns the Annex VI entry as sourced, dated, versioned facts. It is never legal advice, a computed classification for a mixture, nor a generated safety data sheet. That line is the boundary of the service — and an absence from Annex VI does not mean the substance is not hazardous.
x402 golden rule: the agent pays for the answer to its question. A
well-formed key that Annex VI does not carry is a successful answer too →
200 billed with verdict: "not_harmonised" — the absence is the
information, never a 404. Only requests the service cannot answer — no key,
several keys, a bad checksum, a malformed at — leave the 200 range and are
not billed.
Parameters
Exactly one of the three keys, as a query parameter. Zero or several keys, a
bad checksum, an empty name or a malformed at is a 400 INVALID_INPUT.
| Parameter | Type | Required | Description |
|---|---|---|---|
cas | string | one of the three | CAS number — check-digit validated (weighted-sum mod 10, the same rigour as the IBAN mod-97) |
ec | string | one of the three | EC number (EINECS/ELINCS) — check-digit validated |
name | string | one of the three | Exact Annex VI name (case and spacing folded, no fuzzy match) |
at | string | no | YYYY-MM-DD — serves the classification in force at that date (defaults to today) |
GET /label/chem/classify?cas=100-41-4&at=2026-06-01
cas and ec are validated by their check digit before any lookup — a wrong
check digit is a 400 INVALID_INPUT with the rejected literal in the message,
not a billed miss. An exact name is never disambiguated silently: an ambiguous
name returns all of its Annex VI entries. at lets an agent probe around
an ATP switchover — the same CAS can be not_harmonised before a date and
harmonised after it, or carry different hazard classes across the boundary.
200 response — UnifiedResponse
{
"data": { "verdict": "harmonised", "entries": [ ... ] },
"provenance": {
"source": "ECHA Annex VI (CLP)",
"fetched_at": "2026-07-24T07:45:50Z",
"freshness": { "kind": "snapshot", "as_of": "2026-05-01T00:00:00Z" }
}
}
freshness.as_of is dated by the ATP in force at the queried date, not by
the ingestion date — the regulatory version is the point of the answer.
Fields of data
| Field | Type | Description |
|---|---|---|
verdict | string | harmonised (≥ 1 Annex VI entry in force) or not_harmonised — both are billed 200s |
entries | object[] | The Annex VI entries as published (see below); empty on not_harmonised |
note | string | not_harmonised only — the contractual self-classification note (see below) |
upcoming | object | Present when a published but not-yet-applicable ATP will add this substance (not_harmonised) |
A field the source does not carry is omitted from the JSON, never served
null — the one exception being notes inside an entry, a full member of the
published entry.
An entries[] entry — Annex VI as published
| Field | Type | Description |
|---|---|---|
index_number | string | Annex VI index number — the stable identity across ATPs |
name | string | Substance name as published |
ec_numbers | string[] | EC number(s) as published |
cas_numbers | string[] | CAS number(s) as published |
hazard_classes | object[] | { class, category, h_statement } — the H-statement is paired only when the source columns align, otherwise omitted, never guessed |
hazard_statements | string[] | The flat list of H-statements, served exactly as published |
pictograms | string[] | GHS pictogram codes (e.g. GHS02) |
signal_word | string | Dgr (Danger) or Wng (Warning) |
label_hazard_statements | string[] | H-statements required on the label |
suppl_hazard_statements | string[] | Supplemental EUH statements |
concentration_limits | string | Specific concentration limits as published |
m_factors | string | M-factors as published |
notes | string | Annex VI notes; null when the entry carries none |
cmr_1a_1b | bool | Derived convenience flag: true iff a Carc./Muta./Repr. 1A or 1B class is among those served — a screening shortcut, not a judgment of ours |
atp | object | { regulation, applicable_from } — the ATP version this entry is served from |
upcoming | object | { change, regulation, applicable_from } when a published, not-yet-applicable ATP touches this entry |
The hazard_classes pairing is positional and cautious: h_statement is
filled only when the source columns have matching lengths; otherwise it is
omitted rather than paired at random, and the flat hazard_statements list still
carries everything. Classes, statements, pictograms, limits and M-factors are
relayed exactly as published — they are the regulatory text, not a
reformulation.
ATP versioning is the freshness differentiator
Each entry carries its atp (the version served) and, when relevant, an
upcoming block: change is added, modified or removed, with the
regulation and applicable_from of the ATP that will apply. This is what an
LLM cannot hold — the classification a substance will have on a future date,
and the date it flips. freshness.as_of is the applicability date of the ATP in
force at your at.
Example — harmonised substance, resolved by CAS
GET /label/chem/classify?cas=100-41-4&at=2026-06-01 (ethylbenzene), served from
the ATP in force on that date:
{
"data": {
"verdict": "harmonised",
"entries": [
{
"index_number": "601-020-00-8",
"name": "ethylbenzene",
"ec_numbers": ["202-849-4"],
"cas_numbers": ["100-41-4"],
"hazard_classes": [
{ "class": "Flam. Liq.", "category": "2", "h_statement": "H225" },
{ "class": "Acute Tox.", "category": "4", "h_statement": "H332" },
{ "class": "STOT RE", "category": "2", "h_statement": "H373" },
{ "class": "Asp. Tox.", "category": "1", "h_statement": "H304" }
],
"hazard_statements": ["H225", "H332", "H373", "H304"],
"pictograms": ["GHS02", "GHS07", "GHS08"],
"signal_word": "Dgr",
"label_hazard_statements": ["H225", "H332", "H373", "H304"],
"suppl_hazard_statements": [],
"concentration_limits": "STOT RE 2; H373: C >= 10 %",
"m_factors": "M = 1",
"notes": null,
"cmr_1a_1b": false,
"atp": {
"regulation": "Regulation (EU) 2024/2564 (22nd ATP)",
"applicable_from": "2026-05-01"
}
}
]
},
"provenance": {
"source": "ECHA Annex VI (CLP)",
"fetched_at": "2026-07-24T07:45:50Z",
"freshness": { "kind": "snapshot", "as_of": "2026-05-01T00:00:00Z" }
}
}
Example — an upcoming ATP change
GET /label/chem/classify?cas=111-15-9&at=2026-06-01 returns a harmonised
entry that additionally carries:
"upcoming": {
"change": "modified",
"regulation": "Regulation (EU) 2025/… (23rd ATP)",
"applicable_from": "2027-02-01"
}
The entry served is still the one in force on 2026-06-01; the upcoming block
tells the agent the classification will change on 2027-02-01 and how.
Example — a well-formed key that is not harmonised (billed)
GET /label/chem/classify?cas=7732-18-5 (water) — a valid CAS that Annex VI does
not carry — returns a 200, billed, with the self-classification note in
plain sight, never a 404:
{
"data": {
"verdict": "not_harmonised",
"entries": [],
"note": "Absence does not mean the substance is not hazardous: self-classification under CLP remains the responsibility of the entity placing it on the market."
},
"provenance": {
"source": "ECHA Annex VI (CLP)",
"fetched_at": "2026-07-24T07:45:50Z",
"freshness": { "kind": "snapshot", "as_of": "2026-05-01T00:00:00Z" }
}
}
The agent asked “is this substance harmonised under CLP?”, and “Annex VI does not
carry it” is the answer — with the note making explicit that a not-harmonised
substance can still be hazardous. When a published ATP will add the
substance, the response also carries the upcoming block. Contrast this with the
400/503 below, which are not billed.
Freshness
Answers are served from a snapshot of Annex VI, dated by the ATP in force at
the queried date. Annex VI is refreshed off-band from the official ECHA dataset
(a manual deposit, once or twice a year); if a refresh is missed, the service
keeps serving the last good snapshot — honestly dated by as_of — and never
turns staleness into a 5xx. A dataset not yet ingested at all is a
503 DATA_UNAVAILABLE (there is no last good snapshot to serve).
Errors
Only requests the service cannot answer leave the 200 range — none of them are billed.
| Status | code | Case |
|---|---|---|
| 400 | INVALID_INPUT | Zero or several keys; a bad CAS/EC check digit; an empty name; a malformed at |
| 503 | DATA_UNAVAILABLE | The Annex VI dataset has not been ingested yet — there is no last good snapshot to serve |
| 404 | NOT_FOUND | Unknown route |
| 500 | INTERNAL | Internal error (detail logged, not exposed) |
{ "error": "invalid CAS check digit: '100-41-5'", "code": "INVALID_INPUT" }
A 503 for a dataset that is not yet ingested is distinct from staleness: a
missed refresh serves the last snapshot, flagged by freshness.as_of, and is
never a 5xx.
Attribution
Harmonised classification data is derived from Annex VI of the CLP Regulation, published by ECHA (legal notice — commercial reuse permitted). Annex VI is the regulatory text and is also available on EUR-Lex.
See also
POST /label/chem/classify/batch— screen a whole BOM or formulation (up to 200 substances) in one settlement.GET /medication/resolve— resolve a medication from its official register before acting on a prescription.- For agents — discovery surfaces, the live
/catalogand how settlement works.