Certify a supplier file: 200 SIRENs checked, signed, verifiable offline
An agent cleaning a French customer or supplier file before the e-invoicing switchover gets 200 sourced verdicts in one paid call — and a signed batch whose lines the buyer of the file can verify without trusting the agent that produced it.
By Matthias Begot · · Updated
An agent asked to clean a French customer or supplier file before the
e-invoicing switchover has two jobs, not one. The first is to check every line
against the official registers. The second is harder and usually skipped: to
hand the cleaned file to somebody else — a client, an auditor, the finance team
downstream — in a form they can check without trusting the agent that produced
it. POST /certify/batch does both in one paid
call: up to 200 lines keyed by SIREN or SIRET, one sourced verdict each, and an
Ed25519-signed Merkle root that makes any single line verifiable offline with a
published script.
Before anything else, the thing that kills this kind of product: a “certified file” certifies nothing about email deliverability, nothing about presence in the French e-invoicing directory (AIFE), and it never resolves a company name to a SIREN. It certifies existence, administrative state and gazetted legal events, as of dated snapshots. Everything below is inside that boundary.
Who is actually under pressure, and when
From 1 September 2027, small and micro enterprises in France must issue their invoices electronically. That is the wave worth building for. The larger cohort ahead of it is already tooled up and already has a platform vendor doing this work; the businesses hitting the 2027 date generally do not, and they are the ones sitting on a customer and supplier file that has never been checked against the register.
The failure mode is a valid SIREN belonging to somebody else
A SIREN is nine digits closed by a Luhn check, and the checksum is good at its
job: it catches every single-digit substitution and every adjacent
transposition except 09↔90. Which is precisely the problem. The errors it
catches never reach your file. The ones that survive in it are the ones no
offline validator can catch, because the result is a perfectly valid SIREN:
- a value copied from the wrong row, or from the wrong document;
- the SIREN of another entity in the same group, or of a head office where the invoice is owed by a subsidiary;
- a company that has since been struck off, with its identifier still sitting in your file;
- the rare
09↔90swap the checksum lets through.
None of those lines will fail. They will succeed, quietly, against a real and often active company that is not the one you meant — and the invoice will go to them.
The only way to catch it is to compare the name you hold against the name the
register publishes. That is what the optional expected_name field on each
line does, and it is why the endpoint takes a name it does not use as a lookup
key: there is no search by name here, only a check against one. The comparison
folds typography and not words
— case, accents and punctuation are ignored, a substituted word or a legal form
added or dropped is not — so an apostrophe in your file costs you nothing, while
the wrong company still comes back as a review.
What one call returns
Each line comes back as a complete, sourced answer built from two blocks:
| Block | Question | Source |
|---|---|---|
identity | Does it exist, and is it still active? | INSEE Sirene |
events | Insolvency, deregistration — what has been gazetted? | BODACC |
Plus one verdict per line — ok, review, stop or insufficient_coverage —
from the same decision table as
GET /preflight/supplier, with every reason
attached to the block it came from. The verdict is exactly the maximum severity
among the reasons, so you can recompute it from the blocks rather than take it
on trust.
The profile runs fully offline against local snapshots, which is why 200 lines come back in one synchronous response — 269 ms measured on 200 heavy subjects, 434–545 ms on 200 varied real ones. Indicative measurements on the production store, not an SLA.
A real batch of four lines
Captured against production on 2026-08-14, four lines carrying the four outcomes the product has:
ref | Submitted | Register publishes | Issue | Verdict |
|---|---|---|---|---|
L-0001 | 395030844 + SANOFI | SANOFI | certified | ok |
L-0002 | 662042449 + BNP PARIBAS FACTOR | BNP PARIBAS | certified | review |
L-0003 | 321875205 | SAN MARINA | certified | stop |
L-0004 | 000000000 | — | unresolved | null |
L-0003 is what the file was cleaned for: an insolvency proceeding gazetted
against SAN MARINA, returned as a stop with the reason attached to the block
and the source that published it.
L-0002 is the interesting one. The name on that row, BNP PARIBAS FACTOR, is a
different legal entity from the one that SIREN designates — a real and common
shape of error, and one no checksum can see. It also picks up two further
reasons: a signalling notice from April 2026 under a year old, and an
undetermined open-insolvency flag, which counts as a coverage gap rather than
a finding. Maximum severity wins, so the verdict is review.
That is deliberate. A mismatch is never a stop: the SIREN exists and may still
be the right one — a trade name, a renamed entity, a group parent. The endpoint
flags the discrepancy and refuses to decide which company you meant. But it is
never a silent ok either, because the failure mode being targeted is precisely
the one that reads well.
A recent notice is not automatically a signal
L-0001 returns ok although its timeline carries a sale notice dated
2026-05-22, among 42 notices in total. That is not an oversight, and it is the
detail most likely to be got wrong by anyone building this themselves.
Only signalling families count — court judgments, deregistrations, notices to
creditors. The sale family does not, because none of its roughly 1.5 million
labels says the direction of the operation: the indexed SIREN can be the
buyer as easily as the seller. An active company publishes several a year.
Treating every recent notice as a risk signal produces a detector of ordinary
business activity, and a file where the healthiest counterparties look worst.
Submitted, not found — and why that is the honest way round
The price is counted on the lines you submit, before the service knows which of them exist. That is not a rounding convenience; it is the product. “This SIREN is not in the register” is precisely the answer being bought, and it is the answer a stale file produces most often.
To put a number on how little “well-formed” tells you: we ran 200 randomly
generated, Luhn-valid SIRENs through the endpoint. 142 were not in the
register at all. Of the 58 that were, 30 came back stop — 28 ceased legal
units and 10 carrying a BODACC deregistration notice. Twenty-eight returned
ok. Random SIRENs are not a customer file, and the proportions in yours will
differ; the point is narrower and holds either way — passing a checksum
predicts almost nothing about being real and active, which is the whole reason
a register call is unavoidable.
Those lines are served and billed. A line whose key is malformed is the
opposite case: it rejects the entire batch with a 400 listing every offending
line by index and reference, before any work, and nothing is settled. Half a
certified file would be worse than none.
{
"code": "INVALID_SUBJECTS",
"error": "1/5 subjects have a malformed key: nothing was certified, nothing is billed",
"rejected": [
{ "code": "INVALID_CHECKSUM", "index": 3, "ref": "row-4", "reason": "siren failed its checksum (Luhn): likely a typo" }
]
}
The distinction is the one that runs through the whole platform: a request we
cannot answer is a 4xx and is free; an answer you did not want to hear is a
200 and is billed.
The part that is new: an answer you can pass on
Every other paid endpoint gives you an answer you have to be trusted about. If your client asks “how do I know you actually checked this supplier against Sirene on 14 August?”, the honest reply has so far been “you can call the API yourself”.
An x402 settlement does not fix this. It proves a payment happened — it says nothing about what came back. The receipt and the content are different claims.
This endpoint closes that gap. The service canonicalises each record
(JCS, RFC 8785), hashes it into a leaf,
and builds a Merkle root over the batch. The gateway then signs that root with
Ed25519 using a key the service never
holds, and writes exactly three fields — alg, key_id, signature. It reads
one string at one fixed path; it never interprets a business block or reads a
verdict.
The separation matters twice over. It keeps the microservice zero-secret and the gateway ignorant of the business domain — and, because the signature covers a root rather than a document, it makes a single line independently checkable.
Verifying one line, without the other 199 and without us
Your buyer receives one record — not the file — plus the batch attestation, and runs the published verifier: Python standard library only, no network, no dependency on Invoket, Ed25519 implemented in the script itself. They install nothing and trust nobody.
curl -s https://api.invoket.com/.well-known/certificate-keys > keys.json
python3 verify_certificate.py record.json --key keys.json --proof proof.json
It runs five checks, and the split between them is the whole point:
| # | Check | Proves |
|---|---|---|
| 1 | The attestation is present and of a known version | coherence |
| 2 | The attested scope, canonicalised and hashed, equals the published digest | coherence |
| 3 | On a batch: the Merkle tree of the records rebuilds the root | coherence |
| 4 | The inclusion path lifts one record alone to the batch root | coherence |
| 5 | The Ed25519 signature on that root | origin |
Checks 1 to 4 prove a response is internally coherent. Only check 5 proves it came from us. Digests are pure computation, so an intermediary can fabricate a wholly coherent file — a plausible verdict, a matching fingerprint, a tree that closes. What it cannot fabricate is the signature.
Which is why running without --key makes the script announce, on every run,
that the signature was not checked — and why it will not go and fetch the key
itself. A verifier that reaches out to the network is no longer verifiable
offline.
This works because a record is a response — { data, provenance } plus its
fingerprint, in the exact shape of the standalone endpoints — so it survives
extraction unmodified and the verifier accepts it as is. Three design details
make it hold:
refand the line’s rank are inside the signed scope, alongside the verdict itself. Your own reference is what ties a proof back to your file, and two records of one batch are never interchangeable.- The service holds no key. It computes the root; the gateway signs it. So a compromised key cannot forge a past verdict that was never issued, and the root stays reproducible by anyone.
- An odd level promotes its last digest rather than duplicating it. Duplication is the known malleability of the Bitcoin tree, where two distinct batches can share a root.
One word of care: what is produced is a signed attestation, not a certificate
in the eIDAS sense — no qualified timestamp, no regulated status. The JSON field
is called certificate; the legal meaning is not implied.
What it does not certify
Beyond the three exclusions at the top, the scope is narrow on purpose, and the response states its own limits in every batch:
- Two blocks, not eight. The
e-invoicing-frprofile answersidentityandevents. VAT, Peppol reachability, the control chain and sanctions screening are not in it — so anokhere is anokon those two blocks, and such a proof must never be passed off as a complete counterparty check. That isGET /preflight/supplier, one at a time. - Not an e-invoicing readiness check. Peppol reachability — a different
network and a different directory — is
GET /company/peppol. - An attestation is a dated verdict, not a present truth. Each record carries
the
as_ofof its sources and the attestation itsissued_at. It does not expire on its own; it says what it was rendered against. - A verdict is a decision table, not an opinion. Deterministic, over sourced and dated facts. Not a credit score, not a file grade, not a recommendation to act.
- Each line’s timeline is capped at 10 notices, against 100 on
GET /company/events— but the flags that drive the verdict come from the full history, so no verdict depends on the cap.
Wiring it up
POST the file, get a 402 quoting the exact amount for your N lines, sign,
replay. The
quickstart has the loop in full; the live
/catalog is the single source of truth for the base and
per-unit amounts, and for any active trial. The cap is 200 lines per call —
beyond it the gateway returns a 400 before issuing a challenge, so an
oversized batch never costs anything.
Need one line rather than a file? The two blocks are
GET /company/resolve and
GET /company/events; the five-block counterparty
view of a single SIREN is
GET /company/report. And where this endpoint covers
the counterparty of the French reform, the document side is
POST /invoice/validate and
POST /invoice/generate.
Full field reference, error codes and the verifier’s five checks:
POST /certify/batch.