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.

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" }
  ]
}
FieldTypeRequiredDescription
refsobject[]yesDiff requests to resolve, 1 to 50 entries

Each item is either a French reference or an EU reference, carrying its own date window:

FieldTypeRequiredDescription
codestringFRFrench text identifier, for example code-civil
articlestringFR onlyArticle number for code; optional for EU act-level lookups
celexstringEU idCELEX identifier, for example 32016R0679
elistringEU idELI identifier, used when celex is absent
fromstringyesStart date YYYY-MM-DD (date A - the older snapshot)
tostringnoEnd 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 to results.length.
  • results: one verdict per input item, in the same order as the input. Each verdict echoes the resolvable identifiers and its effective from / to. Covered items add the same fields as the data of GET /legal/diff: label, from_version, to_version, changed and the diff object (segments, unified, added, removed).
  • Unknown references remain item-level verdicts in the 200 response. They echo the identity and dates, set coverage.complete to false with a reason, 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: null or to_version: null, and the diff represents an insertion from nothing or a deletion to nothing (see the single endpoint’s boundary semantics).
  • A single provenance block covers the whole batch. When the batch touches both legal corpora, provenance.source names both; freshness.kind remains snapshot.

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.

StatuscodeCase
400INVALID_BODYBody missing or not JSON, not an object, or refs missing
400EMPTY_BATCHrefs is an empty array ([])
400BATCH_TOO_LARGEMore than 50 references in a single call
400INVALID_REFAn item has no usable identity, or a missing/malformed from or to date; error names refs[i]
500INTERNALInternal 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 /catalog and how settlement works.