Check a French company for insolvency: its BODACC timeline

One paid call returns a French company's BODACC timeline — insolvency judgments, deregistrations, business sales — with factual flags, and a flag it refuses to guess.

By Matthias Begot · · Updated

An agent about to invoice a customer on 30-day terms, release a first order or onboard a supplier is making a credit decision. The question it can actually answer from an official source is narrow and useful: has anything been published in the French legal gazette about this company? GET /company/events answers it by SIREN — a dated timeline of insolvency judgments, deregistrations and business sales drawn from the BODACC, plus three flags derived mechanically from that timeline. No account, no key, no network call at request time.

Two properties decide whether an integration reads it correctly. An empty list is a paid answer — the most common one, in fact — and the insolvency flag is nullable, because when the published label does not qualify what happened, the service says so instead of guessing. Everything below was captured against production on 11 August 2026, against the BODACC snapshot dated 2026-08-09.

The volume behind the question is not theoretical: INSEE, republishing Banque de France data, counted 6,139 business failures in May 2026 alone (Insee, Défaillances d’entreprises, published 21 July 2026). Roughly six thousand counterparties a month stop being counterparties.

The timeline, captured

GET /company/events?siren=321875205&family=insolvency — a retail chain whose receivership was converted into judicial liquidation:

{
  "data": {
    "siren": "321875205",
    "company": { "name": "SAN MARINA", "natural_person": null, "status": "active" },
    "events": [
      {
        "family": "insolvency",
        "nature": "Dépôt de l'état des créances",
        "effective_on": "2023-08-25",
        "published_on": "2023-09-05",
        "gazette_issue": "20230170",
        "notice_number": 2594,
        "court": "Greffe du Tribunal de Commerce de Marseille",
        "text": "L'état des créances est déposé au greffe où tout intéressé peut présenter réclamation…"
      },
      {
        "family": "insolvency",
        "nature": "Jugement de conversion en liquidation judiciaire",
        "effective_on": "2023-02-20",
        "published_on": "2023-02-23",
        "gazette_issue": "20230038",
        "notice_number": 1758,
        "court": "Greffe du Tribunal de Commerce de Marseille",
        "text": "Jugement prononçant la liquidation judiciaire désignant liquidateur…"
      },
      {
        "family": "insolvency",
        "nature": "Jugement d'ouverture d'une procédure de redressement judiciaire",
        "effective_on": "2022-09-22",
        "published_on": "2022-09-27",
        "gazette_issue": "20220187",
        "notice_number": 3494,
        "court": "Greffe du Tribunal de Commerce de Marseille",
        "text": "Jugement prononçant l'ouverture d'une procédure de redressement judiciaire…"
      }
    ],
    "total": 3,
    "truncated": false,
    "flags": {
      "has_open_insolvency_announcement": true,
      "is_deregistered": false,
      "latest_event": { "family": "sale", "date": "2024-02-01" }
    },
    "coverage": { "window": { "from": "2008-01-01", "to": "2026-08-09" } }
  },
  "provenance": {
    "source": "dila-bodacc",
    "fetched_at": "2026-08-11T15:31:00Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-08-09T00:00:00Z" }
  }
}

Every announcement carries its gazette issue, notice number and court — the coordinates a human can use to pull the original notice. The endpoint serves the published fact, not a summary of it.

Note the relationship between the view and the flags. family=insolvency narrowed events to three, and total: 3 honestly counts that view — but this SIREN has 36 announcements in full, and latest_event points at one of the 33 not shown: a business sale gazetted in February 2024. The filter changes what the agent looks at, never the facts the flags are derived from. An agent that filters and then reads flags is reading the complete timeline either way.

Flags are facts, not a score

Three flags, each a mechanical derivation over the complete timeline:

FlagWhat it states
has_open_insolvency_announcementtrue / false / null — read on the most recent insolvency announcement that qualifies the state of the proceeding
is_deregisteredA deregistration has been published in the gazette
latest_event{ family, date } of the announcement with the most recent effect date, across all families; null when the timeline is empty

true is a fact about the gazette, not a solvency opinion. There is no score, no rating, no probability of default — those would be an inference wearing an official register’s authority, which is the line this endpoint does not cross.

The third state exists because the labels do not always answer

GET /company/events?siren=662042449&family=insolvency returns one announcement, filed under the gazette’s collective-proceedings rubric:

"events": [
  {
    "family": "insolvency",
    "nature": "Autre jugement prononçant",
    "effective_on": "2026-04-07",
    "published_on": "2026-04-16",
    "court": "Greffe du Tribunal de Commerce de Draguignan",
    "text": "Jugement d'homologation de l'accord intervenu dans la procédure de conciliation…"
  }
],
"flags": {
  "has_open_insolvency_announcement": null,
  "is_deregistered": false,
  "latest_event": { "family": "insolvency", "date": "2026-04-07" }
},
"coverage": {
  "window": { "from": "2008-01-01", "to": "2026-08-09" },
  "insolvency_flag": "undetermined_latest_notice"
}

The SIREN is 662042449 — BNP Paribas. Under a default rule of “anything filed under the insolvency rubric that is not a closure is an opening”, a systemically important bank would have been flagged as being in collective proceedings. What the text actually shows is the homologation of a conciliation agreement: a preventive, amicable procedure that opens no collective proceeding, and arguably a resolution signal rather than a distress one.

So the derivation runs off an explicit, closed table of gazette labels — one list of closures, one list of collective acts, everything else undetermined — and the flag comes back null with coverage.insolvency_flag naming the reason. "Autre jugement prononçant" is the catch-all DILA files under when nothing else fits.

Two consequences for the code that consumes this:

  • if (flags.has_open_insolvency_announcement) reads null as “nothing to report” — precisely the reading the third state exists to prevent. Branch on the three states explicitly.
  • A non-qualifying announcement does not extinguish the ones before it. In the SAN MARINA capture, the most recent insolvency notice is a registry filing that qualifies nothing; the derivation walks back to the conversion judgment underneath, and the flag stays true.

The refusal is to over-qualify, never to hide: the announcement is served in full — nature, court, dates, text — and the agent decides whether a human should look.

The most common answer is an empty list, and it is billed

To measure what an agent actually gets, we queried 1,200 random checksum-valid SIRENs against the production store on 11 August 2026:

OutcomeCount
404 — SIREN unknown to the Sirene register (not billed)796
200 — legal unit exists, no announcement over the window331
200 — legal unit exists, at least one announcement73

So of the 404 real legal units, 82 % had a completely clean gazette record. That is the answer the agent asked for, and it is only meaningful because coverage.window { from, to } is served on every response, empty or not: “nothing known between 2008-01-01 and 2026-08-09” is a statement; “nothing found” is not.

Among the 73 with a record, the families were 61 deregistrations, 33 insolvency announcements and 23 sales; 59 companies carried is_deregistered: true and 3 an open insolvency announcement. Deregistration — the counterparty no longer being on the register at all — is the single most frequent thing the gazette will tell you about a random French company. One of them, 811791888:

{
  "company": { "name": null, "natural_person": null, "status": "ceased" },
  "events": [
    { "family": "deregistration", "nature": "Radiation", "effective_on": "2024-02-27", "published_on": "2024-03-05", "court": "Greffe du Tribunal de Commerce de Montpellier", "text": null },
    { "family": "deregistration", "nature": "Radiation", "effective_on": "2023-06-21", "published_on": "2023-06-25", "court": "Greffe du Tribunal de Commerce de Montpellier", "text": null },
    { "family": "deregistration", "nature": null, "effective_on": null, "published_on": "2018-07-25", "court": "GREFFE DU TRIBUNAL DE COMMERCE DE MONTPELLIER", "text": null }
  ],
  "total": 3,
  "flags": { "has_open_insolvency_announcement": false, "is_deregistered": true, "latest_event": { "family": "deregistration", "date": "2024-02-27" } }
}

Two things to read here. The Sirene echo says status: "ceased", agreeing with the gazette — a cross-check available in the same payload, without a second call. And the 2018 announcement carries nature: null and effective_on: null, because the publication itself states neither. Across the 117 announcements in the sample, 49 carried no nature at all. A null there is the gazette’s silence, faithfully transmitted; the alternative — inferring a label from the surrounding fields — would be a home-made classifier served as an official fact.

Large timelines are capped, honestly. GET /company/events?siren=652014051 (Carrefour) returns total: 117 with truncated: true: the view is limited to the 100 most recent announcements, and total keeps the count from before the cap rather than reporting what fits in the payload.

”Unknown to the register” is not “known, without announcements”

These are different answers, and only one of them is billed. A well-formed SIREN that INSEE never assigned is a 404 NOT_FOUND, unbilled — the existence check runs against Sirene before the timeline is read, so a nonexistent key never reaches the billing path:

GET /company/events?siren=000000000  → 404 { "code": "NOT_FOUND", "error": "siren not found in the Sirene register" }
GET /company/events?siren=123456782  → 404 { "code": "NOT_FOUND", … }
GET /company/events?siren=321875206  → 400 { "code": "INVALID_CHECKSUM", "error": "siren failed its checksum (Luhn): likely a typo" }

The check matters more than it looks. 000000000 passes the Luhn test — its digits sum to zero — and it is the filler the BODACC export carries on lines whose SIREN field was empty, which makes it one of the most frequent “SIRENs” in the raw gazette, with hundreds of unrelated announcements attached to it. A misconfigured ERP, a truncated CSV or an empty spreadsheet cell produces exactly that input. Billing a timeline for it would return announcements that are individually real and collectively meaningless, with affirmative flags on top.

123456782 — the SIREN used as an example in manuals — is the same story. And the boundary stays narrow in the other direction: if the Sirene stock itself cannot be consulted, the timeline is served anyway with coverage.identity: "sirene_dataset_unavailable". Ignorance is never turned into a 404.

The blind spot to plan for: the gazette publishes late

This is the honest limit of the endpoint, and it is measurable. Across the sampled announcements that carry both dates, the gap between the effect date and the publication date had a median of 16 days, a 90th percentile of 79 days, and a maximum of 777 days.

An announcement enters the store only once DILA has published it. So “no insolvency announcement today” means nothing has been gazetted yet — a judgment handed down two weeks ago will typically not be there. For a credit decision, that is a bounded, declared blind spot rather than a hidden one, and it is the reason effective_on and published_on are two separate fields instead of one convenient “date”.

The same lag is why latest_event ranks by effect date and not by publication: in the SAN MARINA timeline, the most recently published announcement (May 2024) carries an effect date of May 2017 — a sale gazetted seven years after the fact. Ranking by publication would have made a 2017 event look like the company’s latest news.

Three more scope limits worth stating plainly:

  • No score, no prediction. The flags are derived facts. Nothing here rates a company or forecasts a default.
  • BODACC only, by SIREN. No search by name, no SIRET input — announcements are published per legal unit. The history ingested starts in 2008.
  • A snapshot, honestly dated. freshness.kind is snapshot and as_of carries the extraction date. A missed refresh means the last good snapshot keeps being served under its true date — staleness is never turned into a 5xx.

Wiring it up

  1. Branch on three states, not two. true, false and null for has_open_insolvency_announcement. Treat null as “a human reads the text”, never as “clear”.
  2. Read is_deregistered first for onboarding. It is the most frequent finding, and a deregistered counterparty is not one you invoice.
  3. Store provenance.freshness.as_of with the verdict. “On what basis did the agent extend credit on 11 August 2026” is answerable only if the snapshot date sits beside the decision.
  4. Keep the two dates apart. Age your decision on effective_on, plan for the publication lag on published_on.
  5. Distinguish 400 from 404. A checksum failure means fix the input; a NOT_FOUND means this key names no company — stop, do not retry.
  6. Discover, then pay. The 402 → pay → replay cycle is walked with runnable snippets in the Quickstart, and the machine discovery surfaces are described in For agents.

Where it sits in a counterparty check

The gazette answers what has legally happened. It does not answer who this is — that is /company/resolve, whose Sirene identity echo is already embedded in every events response — nor can I invoice them across borders (/company/vat), can they receive the e-invoice (/company/peppol), or who controls them (/company/ownership). If the agent wants all of it in a single settlement, GET /company/report bundles the blocks by SIREN with provenance per block, and /preflight/supplier turns that bundle into a one-call onboarding verdict.

Legal health is also not sanctions screening: that is /screen/entity for a name and /iban/screen for the account you are about to pay. A SIREN read off an inbound invoice comes from /invoice/read.

Every amount and every accepted settlement rail lives in the gateway’s /catalog, which is the single source of truth — no price appears in this article.

Announcement data is derived from the BODACC, published by DILA under the Licence Ouverte / Open Licence (Etalab 2.0); the identity echo comes from the INSEE Sirene register under the same licence.

The captures and the sample above are snapshot-dated illustrations, not a contract: a new gazette issue moves latest_event at any refresh. Only the rules are guaranteed — one SIREN per call, an empty timeline served as a paid answer over a stated window, a flag left null rather than guessed, and a key the register has never seen never billed. The full parameter and field contract is on the GET /company/events documentation.