GET /legal/diff
Returns what changed in a legal article’s consolidated text between a
date A (from) and a date B (to): the two versions in force at each
date are resolved from the local store and their texts are diffed, giving you
both a structured segment list (agent-friendly, word-granularity) and a
unified patch string (git-style, line-granularity). The lookup is served in
milliseconds without an account or API key.
This is the hardest legal signal to reconstruct independently: an agent that
calls /legal/article twice and tries to diff the texts cannot do so reliably
at scale or across corpus updates. Use this endpoint when you need to know
what the law says now that it did not say before - due diligence, regulatory
watch, compliance delta, amendment tracking.
- For the full consolidated text at a date, call
GET /legal/articleorGET /legal/eu-act. - For the list of all versions (without text or diff), call
GET /legal/history.
See the live /catalog for the authoritative endpoint
listing and price.
x402 golden rule: the agent pays for the answer to its question. A well
formed, known article with readable dates returns a diff -> 200, including
when nothing changed (changed: false). An unchanged article is a successful
answer, not an error. Requests the service cannot answer - malformed references,
missing from, unknown articles - leave the 200 range.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | FR identity | French text identifier, for example code-civil |
article | string | FR identity; optional for EU | Article number within the text, for example 1240 |
celex | string | EU identity | CELEX identifier, for example 32016R0679 |
eli | string | EU identity | ELI identifier; used when celex is absent |
from | string | required | 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 |
Provide a French identity (code + article) or an EU identity (celex
or eli, with optional article). from is always required.
Dates use the strict ISO calendar format YYYY-MM-DD, zero-padded:
2026-07-05 is accepted, 2026-7-5 is rejected with 400 INVALID_DATE. And
from must not be after to: an inverted interval would describe the
evolution backwards (insertions read as deletions) with no signal, so it is
rejected with 400 INVALID_RANGE rather than answered and billed.
GET /legal/diff?code=code-civil&article=1240&from=2000-01-01&to=2026-01-01
GET /legal/diff?celex=32016R0679&article=17&from=2018-01-01
200 response - UnifiedResponse
{
"data": { ... },
"provenance": {
"source": "legifrance-legi",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
provenance.source: source identifier from the served store, commonlylegifrance-legifor French law oreur-lex-cellarfor EU law.freshness.kind:snapshot;as_ofis the store dump date that backed the answer.
Fields of data
| Field | Type | Description |
|---|---|---|
code | string | Echo of the requested French text identifier (FR requests) |
article | string | Echo of the requested article number, when supplied |
celex | string | Echo of the requested CELEX identifier (EU requests) |
eli | string | Echo of the requested ELI identifier (EU requests) |
label | string | Human label for the article |
from | string | Effective date A, YYYY-MM-DD |
to | string | Effective date B, YYYY-MM-DD |
from_version | object | null | Version retained at from; null when no version covered that date |
to_version | object | null | Version retained at to; null when no version covered that date |
from_in_force | bool | Whether the from side text was the applicable law on that date |
to_in_force | bool | Whether the to side text is the applicable law on that date |
changed | bool | true when the two resolved texts differ |
status_changed | bool | true when the status (etat) differs between the two dates |
in_force_changed | bool | true when applicability flips between the two dates |
differs | bool | changed OR status_changed OR in_force_changed - the single field to filter on for a watch |
renumbering_suspected | bool | true when a renumbering break falls between the two dates: the number stopped designating one text and started designating another, so the diff describes a change that never happened |
diff | object | Diff payload; empty/zeroed when changed: false |
from_version and to_version share the same shape:
| Field | Type | Description |
|---|---|---|
version_id | string | Stable source identifier for this version |
etat | string | LEGI / EUR-Lex status carried as stored, never reinterpreted |
date_debut | string | Start date of this version, inclusive, YYYY-MM-DD |
date_fin | string | null | End date, exclusive; omitted for the current version |
continuity | string | Whether this version continues the previous one under the same number: same_article, renumbered, unknown. French law only - omitted for EU law |
diff
| Field | Type | Description |
|---|---|---|
segments | array | Structured word-level diff: [ { op, text } ] |
unified | string | Git-style unified patch (line-granularity) |
added | number | Total characters added |
removed | number | Total characters removed |
op in segments[] is one of equal, insert, or delete.
Boundary semantics
When a date falls outside all known versions for an article - before its first
recorded version, or after the article has been fully repealed and no version
before its first recorded version - the service treats that side as empty
text rather than returning a 4xx. The diff then represents an insertion from
nothing (“added since”) or a deletion to nothing (“removed since”). The
null side is signalled by from_version: null or to_version: null.
A repeal no longer produces a null side: the last known version is served on
both sides, and in_force_changed carries the event.
This preserves the x402 golden rule: a known article with a valid reference is always a 200, even when one date is uncovered. Only unknown articles and malformed requests return 4xx.
Text change vs status change vs applicability change
Four booleans answer four different questions, and a regulatory watch needs all four to stay honest:
changed- did the wording change? It mirrorsdiff.changedand is the answer to “do I need to re-read the text?”.status_changed- did the status (etat) change? It istruewhenfrom_version.etatdiffers fromto_version.etat, and also when only one of the two dates has a version at all (one sidenull). Statuses are compared as stored, never reinterpreted.in_force_changed- did the text stop (or start) being the applicable law between the two dates? It isfrom_in_force != to_in_force. This is the only flag that fires on a French-style repeal, where the article’s last version simply ends: both dates then resolve to the same version, so neitherchangednorstatus_changedmoves.differs-changed OR status_changed OR in_force_changed. This is the field to filter on when monitoring a citation: it catches a rewritten text, a repeal that left the wording untouched, and a repeal that left the status untouched too.
Why the distinction matters: an EU article repealed by a later act keeps its
last known wording in the store, with a new version carrying etat: "abroge".
The text is byte-identical, so changed is legitimately false - but the
article you are about to cite is no longer in force. Filtering on changed
alone silently misses exactly the event that matters most.
Renumbering: when the diff describes a change that never happened
renumbering_suspected answers a fifth, different question: are the two dates
even talking about the same article?
A reform can make an article number stop designating one text and start
designating another. French Civil Code article 1240 carried “payment made
in good faith to the holder of the claim is valid” until 2016-10-01; the reform
of the law of obligations then moved the tort provision of the former article
1382 onto that number. Diffing 1240 between 2000 and today therefore returns
changed: true with a perfectly credible word-level diff - of two texts that
have nothing to do with each other.
- The diff itself is unchanged: the service adds a signal, it never rewrites or suppresses what the store holds.
renumbering_suspecteddeliberately stays out ofdiffers. It is not one more change event to watch, it is a warning about how to read the diff: when it istrue, treat the two sides as two different provisions and follow up withGET /legal/history, where each version carries its owncontinuitymarker.- It is
truewhen a version markedcontinuity: "renumbered"starts inside the interval (from < date_debut <= to). - French law only. EU consolidations keep their numbering, so the flag is
always
falsethere.
Detection happens when the store is built, from the source filiation first (a version whose text was formally transferred to another number) and from a textual heuristic otherwise - hence suspected: a fully rewritten article raises the same signal as a renumbered one.
GET /legal/diff?celex=32014R0910&article=17&from=2024-01-01&to=2026-07-25
{
"data": {
"celex": "32014R0910",
"article": "17",
"label": "Regulation (EU) No 910/2014 (eIDAS), Article 17",
"from": "2024-01-01",
"to": "2026-07-25",
"from_version": {
"version_id": "32014R0910-art-17",
"etat": "vigueur",
"date_debut": "2016-07-01",
"date_fin": "2024-05-20"
},
"to_version": {
"version_id": "32014R0910-art-17-abroge",
"etat": "abroge",
"date_debut": "2024-05-20"
},
"from_in_force": true,
"to_in_force": false,
"changed": false,
"status_changed": true,
"in_force_changed": true,
"differs": true,
"diff": { "segments": [], "unified": "", "added": 0, "removed": 0 }
},
"provenance": {
"source": "eur-lex-cellar",
"fetched_at": "2026-07-25T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-07-20T00:00:00Z" }
}
}
changed keeps its original meaning and is never flipped by a status or
applicability change - that would make diff.added / diff.removed contradict
it. status_changed, in_force_changed, from_in_force, to_in_force and
differs are additive fields; existing integrations are unaffected.
The two corpora encode a repeal in opposite ways. EU law opens a repeal version
that starts at the repeal (the example above): the status flips, so
status_changed fires. French law simply closes the last version’s interval:
both dates resolve to that same version, the text and the status are identical,
and only in_force_changed fires. differs covers both, which is why it is the
field to watch.
Example - article that changed between two dates (and changed meaning)
Civil Code article 1240 between 2000 and 2026 is also the renumbering case
described above: renumbering_suspected is true, and the two sides are two
different provisions rather than two states of one.
{
"data": {
"code": "code-civil",
"article": "1240",
"label": "Code civil, art. 1240",
"from": "2000-01-01",
"to": "2026-01-01",
"from_version": {
"version_id": "LEGIARTI000006437044",
"etat": "modifie",
"date_debut": "1804-03-21",
"date_fin": "2016-10-01",
"continuity": "same_article"
},
"to_version": {
"version_id": "LEGIARTI000032041571",
"etat": "vigueur",
"date_debut": "2016-10-01",
"continuity": "renumbered"
},
"from_in_force": true,
"to_in_force": true,
"changed": true,
"status_changed": true,
"in_force_changed": false,
"differs": true,
"renumbering_suspected": true,
"diff": {
"segments": [
{ "op": "delete", "text": "Le paiement" },
{ "op": "insert", "text": "Tout" },
{ "op": "equal", "text": " fait " }
],
"unified": "@@ -1 +1 @@\n-Le paiement fait de bonne foi a celui qui est en possession de la creance ...\n+Tout fait quelconque de l'homme, qui cause a autrui un dommage ...",
"added": 18,
"removed": 22
}
},
"provenance": {
"source": "legifrance-legi",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
Example - unchanged article (changed: false)
When from and to resolve to the same version, or the text is identical, the
diff is empty and the response is still a 200. “No change” is a successful
answer.
“Empty” is literal: segments is [], unified is "", added and
removed are 0. The endpoint never returns the article’s text as a single
equal segment - the consolidated text at a date is what
GET /legal/article sells. The version metadata
still tells you what you need: both dates resolve to the same version.
{
"data": {
"code": "code-civil",
"article": "1240",
"label": "Code civil, art. 1240",
"from": "2020-01-01",
"to": "2026-01-01",
"from_version": {
"version_id": "v-courant",
"etat": "vigueur",
"date_debut": "2016-10-01"
},
"to_version": {
"version_id": "v-courant",
"etat": "vigueur",
"date_debut": "2016-10-01"
},
"from_in_force": true,
"to_in_force": true,
"changed": false,
"status_changed": false,
"in_force_changed": false,
"differs": false,
"diff": {
"segments": [],
"unified": "",
"added": 0,
"removed": 0
}
},
"provenance": {
"source": "legifrance-legi",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
Example - from before the first known version (null side)
When from predates the article’s first version, the service treats the
from side as empty text. from_version is null and the diff shows a
total insertion.
{
"data": {
"code": "code-civil",
"article": "1240",
"label": "Code civil, art. 1240",
"from": "1800-01-01",
"to": "2026-01-01",
"from_version": null,
"to_version": {
"version_id": "v-courant",
"etat": "vigueur",
"date_debut": "2016-10-01"
},
"from_in_force": false,
"to_in_force": true,
"changed": true,
"status_changed": true,
"in_force_changed": true,
"differs": true,
"diff": {
"segments": [
{ "op": "insert", "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." }
],
"unified": "@@ -0,0 +1 @@\n+Tout fait quelconque ...",
"added": 123,
"removed": 0
}
},
"provenance": {
"source": "legifrance-legi",
"fetched_at": "2026-06-20T12:00:00Z",
"freshness": { "kind": "snapshot", "as_of": "2026-06-01T00:00:00Z" }
}
}
Coverage window
coverage_window reports the period the text or act can actually be read over
(from inclusive, to exclusive, null when open). It matters here: a bound
outside the window is diffed as empty text, which would otherwise read as
“the article was created (or deleted) on that date”. Historical depth varies per
text — the e-Commerce Directive, in force since 2000, is only covered from
2024-02-17. See /legal/article.
Coverage honesty
The service answers from a local snapshot of the LEGI and EUR-Lex corpora. It
does not fetch sources at request time, search by keyword, infer missing
versions or interpret the legal significance of changes. The diff is computed
purely on the consolidated text as stored; etat is carried as-is from the
source store.
provenance.freshness.as_of tells you which dump backed both resolutions. An
amendment that postdates the snapshot will not appear until the store is
refreshed.
Errors
Only requests the service cannot answer leave the 200 range.
| Status | code | Case |
|---|---|---|
| 400 | INVALID_REF | No usable identity: neither French code+article nor EU celex/eli |
| 400 | INVALID_DATE | from or to is present but not strictly formatted as YYYY-MM-DD |
| 400 | INVALID_RANGE | from is after to (inverted interval) |
| 400 | MISSING_PARAMETER | from is absent |
| 404 | UNKNOWN_ARTICLE | The reference is well formed but no matching article exists in the store |
| 404 | UNKNOWN_ACT | A bare EU act identifier is well formed but no matching act exists |
| 500 | INTERNAL | Internal error (detail logged, not exposed) |
{ "error": "query parameter `code`+`article` or `celex`/`eli` is required", "code": "INVALID_REF" }
{ "error": "missing or empty required parameter: from", "code": "MISSING_PARAMETER" }
{ "error": "invalid `date` `2026-7-5`; expected ISO calendar format YYYY-MM-DD", "code": "INVALID_DATE" }
{ "error": "`from` (2026-07-25) must not be after `to` (2000-01-01)", "code": "INVALID_RANGE" }
{ "error": "no article `9999` found for text `code-civil` in the store", "code": "UNKNOWN_ARTICLE" }
{ "error": "no act `39999R9999` found in the store", "code": "UNKNOWN_ACT" }
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/history- the full version timeline of an article (when it changed, not what changed).GET /legal/article- full consolidated text of a French article at a date.GET /legal/eu-act- full consolidated text of an EU act or article at a date.- For agents - discovery surfaces, the live
/catalogand how settlement works.