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 verdictok, 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:

coverageMeaningEffect on the verdict
completeThe question got its answernone
unavailableIt did not — missing dataset, register unreachableforbids ok
not_applicableThere was nothing to checknone
not_requestedYou waived the check explicitlynone

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.

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

  1. Anchor the counterparty on one key — siren or lei, exactly one. Passing both, or neither, is a 400. Entering by lei on an entity whose GLEIF record declares a real SIREN reopens all the French blocks; a foreign entity with no French registration gets them as not_applicable, never as a gap.
  2. Add the bank leg when you are about to pay, by passing iban. That attaches a whole /preflight/payment answer as the bank block — and it uses the supplier’s register name as the beneficiary name, so the name check is genuinely screened rather than skipped.
  3. Branch on the verdict, not on a score. stop and review are decisions; insufficient_coverage is a named open question. There is no numeric risk score to threshold, by design.
  4. Audit against the blocks. Every reason points at a block; every block carries its own provenance with the snapshot date it was answered from. The root provenance reflects only the identity foundation — the registers are dated differently and the VAT check is live, so the dates that matter are on each block.
  5. 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 answer200 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.
  • ok means 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.