What CLP classification applies on the date you ask?
Annex VI moves by annual ATP, past every model cutoff. One call returns the CLP label in force at the date you ask, plus the change already published.
By Matthias Begot ·
“What is the harmonised classification of this substance?” is a question with no
answer. Annex VI to the CLP Regulation is versioned: it is amended by an
Adaptation to Technical Progress (ATP) that is published years before it
becomes mandatory, so the same CAS number carries different hazard classes,
different H-statements and a different pictogram set depending on which date
you are labelling for.
GET /label/chem/classify answers the dated
version of the question: the Annex VI entry in force at the at you pass,
the act it comes from with its CELEX number, and an upcoming block when a
published ATP will change it — served offline, with no network call and no
secret at request time.
The regulation publishes the future, and models never see it
Two adaptations are live objects right now, and they bracket today:
- ATP 22 — Commission Delegated Regulation (EU) 2024/2564, adopted 19 June 2024, published in the OJ on 30 September 2024, in force since 20 October 2024, mandatory from 1 May 2026. It introduced 27 new harmonised classifications, modified 16 existing entries and deleted 7.
- ATP 23 — Commission Delegated Regulation (EU) 2025/1222, adopted 2 April 2025, published in the OJ on 20 June 2025, in force since 10 July 2025, mandatory from 1 February 2027.
Read the gap. ATP 23 has been law since July 2025 and will not bite until February 2027 — a nineteen-month window in which suppliers may apply the new classification voluntarily and are not yet obliged to. During that window two different labels for the same substance are both lawful, and only the compliance date decides which one your agent should print.
That is why an undated answer is not merely imprecise, it is unusable. A language model answering from recall returns whichever Annex VI it last saw — usually one adaptation behind, with no signal that a published successor exists. The failure is silent: fluent hazard classes, correct-looking H-statements, and no date attached to any of it.
Same CAS, two dates, two labels
Nitrous oxide (CAS 10024-97-2) is the clean case. Called today, with no at,
the service resolves against the snapshot in force:
{
"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.",
"upcoming": {
"change": "added",
"regulation": "Annex VI to CLP — ATP23 (applies from 2027-02-01)",
"applicable_from": "2027-02-01"
}
},
"provenance": {
"source": "ECHA Annex VI (CLP), harmonised classification and labelling",
"freshness": { "kind": "snapshot", "as_of": "2026-05-01T00:00:00Z" }
}
}
Not harmonised — and a published adaptation that will add it. The same call
with at=2027-06-01 returns the entry that will then be in force:
{
"index_number": "007-031-00-9",
"name": "dinitrogen oxide",
"ec_numbers": ["233-032-0"],
"cas_numbers": ["10024-97-2"],
"hazard_classes": [
{ "class": "Repr.", "category": "1B", "h_statement": "H360Df" },
{ "class": "STOT SE", "category": "3", "h_statement": "H336" },
{ "class": "STOT RE", "category": "1", "h_statement": "H372 (nervous system)" },
{ "class": "Ozone", "category": "1", "h_statement": "H420" }
],
"pictograms": ["GHS08", "GHS07"],
"signal_word": "Dgr",
"cmr_1a_1b": true,
"atp": {
"regulation": "Annex VI to CLP — entry version ATP23 (CELEX 32025R1222)",
"applicable_from": "2027-02-01",
"celex": "32025R1222",
"eurlex_url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R1222"
}
}
Nothing about the substance changed between the two calls. What changed is the
date the question was asked for: no harmonised entry at all on one side, a
Repr. 1B reproductive toxicant with cmr_1a_1b: true on the other. An agent
that asks the undated question gets one of these two answers by accident.
upcoming is the field that makes a stale answer visible
Every response carries the change that is already published but not yet
applicable — added, modified or removed, with the ATP that will apply and
its date. On a not_harmonised verdict it sits at the root of data; on a
harmonised entry it sits on the entry itself.
This is the part no model can hold, because it is about a version of the law that does not govern yet. It is also the part that decides real work: a formulation being re-labelled for a 2027 shipment, an SDS being drafted now for a product with an eighteen-month lead time, a supplier questionnaire that must flag substances about to become CMR.
One deliberate exclusion keeps the flag honest: the act reference
(celex / eurlex_url) is excluded from the in-force ↔ upcoming
comparison. An entry merely re-attributed to a different act never raises a
false upcoming: modified.
Two dates in atp, and they are not the same date
The atp block is where the versioning becomes readable, and it carries a
distinction worth wiring into an agent. Ethylbenzene, served on 2026-06-01:
"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"
}
applicable_fromis the date of the snapshot served — the latest adaptation in application at yourat.celexis the act that set this entry’s version, which is usually much older. Ethylbenzene was last touched by the 2014 adaptation ATP 06; no adaptation since has changed it.
So two entries returned from the same snapshot routinely come from different
acts: n-hexane is served in its ATP22 version (CELEX 32024R2564), while most
of Annex VI is still served in its base-regulation version CLP00 (CELEX
32008R1272). celex and eurlex_url are copied from the source row, never
derived — an entry whose version the source leaves without a CELEX carries
neither field. They are what lets an agent walk from a served hazard class back
to the legal text that founds it.
not_harmonised is a paid answer, and it says so out loud
A well-formed CAS that Annex VI does not carry returns a 200, billed, with
an empty entries and the self-classification note quoted above. This is the
x402 golden rule applied to a register: the agent asked “is this substance
harmonised under CLP?”, and “no, at this date” is the answer it paid for — never
a 404.
The note matters as much as the verdict. Absence from Annex VI does not mean the substance is not hazardous: harmonised classification covers a minority of substances on the market, and self-classification under CLP remains the duty of whoever places the substance there. A verdict field alone would invite exactly the wrong inference, so the note ships with every one of them.
Only requests the service cannot answer leave the 200 range, and none of them are billed:
{ "code": "INVALID_INPUT", "error": "parameters 'cas', 'ec' and 'name' are mutually exclusive" }
cas and ec are check-digit validated before any lookup, with the same
rigour as an IBAN’s mod-97 — a transposed digit is rejected by name rather than
resolved confidently into the wrong substance.
What the date window actually covers
Three boundaries are worth knowing before an agent starts passing at freely.
- Backwards, the store has a floor. A date before the earliest adaptation
held returns a
400that says so:'at' predates the earliest Annex VI adaptation in store (2026-05-01). The endpoint serves the current and the coming Annex VI, not a historical archive of every adaptation since 2008. - Forwards, it stops at what is published. A date beyond the last published
ATP is served from that last ATP —
at=2030-01-01today answers from the 2027-02-01 snapshot, andfreshness.as_ofsays so. That is the honest behaviour: adaptations that do not exist yet cannot be predicted. - Names are exact, never fuzzy.
name=nitrous oxidereturnsnot_harmonisedat every date; the Annex VI name is dinitrogen oxide. Common names, trade names and synonyms are not resolved — an agent working from free text should resolve to a CAS or EC number first. An ambiguous exact name is never disambiguated silently either: all of its entries come back.
Two more lines that are the service’s boundary rather than its limitations. The
class ↔ H-statement pairing is positional and cautious — h_statement is
filled only when the source columns align, and is served null rather than
paired at random, with the flat hazard_statements list still carrying
everything. And cmr_1a_1b is a derived screening shortcut, true when a
Carc./Muta./Repr. 1A or 1B class is among those published; it is a convenience
over the published classes, not a judgment of ours.
What the endpoint never does: classify a mixture from its components, generate a safety data sheet, or give legal advice. It relays Annex VI as dated, sourced, versioned facts. That line is the product.
Where it sits in the x402 loop
No account, no key, no contract review precedes the first call:
- Discover the endpoint in the live catalog, call it, receive the
402challenge. - Pay — sign the chosen rail and replay the request.
- Read —
verdict, the entry’s classes, pictograms and signal word,atp.celex,upcoming, and theas_ofthat dates all of it. - Branch — print the label, schedule a re-label for the
upcomingdate, or route the substance to a human with the EUR-Lex link cited.
A bill of materials does not need one settlement per line:
POST /label/chem/classify/batch screens
up to 200 substances — mixing CAS, EC and exact names — in a single call,
all evaluated at the same at, priced per unit. Running a whole formulation
at at=2027-02-01 is how you find out today which lines change under ATP 23.
The Quickstart walks the full discover → 402 → pay →
replay cycle with runnable snippets. Price and accepted rails are served live by
the catalog, never pinned in an article.
Freshness, and why staleness is never a 5xx
Answers come from a snapshot of the official ECHA Annex VI dataset, dated by
the ATP in force at your at rather than by the ingestion date — the regulatory
version is the answer. Refreshes happen off-band, once or twice a year, on the
rhythm of the adaptations themselves. 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
A dated CLP verdict composes with the other dated verdicts:
GET /label/chem/svhc— whether the substance is on the REACH Candidate List, with its Article 57 grounds and the supply-chain duties inclusion triggers.GET /label/cosmetic/ingredient— prohibited, restricted or annex-authorised status under Regulation (EC) No 1223/2009, versioned the same way.GET /label/food/additive— whether an additive is authorised in a given food category, at which level.
chem/classify and chem/svhc are the usual pre-market pair: the first says
what the label must carry, the second says what the supply chain must be told.
Both answer from a dated publication rather than from recall — the only property
that makes either safe to put behind an autonomous agent.
For the full field reference and error codes, see the
GET /label/chem/classify documentation; for
how agents discover and call Invoket endpoints, see
For agents.