Peppol pre-send check · XRechnung · ZUGFeRD
Check if the buyer is on Peppol before you send
One API call tells you whether a buyer can receive a Peppol invoice, under which participant id, and which format to create. A second call can refuse the send if they are not registered, before anything is transmitted.
No card needed. GET /einvoice/route is free for 20 calls a day per IP without a key.
Short answer: call GET https://api.wsystem.dev/einvoice/route?id=BE0403170701 (VAT number or Peppol id). The response includes peppol.registered, the participant id when found, whether BIS Billing 3.0 invoices are accepted, and a recommended format for POST /einvoice/create. To stop a send when the buyer is missing, use POST /einvoice/preflight with mode=live, or recipient_check=strict on POST /einvoice/send — you get HTTP 422 recipient_not_on_peppol and nothing is transmitted.
Look up the buyer
The lookup checks the public Peppol Directory first, then the Peppol SML/SMP (the same DNS + SMP path access points use). It also returns the VIES VAT result and the country's e-invoicing mandate. On 12 November 2026 this Belgian VAT number was registered; the German example from our sample invoice was valid in VIES but not on Peppol.
# Is this buyer on Peppol? (also works without a key: 20 calls/day per IP)
curl -s "https://api.wsystem.dev/einvoice/route?id=BE0403170701" \
-H "X-API-Key: $KEY"
# A German VAT that is valid in VIES but not registered on Peppol:
curl -s "https://api.wsystem.dev/einvoice/route?id=DE811193231" \
-H "X-API-Key: $KEY"Response (trimmed, from a real run on 2026-11-12, API 0.3.97): Buyer on Peppol.
{
"query": "BE0403170701",
"vat": {
"vat": "BE0403170701",
"valid": true,
"status": "valid"
},
"peppol": {
"registered": true,
"participant_id": "iso6523-actorid-upis::0208:0403170701",
"supports_peppol_bis3_invoice": true,
"source": "directory"
},
"recommendation": {
"format": "peppol-bis3",
"channel": "Peppol",
"create_with": "POST /einvoice/create {\"format\":\"peppol-bis3\"}",
"why": "The company is registered on Peppol for BIS Billing 3.0 invoices."
}
}Same call with the German sample buyer: valid VAT, not registered on Peppol. Recommended format remains xrechnung; channel is e-mail or Peppol.
{
"query": "DE811193231",
"vat": {
"vat": "DE811193231",
"valid": true,
"status": "valid"
},
"peppol": {
"registered": false
},
"recommendation": {
"format": "xrechnung",
"channel": "E-mail or Peppol",
"create_with": "POST /einvoice/create {\"format\":\"xrechnung\"}",
"why": "German businesses must accept EN 16931 e-invoices; XRechnung is the national CIUS."
}
}Check before a live send
POST /einvoice/preflight runs every check a live send runs, without sending: buyer VAT in VIES, Peppol reachability, a sanctions screen of the buyer name (EU, UN, OFAC, UK), and EN 16931 + XRechnung validation on the official KoSIT and CEN rules. With mode=live, a buyer who is not on Peppol is a block. The body can be the same JSON as /einvoice/create, or your own UBL, CII or ZUGFeRD/Factur-X file.
# Same checks a live send runs, without sending anything
curl -s https://api.wsystem.dev/einvoice/sample \
| jq '{invoice: ., mode: "live"}' \
| curl -s -X POST https://api.wsystem.dev/einvoice/preflight \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d @-Response (trimmed, from a real run on 2026-11-12, API 0.3.97): Sample invoice, mode=live: the invoice and VAT pass, Peppol blocks the send.
{
"verdict": "block",
"safe_to_send": false,
"blocking": [
"peppol"
],
"checks": {
"vat": {
"valid": true,
"result": "pass",
"message": "Buyer VAT ID DE811193231 is valid in VIES."
},
"peppol": {
"registered": false,
"result": "block",
"message": "The buyer is not registered on Peppol (Directory and SML/SMP), so there is nobody to deliver a live Peppol invoice to. Ask the buyer for their Peppol ID, or send by another channel."
},
"sanctions": {
"result": "pass",
"message": "The buyer name is not on the EU, UN, OFAC or UK sanctions lists."
},
"invoice": {
"valid": true,
"result": "pass",
"message": "The invoice passes the EN 16931 and XRechnung rules."
}
}
}Refuse the send if they are not on Peppol
On POST /einvoice/send, set recipient_check=strict (live sends are always strict). If the buyer is not registered, the API answers 422 and does not transmit. In the sandbox without strict, the same buyer is a warning and the send is still simulated, which is useful while you wire the flow.
# Refuse the send if the buyer is not on Peppol (nothing is transmitted)
jq '{invoice: ., mode: "sandbox", transport: "mock", recipient_check: "strict"}' invoice.json \
| curl -s -X POST https://api.wsystem.dev/einvoice/send \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d @-Response (trimmed, from a real run on 2026-11-12, API 0.3.97):
{
"error": "recipient_not_on_peppol",
"message": "The buyer is not registered on Peppol (Peppol Directory/SMP), so there is nobody to deliver to; nothing was sent and no Peppol document was counted. Send this invoice by another channel.",
"transmitted": false,
"recipient_check": "strict"
}Then validate the invoice (XRechnung or ZUGFeRD)
A reachable buyer is not enough: the file still has to pass the official rules. If the buyer is on Peppol, create a Peppol BIS 3 or XRechnung invoice and send it. If they are not — the usual case for German B2B today — create or accept an XRechnung or ZUGFeRD file, validate it, and deliver it through your own channel, such as e-mail. POST /einvoice/validate checks UBL, CII and ZUGFeRD/Factur-X PDFs against EN 16931 and, for an XRechnung, KoSIT XRechnung 3.0.2. A failed public example:
curl -s https://wsystem.dev/examples/broken-xrechnung.xml | curl -s -X POST https://api.wsystem.dev/einvoice/validate -H 'Content-Type: application/xml' --data-binary @-valid false, official rule BR-CO-16, with a fix hint.
{
"valid": false,
"profile": "XRechnung",
"error_count": 1,
"errors": [
{
"rule_id": "BR-CO-16",
"message": "Amount due for payment (BT-115) = Invoice total amount with VAT (BT-112) -Paid amount (BT-113) +Rounding amount (BT-114).",
"fix": "Amount due must be total with VAT minus prepaid amount plus rounding."
}
]
}The same create, KoSIT validation and Peppol send flow, written for invoicing software: XRechnung API for invoicing software and, in German, E-Rechnung API für Rechnungssoftware.
Questions
What if the buyer is not on Peppol?
GET /einvoice/route returns peppol.registered=false, and a live preflight or a send with recipient_check=strict is refused with HTTP 422 recipient_not_on_peppol before anything is transmitted. In Germany that is normal: the invoice can still be a valid XRechnung or ZUGFeRD delivered by e-mail. In Belgium, B2B invoices have to go over Peppol, so you need the buyer's Peppol participant id.
Which identifier should I look up?
An EU VAT number (for example DE811193231 or BE0403170701) or a Peppol participant id (for example 0208:0403170701). The API checks the public Peppol Directory first, then the SML/SMP lookup access points use. A company registered under another identifier can still be missed, so ask the buyer for its Peppol id if the answer is no.
Does the check also validate the invoice?
GET /einvoice/route looks up the buyer only. POST /einvoice/preflight also validates the invoice against EN 16931 and, for an XRechnung, the official KoSIT rules, and it accepts a UBL, CII or ZUGFeRD/Factur-X file as the body as well as JSON. Use it when you already have the document and want one verdict before sending.
Do I need a key?
No for a first try: /einvoice/route and /einvoice/validate are free for 20 calls a day per IP. A free API key (work email only, no card) raises the limit to 500 units a month. Live Peppol sending needs a Peppol or Platform plan, a mandate and our KYC check, per company.
Related
- Free Peppol ID lookup by VAT number
- Peppol participant identifier schemes
- XRechnung API for invoicing software
- E-invoicing API for invoicing and ERP software
- Browser e-invoice validator
- Quickstart and API reference (OpenAPI)
Technical check, not legal or tax advice. Mandate dates are curated; confirm with the official source.