Sending and receiving Peppol documents in other countries for the companies you onboard

Getting started with Peppol e-invoicing for companies in other countries you onboard from your own software platform: register and verify the company, send Peppol invoices and credit notes, pull incoming documents through webhooks, go live. Around thirty other countries work the same way: Peppol BIS 3 UBL on Recommand's own access point, with that country's own identifier schemes.

This guide walks through everything needed to exchange Peppol documents for a company registered in a country other than Belgium, France or the Netherlands, assuming you are integrating Recommand into your own product and onboarding companies in other countries as your users, and that the company sends and receives documents over Peppol. Change any of the three answers at the top of the page to get the guide for a different situation.

What you are building

You are onboarding companies that are not your own: one Recommand team, and a company inside it for every customer you put on the network. Your customers never need a Recommand account โ€” they see your interface, and Recommand stays behind your API calls.

That shape has a few consequences worth knowing before you write code:

  • One team, many companies. There is no limit on companies per team, and billing is per team with the document volume of all companies pooled together, so the more companies you onboard the lower your price per document. See pricing per team.
  • Every company is registered and verified individually. Peppol identifies companies, not platforms. Each company gets its own Peppol address and its own authorisation record.
  • Verification is taken care of. Recommand hands you a URL that the company's authorised representative opens to confirm their identity. You present or forward that link; you never need to handle identity documents yourself. If you prefer to handle verification yourself, reach out to us at support@recommand.eu, we have a few other flows we can set up for you.
  • You can run the whole flow under your own brand. The API is designed for white-label use, see can I whitelabel Recommand.

The Recommand dashboard shows the same teams, companies and documents your API calls produce, which is the quickest way to see what a customer is looking at while you are debugging.

Only setting up your own company?

If the only company you will register is your own, switch the first answer above to One company for the shorter version of this guide. The endpoints are the same; there is simply less to organise.

Peppol internationally

Each country carries one of three support levels:

LevelWhat it means
SupportedCompanies work end to end, and the country's own identifier schemes are registered for you.
PartialCompanies are functional, but country-specific features or local requirements may not be covered yet.
UnsupportedNew companies cannot be created there, and existing companies cannot be switched to it.

The countries page on our website is the source of truth: it shows the level for the country you select.

Missing a country?

Support is added country by country. If the one you need is unsupported, or supported only partially in a way that blocks you, mail support@recommand.eu โ€” knowing there is demand is what moves a country up the list.

Create your team and API credentials

  1. Sign up at app.recommand.eu/signup. The team you get is the container for every company you will onboard.
  2. Create an API key on the API keys page. Note the key, the secret and your team ID.
  3. Check the credentials with a request that needs no data of its own:
curl -X GET https://app.recommand.eu/api/v1/companies \
  -u key_xxx:secret_xxx

All endpoints in this guide live under https://app.recommand.eu/api/v1 and accept HTTP Basic authentication with the key as username and the secret as password. If you would rather not store a long-lived secret, JWT API keys and OAuth2 with a JWT assertion are available too, see the authentication guide.

Try it safely first

Build the whole flow against a playground team before you touch production. Playgrounds look and behave like production teams, but nothing is delivered over the real Peppol network, there are no SMP registrations, no subscription checks and no billing.

Create one from the team switcher at the top of the dashboard: Add playground, give it a name, and you are switched into it. There is no limit on how many you create.

Everything that follows in this guide is identical there: same endpoints, same validation, same webhooks (triggered by simulated inbound delivery). Register a company in the playground and use it as both sender and recipient to see a document arrive.

Three things worth knowing before you start:

  • POST /:companyId/generate takes the same body as the send endpoint and returns the exact XML that sending would produce, fully validated, without transmitting, storing or billing it. It also returns the resolved documentType, doctypeId and processId, which is the quickest way to check that your country-specific fields map to the format and process you expect.
  • Failure addresses. In a playground that is not connected to the Peppol Test Network, sending to 404:404 or 0208:1234567894 always fails, so you can exercise your error handling. Any other unregistered recipient is skipped without an error.
  • The Peppol Test Network. For genuine end-to-end tests with real counterparties, tick Use Peppol Test Network when you create the playground. It then uses dedicated test access point and SMP endpoints while staying fully separated from production. The setting cannot be changed after creation, so make a second playground if you want both.

More detail in the getting started guide and how do I use the playground environment.

Register the company

Create one company per customer with the create company endpoint. Registration on the Peppol network happens as part of this call: identifiers and document types are set up for you, based on the company's country.

const auth =
  "Basic " + Buffer.from("key_xxx:secret_xxx").toString("base64");

const response = await fetch("https://app.recommand.eu/api/v1/companies", {
  method: "POST",
  headers: { Authorization: auth, "Content-Type": "application/json" },
  body: JSON.stringify(company),
});

const result = await response.json();
if (!result.success) throw new Error(JSON.stringify(result.errors));

const companyId = result.company.id;
const verificationUrl = result.verificationUrl; // hand this to your user
curl -X POST https://app.recommand.eu/api/v1/companies \
  -u key_xxx:secret_xxx \
  -H "Content-Type: application/json" \
  -d @company.json

The response carries a verificationUrl straight away. Keep it: the next step is to put it in front of the company's representative.

Only register companies you are allowed to act for

Register the companies that use your platform, not the companies they invoice. Customers and suppliers manage their own Peppol registration; adding them causes registration conflicts. See managing companies.

The exact identifier fields to send depend on the country, which is what the next section covers. If you would rather create identifiers and document types yourself instead of accepting the country defaults, pass skipDefaultCompanySetup: true and use the company identifiers and company document types endpoints.

Identifiers and the Peppol address

Three fields decide how the company is published on the network:

FieldValue
countryISO 3166-1 alpha-2, e.g. "DE", "SE", "IT"
enterpriseNumberThe national company or organisation number
vatNumberThe VAT number, starting with the same country code
company.json
{
  "name": "Exempel AB",
  "address": "Storgatan 1",
  "postalCode": "111 51",
  "city": "Stockholm",
  "country": "SE",
  "enterpriseNumber": "5567321707",
  "vatNumber": "SE556732170701"
}

A VAT number whose country code does not match country is rejected โ€” that is the one identifier rule enforced for every country. National format checks (the modulo-97 check digit for Belgium, the KVK length for the Netherlands, the CVR length for Denmark) only exist for the countries that have them; elsewhere the number is taken as given, so check it before you send it.

There is also an optional enterpriseNumberScheme. It does not decide which Peppol identifier is registered โ€” the country does, as the next section shows โ€” but it is written into the documents the company sends, as the scheme of the seller's legal identifier. Leave it out unless the country expects a specific one.

Which schemes are registered for you

Each country has its own Peppol Electronic Address Scheme, and Recommand tries to automatically register the correct scheme for you.

No identifier means no sending

The Peppol address is also what a document is sent from. A company with no identifier at all cannot send: the attempt fails with no sending company identifier found, and it cannot be published as a recipient either, because there is no address to publish. In some countries, adding an identifier yourself is part of onboarding rather than an optional extra.

Adding identifiers by hand

Whatever the defaults do, the full set is yours to manage: list identifiers, add one, update one or remove one. Every identifier you add is registered in the SMP as another address the company can be reached on.

curl -X POST https://app.recommand.eu/api/v1/{companyId}/identifiers \
  -u key_xxx:secret_xxx \
  -H "Content-Type: application/json" \
  -d '{"scheme":"0007","identifier":"5567321707"}'

This is also the escape hatch for a country with no defaults, and for the case where your customers know a company by a different identifier than the one registered for it.

Registering for both directions

Sending needs no registration of its own; receiving does. So register the company as a recipient, which is the default:

{
  "isSmpRecipient": true
}

What that means:

  • The company becomes findable on the Peppol network and can be delivered to through Recommand's access point, while sending its own documents out through the same access point.
  • Recipient registration is exclusive. If the company already receives through another Peppol provider, registration fails until it is deregistered there. What that takes depends on the country, which the next section covers.
  • The document types the company accepts are registered along with it, based on its country. Which ones those are is covered further down.
  • The company is only published once it is verified.

Sending first, receiving later

If the company still receives elsewhere and you do not want to move that yet, register it with isSmpRecipient: false and start with sending only. Flipping the field later publishes it as a recipient.

Moving an existing registration

Recipient registration is exclusive everywhere: a company that already receives through another Peppol provider has to be deregistered there before it can be registered with Recommand.

If you do not know who the current provider is, look the company up as a recipient: the verify endpoint returns the SMP it is published on, which names the provider to ask.

Verify the company

A company cannot exchange documents on Peppol until an authorised representative has confirmed their identity. The company object exposes this as isVerified, and it stays false until the check is done.

The flow is short:

  1. Open the verification URL (from the create-company response, the dashboard, or a fresh one from the verify company endpoint).
  2. The representative fills in their name and completes the identity check.
  3. isVerified flips to true and the company is published on the Peppol network.

In some cases manual verification by our team will be required. If that's the case, isVerified will remain false until this manual verification is completed. The user is informed of this in the verification process.

Building verification into your onboarding

With one company you would click through this once. With many, verification is part of the flow you build: every company you register needs its own, and it is the step most likely to leave a customer stuck halfway.

Show the URL immediately. The create-company response already carries verificationUrl โ€” no extra call needed. Put it in front of the user while they are still in your onboarding.

Ask for a fresh one when the moment has passed. Links get lost, and companies you created earlier never had one shown. The verify company endpoint starts a new verification session:

curl -X POST https://app.recommand.eu/api/v1/companies/{companyId}/verify \
  -u key_xxx:secret_xxx

The company.verification webhook fires when verification reaches a final state: verified, rejected or error. A company that comes back rejected or error and is not surfaced anywhere sits silently unusable. See working with webhooks.

Respect isVerified in your own UI. Do not let a user press send for a company that is not verified yet; this will result in an error. You should inform the user what is missing instead.

Re-verify after identifier changes. Updating a company's vatNumber or enterpriseNumber resets isVerified to false. Check the field after an update and present a new verificationUrl if it flipped.

The full mechanics are in the company verification guide.

Pick the document format

Send Peppol BIS 3 UBL, over the standard Peppol billing process. It is the format most Peppol participants accept.

Some countries have a national CIUS on top of EN 16931 โ€” SI-UBL 2.0 in the Netherlands, for instance. If a buyer asks for one and Recommand supports it, you name its document type in doctypeId on the send request; the document you post stays the same, because the format decides how the XML is written, not which fields you fill in.

Before a first send to a new recipient, check two things:

  • Is the recipient on the network? The verify endpoint answers that, and sending performs the same check automatically.
  • Does the recipient accept this document type? The verify document support endpoint answers that, which matters more abroad than at home: a recipient in another country may be registered for its own national profile only.

See verifying recipients for the full flow, including what to do when a recipient cannot be reached.

Send a document

One endpoint sends everything: POST /:companyId/send. The companyId is the sender, recipient is the Peppol address of the receiver, and document is the invoice or credit note as JSON. Recommand will validate the document and generate the XML.

Already producing UBL or CII XML?

Raw XML sending is supported as well: set documentType to xml and pass the document string in document, along with the correct doctypeId. See working with raw UBL.

const response = await fetch(
  `https://app.recommand.eu/api/v1/${companyId}/send`,
  {
    method: "POST",
    headers: { Authorization: auth, "Content-Type": "application/json" },
    // The body below, with doctypeId and countrySpecific where the country
    // needs them.
    body: JSON.stringify(sendRequest),
  }
);

const result = await response.json();
if (!result.success) {
  // result.errors is keyed by field path, e.g. { "buyer.vatNumber": [...] }
}
curl -X POST https://app.recommand.eu/api/v1/{companyId}/send \
  -u key_xxx:secret_xxx \
  -H "Content-Type: application/json" \
  -d @send-invoice.json
send-invoice.json
{
  "recipient": "0208:0123456789",
  "documentType": "invoice",
  "document": {
    "invoiceNumber": "INV-2026-001",
    "issueDate": "2026-08-17",
    "dueDate": "2026-09-16",
    "currency": "EUR",
    "buyer": {
      "name": "Customer Company",
      "street": "Customer Street 1",
      "city": "Antwerp",
      "postalZone": "2000",
      "country": "BE",
      "vatNumber": "BE0987654321"
    },
    "paymentMeans": [{ "iban": "BE68539007547034" }],
    "lines": [
      {
        "name": "Consulting Services",
        "quantity": "10.00",
        "unitCode": "HUR",
        "netPriceAmount": "100.00",
        "vat": { "category": "S", "percentage": "21.00" }
      }
    ]
  }
}

The seller block is filled in from the company when you leave it out, which is usually what you want: it keeps the company's registered identifiers and the document in agreement.

Things worth wiring up while you are here:

  • Validation errors. Outgoing documents are always validated. A success: false response with errors keyed by field path is a document your recipient would have rejected, so surface it to the user who typed the data.
  • Email fallback. Pass email.to with when: "on_peppol_failure" to fall back to email when a recipient turns out not to be reachable over Peppol, or send with recipient: null for email-only delivery. See email delivery and notifications.
  • PDFs. pdfGeneration.enabled attaches a generated PDF of the document. You can also attach an existing PDF (or other files) via attachments; see adding attachments.

For a full field reference, see sending invoices and sending credit notes.

Document types registered for you

When you register the company as a recipient, it is published for the two document types that carry almost all cross-border traffic:

Document typeProcess
Invoice (Peppol BIS 3 UBL)urn:fdc:peppol.eu:2017:poacc:billing:01:1.0
Credit note (Peppol BIS 3 UBL)urn:fdc:peppol.eu:2017:poacc:billing:01:1.0

Anything sent to the company in one of these formats is accepted, validated, stored and handed to you. A sender who tries a document type the company is not published for gets an error before delivery, which is exactly the point of the registration.

Need more document types, such as a national CIUS your suppliers use, self-billing, message level responses, invoice responses? Register the combinations you want with the create company document type endpoint or through the dashboard.

Get incoming documents into your product

Recommand receives, validates and stores incoming documents for every company in your team. You pick them up in one of two ways.

Register an endpoint once with the create webhook endpoint or through the dashboard and events are pushed to you as they happen, document.received among them:

await fetch("https://app.recommand.eu/api/v1/webhooks", {
  method: "POST",
  headers: { Authorization: auth, "Content-Type": "application/json" },
  body: JSON.stringify(webhook), // the body below
});
curl -X POST https://app.recommand.eu/api/v1/webhooks \
  -u key_xxx:secret_xxx \
  -H "Content-Type: application/json" \
  -d @create-webhook.json
create-webhook.json
{
  "url": "https://your-app.example/webhooks/recommand",
  "companyId": null,
  "secret": "your_webhook_signing_secret"
}

companyId: null covers every company in the team.

Pass a secret and verify the HMAC SHA-256 signature on every delivery before you trust the payload. Switch on event.eventType, and acknowledge with a 200 before doing the heavy processing. Both are covered in working with webhooks.

Polling the inbox

If you would rather pull, the inbox endpoint lists unread documents:

const inbox = await fetch("https://app.recommand.eu/api/v1/inbox", {
  headers: { Authorization: auth },
}).then((r) => r.json());

Mark each document as read with the mark as read endpoint once your system has it, so it drops off the list.

After you have the document

For the full picture, including retries and idempotency, see receiving documents.

Going live

Before you switch your first real customer over, walk this list:

  • A valid subscription in production. Playgrounds skip the subscription check; production does not.
  • Verification handled in your UI. Show the verificationUrl at the right moment, make it forwardable, and handle the company.verification webhook so a company that comes back rejected or error does not sit silently unusable.
  • isVerified respected. Do not let a user press send for a company that is not verified yet; explain what is missing instead.
  • Webhook endpoint hardened. Signature verification, a fast 200, retries and idempotency on your side.
  • Errors surfaced, not swallowed. Validation errors name the field that is wrong; put that in front of the person who can fix it.
  • One real document, end to end. Send an invoice between two companies you control on production before letting customers in.

Volume and pricing

Documents are counted per team, with the volume of all your companies pooled, so onboarding more companies lowers your price per document rather than adding per-company fees. Generated XML through generate is not billed; emails and submitted reports are.

Where to get help

  • Troubleshooting. Common rejections, delivery failures and validation errors are collected in the troubleshooting guide.
  • Questions. The FAQ covers Peppol, addressing, VAT and billing.
  • Email. support@recommand.eu:include the company ID and, for a delivery problem, the document ID.
  • Discord. Join the server for release announcements and quick questions.
  • Keep track of changes. New endpoints and behaviour are recorded in the changelog.