Over the API
Send a document, collect what arrives, with HTTP calls.
Your key goes in X-API-Key on every request. Keys start with pap_.
curl -sS -H "X-API-Key: $KEY" https://api.businessgateway.se/v1/accountThat call is worth making first: it tells you the account and company your key belongs to, which is a quick way to confirm the key works before you build anything on it.
Keep the key on a server. It is not safe in a browser, a mobile app or anything else a customer of yours can read out.
Sending a document
curl -sS -X POST https://api.businessgateway.se/v1/messages \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"sender": "0007:5561234567",
"receiver": "0007:5569876543",
"type": "invoice",
"payload": "<?xml version=\"1.0\"?>…"
}'sender is your Peppol address, receiver is your trading partner's, payload is the document XML as a string, and type says what kind of document it is — invoice, credit-note, order. The full list is in Document types.
Documents larger than about 1 MB are uploaded separately: POST /v1/messages/uploads gives you a URL to PUT the bytes at, then you submit with payloadObjectKey instead of payload. The ceiling is about 40 MB.
Always send an Idempotency-Key
Idempotency-Key is a value you make up — a UUID is fine — that makes it safe to retry. If the same key arrives again with the same document, you get the original back instead of a second copy. If your network drops the response and you retry without one, your customer receives the invoice twice.
What happens after you submit
You get 202 back with a document id, and a status of QUEUED_VALIDATION. That is not "valid". It means we accepted the document and will now check it against the Peppol rules.
The check takes a moment, which is why it does not happen inside your request. Read the verdict on the document:
curl -sS -H "X-API-Key: $KEY" \
"https://api.businessgateway.se/v1/messages/$ID"| Status | What it means for you |
|---|---|
QUEUED_VALIDATION | Accepted, being checked. If the checker is briefly down, the document waits here rather than failing |
VALIDATION_FAILED | Rejected. validationErrors lists what failed — fix the XML and send again |
VALIDATION_ERROR | The check ran but its answer could not be read. Not your document's fault; we pick these up |
VALIDATION_UNAVAILABLE | There are no rules to check this document type against, and its capability does not allow sending it unchecked. The send was refused with 400 |
UNVALIDATED | A document type with no rules to check, sent under a capability that allows that. Waiting to go out |
QUEUED | Passed, waiting to go out |
SENDING / SEND_RETRY | On its way, or being retried |
AS4_ACCEPTED | Delivered to the recipient's provider. This is success |
DISCOVERY_FAILED | Terminal. The recipient's address could not be found on the network, or is not registered for this document type |
REJECTED | Terminal. The recipient's provider refused the delivery |
SEND_FAILED | Terminal. lastErrorMessage says why |
A SEND_FAILED document with deliveryUnconfirmed: true is one where we could not learn whether the delivery went through — the recipient may well have it. It is never resent automatically, and you should not resend it either without checking with the recipient first.
You hear about AS4_ACCEPTED and the terminal failures — a failed validation included — on a webhook. A failed validation is also emailed to your company's notification contacts, and its findings show on the document.
AS4_ACCEPTED is as far as Peppol's guarantee goes: the recipient's access point has the document. What their system made of it may come back afterwards — see the next section.
Did the recipient accept it?
Every document we send asks the recipient's provider for a Message Level Status: a short answer about whether the document reached the recipient and was accepted. When it comes back, GET /v1/messages/{id} carries it as recipientStatus:
recipientStatus | Peppol code | Meaning |
|---|---|---|
CONFIRMED | AP | Delivered, and the recipient confirmed it |
DELIVERED | AB | Delivered, without further confirmation |
REJECTED | RE | Refused. recipientStatusReasons says why |
| Reason | Meaning |
|---|---|
SV | Syntax violation — the document does not follow the format |
BV | Business rule violation — a rule the document must satisfy was broken |
BW | Business rule warning — reported alongside, not on its own a reason to refuse |
FD | Failure of delivery — the provider could not hand it on to the recipient |
No recipientStatus means no answer, not a failure. Not every provider on the network sends one, and an answer can take a while to arrive. status stays AS4_ACCEPTED either way — a recipient status is added to the document, it does not replace its status.
The full answer is in mlsEvents, oldest first. The entry from the recipient's provider has direction: "INBOUND" and status: "RECEIVED", and carries responseCode, description, issues — each {location, reasonCode, description}, pointing at the part of the document concerned — and receivedAt. Older documents may also have entries with direction: "OUTBOUND"; they carry none of these fields and can be ignored.
The same answer arrives as a message.mls webhook the moment it comes in.
Checking a document before you send it
POST /v1/validate runs a document through exactly the checks a send gets, and answers with every finding — warnings included. Nothing is stored and nothing is sent.
curl -sS -X POST https://api.businessgateway.se/v1/validate \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"type": "invoice", "payload": "<?xml version=\"1.0\"?>…"}'
# → {"valid": false, "type": "invoice", "documentTypeId": "…", "processId": "…",
# "findings": [{"severity": "fatal", "rule": "BR-CO-15", "location": "…", "message": "…", "layer": "…"}]}valid is false only when a finding is fatal. Leave out type and it is read off the document itself. A document over the 1 MB body limit goes through the same upload as a send: name its payloadObjectKey instead of payload, and the same key can be sent afterwards.
It is the quickest way to find out why a document fails — and worth wiring into whatever produces your documents, so a broken one is caught before your trading partner waits for it.
Finding a trading partner's address
Before you send to someone, you can ask the network whether they are there and what they can receive:
# by Peppol address
curl -sS -H "X-API-Key: $KEY" \
"https://api.businessgateway.se/v1/network/participants/0007:5569876543"
# → {"exists": true, "canonicalId": "…", "smpUrl": "…", "capabilities": ["…"]}
# by name or organisation number
curl -sS -H "X-API-Key: $KEY" \
"https://api.businessgateway.se/v1/network/participants/search?q=Kinnefrukt"exists: false from the address lookup is a definite answer: that address is not registered on Peppol. capabilities lists the document types it can receive — if yours is not among them, the send will fail with DISCOVERY_FAILED.
The search takes either a full address, looked up exactly, or free text, tried as an organisation number in every format we know and as a name in the Peppol Directory. It returns only confirmed hits, so an empty result means "not found this way", not "does not exist": many businesses never publish a directory entry. directoryConfigured says whether the name search was run at all.
GET /v1/network/participants/{peppolId}/capabilities answers just the capabilities, and POST /v1/network/participants/lookup checks up to 100 addresses in one call.
Collecting documents
Ask what is waiting:
curl -sS -H "X-API-Key: $KEY" \
"https://api.businessgateway.se/v1/messages?delivery=pending"delivery=pending is every inbound document that has been checked and not yet handed to you, whatever stage of delivery it is at.
Fetch the XML:
curl -sS -H "X-API-Key: $KEY" \
"https://api.businessgateway.se/v1/messages/$ID/payload"Then acknowledge it, once you have stored it somewhere you trust:
curl -sS -X POST -H "X-API-Key: $KEY" \
"https://api.businessgateway.se/v1/messages/$ID/ack"Acknowledge after your own system has the document, not before. Until you do, the document stays in the waiting list — so if your import crashes halfway, it is still there on the next run. Acknowledging twice is harmless.
Two other things also count as handing the document over: your webhook answering 2xx to its message.inbound.received event, and the document being written to your SFTP in/ directory. Either one marks it delivered, the same as an ack.
| Status | What it means |
|---|---|
AS4_RECEIVED / BUSINESS_VALIDATING | Arrived, being checked |
VALID / UNVALIDATED | Ready for you. UNVALIDATED is a document type with no rules to check |
CUSTOMER_DELIVERY_QUEUED / CUSTOMER_DELIVERY_RETRY | Ready, and we are delivering it to your webhook or SFTP directory |
CUSTOMER_DELIVERY_FAILED | We gave up delivering it. It is still there for you to fetch and acknowledge |
CUSTOMER_DELIVERED | You have it |
BUSINESS_INVALID | Failed validation. The sender has been told |
VALIDATION_UNAVAILABLE / VALIDATION_ERROR | Could not be checked. The sender is told it was not delivered |
Listing and searching
GET /v1/messages takes direction (INBOUND or OUTBOUND), status, from and to as dates, and q as a search string. Results are paginated: read nextCursor from the response and pass it back as cursor until it stops appearing.
format=csv returns the same filter as a download, which is often the fastest way to answer a question from your finance team.
Errors
Failures come back in one shape:
{ "error": { "code": "VALIDATION_FAILED", "message": "…" } }The code is stable and safe to branch on; message is for people and may change. The full list is in Error codes.