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), harmonised classification and labelling",
"fetched_at": "2026-08-08T11:59:20Z",
"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, note_marker, h_statement } — the H-statement is paired only when the source columns align, null otherwise, 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; null when the entry publishes none |
m_factors | string | M-factors as published; null when the entry publishes none |
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, celex, eurlex_url } — the version this entry is served from, and the act it comes from (see below) |
upcoming | object | { change, regulation, applicable_from } when a published, not-yet-applicable ATP touches this entry |
Inside hazard_classes, class carries the hazard class alone, category
its category ("2", "1B", "1.1", or a letter A–G for Org. Perox. /
Self-react. / Self-heat. / Pyr.) and note_marker the Annex VI note
marker (*, ****) when the published class carries one — relayed, never
interpreted. category is null for the classes that publish none
(Press. Gas, Lact.), and note_marker is absent rather than null when
the class carries no marker.
The class ↔ H-statement pairing is positional and cautious: h_statement is
filled only when the source columns have matching lengths; otherwise it is
served as null rather than paired at random, and the flat hazard_statements
list still carries everything. 2-ethoxyethyl acetate (?cas=111-15-9) is that
case: three classes with h_statement: null (Flam. Liq. 3, Repr. 1B,
Acute Tox. 4 *, hence cmr_1a_1b: true) against five H-statements in the flat
list. Classes, statements, pictograms, limits and M-factors are relayed
exactly as published — they are the regulatory text, not a reformulation.
atp — which act this entry comes from
| Field | Type | Description |
|---|---|---|
regulation | string | Label of the entry version served, composed of published values only (e.g. Annex VI to CLP — entry version ATP06 (CELEX 32014R0605)) |
applicable_from | string | Applicability date of the snapshot served — not the date of the act named above |
celex | string | CELEX number of the act this entry’s version comes from; absent when the source publishes none |
eurlex_url | string | EUR-Lex link to that act, as published; absent when the source publishes none |
celex and eurlex_url are copied from the ECHA workbook row of that entry’s
version — they are what lets an agent walk from a served verdict back to the
legal text that founds it. They are never derived or guessed: an entry whose
version the source leaves without a CELEX carries neither field, and
regulation falls back to the snapshot label.
Two entries of the same snapshot commonly come from different acts:
ethylbenzene is served in its ATP06 version (CELEX 32014R0605, 2014), while
most entries are still served in their base-regulation version CLP00 (CELEX
32008R1272). That is why applicable_from stays the date of the snapshot
served — the latest adaptation in application — and not the date of the act
named by regulation, which is often far older: an entry that no recent
adaptation has touched keeps the version of the act that created it.
ATP versioning is the freshness differentiator
Each entry carries its atp (the version served, and the act it comes from) 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. The act reference (celex / eurlex_url) is
excluded from the in-force ↔ upcoming comparison: a mere relabelling never
raises a false upcoming: modified.
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-023-00-4",
"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", "note_marker": "*", "h_statement": "H332" },
{ "class": "Asp. Tox.", "category": "1", "h_statement": "H304" },
{ "class": "STOT RE", "category": "2", "h_statement": "H373 (hearing organs)" }
],
"hazard_statements": ["H225", "H332", "H304", "H373 (hearing organs)"],
"pictograms": ["GHS02", "GHS07", "GHS08"],
"signal_word": "Dgr",
"label_hazard_statements": ["H225", "H332", "H304", "H373 (hearing organs)"],
"suppl_hazard_statements": [],
"concentration_limits": null,
"m_factors": null,
"notes": null,
"cmr_1a_1b": false,
"atp": {
"regulation": "Annex VI to CLP — entry version ATP06 (CELEX 32014R0605)",
"applicable_from": "2026-05-01",
"celex": "32014R0605",
"eurlex_url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32014R0605"
}
}
]
},
"provenance": {
"source": "ECHA Annex VI (CLP), harmonised classification and labelling",
"fetched_at": "2026-08-08T11:59:20Z",
"freshness": { "kind": "snapshot", "as_of": "2026-05-01T00:00:00Z" }
}
}
Read the two dates apart: the entry is served in the snapshot applicable since
2026-05-01 (applicable_from), but the act that set this version is the
2014 adaptation ATP06 (celex), because no later adaptation has touched
ethylbenzene. The entry publishes no specific concentration limit and no
M-factor — hence null, not an invented value — and the * on Acute Tox. 4 is
the Annex VI note marker, relayed as note_marker.
Example — an upcoming ATP change
GET /label/chem/classify?cas=75-35-4&at=2026-06-01 (1,1-dichloroethylene)
returns the entry in force on that date — served in its base regulation
version, CLP00 — carrying in addition an upcoming block:
{
"index_number": "602-025-00-8",
"name": "1,1-dichloroethylene; vinylidene chloride",
"ec_numbers": ["200-864-0"],
"cas_numbers": ["75-35-4"],
"hazard_classes": [
{ "class": "Flam. Liq.", "category": "1", "h_statement": "H224" },
{ "class": "Carc.", "category": "2", "h_statement": "H351" },
{ "class": "Acute Tox.", "category": "4", "note_marker": "*", "h_statement": "H332" }
],
"hazard_statements": ["H224", "H351", "H332"],
"pictograms": ["GHS02", "GHS08", "GHS07"],
"signal_word": "Dgr",
"label_hazard_statements": ["H224", "H351", "H332"],
"suppl_hazard_statements": [],
"concentration_limits": "*",
"m_factors": null,
"notes": "D",
"cmr_1a_1b": false,
"atp": {
"regulation": "Annex VI to CLP — entry version CLP00 (CELEX 32008R1272)",
"applicable_from": "2026-05-01",
"celex": "32008R1272",
"eurlex_url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32008R1272"
},
"upcoming": {
"change": "modified",
"regulation": "Annex VI to CLP — ATP23 (applies from 2027-02-01)",
"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": "Not listed in the harmonised classification (Annex VI CLP) in force at this date. 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), harmonised classification and labelling",
"fetched_at": "2026-08-08T11:59:21Z",
"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) |
{ "code": "INVALID_INPUT", "error": "invalid CAS check digit: '100-41-5'" }
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.