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.

ParameterTypeRequiredDescription
casstringone of the threeCAS number — check-digit validated (weighted-sum mod 10, the same rigour as the IBAN mod-97)
ecstringone of the threeEC number (EINECS/ELINCS) — check-digit validated
namestringone of the threeExact Annex VI name (case and spacing folded, no fuzzy match)
atstringnoYYYY-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

FieldTypeDescription
verdictstringharmonised (≥ 1 Annex VI entry in force) or not_harmonisedboth are billed 200s
entriesobject[]The Annex VI entries as published (see below); empty on not_harmonised
notestringnot_harmonised only — the contractual self-classification note (see below)
upcomingobjectPresent 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

FieldTypeDescription
index_numberstringAnnex VI index number — the stable identity across ATPs
namestringSubstance name as published
ec_numbersstring[]EC number(s) as published
cas_numbersstring[]CAS number(s) as published
hazard_classesobject[]{ class, category, note_marker, h_statement } — the H-statement is paired only when the source columns align, null otherwise, never guessed
hazard_statementsstring[]The flat list of H-statements, served exactly as published
pictogramsstring[]GHS pictogram codes (e.g. GHS02)
signal_wordstringDgr (Danger) or Wng (Warning)
label_hazard_statementsstring[]H-statements required on the label
suppl_hazard_statementsstring[]Supplemental EUH statements
concentration_limitsstringSpecific concentration limits as published; null when the entry publishes none
m_factorsstringM-factors as published; null when the entry publishes none
notesstringAnnex VI notes; null when the entry carries none
cmr_1a_1bboolDerived convenience flag: true iff a Carc./Muta./Repr. 1A or 1B class is among those served — a screening shortcut, not a judgment of ours
atpobject{ regulation, applicable_from, celex, eurlex_url } — the version this entry is served from, and the act it comes from (see below)
upcomingobject{ 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

FieldTypeDescription
regulationstringLabel of the entry version served, composed of published values only (e.g. Annex VI to CLP — entry version ATP06 (CELEX 32014R0605))
applicable_fromstringApplicability date of the snapshot served — not the date of the act named above
celexstringCELEX number of the act this entry’s version comes from; absent when the source publishes none
eurlex_urlstringEUR-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.

StatuscodeCase
400INVALID_INPUTZero or several keys; a bad CAS/EC check digit; an empty name; a malformed at
503DATA_UNAVAILABLEThe Annex VI dataset has not been ingested yet — there is no last good snapshot to serve
404NOT_FOUNDUnknown route
500INTERNALInternal 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