Skip to content
einvoicing.dev

The guideChapter 06

Sending and receiving

Getting a document onto the network through somebody else's Access Point, what that costs, and what receiving obliges you to do.

Everything so far has been about producing a document. This chapter is about the moment it leaves your system, which is the part that looks hardest and turns out to be the smallest.

You are buying a corner

Chapter 2 laid out the four corners. Corner 2, the sending Access Point, is a service you buy. Nobody sensible builds it.

The reason is not difficulty, it is obligation. An Access Point runs AS4 with certificates and signed receipts, tests interoperability against every other provider, and answers when somebody sends a document at three in the morning. OpenPeppol’s own fees for a small organisation are around €1,850 a year for an Access Point plus about €1,500 for certification, and roughly €2,750 plus €2,500 if you want to run an SMP as well. From 1 July 2027 an ISO 27001 certification is mandatory for providers. None of that buys you a feature your customers will notice.

So the shape of the work is: produce a valid document, look up the receiver, hand the document to a provider over HTTPS, and deal with what comes back.

What it costs

Published list prices, at the time of writing, for the providers a UK developer is most likely to meet:

  • peppol.sh: from about €0.10 per document. Their SDK is TypeScript only, though the API is plain HTTP.
  • e-invoice.be: about €0.25 per invoice.
  • Storecove: around €495 a month, which I have not been able to verify directly.

Per-document pricing is the norm, and it is worth doing the arithmetic early. At ten thousand invoices a month, ten cents a document is a thousand euros a month, which is a different conversation with your finance team than a subscription. It also means that sending an invalid document twice costs twice, which is one more argument for validating before you send.

None of those providers cares what your document says. They carry it. Whether it is correct is your problem, and remains your problem after delivery.

Sending, concretely

Every provider’s API differs in detail and is the same in shape. You post the document, the sender and receiver identifiers, and the document type. You get back an identifier for the transmission.

POST /v1/documents HTTP/1.1
Host: api.your-access-point.example
Authorization: Bearer <key>
Content-Type: application/json

{
  "sender": "9932:GB123456789",
  "receiver": "9932:GB987654321",
  "document_type": "invoice",
  "document": "<base64 UBL>"
}

Two things about that call surprise people.

It is asynchronous. The response tells you the provider accepted the document, not that the receiver got it. Delivery happens afterwards, and the receipt that proves it arrives later still, usually as a webhook. Anything in your UI that says “sent” on the strength of a 200 is telling a small lie; the honest states are accepted, delivered, and failed.

It can fail after acceptance. A receiver’s Access Point can reject a document that your provider was happy to carry, most often because the receiver does not accept that document type, or because their SMP entry changed between your lookup and your send. Your code needs somewhere to put that, and somebody to tell.

Because the transport is asynchronous, keep the transmission identifier your provider returns and store it against the invoice. When a document goes missing, that identifier is the only thing that lets a provider’s support team find it.

Receiving is a registration, not an endpoint

Sending is something you do. Receiving is something you have to be set up for, and this is the part most people discover late.

To receive, your participant identifier has to be registered in an SMP, and that SMP entry has to say which document types you accept and which Access Point sits in front of you. Your provider does that registration on your behalf. Until it exists, you are invisible: somebody trying to send you an invoice gets the same answer as for a business that has never heard of Peppol.

Three consequences.

Registration takes real time. It is paperwork and a provider’s process, not an API call you make on the afternoon you need it.

Your registration says what you accept. Registering for invoices does not mean you receive credit notes or orders. The list in your SMP entry is a promise, and senders check it, which is exactly the lookup from chapter 3.

Moving providers means moving the registration. Your identifier stays yours, but the SMP entry and the SML pointer have to be repointed, and there is a window during which senders may cache the old answer. Plan a migration, do not just switch.

On the inbound side your provider hands you documents, normally by calling a webhook you expose. What arrives is the same UBL you would have sent: you validate it, map it into your own model, and acknowledge it. Peppol guarantees that a document was delivered. It does not guarantee you like it: disputing an invoice is a commercial conversation, not a protocol message.

What stays yours

Handing sending to a provider does not hand over responsibility. Four things stay on your side of the line.

The document is yours. If it is wrong, it is wrong when it arrives, and a rejection at the receiving end is slower and more embarrassing than a failed test.

The identifier is yours. Sending a perfectly valid invoice to the wrong participant is a data protection incident, not a bug. Look it up, and check the document type.

The record is yours. Your VAT record-keeping obligations do not change because the invoice is now XML. The document you sent is the record, and the one to keep, exactly as it was sent.

Duplicates are yours. Nothing in the network stops you sending the same invoice twice, and the receiver’s system may well post it twice. If your send path can retry, give each invoice a stable identifier and make the retry idempotent on your side.

Where this leaves the code

A sensible integration has three parts. A writer that turns your data into a valid document, which is chapter 4. A validator you run before anything leaves, which is chapter 5. And a thin adapter for one provider’s API, behind an interface of your own, because provider APIs differ in ways that have nothing to do with Peppol and you will want to change provider one day without touching anything else.

That last file should be the most boring in your codebase, and the next chapter builds those three parts in Laravel and Go.

Checked against its sources on 18 September 2026

Get the rest by email

One email when the guide is finished, and an explainer on Budget day.

Developer docs

Getting a key, validating a document, and the API reference.