POST /label/claims/check
Checks a proposed labelling wording against the EU Register of nutrition and health claims (Regulation (EC) 1924/2006). For a wording — and an optional nutrient to scope the search — the answer says whether the claim is authorised (with the exact legal wording and the conditions of use as written), non-authorised (with the full official rejection reason and the referenced EFSA opinion), only a partial fragment of a registered claim, or absent from the register. The register is served from a local store, so a check resolves in milliseconds with no network call and no secret at request time.
Use it when an agent needs to check a label wording before placing a product
on the EU market — the natural sequel to /label/chem/classify
and /label/chem/svhc. This is the showcase of the
claims domain: the register carries the rejections at character-binding
precision, and there is zero x402 offer on this verdict — so where an LLM
validates a plausible-sounding health claim that is in fact rejected, this
endpoint answers from the register as published, dated and sourced. The
sentinel is isomaltulose (“contributes to normal energy-yielding metabolism”):
a claim a model happily approves, that the register rejects with an official
reason. See the live /catalog for the authoritative
endpoint listing and price.
This endpoint returns the register content exactly as published — the legal wording, the conditions of use and the rejection reasons as written. It is never legal advice, nor a compliance assessment of your label — it relays the register status of a wording, it does not decide whether your specific label is compliant. That line is the boundary of the service.
x402 golden rule: the agent pays for the answer to its question. A
well-formed wording the register does not carry is a successful answer too →
200 billed with verdict: "not_in_register" — the absence is the
information, never a 404. Only requests the service cannot answer — an empty
wording, an empty nutrient — leave the 200 range and are not billed.
Request
POST with a JSON body — the wording is passed in the body because labelling
wordings are long. Set Content-Type: application/json.
POST /label/claims/check
Content-Type: application/json
{
"wording": "Isomaltulose contributes to normal energy-yielding metabolism",
"nutrient": "Isomaltulose"
}
| Field | Type | Required | Description |
|---|---|---|---|
wording | string | yes | The proposed labelling wording to check. Absent, empty, or empty after normalisation (no word) → 400 INVALID_INPUT, not billed |
nutrient | string | no | Scopes the match to the claims of that nutrient; without it, the whole register (~2,300 entries) is scanned. Provided but empty → 400 INVALID_INPUT, not billed |
Matching is strict — never fuzzy
Matching is strict-normalised, never fuzzy (a scoping decision): case,
punctuation and whitespace are folded, then the comparison is exact or
word-level containment — zero false positives, never a score. The level
that matched is exposed in match_kind:
match_kind | Meaning |
|---|---|
exact | The normalised wording is exactly a registered claim |
contains_registered | The label wording contains a whole registered claim inside a longer text |
contained_in_registered | The submitted wording is a fragment of a longer registered claim |
match_kind is absent on not_in_register. There is deliberately no fuzzy
match: a health claim is binding to the character, so a near-match is not
the legal claim — approving it would be exactly the false positive this endpoint
exists to prevent.
200 response — UnifiedResponse
{
"data": { "verdict": "non_authorised", "match_kind": "exact", "claims": [ ... ] },
"provenance": {
"source": "EU Register on nutrition and health claims",
"fetched_at": "2026-07-24T09:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-23T00:00:00Z" }
}
}
freshness.as_of is dated by the extraction of the register served
(meta.json), not by the ingestion date — the dated register is the point of the
answer.
Fields of data
| Field | Type | Description |
|---|---|---|
verdict | string | authorised | non_authorised | partial_match | not_in_register — all four are billed 200s |
match_kind | string | The strict level that matched (see above); absent on not_in_register |
claims | object[] | The matched register entries as published (see below); empty on partial_match and not_in_register |
registered_claims | object[] | partial_match only — the full claims the fragment appears in, status verbatim (never a green light) |
truncated | boolean | partial_match only — true when the registered_claims list was capped |
authorised_for_nutrient | object[] | not_in_register only — the authorised claims of the nutrient supplied (empty if none / no nutrient) |
note | string | partial_match and not_in_register only — the dated contractual note (see below) |
A field the source does not carry is omitted from the JSON, never served
null.
The four verdicts
All four are 200 billed — the golden rule: a well-formed wording is a well-formed question, and “absent from the register” is the answer, never a 404.
authorised— the wording matches one or more claims, all authorised.non_authorised— the wording matches at least one non-authorised claim (or one of a status outside the nomenclature). A mixed multi-match tips the whole verdict tonon_authorised: a wording that matches a rejected claim is never presented as authorised. This is the anti-false-positive guard-rail — the isomaltulose sentinel.partial_match— the wording is only a fragment (match_kind: contained_in_registered) of one or more registered claims. A fragment cannot be certified either way (the “flexibility of wording” of 1924/2006 is a semantic judgment out of scope — zero fuzzy), so the service returnsregistered_claims[](the full claims, status verbatim, never a green light), anoteandtruncatedif the list was capped. Fragments below a minimum token floor are treated asnot_in_registerinstead. The reverse direction — the label contains the whole claim (contains_registered) — stays a firm verdict.not_in_register— no match at all: ≠ authorised, ≠ prohibited, never a 404.
A claims[] entry — the register as published
The register content is relayed as published, never reformulated:
| Field | Type | Description |
|---|---|---|
code | string | Register code (e.g. POL-HC-11221) |
type | string | Base article (e.g. Art. 13.5); omitted if not published |
status | string | Verbatim register status: authorised / non_authorised / any out-of-nomenclature value relayed as-is |
nutrient | string | The nutrient / substance the claim is about |
wording | string | The exact LEGAL wording of the register — not the wording the caller submitted (a key point) |
conditions_of_use | string | Conditions of use as written; omitted if none |
rejections | object[] | { code, reason } — the full official rejection reason; absent on an authorised claim |
efsa | object | { question, url } of the referenced EFSA opinion; omitted if none |
legislation | string | The implementing act; omitted if none |
A multi-match returns an explicit list, never a silent pick. The wording
field is the register’s legal wording, not the caller’s submission — an agent
that wants a safe label copies this, character for character.
Example — the isomaltulose sentinel, rejected (billed)
POST /label/claims/check with { "wording": "Isomaltulose contributes to normal energy-yielding metabolism", "nutrient": "Isomaltulose" } — a plausible-looking
claim an LLM would approve, that the register rejects:
{
"data": {
"verdict": "non_authorised",
"match_kind": "exact",
"claims": [
{
"code": "POL-HC-11221",
"type": "Art. 13.5",
"status": "non_authorised",
"nutrient": "Isomaltulose",
"wording": "Isomaltulose contributes to normal energy-yielding metabolism",
"rejections": [
{
"code": "HC_RR_166",
"reason": "On the basis of the data presented, the Panel concluded that a cause and effect relationship has not been established between the consumption of isomaltulose and a normal energy-yielding metabolism."
}
],
"efsa": {
"question": "Q-2021-00073",
"url": "https://www.efsa.europa.eu/en/efsajournal/pub/Q-2021-00073"
}
}
]
},
"provenance": {
"source": "EU Register on nutrition and health claims",
"fetched_at": "2026-07-24T09:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-23T00:00:00Z" }
}
}
The claim is grammatical, on-topic and exactly the sort of wording a model
green-lights — and the register carries it as rejected, with the full
official reason (HC_RR_166) and the EFSA opinion (Q-2021-00073). That
is the whole value: an accountable, dated, sourced no.
Example — an authorised claim (billed)
A wording that matches an authorised claim returns verdict: "authorised", with
the conditions of use and the implementing act as published:
{
"data": {
"verdict": "authorised",
"match_kind": "exact",
"claims": [
{
"code": "CALCIUM_BONES",
"type": "Art. 13.1",
"status": "authorised",
"nutrient": "Calcium",
"wording": "Calcium is needed for the maintenance of normal bones",
"conditions_of_use": "The claim may be used only for food which is at least a source of calcium as referred to in the claim SOURCE OF [NAME OF VITAMIN/S] and/or [NAME OF MINERAL/S] as listed in the Annex to Regulation (EC) No 1924/2006.",
"legislation": "Commission Regulation (EU) No 432/2012"
}
]
},
"provenance": {
"source": "EU Register on nutrition and health claims",
"fetched_at": "2026-07-24T09:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-23T00:00:00Z" }
}
}
rejections and efsa are omitted on an authorised claim — an absent field
is never served as null.
Example — a wording absent from the register (billed)
A wording the register does not carry returns a 200, billed, with an honest fallback and a dated note in plain sight, never a 404:
{
"data": {
"verdict": "not_in_register",
"authorised_for_nutrient": [
{
"code": "VIT_C_IMMUNE",
"type": "Art. 13.1",
"status": "authorised",
"nutrient": "Vitamin C",
"wording": "Vitamin C contributes to the normal function of the immune system",
"conditions_of_use": "The claim may be used only for food which is at least a source of vitamin C as referred to in the claim SOURCE OF [NAME OF VITAMIN/S] as listed in the Annex to Regulation (EC) No 1924/2006.",
"legislation": "Commission Regulation (EU) No 432/2012"
}
],
"note": "Absence from the register is not an authorisation and not a prohibition. In particular, botanical claims 'on hold' under Article 13(1) are not in the register and may still be usable during the transitional regime."
},
"provenance": {
"source": "EU Register on nutrition and health claims",
"fetched_at": "2026-07-24T09:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-23T00:00:00Z" }
}
}
The honest limit: absent from the register ≠ prohibited. When a nutrient
was supplied, the fallback lists its authorised claims so the agent has a safe
alternative to reach for; the note is careful never to read “not in the register”
as “forbidden” — the botanicals ‘on hold’ regime (Article 13(1)) is the
canonical case of a usable claim that is not in the register.
Example — a partial fragment (billed)
A wording that is only a fragment of a longer registered claim cannot be
certified either way — it returns verdict: "partial_match" with the full claims
it appears in, their status verbatim:
{
"data": {
"verdict": "partial_match",
"match_kind": "contained_in_registered",
"registered_claims": [
{
"code": "CALCIUM_BONES",
"type": "Art. 13.1",
"status": "authorised",
"nutrient": "Calcium",
"wording": "Calcium is needed for the maintenance of normal bones",
"legislation": "Commission Regulation (EU) No 432/2012"
}
],
"note": "The submitted wording is only a fragment of the registered claim(s) listed above. A fragment is not certified as authorised or non-authorised; use the exact legal wording of a claim.",
"truncated": false
},
"provenance": {
"source": "EU Register on nutrition and health claims",
"fetched_at": "2026-07-24T09:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-23T00:00:00Z" }
}
}
A fragment is never a green light: the service refuses to certify a partial
wording and points at the full registered claim(s) so the agent can adopt the
exact legal text. truncated is true when the list of registered claims was
capped.
Freshness
Answers are served from a snapshot of the register, dated by the extraction
served (freshness.as_of), not by the ingestion date. The register is refreshed
off-band; 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 | wording absent, empty, or empty after normalisation (no word); nutrient provided but empty |
| 503 | DATA_UNAVAILABLE | The register 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": "wording is empty", "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
Register data is the European Commission — EU Register of nutrition and health claims made on foods, reused with attribution under Commission Decision 2011/833/EU. EFSA opinions are referenced by their question number and URL.
See also
GET /label/claims/permitted— the counterpart: list the authorised claims for a nutrient (what you may write) from the same register.GET /label/chem/classify— the harmonised CLP classification of a substance (Annex VI), versioned by ATP.GET /label/chem/svhc— the SVHC Candidate List status of a substance (REACH Article 59(10)).- For agents — discovery surfaces, the live
/catalogand how settlement works.