# Ldata.fr > French open **legal data** API for developers and AI agents — **fourteen corpora**: > judicial case-law, LEGI codes/TNC, EU law (EUR-Lex/CJUE/ECHR), administrative > case-law (CE/CAA/TA), collective agreements (IDCC), tax doctrine (BOFiP), > Conseil constitutionnel, independent authorities (ADLC/CNIL), circulaires, > Journal officiel (JORF), legislative dossiers (DOLE), company agreements (ACCO), > commercial notices (BODACC), public procurement (BOAMP). > One auth scheme, one MCP server, 38 tools. Licence Ouverte 2.0 (DILA) plus > per-corpus terms where noted (EUR-Lex, HUDOC, ADLC — see each section below). ## Start here | Resource | URL | |----------|-----| | This index | `/llms.txt` | | Full agent manual | `/llms-full.txt` | | **System prompt pack** | `/agent-system.txt` | | Function-calling tools | `/api-tools.json` | | **MCP server** (Streamable HTTP) | `POST /mcp` | | OpenAPI | `/openapi.yaml` | | Human docs | `/docs` · Swagger `/swagger` | | HTML browse | `/browse` | ## Critical rules 1. **Find ≠ cite.** Never cite in a pleading from this API alone — re-check Judilibre/Légifrance (`verify_url` / `verify_note`). 2. **Never invent** numbers, dates, courts, or quotations. Use API fields only. 3. Always show **`citation`** + verify line for every decision you mention. 4. `possibly_reversed: true` = **high-confidence** reverse signal only (unique city+date peer or RG match). Ambiguous city+date collisions are `reversal_signal.confidence=low` and stay `possibly_reversed=false`. Never assert a specific RG was cassé — re-check Judilibre. Filter `reversed=true` uses the same high-confidence rule. 5. `GET /decisions/:id` includes `cites_pourvois` (pourvoi → 0..N CC, `resolved` false if missing), materialized `cites_edges` / `cited_by`, and `graph.reversed_by` (CA) / `graph.reverses_ca` (CC, high-confidence only). Graph fields are find-aids — re-check Judilibre before citing. 5. **No field aliases:** use `decision_date`, `instrument_slug`, `legi_status`, `in_force_on_as_of`, caselaw **`visa`**. ## Auth ``` Authorization: Bearer oj_ # or: X-Api-Key: oj_ ``` **Local / agent setup (one command):** ```bash bin/rails agent:dev_key # prints oj_… once → export LDATA_API_KEY=... ``` Web: register → confirm the email → subscribe (Pro or VIP) → Dashboard → New key. No free tier; without a subscription the API answers 402 `subscription_required`. ## Base URL ``` {origin}/api ``` ## MCP (same tools, zero setup for MCP clients) Endpoint: `POST {origin}/mcp` — Streamable HTTP, stateless, JSON responses. Production: `https://mcp.ldata.fr` (MCP at the root) or `https://ldata.fr/mcp`. `tools/list` is a **pack** (`core` 11 · `public` 30 · `full` 42). Default `core` = `search` + `fetch` over every corpus, read-only SQL (`get_sql_schema`, `sql_query`), plus caselaw / LEGI precision tools (filters, article as of a date, version history), from `config/corpus.yml`; override with `?pack=full`. `tools/call` still dispatches any tool in `/api-tools.json`. Auth and quotas match plain HTTP. ```bash claude mcp add --transport http ldata {origin}/mcp \ --header "Authorization: Bearer oj_…" # or single URL (MCP only): "{origin}/mcp?api_key=oj_…" ``` Every tool carries MCP `annotations` (`readOnlyHint: true`): nothing writes. Unauthenticated tools (`health_check`, `list_codes`, `get_instrument`) work without a key; the rest return a JSON error explaining how to get one. ## Quota budget (1 authenticated request = 1 day unit) | Plan | day | rpm | max hits/req | |------|-----|-----|--------------| | pro (25 €/month) | 500 | 30 | 50 | | vip (75 €/month) | 2000 | 60 | 50 | | internal | 100k | 300 | 50 | **Per user turn:** ≤5 searches, ≤3 `get_decision` (prefer truncated text), check `GET /me` near limit. Do not spam `/health`. ## Tools (`/api-tools.json` v2) `search` · `fetch` · `get_sql_schema` · `sql_query` · `health_check` · `search_caselaw` · `get_decision` · `search_caselaw_by_article` · `list_codes` · `get_instrument` · `list_code_articles` · `get_statute_article` · `get_article_versions` · `search_legislation` · `list_statutes` · `get_statute` · `search_statutes` · `search_eu_law_and_caselaw` · `get_eu_document` · `search_admin_caselaw` · `get_admin_decision` · `search_collective_agreements` · `get_collective_agreement_document` · `search_tax_doctrine` · `get_tax_doctrine_document` · `search_constitutional_caselaw` · `get_constitutional_decision` · `search_authority_decisions` · `get_authority_document` · `search_circulars` · `get_circular_document` · `search_official_journal` · `get_official_journal_document` · `search_commercial_notices` · `get_commercial_notice` · `search_public_procurement` · `get_public_procurement_notice` · `get_quota` ## Playbooks ### 0. Not sure which corpus? Search them all 1. `GET /find?q="pension alimentaire"` (one quota unit, 3 hits per corpus; `corpora=` to narrow) 2. `GET /fetch/{id}` on the best hits (`id` = `:`, e.g. `codes:code-civil/373-2-2?as_of=2010-06-01`) 3. Switch to the corpus-specific tool when you need its filters (jurisdiction, dates, IDCC…) ### 0b. Counts, lists, joins → read-only SQL 1. `GET /sql/schema` (databases) then `GET /sql/schema?database=legis` (tables, columns, FTS tables) 2. `POST /sql` `{"database":"legis","sql":"SELECT num, date_debut, date_fin, etat FROM articles WHERE instrument_slug = ? AND num = ? ORDER BY date_debut","bind":["code-civil","373-2-2"]}` 3. One SELECT per request; ATTACH, writes and pragmas are refused; stops after 60 s; pages of `max_rows` (default 100, max 1000) — follow `next_offset` with a stable `ORDER BY`, but aggregate in SQL rather than paging raw rows 4. Over the time budget → 422 `sql_limit_exceeded` (charged): add LIMIT, use the FTS table or an indexed column ### A. Topic research (civil / family) 1. `GET /health` 2. `GET /search?q="pension alimentaire"&jurisdiction=ca&after=2022-01-01&syn=true&limit=8` 3. `GET /decisions/{id}` on 2–3 hits (`full` omit) 4. Show citation + verify_note + snippet paraphrase ### B. Exact case by number 1. `GET /search?number=17/07602&location=Versailles` (no RG/n° prefix) 2. If clear hit → `GET /decisions/{id}` ### C. Law as of a date 1. `GET /codes/code-civil/articles/373-2-2?as_of=2010-06-01` 2. Read `vigueur.legi_status` + `vigueur.in_force_on_as_of` 3. Optional: `…/versions` for timeline ### C2. Consolidated law / decree (not a code) 1. `GET /statutes/search?q=délégations+de+signature&nature=DECRET` 2. `GET /statutes/{slug_or_nor}` then `…/articles/{num}` 3. `verify_url` is Légifrance LODA — re-check before citing ### D. Visa → case-law (not statute text) 1. `GET /caselaw/by-article/373-2-2?jurisdiction=cc` 2. Or `GET /search?article=L132-1` 3. Code text is under `/codes/.../articles/...`. Consolidated laws/decrees are under `/statutes/...`. ### E. Empty results Drop filters → simplify FTS → `syn=true` → broaden dates → tell user no open-corpus hit (do not invent). ## Endpoints (quick) | Method | Path | Auth | Purpose | |--------|------|------|---------| | GET | /api/health | no | Corpus OK / age / counts | | GET | /api/find | yes | One FTS query over every corpus (1 quota unit); ids `:` | | GET | /api/fetch/{corpus}:{id} | yes | Open a /find hit: text + metadata + official url (`as_of` for LEGI) | | GET | /api/sql/schema | yes | Corpus databases, or tables/columns/FTS tables of `database=` | | POST/GET | /api/sql | yes | One read-only SQLite SELECT on one corpus database (`database`, `sql`, `bind[]`, `max_rows`) | | GET | /api/search | yes | Case-law search (`mode=fts\|semantic\|hybrid`; hybrid = FTS+vector RRF, best for plain language) | | GET | /api/decisions/{id} | yes | One decision | | GET | /api/caselaw/by-article/{visa} | yes | Case-law citing visa | | GET | /api/codes | no | Code catalogue | | GET | /api/codes/{slug} | no | One instrument | | GET | /api/codes/{slug}/articles | yes | TOC | | GET | /api/codes/{slug}/articles/{num} | yes | Statute text (+ as_of) | | GET | /api/codes/{slug}/articles/{num}/versions | yes | Version graph | | GET | /api/codes/search | yes | Legislation FTS | | GET | /api/statutes | no | TNC catalogue (lois/décrets, never codes) | | GET | /api/statutes/{id} | no | One TNC (slug, cid, or NOR) | | GET | /api/statutes/{id}/articles | yes | TNC TOC | | GET | /api/statutes/{id}/articles/{num} | yes | TNC article (+ as_of) | | GET | /api/statutes/search | yes | TNC FTS (LODA-equivalent) | | GET | /api/eu/search | yes | EU corpus FTS (EUR-Lex + CJEU + ECHR) | | GET | /api/eu/documents/{id} | yes | One EU document (id, CELEX or ECLI) | | GET | /api/admin-caselaw/search | yes | Administrative case-law FTS (CE + CAA + TA) | | GET | /api/admin-caselaw/decisions/{id} | yes | One administrative decision (CETATEXT id) | | GET | /api/collective-agreements/search | yes | Collective bargaining agreements FTS (IDCC) | | GET | /api/collective-agreements/documents/{id} | yes | One convention/texte/article (KALI id) | | GET | /api/tax-doctrine/search | yes | Tax doctrine FTS (BOFiP, `as_of` for opposable version) | | GET | /api/tax-doctrine/documents/{id} | yes | One BOFiP document (version/BOI id, `as_of`) | | GET | /api/constitutional/search | yes | Conseil constitutionnel decisions FTS (DC/QPC/LP/AN/SEN) | | GET | /api/constitutional/decisions/{id} | yes | One Conseil constitutionnel decision (CONSTEXT id) | | GET | /api/authorities/search | yes | Independent authority documents FTS (ADLC + CNIL + AMF OAM) | | GET | /api/authorities/documents/{id} | yes | One authority decision (ADLC or CNIL id) | | GET | /api/circulars/search | yes | Circulaires et instructions FTS (full text where a PDF was extracted, else metadata) | | GET | /api/circulars/documents/{id} | yes | One circular (ID_CIRCULAIRE), full text or metadata per `full_text` | | GET | /api/official-journal/search | yes | Journal officiel FTS (DILA JORF as published) | | GET | /api/official-journal/documents/{id} | yes | One JORFTEXT | | GET | /api/legislative-dossiers/search | yes | Legislative dossiers FTS (DILA DOLE, metadata) | | GET | /api/legislative-dossiers/documents/{id} | yes | One JORFDOLE | | GET | /api/company-agreements/search | yes | Company agreements FTS (DILA ACCO; full_text=true if office body unzipped) | | GET | /api/company-agreements/documents/{id} | yes | One ACCOTEXT | | GET | /api/commercial-notices/search | yes | Commercial notices FTS (DILA BODACC) | | GET | /api/commercial-notices/notices/{id} | yes | One BODACC notice (nojo or public_id) | | GET | /api/public-procurement/search | yes | Public procurement FTS (DILA BOAMP) | | GET | /api/public-procurement/notices/{id} | yes | One BOAMP notice (IDWEB) | | GET | /api/me | yes | Quota | ## EU corpus (EUR-Lex + CJEU + ECHR) - `GET /api/eu/search?q=…` — one FTS over three sources; filters `source=eurlex_law|cjeu|echr`, `after`/`before` on the **document date**. - `GET /api/eu/documents/{id}` — full text by id, CELEX number or ECLI (`full=true` lifts the 50k-char cap). - Coverage: EU legislation (EUR-Lex, consolidated versions, French) + CJEU case-law + ECHR (HUDOC) judgments. - Find ≠ cite here too: every hit carries `url` — re-verify on EUR-Lex / HUDOC before citing. - Licences: French data Licence Ouverte 2.0; EUR-Lex © European Union, reuse allowed (Decision 2011/833/EU); HUDOC © Council of Europe. ## Administrative case-law (CE + CAA + TA) - `GET /api/admin-caselaw/search?q=…` — FTS over French administrative case-law; filters `source=ce|caa|ta|autre`, `after`/`before` on the **decision date**. - `GET /api/admin-caselaw/decisions/{id}` — full text by CETATEXT id, or `OJA_...` id for the TA backfill below (`full=true` lifts the 50k-char cap). - Coverage: DILA JADE fund — Conseil d'État, cours administratives d'appel, tribunaux administratifs. JADE's TA versement is effectively empty (~200 decisions); tribunaux administratifs are backfilled from the Conseil d'État's own open data platform (opendata.justice-administrative.fr, TA coverage since 2022-06-30) — verify links there differ (`url` points to that platform, not Légifrance, for backfilled TA hits). - Find ≠ cite here too: every hit carries `url` — re-verify on Légifrance (JADE) or opendata.justice-administrative.fr (TA backfill) before citing; never invent decision numbers. - Licence: Licence Ouverte 2.0 (DILA JADE open data). ## Collective agreements (IDCC conventions) - `GET /api/collective-agreements/search?q=…` — FTS over conventions collectives, textes attachés (avenants/accords) and articles; filters `idcc` (agreement number), `doc_type=convention|texte|article`, `after`/`before` on `date_start`. - `GET /api/collective-agreements/documents/{id}` — full text by DILA KALI id (`full=true` lifts the 50k-char cap). Only `article` documents carry body text; `convention`/`texte` return their indexed titles. - Coverage: DILA KALI fund — French collective bargaining agreements (conventions collectives nationales, IDCC). - Find ≠ cite here too: every hit carries `url` — re-verify on Légifrance before citing; never invent article numbers. - Licence: Licence Ouverte 2.0 (DILA KALI open data). ## Tax doctrine (BOFiP) - `GET /api/tax-doctrine/search?q=…` — FTS over BOFiP-Impôts doctrine; filters `doc_type` (source-driven, no fixed enum), `serie` (e.g. IF, TVA, IR, RFPI), `as_of=YYYY-MM-DD` restricts to the version opposable that day; `after` / `before` filter on document `date` (inclusive / exclusive). - `GET /api/tax-doctrine/documents/{id}` — full text by version id, BOI-… identifiant or node id, optional `as_of` (default latest version; `full=true` lifts the 50k-char cap). - Coverage: BOFiP-Impôts (DGFiP open data) — versioned, opposable administrative tax doctrine (`date_start`/`date_end` validity windows). - Find ≠ cite here too: every hit carries `url` — re-verify on bofip.impots.gouv.fr before citing; never invent BOI references. - Licence: Licence Ouverte 2.0 (DGFiP open data). ## Conseil constitutionnel (DC, QPC, LP, AN, SEN, …) - `GET /api/constitutional/search?q=…` — FTS over Conseil constitutionnel decisions; filters `decision_type=dc|qpc|lp|an|sen|autre`, `after`/`before` on the **decision date**. - `GET /api/constitutional/decisions/{id}` — full text by CONSTEXT id (`full=true` lifts the 50k-char cap). - Coverage: DILA CONSTIT fund — contrôle de constitutionnalité (DC), questions prioritaires de constitutionnalité (QPC), lois de pays (LP), contentieux électoral (AN/SEN), and other contentieux (présidentielle, référendum, incompatibilités, déchéance, article 16, …) since 1958. - Find ≠ cite here too: every hit carries `url` — re-verify on conseil-constitutionnel.fr before citing; never invent decision numbers. - Licence: Licence Ouverte 2.0 (DILA CONSTIT open data). ## Independent administrative authorities (ADLC, CNIL) - `GET /api/authorities/search?q=…` — FTS over ADLC + CNIL + AMF OAM; filters `authority=adlc|cnil|amf`, `doc_type` (source-driven, no fixed enum), `after`/`before` on the **document date**. AMF is listed-issuer regulated information (metadata + PDF url), not AMF sanctions. - `GET /api/authorities/documents/{id}` — full text by document id (CNILTEXT id for CNIL, decision number e.g. `26-DCC-149` for ADLC; `full=true` lifts the 50k-char cap). - Coverage: **ADLC** (Autorité de la concurrence) — every published decision since 1988 (`doc_type`: `d` contentious decisions, `a` avis, `mc` mesures conservatoires, `dcc` merger control, plus rare `dex`/`soa`), sourced from its official data.gouv.fr dataset (weekly refresh, no DILA fund exists for ADLC). **CNIL** — deliberations/decisions since ~1978 (`doc_type` derived from NATURE_DELIB: `sanction`, `mise_en_demeure`, `autorisation_transfert`, `autorisation_recherche`, `avis`, `recommandation_lignes_directrices`, …), sourced from the DILA CNIL fund. - Find ≠ cite here too: every hit carries `url` — re-verify on autoritedelaconcurrence.fr (ADLC) or legifrance.gouv.fr (CNIL) before citing; never invent decision numbers. - Licence: Licence Ouverte 2.0 (ADLC data.gouv.fr dataset; DILA CNIL open data). ## Circulaires et instructions des administrations - `GET /api/circulars/search?q=…` — FTS over circulaires; filters `doc_type=organisation_services|directives_ministre|interpretation_juridique|autre`, `after`/`before` on the **date de signature**. - `GET /api/circulars/documents/{id}` — full record by `ID_CIRCULAIRE` (`full=true` lifts the 50k-char cap). - **Full text where a PDF exists**: circulaires are published as PDF; the XML carries only metadata. The PDF body is extracted with `pdftotext` at index time and folded into `snippet`/`text` alongside title + résumé + mots-clés + destinataire. Each hit/document carries `full_text: true|false` — `true` means `text` is the actual body of the circulaire; `false` means metadata only (the 2009-2014 legacy bulk export, XML-only with no PDF published, or a document whose PDF was missing/failed to extract). `url` always links the official PDF (circulaires.legifrance.gouv.fr) — the document of record regardless of `full_text`. - Coverage: DILA CIRCULAIRES fund — legacy 2009-2014 bulk export (metadata-only) plus per-drop increments since 2014 (~4,000+ tarballs, PDF + XML, full text). Archived circulaires (`ETAT=A`, DILA: "must be withdrawn from distribution") are excluded from the index. - Find ≠ cite here too: every hit carries `url` — re-verify on the official PDF before citing; never invent NOR numbers. - Licence: Licence Ouverte 2.0 (DILA CIRCULAIRES open data). ## Commercial notices (DILA BODACC) - `GET /api/commercial-notices/search?q=…` — FTS; filters `edition=a|b|c`, `doc_type` (rcs_a/rcs_b/pcl/bilan), `siren` (9 digits), `after`/`before`. - `GET /api/commercial-notices/notices/{id}` — by nojo or public_id (`edition+parution+numero`). - Publication events, not a live SIRENE register. Find ≠ cite: every hit carries `url` on bodacc.fr. ## Public procurement (DILA BOAMP) - `GET /api/public-procurement/search?q=…` — FTS; filters `doc_type` (free facet), `after`/`before`. - `GET /api/public-procurement/notices/{id}` — by IDWEB. - Find ≠ cite: every hit carries `url` on boamp.fr. ## Search cheat-sheet - Phrase: `q="pension alimentaire"` - Proximity: `q="avantage en nature" NEAR/20 pension` - Courts: `jurisdiction=cc|ca|tj|tcom` or `cc,ca` - Number: `number=21-14.490` exact after normalize - Dates: `after=` inclusive, `before=` exclusive - Sort: `authority` (default) \| `rank` \| `date` — prefer `rank` for deep pages; `authority`/`date` re-rank a top pool (offset+limit max 2000) - Pagination: `limit` + `offset` (offset max 1000). Hybrid ranks a fused pool of depth ≤ 200 (`meta.hybrid_depth`); deeper hybrid pages → 400, use `mode=fts` - `meta.mode` always set (`fts` \| `semantic` \| `hybrid`); semantic/hybrid may fall back to FTS with `meta.semantic_fallback` - Semantic corpus coverage is a **subset** of the full caselaw index — check `GET /api/health` → `embeddings.by_jurisdiction` (often Cassation-first while CA/TJ embed runs) - Validation `400` does not consume daily quota (refunded); `404` still does ## Field naming (explicit only) | Field | Meaning | |-------|---------| | `decision_date` | Day of the ruling | | `source_updated_on` | Judilibre snapshot freshness (if stored) | | `instrument_slug` | Code/TNC URL key | | `legi_status` | LEGI ETAT label | | `in_force_on_as_of` | Date window covers `as_of` | | `visa` | Article number in caselaw-by-article response | ## Errors | HTTP | Meaning | |------|---------| | 400 | Bad query | | 401 | Bad/missing key | | 404 | Unknown id / no version for as_of | | 429 | Rate limit | | 503 | Corpus degraded | ## Full specification See **/llms-full.txt** · tools **/api-tools.json** · system pack **/agent-system.txt**