One REST API to send and receive compliant e-invoices across the Peppol network and 40+ countries.
Certified Access Point, SMP, Schematron validation, PINT support, AS4 delivery — all behind a single
X-API-Key header.
curl -X POST https://app.goroute.ai/peppol-api/api/v1/documents \
-H "X-API-Key: $GOROUTE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: INV-2026-0412" \
-d '{
"receiver_scheme": "0208",
"receiver_id": "0987654394",
"document": "<Invoice xmlns=...>...</Invoice>"
}'
HTTP/1.1 202 Accepted
{
"transaction_id": "1d5c8f92-3b47-4c2e-9a61-0f7d2e8b4c15",
"status": "queued",
"message": "Document queued for delivery"
}
A Peppol API is a web interface, run by a certified Peppol Access Point, that lets your software send and receive e-invoices on the Peppol network with ordinary HTTPS calls. You post an invoice and read back its delivery status. The Access Point handles the AS4 messaging, certificates, validation and receiver discovery that Peppol requires.
Peppol itself has no central sending API. The network is a set of certified Access Points that exchange documents with each other, plus a directory (the SML and the SMPs) that says which Access Point serves which business. OpenPeppol publishes the rules and a public directory you can search, but an invoice only travels from one Access Point to another. So every Peppol API you can send through is an Access Point's own interface, and the questions worth asking are how it authenticates you, how you test against it, how you find a receiver and how it tells you an invoice arrived. The rest of this page answers each of those for GoRoute's API.
Post the invoice to one REST endpoint with an API key. GoRoute's certified Access Point (POP000991) converts it to UBL, validates it against the profile the receiver's country requires, signs it, looks the recipient up in the SMP and delivers it over AS4. Your ERP never speaks Peppol itself.
There are two ways in. If your system already produces a Peppol BIS 3.0 UBL file, send it as a string to
POST /api/v1/documents with the
receiver's scheme and identifier, as in the example above. GoRoute transmits that document exactly as you sent it.
If your system holds invoice data but no UBL, send JSON to
POST /api/v1/invoices and GoRoute
validates it, renders the UBL and queues it. Both return 202 Accepted
with a transaction_id, because delivery
happens afterwards, not during your call.
That is the job of this page. Three neighbouring questions have their own pages: whether to build Peppol into a product you sell is answered on Peppol for software vendors; validating invoices and filing them with a tax authority is invoice validation and tax reporting; and reselling e-invoicing under a commercial agreement is the partner program.
Peppol API integration is the process of connecting a business system — an ERP such as SAP S/4HANA or Microsoft Business Central, an accounting package such as Odoo or Zoho Books, or a SaaS billing platform such as Stripe or Zuora — to the Peppol network via a certified Access Point exposed as a REST API.
Instead of implementing AS4 messaging, G3 PKI certificates, EN 16931 Schematron validation and SMP/SML discovery yourself, you POST a UBL or JSON document to a single endpoint. GoRoute's certified Access Point (POP000991) handles conversion, validation, signing, routing and delivery to any business registered to receive on Peppol, whichever Access Point that business uses.
The same API also covers DBNAlliance in the United States, Finvoice in Finland, and national CTC systems including Fawtara Oman, KSeF Poland, SDI Italy, XRechnung Germany and MyInvois Malaysia (ZATCA Saudi Arabia is in development) — so a single integration unlocks 40+ countries.
POST a finished UBL 2.1 document, or JSON that GoRoute renders to BIS 3.0 or PINT for the receiver's country.
A signed webhook tells you a document arrived; fetch the original UBL XML with one call.
Four-layer validation: UBL XSD → business rules → eDEC code lists → CEN/BIS + PINT Schematron.
Check any Peppol participant and the document types it accepts with GET /api/v1/participants/lookup.
A-NZ, Oman, Japan, Malaysia, Singapore, EU — auto-detected from CustomizationID.
Fawtara (Oman), KSeF (Poland), SDI (Italy), MyInvois (Malaysia), FIRS (Nigeria). ZATCA (Saudi) is in development.
Poll a transaction or subscribe to HMAC-signed webhooks for queued, delivered, retrying, failed and received.
Plain HTTPS and JSON, nothing to install. An OpenAPI 3.0 document and a Postman collection generate a client if you want one.
Every key is bound to one organisation. Ideal for white-label and multi-subsidiary deployments.
Every request carries one API key in the X-API-Key
header. Test keys start sk_test_ and live keys
start pk_live_, so the two cannot be confused in a
config file. A key belongs to exactly one organisation, and every call it makes is scoped to that organisation: there is no
organisation ID to pass.
Create a key in the dashboard under Settings → API Keys, or with
POST /api/v1/api-keys once you hold one. The secret is
shown once, in the response that creates it, and never again. Each key carries scopes, so an integration that only sends
invoices does not also need the power to manage webhooks:
| Scope | What it allows |
|---|---|
| send | Send documents over Peppol |
| read | Read transactions, participants and lookups |
| write | Create and update participants, delete transactions |
| admin | Manage webhooks and API keys; satisfies every other scope |
A new key from the API gets send and
read unless you ask for more. Keys can carry an
expiry date, or none. To rotate without downtime, create the new key, deploy it, confirm it works, then revoke the old one with
DELETE /api/v1/api-keys/{id}. Revocation is permanent.
Errors name their cause in a top-level error_code:
MISSING_API_KEY,
INVALID_API_KEY or
EXPIRED_API_KEY with HTTP 401,
INSUFFICIENT_SCOPE with 403, and a 429 with a
Retry-After header when you pass your plan's rate
limit. Limits apply per organisation, not per key. The full rules are in the
authentication guide.
Use the test environment. It is a separate host, on the Peppol test network, where nothing you send is a real invoice and nothing is billed. Its data is separate too: an organisation or participant created in test does not exist in production. The free 14-day developer sandbox gives you a test key and a registered test participant, provisioned within 24 hours on business days, with no credit card.
First, ask the host which environment it is. GET /health
needs no key and answers "environment": "test" or
"production". Then send to the fixed sandbox
receivers. Each one always behaves the same way, so you can prove your code handles success and each kind of failure:
| Receiver | Outcome |
|---|---|
| 9999:test-success | Delivered |
| 9999:test-recipient-not-found | Failed, RECIPIENT_NOT_FOUND |
| 9999:test-as4-failure | Failed, AS4_DELIVERY_FAILED |
| 9999:test-validation-failure | Failed, VALIDATION_FAILED |
| 9999:test-timeout | Retrying, then failed, TIMEOUT |
test-timeout is the one to test last: it moves
through retrying several times before it settles,
which is where duplicate records on your side usually show up. A sandbox receiver sent from production is refused with
SANDBOX_NOT_ALLOWED, so a misconfigured
deployment is never handed a fake success.
The sandbox simulates delivery; it does not apply a country's invoice rules. To check a document against those, call the validation endpoint. The complete list of receivers is on Test Identifiers, and what OpenPeppol's own testbed is for is explained in Peppol test bed explained.
Apply on the developer page. A test key and a test Peppol participant are provisioned within 24 hours on business days — no credit card.
Send UBL to /api/v1/documents or JSON to /api/v1/invoices. Validation errors come back on the call, so you fix the mapping before going live.
Request production access from the dashboard; the live key usually follows within 24 hours. Register your production participant, switch keys and exchange real invoices over AS4.
Call GET /api/v1/participants/lookup with the
identifier as one value in scheme:value form. The
answer says whether the network confirmed the participant, its country, and the document types it can receive. Check that
list before sending a credit note or an order, not only an invoice.
curl "https://app.goroute.ai/peppol-api/api/v1/participants/lookup?peppol_id=0208:0987654394" \
-H "X-API-Key: $GOROUTE_KEY"
{
"found": true,
"participant_id": "0208:0987654394",
"country": "BE",
"capabilities": [
"urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
"urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
]
}
Read "found": false as "not confirmed", not as
"not registered". The same answer comes back when a directory fails to respond, so treat it as a reason to hold and retry,
not as proof the customer's identifier is wrong. A lookup that is not found is not an HTTP error: it answers 200.
Two related calls cover what this one leaves out. GET /api/v1/lookup/participant
returns the business name and the Access Point behind each document type. GET /api/v1/public/lookup/{scheme}/{identifier}
needs no key at all, for a web page that shows a result inline. If you are not sure which identifier a customer is
registered under, which Peppol identifier is yours
explains the schemes country by country.
Every send returns 202 with status queued and a
transaction_id. From there, either poll
GET /api/v1/transactions/{transaction_id} or let a
webhook tell you. There is no synchronous send mode, so do not write code that waits on the first response. A transaction
moves through these states:
| Status | Meaning |
|---|---|
| queued | Accepted by the API, waiting to be processed |
| submitted | Handed to the AS4 sender |
| accepted | The receiving Access Point acknowledged the AS4 message |
| delivered | Final success |
| retrying | A temporary failure; GoRoute will try again |
| failed | Terminal failure, with an error code saying why |
To be told rather than poll, register a URL with POST /api/v1/webhooks
and choose events. The ones a sender needs are transaction.queued,
transaction.delivered,
transaction.retrying and
transaction.failed. A failed event carries the
error_code, the message and the retry count.
Each delivery is signed. The X-Webhook-Signature header holds a
timestamp t and an HMAC-SHA256 value
v1, computed with your webhook secret over the timestamp,
a full stop and the raw request body. Verify the raw bytes before parsing the JSON, or the digest will not match.
Retries are safe if you send an Idempotency-Key header:
a repeat within 24 hours returns the first transaction_id
and its current status, and nothing is sent twice. Field-by-field detail is in the
webhook events reference. If a shop
platform rather than your own code reports a failure, start with
what to do when an e-invoice was not delivered.
When a document addressed to you arrives from the Peppol network, GoRoute posts a transaction.received event to your webhook. The event identifies the document; fetch the original UBL XML with GET /api/v1/transactions/{transaction_id}/document and archive it as your jurisdiction requires.
POST https://your-app.example.com/hooks/peppol
X-Webhook-Signature: t=1791797462,v1=1f8ac10f23c5...
X-Event-Type: transaction.received
Content-Type: application/json
{
"id": "evt_4d5e6f708192a3b4c5d6e7f809122334",
"event_type": "transaction.received",
"created_at": "2026-10-12T09:31:02.123456",
"data": {
"transaction_id": "550e8400-e29b-41d4-a716-446655440000",
"direction": "received",
"status": "delivered",
"sender_peppol_id": "0208:0123456749",
"receiver_peppol_id": "0208:0987654394",
"document_type": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##...",
"message_id": "phase4-msg-abc123",
"received_at": "2026-10-12T09:31:01+00:00"
}
}
Since 1 January 2026, a Belgian VAT-liable business invoicing another must send a structured e-invoice in Peppol BIS Billing 3.0 over Peppol, and every such business must be able to receive one, as the Belgian government's e-invoicing site sets out. Belgian companies are usually addressed by their enterprise number (KBO/BCE) under Peppol scheme 0208; the VAT number under scheme 9925 is also in use. No Belgian accreditation is needed beyond Peppol certification. Here is the whole flow through the API.
Look the buyer up as 0208: plus its ten-digit
enterprise number, as in the lookup example above. If 0208 is not confirmed, try the buyer's VAT number under 9925 before
concluding anything. The enterprise number has a mod-97 check on its last two digits, so validate it in your own system before
you call. The numbers on this page pass that check but are illustrations: replace them with your own and your customer's.
curl -X POST https://app.goroute.ai/peppol-api/api/v1/invoices \
-H "X-API-Key: $GOROUTE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: BE-2026-0412" \
-d '{
"invoice": {
"document_type": "invoice",
"number": "BE-2026-0412",
"issue_date": "2026-10-12",
"due_date": "2026-11-11",
"currency": "EUR",
"seller": {
"name": "Voorbeeld NV",
"peppol_id": "0208:0123456749",
"vat_number": "BE0123456749",
"address": { "street": "Koningsstraat 1", "city": "Brussel",
"postal_code": "1000", "country": "BE" }
},
"buyer": {
"name": "Klant BV",
"peppol_id": "0208:0987654394",
"vat_number": "BE0987654394",
"address": { "street": "Meir 10", "city": "Antwerpen",
"postal_code": "2000", "country": "BE" }
},
"lines": [
{ "description": "Consulting, October 2026", "quantity": 10,
"unit": "HUR", "unit_price": 100.00,
"tax_rate": 21, "tax_category": "standard" }
],
"totals": { "net_amount": 1000.00, "tax_amount": 210.00,
"payable_amount": 1210.00 }
}
}'
GoRoute validates the data, renders a Peppol BIS Billing 3.0 UBL invoice and answers 202 with
"status": "queued". To check the same body without
sending it, post it to /api/v1/invoices/validate first.
Belgium's reduced rates of 12% and 6% go in as the standard category with the lower percentage; they are not separate codes.
The transaction.delivered webhook, or a poll that reads
delivered, closes the loop. One Belgian rule is
enforced for you: asking GoRoute to deliver a Belgian seller's invoice by email instead is refused with
EMAIL_NOT_ALLOWED, because a PDF by email no longer
counts between Belgian businesses.
For the mandate itself, see Belgium e-invoicing compliance. For why 0208 matters more than the VAT number, read the Belgian enterprise number on Peppol.
Every outbound invoice passes through four sequential validation layers. A Schematron error rejects the document before AS4 transmission; only warnings are allowed to pass.
Structural validation of the OASIS UBL 2.1 schema using lxml.
Cross-field checks on totals, line calculations, tax bases, and currency consistency.
Participant schemes, document types, process IDs and country codes — synced from OpenPeppol.
CEN-EN16931-UBL + PEPPOL-EN16931-UBL baseline for EU flows, or PINT OM / A-NZ / SG / MY / JP / EU for non-EU jurisdictions, compiled to XSLT via Saxon-HE 12.4.
From the GoRoute dashboard under Settings → API Keys, or with POST /api/v1/api-keys once you hold a key. Test keys start sk_test_ and live keys start pk_live_. The secret is shown once, when the key is created.
Yes. The developer sandbox is free for 14 days and needs no credit card. It gives you a test key and a registered test participant, provisioned within 24 hours on business days, plus fixed 9999:test- receivers that simulate success and each kind of failure.
Poll GET /api/v1/transactions/{transaction_id}, or subscribe to the transaction.delivered, transaction.retrying and transaction.failed webhooks. A send always starts as queued; delivered is final success and failed is terminal, with an error code.
Usually its ten-digit enterprise number (KBO/BCE) under scheme 0208, written as 0208:0123456749. The VAT number under scheme 9925 is also in use, so if a 0208 lookup is not confirmed, try 9925 before concluding the company is not on Peppol.
At docs.goroute.ai: a quickstart, the authentication and environment guides, an endpoint reference generated from the OpenAPI 3.0 document, a Postman collection, and a webhook events reference.
Typical REST-API integrations are live in 1–2 weeks. Full ERP connector deployments (SAP, Microsoft, Oracle) take 2–4 weeks. The sandbox is provisioned within 24 hours on business days.
An Access Point (AP) sends and receives AS4 messages. An SMP (Service Metadata Publisher) stores each participant's capability profile so other APs can discover where to route messages. GoRoute operates both a certified AP and a certified SMP under seat POP000991.
Yes — each legal entity needs its own participant ID (typically based on VAT number, ABN, NZBN, UEN, etc.). GoRoute registers and maintains your participant ID in the SMP and SML as part of the integration.
Yes. Our Hosted Infrastructure product deploys a complete AP + SMP under your brand, with your own G3 certificates and domain, in about four weeks.
Limits apply per organisation and come from the plan: 120 requests a minute and 5,000 a day on the free, starter and professional plans, 300 and 20,000 on business, and 1,000 and 100,000 on enterprise. The developer sandbox allows 30 a minute. A key's own limit can only lower its plan's.
In encrypted PostgreSQL on AWS (ISO 27001 regions). Retention matches the sender's legal jurisdiction (typically 5–10 years). GDPR and HIPAA compliant.
SAP (S/4HANA, Business ByDesign, ECC), Microsoft (Dynamics 365, Business Central), Oracle, Odoo, Sage, Zoho, Certinia, Stripe, Zuora and Tally.
Start with the documentation, or talk it through with us. Free 14-day sandbox. Certified Access Point. Global coverage.
Prefer to watch first? The tutorials show recorded ERP integrations end to end. Next read: how to choose a Peppol Access Point, or one API for multi-country e-invoicing. Plans and per-invoice prices are on the pricing page.