POST /preflight/invoice-payment
Takes the invoice your agent is about to pay - the document itself, as it
arrived - and returns one payment verdict: pay, hold, stop or
insufficient_coverage, over five blocks, each with its own source and date.
In one call and one settlement, it runs what would otherwise be six:
/invoice/validate on the document, the whole
/preflight/supplier check on the seller -
identity, legal events,
VAT number, and the IBAN checks of
/preflight/payment - the same on the real
payee when the invoice declares one, the VAT consistency of the document, and
the statutory due date with what lateness costs
(/invoice/late-payment).
What the agent buys is not the data - it could chain those calls itself - but the decision logic: a verdict it can reconstruct, motivated block by block, sourced and dated. Read first what that verdict cannot decide.
What this does not attest - and why it does not stop redirection fraud
Every response carries this sentence in data.limits, verbatim:
This verdict is a deterministic decision table over the invoice as posted and over sourced, dated facts from public registers; it is not a credit opinion, a risk score or a recommendation to act. It does not attest that the goods or services billed were actually supplied, that the payment instruction was not tampered with (invoice redirection fraud), nor that the bank account belongs to the supplier named on the invoice — none of the three is decidable from the document and the registers. ‘pay’ only means that every requested check was answered and none of them raised a signal, on the data served (see each block’s coverage and provenance). The VAT treatment of the transaction (place of supply, reverse charge) is not determined here: it depends on facts the document does not carry — POST /invoice/vat-treatment renders it.
Three things are not decidable from the document and the registers, and this route does not pretend otherwise:
- That the goods or services were delivered. No register knows.
- That the payment instruction was not tampered with. This route does not stop invoice redirection fraud - the “your supplier changed bank details” scam. A fraudster who swaps the IBAN on a genuine invoice produces a document that is compliant, from a healthy supplier, with a valid and reachable account. Every block here can answer cleanly on it.
- That the bank account belongs to the supplier named on the invoice. The IBAN is checked for validity, reachability and sanctions - never for ownership. Account-holder verification is a bank-side service this route does not have access to.
A pay means “every requested check was answered and none raised a signal”,
on the data each block dates - nothing more. And no score, no supplier grade,
no recommendation beyond the four values below.
The verdict, and the rule that makes it worth buying
| Verdict | Meaning |
|---|---|
stop | A published fact stands against the payment |
hold | A signal calls for a human look before paying |
insufficient_coverage | At least one check could not be rendered - so pay is impossible |
pay | Every requested check was answered, and none raised a signal |
A single block whose coverage is unavailable forbids pay. That is the
point of the product, not a limitation of it: a payment verdict that reads
pay while one of its checks never happened would be worse than no verdict -
it would be a green light on something nobody looked at. A register that timed
out, a live VAT service that refused the query, a seller identifier missing
from the document: each one turns a would-be pay into
insufficient_coverage, with the reason named. The example below is exactly
that - a clean invoice, a healthy supplier, and one live check that did not
answer.
Each reason carries its block, a code, a severity and a detail. The
verdict is the maximum severity - stop > review (hold) > gap
(insufficient_coverage) > notice - and reasons are served most severe
first, so the first one explains the verdict. A signal outranks a gap here: a
past-due invoice from a supplier whose VAT number could not be checked is a
hold, with the gap still listed beneath it.
| Block | Code | Severity | When |
|---|---|---|---|
document | payment_account_implausible | stop | The IBAN to credit fails its mod-97 check or its ISO 13616 length |
document | implausible_identifier | review | Another plausibility check fails (SIREN, VAT number, BIC, date, electronic address, currency) |
document | document_not_compliant | review | XSD errors or fatal EN 16931 violations - the detail cites the official rule IDs |
document | profile_mismatch | review | The content does not fit the declared Factur-X profile |
document | multiple_payment_accounts | review | Several distinct accounts: one was checked, the agent must choose |
document | payment_account_absent | gap | No IBAN in BG-17 / BT-84: the bank check did not happen |
supplier / payee | supplier_preflight_stop | stop | A stop from the supplier check (open insolvency, ceased unit, deregistration, sanctions hit…), quoted verbatim |
supplier / payee | supplier_preflight_review | review | A review from the supplier check (unreachable IBAN, invalid VAT number, recent event…) |
supplier / payee | supplier_preflight_gap | gap | A coverage gap of the supplier check, relayed |
supplier / payee | registered_name_mismatch | review | The register’s name is not the invoice’s |
supplier / payee | registered_name_not_comparable | notice | No published name to compare, or names that cannot be compared |
vat | vat_consistency_finding | review | A consistency check INV-VAT-xx found something |
payment_terms | due_date_passed | review | The invoice is already due - the block quantifies the interest owed |
| any block | coverage_unavailable | gap | The block could not be rendered, with its reason |
payee | payee_screening_skipped_by_caller | notice | screen_payee=false while a payee is declared |
The supplier check’s own reasons are relayed as block/code: detail, never
rewritten - identity/entity_ceased, vat/coverage_unavailable - so they
stay the supplier route’s vocabulary and do not drift.
Input: the document
POST the invoice itself, exactly as for
/invoice/read and
/invoice/validate:
application/pdf- a Factur-X PDF, raw;application/xml- a CII or UBL invoice, raw;application/json-{ "document_base64": "…" }.
Body cap: 10 MB (413 above it, not billed). The input is what the agent
has in hand - not a SIREN it would first have to find.
One query parameter:
| Parameter | Default | Effect |
|---|---|---|
screen_payee | true | Also checks the real payee (BG-10) when the invoice declares one distinct from the seller. At false, the payee block is not_requested and a notice says so - the caller’s choice is respected, never silent |
Any other query key is a 400 INVALID_INPUT.
What the document must carry, and what happens when it does not. A missing field is a named coverage gap, never a guessed value:
| Element | EN 16931 term | Missing → |
|---|---|---|
| Seller legal identifier | BT-30 | supplier → unavailable (supplier_legal_id_absent), no call made |
| Account to credit | BT-84 (BG-17) | payment_account_absent, the bank check is not rendered |
| Amount, currency, dates | BT-115, BT-5, BT-9 / BT-2 | payment_terms → unavailable, naming the missing terms |
| Real payee | BG-10 | payee → not_applicable: the seller is the payee - an answer, not a gap |
The seller identifier is treated as a French SIREN only when the document
says so - ISO 6523 scheme 0002/0009, or a numeric value on a party
established in France. A nine-digit foreign registration number is never sent
to the Sirene register: it would return a verdict on another company.
In practice, the supplier check runs on French sellers. An invoice from a
seller identified otherwise - a German company carrying its LEI (scheme
0199), for instance - leaves the supplier block unavailable
(supplier_legal_id_absent), no call is made, and the verdict is
insufficient_coverage. The IBAN checks run inside the supplier check, so
they do not happen either (query.iban_submitted: false): the document,
VAT and payment-terms blocks are still served.
The five blocks
Each block carries its coverage (complete, unavailable,
not_applicable or not_requested), a reason when it is not complete, its
data - the shape of the corresponding unit route - and its own
provenance:
| Block | Contains | Provenance |
|---|---|---|
document | The full /invoice/validate verdict - four layers, BR-xx violations, plausibility | EN 16931 artefacts, snapshot |
supplier | The /preflight/supplier answer as is - identity, events, VAT, Peppol, group, screening, IBAN | the supplier check’s own |
payee | The same check on the real payee (BG-10), when distinct | idem |
vat | The document’s VAT consistency checks (INV-VAT-01..04) | Directive 2006/112/EC + TEDB, snapshot |
payment_terms | The due date retained, the delay, and its cost (interest by period + the fixed recovery sum) | late-payment snapshots |
The root provenance is the document’s (invoice-document, live: the
composite is computed on demand); each block’s own provenance is the one that
counts for that block - four sets of sources at four different cadences.
Example - hold
A test invoice generated with /invoice/generate,
naming a real registered company as seller so that the registers have
something to answer, due on 2026-09-15 and assessed on 2026-10-15. Blocks
abbreviated:
{
"data": {
"verdict": "hold",
"verdict_reasons": [
{
"block": "payment_terms",
"code": "due_date_passed",
"severity": "review",
"detail": "payment was due on 2026-09-15 (30 day(s) ago): statutory interest and the fixed recovery sum are quantified in this block"
},
{"block": "supplier", "code": "supplier_preflight_gap", "severity": "gap", "detail": "vat/coverage_unavailable: MS_MAX_CONCURRENT_REQ"}
],
"assessed_on": "2026-10-15",
"query": {"supplier_siren": "542051180", "payee_siren": null, "iban_submitted": true, "screen_payee": true},
"subject": {
"invoice_number": "INV-2026-0902",
"issue_date": "2026-09-01",
"due_date": "2026-09-15",
"currency": "EUR",
"payable_amount": "1440",
"payable_amount_term": "BT-115 (amount due for payment)",
"seller_name": "TOTALENERGIES SE",
"seller_country": "FR",
"seller_legal_id": "542051180",
"seller_legal_id_scheme": "0002",
"seller_vat_id": "FR59542051180",
"payee_name": null,
"payee_legal_id": null,
"payee_legal_id_scheme": null,
"payment_account": "FR7630006000011234567890189",
"payment_account_count": 1
},
"blocks": {
"document": {
"coverage": "complete",
"data": {
"verdict": "valid",
"layers": {
"xsd": {"ok": true},
"business_rules": {"ok": true, "violations": []},
"profile": {"ok": true},
"plausibility": {
"ok": true,
"iban_checksum": {"status": "pass"},
"seller_legal_id_luhn": {"status": "pass"},
"seller_vat_syntax": {"status": "pass"},
"…": "…"
}
}
},
"provenance": {
"source": "en16931-artefacts",
"freshness": {"kind": "snapshot", "as_of": "2026-07-13T00:00:00Z"}
}
},
"supplier": {
"coverage": "complete",
"data": {
"verdict": "insufficient_coverage",
"verdict_reasons": [
{"block": "vat", "code": "coverage_unavailable", "detail": "MS_MAX_CONCURRENT_REQ", "severity": "gap"}
],
"blocks": {"bank": "…", "events": "…", "group": "…", "identity": "…", "name_screening": "…", "parent_screening": "…", "peppol": "…", "vat": "…"}
},
"provenance": {
"source": "insee-sirene",
"freshness": {"kind": "snapshot", "as_of": "2026-09-30T00:00:00Z"}
}
},
"payee": {"coverage": "not_applicable", "reason": "no_payee_declared"},
"vat": {
"coverage": "complete",
"data": {"informational": true, "findings": [], "…": "…"},
"provenance": {
"source": "directive-2006-112 + tedb",
"freshness": {"kind": "snapshot", "as_of": "2026-07-27T00:00:00Z"}
}
},
"payment_terms": {
"coverage": "complete",
"data": {
"due_date": "2026-09-15",
"days_late": 30,
"interest_total": {"amount": "14.68", "currency": "EUR"},
"flat_fee": {
"amount": {"amount": "40", "currency": "EUR"},
"source": "flat"
},
"total_due": {"amount": "54.68", "currency": "EUR"},
"…": "…"
},
"provenance": {
"source": "late-payment-snapshots",
"freshness": {"kind": "snapshot", "as_of": "2026-07-27T00:00:00Z"}
}
}
},
"limits": "This verdict is a deterministic decision table over the invoice as posted …"
},
"provenance": {
"source": "invoice-document",
"fetched_at": "2026-10-15T16:01:10.117679561Z",
"freshness": {"kind": "live"}
}
}
Thirty days late: the payment_terms block quantifies what that costs under
French law (12.40 % statutory interest, EUR 14.68, plus the EUR 40 fixed
recovery sum). That is the hold. The VAT-number check the supplier route
runs live against VIES did not answer (MS_MAX_CONCURRENT_REQ, the member
state’s service refusing concurrent queries) - a gap, listed second, because
a signal outranks a gap.
Example - insufficient_coverage
The same invoice, not yet due (2026-10-31). The document is compliant, the supplier is active, the IBAN is valid, reachable and unsanctioned, the name screening is clean - and the live VAT check still did not answer:
{
"data": {
"verdict": "insufficient_coverage",
"verdict_reasons": [
{"block": "supplier", "code": "supplier_preflight_gap", "severity": "gap", "detail": "vat/coverage_unavailable: MS_MAX_CONCURRENT_REQ"}
],
"assessed_on": "2026-10-15",
"query": {"supplier_siren": "542051180", "payee_siren": null, "iban_submitted": true, "screen_payee": true},
"subject": {
"invoice_number": "INV-2026-0901",
"issue_date": "2026-09-01",
"due_date": "2026-10-31",
"currency": "EUR",
"payable_amount": "1440",
"payable_amount_term": "BT-115 (amount due for payment)",
"seller_name": "TOTALENERGIES SE",
"seller_country": "FR",
"seller_legal_id": "542051180",
"seller_legal_id_scheme": "0002",
"seller_vat_id": "FR59542051180",
"payee_name": null,
"payee_legal_id": null,
"payee_legal_id_scheme": null,
"payment_account": "FR7630006000011234567890189",
"payment_account_count": 1
},
"blocks": {
"document": {
"coverage": "complete",
"data": {
"verdict": "valid",
"layers": {
"xsd": {"ok": true},
"business_rules": {"ok": true, "violations": []},
"profile": {"ok": true},
"plausibility": {
"ok": true,
"iban_checksum": {"status": "pass"},
"seller_legal_id_luhn": {"status": "pass"},
"seller_vat_syntax": {"status": "pass"},
"…": "…"
}
}
},
"provenance": {
"source": "en16931-artefacts",
"freshness": {"kind": "snapshot", "as_of": "2026-07-13T00:00:00Z"}
}
},
"supplier": {
"coverage": "complete",
"data": {
"verdict": "insufficient_coverage",
"verdict_reasons": [
{"block": "vat", "code": "coverage_unavailable", "detail": "MS_MAX_CONCURRENT_REQ", "severity": "gap"}
],
"blocks": {"bank": "…", "events": "…", "group": "…", "identity": "…", "name_screening": "…", "parent_screening": "…", "peppol": "…", "vat": "…"}
},
"provenance": {
"source": "insee-sirene",
"freshness": {"kind": "snapshot", "as_of": "2026-09-30T00:00:00Z"}
}
},
"payee": {"coverage": "not_applicable", "reason": "no_payee_declared"},
"vat": {
"coverage": "complete",
"data": {"informational": true, "findings": [], "…": "…"},
"provenance": {
"source": "directive-2006-112 + tedb",
"freshness": {"kind": "snapshot", "as_of": "2026-07-27T00:00:00Z"}
}
},
"payment_terms": {
"coverage": "complete",
"data": {
"due_date": "2026-10-31",
"days_late": 0,
"interest_total": {"amount": "0", "currency": "EUR"},
"flat_fee": {
"amount": {"amount": "0", "currency": "EUR"},
"source": "not_due"
},
"total_due": {"amount": "0", "currency": "EUR"},
"…": "…"
},
"provenance": {
"source": "late-payment-snapshots",
"freshness": {"kind": "snapshot", "as_of": "2026-07-27T00:00:00Z"}
}
}
},
"limits": "This verdict is a deterministic decision table over the invoice as posted …"
},
"provenance": {
"source": "invoice-document",
"fetched_at": "2026-10-15T16:01:09.408675843Z",
"freshness": {"kind": "live"}
}
}
Everything else is clean, and the verdict is not pay. That is the rule
doing its job: one check did not happen, so nobody can claim it passed. The
detail names the cause and the supplier route marks it retryable - the agent
knows what to do: retry later, or decide with the gap in view.
Example - stop
A test invoice naming as seller a company the register records as ceased and deregistered:
{
"data": {
"verdict": "stop",
"verdict_reasons": [
{"block": "supplier", "code": "supplier_preflight_stop", "severity": "stop", "detail": "identity/entity_ceased"},
{"block": "supplier", "code": "supplier_preflight_stop", "severity": "stop", "detail": "events/deregistered"},
{"block": "supplier", "code": "supplier_preflight_gap", "severity": "gap", "detail": "vat/coverage_unavailable: MS_MAX_CONCURRENT_REQ"}
],
"assessed_on": "2026-10-15",
"query": {"supplier_siren": "552100554", "payee_siren": null, "iban_submitted": true, "screen_payee": true},
"subject": {
"invoice_number": "INV-2026-0903",
"issue_date": "2026-09-01",
"due_date": "2026-10-31",
"currency": "EUR",
"payable_amount": "1440",
"payable_amount_term": "BT-115 (amount due for payment)",
"seller_name": "PEUGEOT SA",
"seller_country": "FR",
"seller_legal_id": "552100554",
"seller_legal_id_scheme": "0002",
"seller_vat_id": "FR96552100554",
"payee_name": null,
"payee_legal_id": null,
"payee_legal_id_scheme": null,
"payment_account": "FR7630006000011234567890189",
"payment_account_count": 1
},
"blocks": {
"document": {
"coverage": "complete",
"data": {
"verdict": "valid",
"layers": {
"xsd": {"ok": true},
"business_rules": {"ok": true, "violations": []},
"profile": {"ok": true},
"plausibility": {
"ok": true,
"iban_checksum": {"status": "pass"},
"seller_legal_id_luhn": {"status": "pass"},
"seller_vat_syntax": {"status": "pass"},
"…": "…"
}
}
},
"provenance": {
"source": "en16931-artefacts",
"freshness": {"kind": "snapshot", "as_of": "2026-07-13T00:00:00Z"}
}
},
"supplier": {
"coverage": "complete",
"data": {
"verdict": "stop",
"verdict_reasons": [
{"block": "identity", "code": "entity_ceased", "severity": "stop"},
{"block": "events", "code": "deregistered", "severity": "stop"},
{"block": "vat", "code": "coverage_unavailable", "detail": "MS_MAX_CONCURRENT_REQ", "severity": "gap"}
],
"blocks": {"bank": "…", "events": "…", "group": "…", "identity": "…", "name_screening": "…", "parent_screening": "…", "peppol": "…", "vat": "…"}
},
"provenance": {
"source": "insee-sirene",
"freshness": {"kind": "snapshot", "as_of": "2026-09-30T00:00:00Z"}
}
},
"payee": {"coverage": "not_applicable", "reason": "no_payee_declared"},
"vat": {
"coverage": "complete",
"data": {"informational": true, "findings": [], "…": "…"},
"provenance": {
"source": "directive-2006-112 + tedb",
"freshness": {"kind": "snapshot", "as_of": "2026-07-27T00:00:00Z"}
}
},
"payment_terms": {
"coverage": "complete",
"data": {
"due_date": "2026-10-31",
"days_late": 0,
"interest_total": {"amount": "0", "currency": "EUR"},
"flat_fee": {
"amount": {"amount": "0", "currency": "EUR"},
"source": "not_due"
},
"total_due": {"amount": "0", "currency": "EUR"},
"…": "…"
},
"provenance": {
"source": "late-payment-snapshots",
"freshness": {"kind": "snapshot", "as_of": "2026-07-27T00:00:00Z"}
}
}
},
"limits": "This verdict is a deterministic decision table over the invoice as posted …"
},
"provenance": {
"source": "invoice-document",
"fetched_at": "2026-10-15T16:01:10.846813212Z",
"freshness": {"kind": "live"}
}
}
Two stop reasons from the supplier check, quoted as it serves them -
identity/entity_ceased, events/deregistered - plus the same VAT gap. A
200, billed: “this supplier no longer exists” is the most useful answer this
route can give, and the one that saves the most money.
Example - pay
The same not-yet-due invoice, posted again at 16:10 UTC the same day - when the member state’s VAT service answered:
{
"data": {
"verdict": "pay",
"verdict_reasons": [],
"assessed_on": "2026-10-15",
"query": {"supplier_siren": "542051180", "payee_siren": null, "iban_submitted": true, "screen_payee": true},
"subject": {
"invoice_number": "INV-2026-0901",
"due_date": "2026-10-31",
"payable_amount": "1440",
"seller_name": "TOTALENERGIES SE",
"seller_legal_id": "542051180",
"payment_account": "FR7630006000011234567890189",
"…": "…"
},
"blocks": {
"document": {
"coverage": "complete",
"data": {"verdict": "valid", "…": "…"},
"provenance": {
"source": "en16931-artefacts",
"freshness": {"kind": "snapshot", "as_of": "2026-07-13T00:00:00Z"}
}
},
"supplier": {
"coverage": "complete",
"data": {
"verdict": "ok",
"verdict_reasons": [],
"blocks": {
"vat": {
"coverage": "complete",
"data": {"vat_number": "FR59542051180", "status": "valid", "checked_at": "2026-10-15T16:10:20.366100994Z", "…": "…"},
"provenance": {
"source": "vies + pure-computation",
"freshness": {"kind": "live"}
}
},
"identity": "…",
"events": "…",
"bank": "…",
"peppol": "…",
"group": "…",
"name_screening": "…",
"parent_screening": "…"
}
},
"provenance": {
"source": "insee-sirene",
"freshness": {"kind": "snapshot", "as_of": "2026-09-30T00:00:00Z"}
}
},
"payee": {"coverage": "not_applicable", "reason": "no_payee_declared"},
"vat": {
"coverage": "complete",
"data": {"informational": true, "findings": [], "…": "…"}
},
"payment_terms": {
"coverage": "complete",
"data": {
"due_date": "2026-10-31",
"days_late": 0,
"total_due": {"amount": "0", "currency": "EUR"},
"…": "…"
}
}
},
"limits": "This verdict is a deterministic decision table over the invoice as posted …"
},
"provenance": {
"source": "invoice-document",
"fetched_at": "2026-10-15T16:10:20.368026501Z",
"freshness": {"kind": "live"}
}
}
No reason at all: the VAT number is now valid in VIES (a live check, dated
to the second), every other block is unchanged, and the verdict is pay.
Compare with the insufficient_coverage example above: same invoice, same
supplier, nine minutes apart - the only difference is whether one live check
answered. That is the rule working in both directions.
And even here, pay means what the limits sentence says and nothing more: it
does not know whether the consulting was delivered, whether this IBAN is the
one the supplier really uses, or who holds the account.
Errors
Only requests the service cannot answer leave the 200 range - none of them
are billed. Every verdict, stop included, is a 200 and billed. A
neighbouring service being down is never a 5xx: it is a 200 with that
block unavailable, and therefore never a pay.
| Status | code | Case |
|---|---|---|
| 400 | unreadable_body, malformed_xml, unknown_root, malformed_pdf, no_embedded_xml, invalid_base64, invalid_decimal | The document cannot be read - judged before any internal call |
| 400 | INVALID_INPUT | An unknown query parameter |
| 405 | METHOD_NOT_ALLOWED | Any method other than POST |
| 413 | PAYLOAD_TOO_LARGE | Body above 10 MB |
| 500 | INTERNAL | Internal error (detail logged, not exposed) |
{ "code": "unreadable_body", "error": "document is neither a PDF nor an XML invoice (expected %PDF- or an XML root element)" }
{ "code": "INVALID_INPUT", "error": "invalid query parameters: Failed to deserialize query string: foo: unknown field `foo`, expected `screen_payee`" }
Attribution
Each block keeps the attribution of the source it composes: the EN 16931 semantic model and the official Factur-X, CII and UBL specifications for the document; the INSEE Sirene register, the BODACC gazette, VIES, the Peppol directory, the GLEIF LEI reference, the national bank registries, the EPC register and the OFAC/EU/UN lists for the supplier check; Directive 2006/112/EC and the Commission’s TEDB rate database for the VAT checks; Directive 2011/7/EU and its national transpositions for the payment terms.
See also
GET /preflight/supplier- the supplier check on its own, by SIREN.POST /invoice/validate- the EN 16931 verdict on its own.POST /invoice/vat-treatment- the VAT treatment of the transaction, which this route does not determine.POST /invoice/late-payment- the lateness computation with your own agreed rate or settlement date.POST /preflight/x402-seller- the same kind of verdict before signing an x402 payment.- For agents - discovery surfaces, the live
/catalogand how settlement works.