Due diligence on a French company: five registers, one call

One SIREN, five public registers — Sirene, BODACC, VIES, Peppol, GLEIF — each block dated with its own provenance, and no risk score anywhere.

By Matthias Begot · · Updated

An agent about to invoice, pay or onboard a French company needs five facts that live in five different registers: is the legal unit real and still registered, has anything been gazetted against it, is its VAT number valid right now, can it receive an e-invoice, and which group consolidates it. GET /company/report answers all five for one SIREN in one settlement. The bundling is the convenience; the contract is something else — each block carries its own provenance and its own date, and the response never fuses them into a verdict or a score.

That distinction is the whole point. The five registers do not agree, do not refresh on the same clock, and are not equally authoritative on the same question. A report that flattened them into one green light would be lying about its own inputs.

”Active” is not the same as “solvent”

INSEE Sirene tells you whether a legal unit is registered and not closed. It does not tell you whether a commercial court has opened a liquidation. Those are two registers answering two different questions, and the gap between them is not a rounding error: the Banque de France counted 70,803 business failures in France over the twelve months to June 2026, in its release of 7 August 2026, up from roughly 70,117 a month earlier (via Reuters).

An agent that reads Sirene alone will read status: "active" on a company in judicial liquidation and conclude it is fine to invoice. The report exists so that both facts arrive in the same response, side by side, each pointing at the register that published it.

A capture from production, taken today

GET /company/report?siren=321875205 — SAN MARINA, a French shoe retailer whose liquidation was gazetted in 2023. Abridged from the real response, captured 2026-08-10:

{
  "data": {
    "siren": "321875205",
    "identity": {
      "name": "SAN MARINA", "status": "active",
      "provenance": { "source": "insee-sirene",
        "freshness": { "kind": "snapshot", "as_of": "2026-07-31T00:00:00Z" } }
    },
    "events": {
      "total": 36,
      "flags": { "has_open_insolvency_announcement": true, "is_deregistered": false,
                 "latest_event": { "family": "sale", "date": "2024-02-01" } },
      "provenance": { "source": "dila-bodacc",
        "freshness": { "kind": "snapshot", "as_of": "2026-08-02T00:00:00Z" } }
    },
    "vat": {
      "vat_number": "FR36321875205", "syntax_valid": true,
      "status": "unverifiable", "reason": "MS_MAX_CONCURRENT_REQ",
      "coverage": "syntax_only", "derived_from_siren": "321875205",
      "provenance": { "source": "pure-computation", "freshness": { "kind": "live" } }
    },
    "peppol": {
      "reachable": false, "participants": [],
      "provenance": { "source": "peppol-directory",
        "freshness": { "kind": "snapshot", "as_of": "2026-08-04T00:00:00Z" } }
    },
    "lei": {
      "lei": "969500HF9MVOFPWK0A17", "legal_name": "SAN MARINA",
      "entity_status": "ACTIVE", "registration_status": "LAPSED",
      "ultimate_parent": { "status": "reporting_exception", "link": null,
        "exception": { "category": "ULTIMATE_ACCOUNTING_CONSOLIDATION_PARENT",
                       "reasons": ["NO_LEI"] } },
      "provenance": { "source": "gleif",
        "freshness": { "kind": "snapshot", "as_of": "2026-08-09T00:00:00Z" } }
    },
    "summary": {
      "status": "active",
      "has_open_insolvency_announcement": true,
      "vat_status": "unverifiable",
      "peppol_reachable": false,
      "lei": "969500HF9MVOFPWK0A17"
    },
    "coverage": { "complete": true }
  }
}

Read the dates before you read the facts. Four different snapshot dates and one live check, in a single response: Sirene as of 31 July, BODACC as of 2 August, Peppol as of 4 August, GLEIF as of 9 August, VIES attempted at call time. Nothing here is smoothed under a common fetched_at, because nothing here was actually established on the same day.

And then read the contradiction the report refuses to resolve: Sirene says active, BODACC says the insolvency proceeding is open. Both are true statements about their own register. The report states both and lets the caller decide, rather than adjudicating which register is right.

Provenance per block is the contract

BlockRegisterFreshnessThe question it answers
identityINSEE Sirenemonth-end snapshotDoes this legal unit exist, and is it registered as active?
eventsBODACC / DILAextraction snapshotWhat has been gazetted — insolvency, deregistration, sales?
vatVIES (European Commission)liveIs this VAT number valid right now?
peppolpublic Peppol Directory exportdated exportIs this company listed as reachable on Peppol?
leiGLEIF Golden Copydated imageWhat is the global identity, and who consolidates it?

The root provenance of the response mirrors the identity foundation only. That is deliberate: there is no single date at which this report was true, so the endpoint does not invent one. Every block’s shape is exactly the shape of its unit endpoint — /company/resolve, /company/events, /company/vat, /company/peppol, /company/lei — so the field-level documentation of a block is the documentation of that endpoint.

The VAT block was throttled, and that is still a 200

Look again at the vat block above. The live VIES check did not answer: MS_MAX_CONCURRENT_REQ, the member state’s concurrency limit. What the endpoint does with that is the interesting part.

The block comes back status: "unverifiable" with the reason named, coverage: "syntax_only", and its provenance downgraded to pure-computation — the only thing actually established was the local syntax and check-digit computation, so that is the only thing claimed. summary propagates vat_status: "unverifiable". The other four blocks are intact and coverage.complete stays true, because the block was served, honestly degraded.

This is not an edge case to design around. VIES routes each query to the member state that owns the data, and each national database has its own outages and maintenance windows; “member state service unavailable” is a normal outcome of the system rather than an exception (Your Europe / VIES). The quotas are per member state, not per caller — a check can fail because the rest of Europe is querying that register at that second.

Two things follow, and an agent should branch on both:

  • unverifiable is not invalid. One is the absence of a verdict, the other is a verdict. Treating a throttle as a negative signal is how automated screening starts rejecting good counterparties.
  • unverifiable is not the only degraded shape. When the member state does not answer but a firm verdict on that exact number is already held from the last 24 hours, the block serves that verdict instead — status still valid or invalid, with served_from: "cache", cached_age_secs, and a freshness.kind of cached. That is a complete, dated answer, not a fabricated one. The full contract for both is on /company/vat.

A 5xx never appears in either path. Staleness is not an error.

There is no risk score, on purpose

summary is derived mechanically from the blocks that were served:

FieldFromValue
statusSireneactive / ceased
has_open_insolvency_announcementBODACCboolean — absent when undetermined
vat_statusVIESvalid / invalid / unverifiable
peppol_reachablePeppolboolean
leiGLEIFthe LEI of the canonical record attached to the SIREN

No rating, no recommendation, no number to threshold. And the insolvency flag is nullable: the gazette publishes announcements whose label settles nothing, so when the block was served but the state of the proceeding is undetermined, the field is simply absent rather than folded back to false. An unknown fact never becomes a negative one. When it matters to your logic, read the events block and its timeline, not the summary line.

If what you want is a decision rather than facts, GET /preflight/supplier is the endpoint that returns one — ok, review, stop or insufficient_coverage, with sanctions screening of the company and of its ultimate parent. The two are complementary, not redundant: the report is for the audit trail and for workflows that apply their own policy; the pre-flight is for workflows that want a verdict they can branch on.

What the GLEIF block says, and the two things it does not

The capture above shows a company whose LEI record reads entity_status: "ACTIVE" and registration_status: "LAPSED". Those are different fields answering different questions, and conflating them is a real mistake.

LAPSED is about the record, not the company. It means the entity did not renew and re-certify its LEI registration by its next renewal date. GLEIF is explicit that “a lapsed LEI remains valid” and that lapsed “simply signals that an organization is behind schedule as regards renewal, i.e. re-validation of its information against third party sources” (GLEIF, on timely renewal). It is a statement about the freshness of a reference record — which is exactly the kind of thing this endpoint refuses to hide behind a summary field.

ultimate_parent is accounting consolidation, not beneficial ownership. GLEIF Level 2 records the entity’s direct and ultimate accounting consolidating parent — the entity that consolidates it in its financial statements — self-declared by the entity itself, with no ownership percentage published (GLEIF, Level 2 Data: Who Owns Whom). It is an accounting definition of control, not an AML one. The response repeats this in data.limits on every single call rather than burying it in documentation, and the absence of a declared parent is never evidence of an absence of control. For the full chain — direct parent, ultimate parent, subsidiaries — see /company/ownership.

Note also the shape of the “no parent” case here: status: "reporting_exception" with the official category and reason NO_LEI — the parent exists but has no LEI, so GLEIF cannot publish the link. That is an answer, dated and sourced, not an empty block. The same holds one level down: a company with no LEI at all comes back with total_matches: 0, which is the answer “this company has no LEI” — the situation of the large majority of French companies — and not an unavailability.

Peppol reachability is a directory fact, not French e-invoicing readiness

The timing makes this worth stating precisely. From 1 September 2026, French in-scope businesses must use an approved platform (plateforme agréée) to transmit and receive electronic invoices and to report transaction and payment data to the administration; issuance obligations are phased, with large and mid-sized enterprises in 2026 and SMEs on 1 September 2027 (impots.gouv.fr). Any agent onboarding French suppliers this quarter is going to be asked whether a counterparty can receive an invoice electronically.

The peppol block answers a narrower question than that, and says so. It reports whether the company appears in a dated export of the public Peppol Directory, with its participant identifiers and declared document types. It is:

  • not a live SMP lookup — the answer reflects the export date carried in coverage.directory_as_of;
  • not a statement about the French reform — being absent from the Peppol Directory is not a finding about a company’s registration with an approved platform, and reachable: false is a dated directory fact, nothing more.

reachable: false on SAN MARINA above means the company is not listed in that export. It is a paid, dated, honest answer — and it is not a verdict about anything else.

include= gives you a fully offline report

VIES is the service’s only outbound call. Drop it and the whole report is served from local snapshots:

GET /company/report?siren=321875205&include=identity,events,peppol
{
  "summary": { "status": "active", "has_open_insolvency_announcement": true, "peppol_reachable": false },
  "coverage": { "complete": false, "excluded": ["vat", "lei"] }
}

That request returns in milliseconds instead of the few hundred a live VIES check costs, and — this is the part that matters for an agent — coverage.excluded names what is missing. summary.vat_status is absent because the block was excluded, and the response says so explicitly. There is no silent absence anywhere in this endpoint: a block is complete, excluded by include=, or listed in coverage.unavailable. identity is always served, as the base.

Billing: an empty report is never paid for

The x402 golden rule is that the agent pays for the answer to its question, so the mapping is strict in both directions:

CaseResult
Known SIREN200, billed — including a degraded block or an include= subset
Well-formed SIREN unknown to Sirene404 NOT_FOUND, not billed — no empty report is ever served
Missing / malformed siren, failed Luhn check, unknown include= value400, not billed

A counterparty in liquidation is a useful answer and is billed — the value of the call is the finding, not the reassurance. An identifier no register knows is not an answer, and the gateway does not settle it. That 404 is the deliberate difference with the unit endpoints, where an absence — no gazette entry, not on Peppol — is itself a legitimate paid 200. A report with no identity has no object.

Wiring it up

  1. Anchor on the SIREN. This route takes siren only — no siret=, no vat=, no search by name.
  2. Choose your blocks. Omit include= for all five; pass a subset when you only need part of the picture, or when you want the zero-network path.
  3. Branch on blocks, not on the summary, wherever the distinction between “false” and “undetermined” changes your decision.
  4. Persist the whole response, not a boolean. Six months later, “why did the agent invoice this company” is answerable from the stored blocks and their as_of dates. That is the reason provenance is per block.
  5. Discover, then pay. The usual x402 cycle — discover → 402 → pay → replay — is walked with runnable snippets in the Quickstart, and the discovery surfaces are described in For agents.

Every amount and every accepted rail lives in the gateway’s /catalog, which is the single source of truth; no price appears in this article, and the unit endpoints are listed there too if you would rather orchestrate them yourself.

What it will not do

  • No search by name, no SIRET, no VAT-number entry on this route. The key is the SIREN.
  • No credit opinion, no risk score, no financial statements, no directors.
  • No reconciliation between registers. Sirene active alongside an open BODACC liquidation is reported as exactly that — two sourced facts, not a judgment about which one wins.
  • No beneficial ownership. The control chain is self-declared accounting consolidation, and data.limits says so on every response.
  • French companies only. The Sirene identity is the base of the report.

One caveat on the capture above: it is a snapshot-dated illustration, not a contract. A newly gazetted announcement moves latest_event; a VIES retry a minute later may well succeed. Only the derivation rules — provenance per block, undetermined never folded to false, absence always named — are guaranteed. The parameter contract, the full block shapes and the error codes are on the GET /company/report documentation.