Pre-flight a supplier before an agent onboards it
Identity, insolvency, live VAT, Peppol, control chain and sanctions on the company and on its ultimate parent — one call, one verdict, and the reason behind it.
By Matthias Begot · · Updated
An agent about to onboard, pay or invoice a counterparty has to answer seven
questions at once — does it exist, is it still active, has anything been
gazetted against it, is its VAT number valid right now, can it receive an
e-invoice, who controls it, and is either it or its parent designated on a
sanctions list. GET /preflight/supplier
answers all seven in one call and returns one verdict — ok, review,
stop or insufficient_coverage — with every reason attached to the block and
the source it came from. Its governing rule is the one that makes the verdict
worth reading: ok is impossible while any block is unavailable. A check
that did not happen never passes.
The problem: the answer is not the hard part, the aggregation is
Every one of those seven questions already has a public answer. INSEE Sirene says whether the legal unit is active. BODACC publishes the insolvency judgments. VIES answers on the VAT number. GLEIF publishes the declared control chain. OFAC, the EU and the UN publish their designation lists. The registers are open; nothing here is proprietary intelligence.
What an autonomous agent lacks is not the data, it is a decision it can defend — one call whose result is a verdict, not seven responses in seven shapes that someone still has to reconcile. And the reconciliation is where counterparty checks actually fail: the FBI’s IC3 counted $3.04 billion in verified business-email-compromise losses across 24,768 complaints in 2025, on top of a $20.9 billion total, with the great majority of BEC funds moving through ordinary wire and ACH workflows (2025 IC3 Annual Report). Those payments do not fail a technical filter. They fail a supplier check that was partial, or skipped because it was seven calls long.
The rule: a gap is not a pass, and a gap is not a signal
The composite keeps two distinctions that a naive aggregator collapses, and the whole value of the verdict rests on them.
not_applicable is an answer. unavailable is a gap. “GLEIF publishes that
there is no parent above this entity” is an answer, and an answer never blocks
an ok. “We could not establish the control chain” is a gap — and only the gap
forbids ok. Each block therefore carries one of four coverage states:
coverage | Meaning | Effect on the verdict |
|---|---|---|
complete | The question got its answer | none |
unavailable | It did not — missing dataset, register unreachable | forbids ok |
not_applicable | There was nothing to check | none |
not_requested | You waived the check explicitly | none |
A gap is also not a signal. A VAT number VIES could not check is a gap; a VAT number a member state firmly answers as invalid is a signal, and it weighs on the verdict. Conflating the two produces the two failure modes that make automated screening useless — a green light because a register was down, or a red flag because a register was down.
A block that got no answer carries neither data nor provenance, only its
coverage and a short stable reason. The gap is stated, never filled. And the
verdict is always exactly the maximum severity among the listed reasons, each
carrying its block and a stable code — so an agent can recompute the verdict
from the blocks instead of taking it on trust.
The check no sequence of unit calls gives you: the parent
The /company/* endpoints and /screen/entity will
each answer their own question if you would rather orchestrate them yourself.
One thing an endpoint-by-endpoint review structurally misses: screening the
ultimate parent, because you have to resolve the control chain first in
order to know whose name to screen.
That is the case the composite exists for, and it looks like this in production (abridged from the doc page’s captured example):
{
"subject": { "name": "IBM FRANCE FINANCEMENT", "siren": "316037753", "lei": "0XKTQGPTA3P542CII630" },
"verdict": "review",
"verdict_reasons": [
{ "block": "parent_screening", "code": "parent_weak_sanctions_hit", "detail": "partial_tokens" }
],
"blocks": {
"identity": { "coverage": "complete", "data": { "status": "active", "…": "…" } },
"events": { "coverage": "complete", "data": { "total": 0, "flags": { "has_open_insolvency_announcement": false } } },
"vat": { "coverage": "complete", "data": { "status": "valid", "…": "…" } },
"group": { "coverage": "complete", "data": { "ultimate_parent": { "status": "declared", "link": { "entity": { "legal_name": "INTERNATIONAL BUSINESS MACHINES CORPORATION", "legal_country": "US" } } } } },
"name_screening": { "coverage": "complete", "data": { "verdict": "no_hit", "hits": [] } },
"parent_screening": { "coverage": "complete", "data": { "verdict": "weak_hit", "hits": [ { "list_source": "OFAC-SDN", "match_type": "partial_tokens" } ] } }
}
}
The company itself is spotless — active, VAT valid, nothing gazetted, no_hit
on the lists. The review comes entirely from one level up. Note also that a
weak_hit on the parent is a review and never a stop: a partial token
overlap is a lead to check, not a designation.
Control chain is not beneficial ownership, and the difference is legal
State this one exactly, because an agent that treats a group block as a UBO
check is wrong in a way that matters. GLEIF Level 2 records the entity’s
direct and ultimate accounting consolidating parent — the highest legal
entity preparing consolidated financial statements — self-declared by the
entity itself, with no ownership percentage
(GLEIF, Level 2 Data: Who Owns Whom).
It is an accounting definition of control, not an AML one, and the absence of a
declared parent is not evidence of an absence of control.
There is no open-register substitute either. On 22 November 2022 the Court of
Justice of the EU invalidated the provision of the anti-money-laundering
directive that opened beneficial-ownership registers to the general public
(joined cases C-37/20 and C-601/20), holding that it was a serious interference
with the rights to private life and data protection; access now requires a
demonstrated legitimate interest
(eucrim summary).
So a public, unauthenticated pre-flight can tell you the declared consolidation
parent and screen it. It cannot tell you the beneficial owner, and the endpoint
says so in data.limits on every response rather than in a footnote.
insufficient_coverage is the honest answer, not a failure
Most French companies have no LEI. No LEI means no publishable control chain, which means the parent cannot be screened either — so a perfectly clean supplier comes back like this:
{
"subject": { "name": "DECATHLON FRANCE", "siren": "500569405", "lei": null },
"verdict": "insufficient_coverage",
"verdict_reasons": [
{ "block": "group", "code": "coverage_unavailable", "detail": "no_lei_declared_for_siren" },
{ "block": "parent_screening", "code": "coverage_unavailable", "detail": "group_unavailable" }
]
}
Active, VAT valid, nothing in the gazette, nothing on the lists — and still not
ok. The gap cascades and is named, rather than being reported as “no
parent”. This is a structural limit of the LEI reference, stated as such: not an
outage, and not a silence. For an agent, it is the useful answer — it says
precisely which question is still open, so the workflow can escalate that one
question instead of re-running everything.
When a register throttles, a dated verdict beats a lost ok
VIES routes each query to the member state that owns the data, and each national database has its own maintenance windows and outages; “member state service unavailable” is a normal, frequent outcome of the system rather than an exception (Your Europe / VIES). Quotas are per member state, not per caller: a pre-flight can be throttled because the rest of Europe is querying that register at that second.
Losing an ok over that would blame the supplier for someone else’s traffic. So
when the live check fails and a firm verdict on that exact number is already
held from the last 24 hours, the vat block serves it — coverage: "complete",
served_from: "cache", with cached_age_secs and a freshness.kind of
cached — and ok stays reachable. Two things this does not change: the
verdict is never invented (only a firm valid / invalid really obtained from
VIES ever enters the cache), and a number we have never had a verdict on
stays the gap it always was. Read the age before acting on it; a stricter agent
can apply its own threshold and re-check the number itself through
GET /company/vat.
The onboarding loop
- Anchor the counterparty on one key —
sirenorlei, exactly one. Passing both, or neither, is a400. Entering byleion an entity whose GLEIF record declares a real SIREN reopens all the French blocks; a foreign entity with no French registration gets them asnot_applicable, never as a gap. - Add the bank leg when you are about to pay, by passing
iban. That attaches a whole/preflight/paymentanswer as thebankblock — and it uses the supplier’s register name as the beneficiary name, so the name check is genuinely screened rather than skipped. - Branch on the verdict, not on a score.
stopandrevieware decisions;insufficient_coverageis a named open question. There is no numeric risk score to threshold, by design. - Audit against the blocks. Every reason points at a block; every block
carries its own
provenancewith the snapshot date it was answered from. The rootprovenancereflects only the identity foundation — the registers are dated differently and the VAT check is live, so the dates that matter are on each block. - Persist the response, not a boolean. Six months later, “why did the agent onboard this supplier” is answerable from the stored blocks and their dates.
The payment side of that loop is the usual discover → 402 → pay → replay
cycle; the Quickstart walks it with runnable snippets and
For agents covers the discovery surfaces.
Billing: an unusable answer is never billed
The x402 golden rule is that the agent pays for the answer to its question, so
the mapping is strict in both directions. A counterparty in liquidation is a
useful answer → 200 with a motivated stop, and it is billed. A
well-formed identifier that no register knows is a 404, and it is
unbilled — no empty pre-flight is ever paid for. stop, review and
insufficient_coverage are all billed, because all three are answers.
This endpoint is not priced at the catalog’s flat per-call rate, and that is
deliberate: one call runs seven checks, eight with an IBAN, in one round-trip
and one settlement. No price appears in this article — the gateway’s
/catalog is the single source of truth for
every amount and every accepted rail, the unit endpoints included, so compare
them there.
What it will not do
- No search by name. The entry key is a SIREN or an LEI. Resolving a name to an identifier is a different problem and is not solved here.
- No credit opinion, no risk score, no financial statements, no directors. The verdict is a deterministic decision table over sourced, dated facts.
- No reconciliation between registers. If Sirene reports a legal unit as active while BODACC shows a conversion to liquidation, the composite reports both and lets the most severe fact decide — it does not adjudicate which register is right.
- Peppol reachability is a directory fact, not a signal. A supplier absent from the Peppol directory weighs nothing on the verdict; it just cannot receive an e-invoice at that address.
okmeans what it says and nothing more: every requested check was answered, and none of them raised a signal, on the data served.
That last sentence travels verbatim in data.limits with every response, which
is the point — the caller and the auditor read the same scope statement. For the
parameter contract, the full verdict and reason-code tables, the block shapes
and the error codes, see the
GET /preflight/supplier documentation.