POST /legal/diff/batch
Returns what changed across a list of legal references between two dates: up
to 50 references, French or European Union, each with its own from and to,
settled with one x402 payment for the whole batch. Each entry runs through the
same local diff engine as GET /legal/diff, so the
response is one ordered array of diffs - each carrying a structured segment
list (agent-friendly, word-granularity) and a unified patch string
(git-style, line-granularity).
This is the due-diligence-at-scale primitive: an agent compares a whole set of
articles across a time window in a single call - regulatory watch over a register,
compliance-delta sweeps across a contract’s citations, amendment tracking on a
portfolio - without issuing one paid call per reference. Reconstructing this
independently is exactly what an agent cannot do reliably: calling /legal/article
twice per reference and diffing the texts does not hold at scale or across corpus
updates.
The lookup is served from local snapshots of the LEGI and EUR-Lex corpora: no account, no API key and no network fetch to Legifrance or EUR-Lex at request time.
- For the full consolidated text at a date, call
GET /legal/articleorGET /legal/eu-act. - For the list of versions (when each article changed, without text or diff)
across a list, call
POST /legal/history/batch.
x402 golden rule: the agent pays for the answer to its question. A
well-formed batch is a successful answer -> 200, even when some references are
unchanged (changed: false), have an uncovered date boundary (from_version or
to_version is null) or are unknown. Those cases are reported per item, never as
a batch error. The 4xx range is reserved for requests the service cannot answer as
a batch: missing or malformed body, empty list, batch over the cap or a malformed
item.
Request
POST with a JSON body. Set Content-Type: application/json. Each item carries
its own dates.
POST /legal/diff/batch
Content-Type: application/json
{
"refs": [
{ "code": "code-civil", "article": "1240", "from": "2000-01-01", "to": "2026-01-01" },
{ "celex": "32016R0679", "article": "17", "from": "2018-05-25" }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
refs | object[] | yes | Diff requests to resolve, 1 to 50 entries |
Each item is either a French reference or an EU reference, carrying its own date window:
| Field | Type | Required | Description |
|---|---|---|---|
code | string | FR | French text identifier, for example code-civil |
article | string | FR only | Article number for code; optional for EU act-level lookups |
celex | string | EU id | CELEX identifier, for example 32016R0679 |
eli | string | EU id | ELI identifier, used when celex is absent |
from | string | yes | Start date YYYY-MM-DD (date A - the older snapshot) |
to | string | no | End date YYYY-MM-DD (date B); defaults to today’s date |
For French references, provide code and article. For EU references, provide at
least one of celex or eli; article is optional when targeting the act head.
from is always required on each item; to defaults to today.
Volume pricing
This endpoint uses per-unit pricing: one x402 settlement for the request, with
the payable amount derived from the number of references in refs - a base plus a
per-reference unit, capped at 50 references. The value of the call grows with the
size of the set you watch. The gateway counts the array length; it does not inspect
legal content to compute price.
The live /catalog is the price authority and exposes the
current base, unit, maximum units, accepted assets and networks. This page
describes the pricing model, not the amounts: do not cache figures from here.
200 response - UnifiedResponse
{
"data": {
"count": 2,
"results": [ { ... }, { ... } ]
},
"provenance": {
"source": "legifrance-legi + eur-lex-cellar",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
count: number of submitted references, equal toresults.length.results: one verdict per input item, in the same order as the input. Each verdict echoes the resolvable identifiers and its effectivefrom/to. Covered items add the same fields as thedataofGET /legal/diff:label,from_version,to_version,changedand thediffobject (segments,unified,added,removed).- Unknown references remain item-level verdicts in the 200 response. They echo the
identity and dates, set
coverage.completetofalsewith areason, and omit the covered block (label, versions,changed,diff). - A boundary date that falls outside all known versions is not an error: that
side is treated as empty text, signalled by
from_version: nullorto_version: null, and the diff represents an insertion from nothing or a deletion to nothing (see the single endpoint’s boundary semantics). - A single
provenanceblock covers the whole batch. When the batch touches both legal corpora,provenance.sourcenames both;freshness.kindremainssnapshot.
See GET /legal/diff for the full diff field table, the
op segment values and the attribution rules.
Example - mixed French and EU batch
{
"data": {
"count": 2,
"results": [
{
"code": "code-civil",
"article": "1240",
"from": "2000-01-01",
"to": "2026-01-01",
"label": "Code civil, art. 1240",
"from_version": {
"version_id": "v-1804",
"etat": "abroge",
"date_debut": "1804-03-21",
"date_fin": "2016-10-01"
},
"to_version": {
"version_id": "v-courant",
"etat": "vigueur",
"date_debut": "2016-10-01"
},
"changed": true,
"diff": {
"segments": [
{ "op": "equal", "text": "Tout fait quelconque de l'homme, qui cause a autrui un dommage, oblige celui par la faute duquel il est arrive a le reparer." },
{ "op": "delete", "text": " (ancien article 1382)" }
],
"unified": "@@ -1 +1 @@\n-Tout fait quelconque ... a le reparer. (ancien article 1382)\n+Tout fait quelconque ... a le reparer.",
"added": 0,
"removed": 22
},
"coverage": { "complete": true }
},
{
"celex": "32016R0679",
"article": "17",
"from": "2018-05-25",
"to": "2026-06-20",
"label": "Regulation (EU) 2016/679 (GDPR), Article 17",
"from_version": {
"version_id": "32016R0679-art-17-v1",
"etat": "vigueur",
"date_debut": "2018-05-25"
},
"to_version": {
"version_id": "32016R0679-art-17-v1",
"etat": "vigueur",
"date_debut": "2018-05-25"
},
"changed": false,
"diff": { "segments": [], "unified": "", "added": 0, "removed": 0 },
"coverage": { "complete": true }
}
]
},
"provenance": {
"source": "legifrance-legi + eur-lex-cellar",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
An unchanged article is a successful item verdict (changed: false with an empty
diff), not an error.
Example - unknown reference is still a 200 item verdict
In a well-formed batch, an unknown article does not fail the whole request. It is returned as an incomplete verdict that echoes the identity and dates and explains the gap.
{
"data": {
"count": 1,
"results": [
{
"code": "code-civil",
"article": "9999",
"from": "2000-01-01",
"to": "2026-01-01",
"coverage": {
"complete": false,
"reason": "`code-civil-art-9999` is not covered by the store"
}
}
]
},
"provenance": {
"source": "legifrance-legi",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
Errors
Only requests the service cannot answer leave the 200 range. The cap is checked
before any resolution, so an oversized batch is rejected without being billed
an answer. Note that a missing or malformed item date is an item-level malformation
reported as INVALID_REF with the faulty index - not the single endpoint’s
MISSING_PARAM / INVALID_DATE codes.
| Status | code | Case |
|---|---|---|
| 400 | INVALID_BODY | Body missing or not JSON, not an object, or refs missing |
| 400 | EMPTY_BATCH | refs is an empty array ([]) |
| 400 | BATCH_TOO_LARGE | More than 50 references in a single call |
| 400 | INVALID_REF | An item has no usable identity, or a missing/malformed from or to date; error names refs[i] |
| 500 | INTERNAL | Internal error (detail logged, not exposed) |
{ "error": "request body must be a JSON object with a `refs` array", "code": "INVALID_BODY" }
{ "error": "batch `refs` must contain at least one reference", "code": "EMPTY_BATCH" }
{ "error": "batch `refs` exceeds the maximum of 50 references", "code": "BATCH_TOO_LARGE" }
{ "error": "refs[1]: `from` date is required (format YYYY-MM-DD)", "code": "INVALID_REF" }
Attribution
French legislation and regulation data is derived from the LEGI dataset made available by the Direction de l’information legale et administrative (DILA) on Legifrance, under the Licence Ouverte / Etalab open licence. EU law data is derived from EUR-Lex / Cellar data made available by the Publications Office of the European Union; reuse must preserve attribution to the European Union and EUR-Lex.
See also
GET /legal/diff- the single-reference text diff, with the full field tables and boundary semantics.POST /legal/history/batch- version timelines for a list of references (when each changed, not what changed), in one settlement.- For agents - discovery surfaces, the live
/catalogand how settlement works.