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_from is the date of the snapshot served — the latest adaptation in application at your at.
  • celex is 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 400 that 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-01 today answers from the 2027-02-01 snapshot, and freshness.as_of says so. That is the honest behaviour: adaptations that do not exist yet cannot be predicted.
  • Names are exact, never fuzzy. name=nitrous oxide returns not_harmonised at 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 cautioush_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:

  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. Readverdict, the entry’s classes, pictograms and signal word, atp.celex, upcoming, and the as_of that dates all of it.
  4. Branch — print the label, schedule a re-label for the upcoming date, 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.