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.

shell
# 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.

shell
# 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.

shell
# 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:

shell
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

Technical check, not legal or tax advice. Mandate dates are curated; confirm with the official source.