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:

  1. That the goods or services were delivered. No register knows.
  2. 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.
  3. 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

VerdictMeaning
stopA published fact stands against the payment
holdA signal calls for a human look before paying
insufficient_coverageAt least one check could not be rendered - so pay is impossible
payEvery 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.

BlockCodeSeverityWhen
documentpayment_account_implausiblestopThe IBAN to credit fails its mod-97 check or its ISO 13616 length
documentimplausible_identifierreviewAnother plausibility check fails (SIREN, VAT number, BIC, date, electronic address, currency)
documentdocument_not_compliantreviewXSD errors or fatal EN 16931 violations - the detail cites the official rule IDs
documentprofile_mismatchreviewThe content does not fit the declared Factur-X profile
documentmultiple_payment_accountsreviewSeveral distinct accounts: one was checked, the agent must choose
documentpayment_account_absentgapNo IBAN in BG-17 / BT-84: the bank check did not happen
supplier / payeesupplier_preflight_stopstopA stop from the supplier check (open insolvency, ceased unit, deregistration, sanctions hit…), quoted verbatim
supplier / payeesupplier_preflight_reviewreviewA review from the supplier check (unreachable IBAN, invalid VAT number, recent event…)
supplier / payeesupplier_preflight_gapgapA coverage gap of the supplier check, relayed
supplier / payeeregistered_name_mismatchreviewThe register’s name is not the invoice’s
supplier / payeeregistered_name_not_comparablenoticeNo published name to compare, or names that cannot be compared
vatvat_consistency_findingreviewA consistency check INV-VAT-xx found something
payment_termsdue_date_passedreviewThe invoice is already due - the block quantifies the interest owed
any blockcoverage_unavailablegapThe block could not be rendered, with its reason
payeepayee_screening_skipped_by_callernoticescreen_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:

ParameterDefaultEffect
screen_payeetrueAlso 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:

ElementEN 16931 termMissing →
Seller legal identifierBT-30supplier → unavailable (supplier_legal_id_absent), no call made
Account to creditBT-84 (BG-17)payment_account_absent, the bank check is not rendered
Amount, currency, datesBT-115, BT-5, BT-9 / BT-2payment_terms → unavailable, naming the missing terms
Real payeeBG-10payee → 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:

BlockContainsProvenance
documentThe full /invoice/validate verdict - four layers, BR-xx violations, plausibilityEN 16931 artefacts, snapshot
supplierThe /preflight/supplier answer as is - identity, events, VAT, Peppol, group, screening, IBANthe supplier check’s own
payeeThe same check on the real payee (BG-10), when distinctidem
vatThe document’s VAT consistency checks (INV-VAT-01..04)Directive 2006/112/EC + TEDB, snapshot
payment_termsThe 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.

StatuscodeCase
400unreadable_body, malformed_xml, unknown_root, malformed_pdf, no_embedded_xml, invalid_base64, invalid_decimalThe document cannot be read - judged before any internal call
400INVALID_INPUTAn unknown query parameter
405METHOD_NOT_ALLOWEDAny method other than POST
413PAYLOAD_TOO_LARGEBody above 10 MB
500INTERNALInternal 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