Business Gateway
For businesses

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/account

That 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"
StatusWhat it means for you
QUEUED_VALIDATIONAccepted, being checked. If the checker is briefly down, the document waits here rather than failing
VALIDATION_FAILEDRejected. validationErrors lists what failed — fix the XML and send again
VALIDATION_ERRORThe check ran but its answer could not be read. Not your document's fault; we pick these up
VALIDATION_UNAVAILABLEThere are no rules to check this document type against, and its capability does not allow sending it unchecked. The send was refused with 400
UNVALIDATEDA document type with no rules to check, sent under a capability that allows that. Waiting to go out
QUEUEDPassed, waiting to go out
SENDING / SEND_RETRYOn its way, or being retried
AS4_ACCEPTEDDelivered to the recipient's provider. This is success
DISCOVERY_FAILEDTerminal. The recipient's address could not be found on the network, or is not registered for this document type
REJECTEDTerminal. The recipient's provider refused the delivery
SEND_FAILEDTerminal. 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:

recipientStatusPeppol codeMeaning
CONFIRMEDAPDelivered, and the recipient confirmed it
DELIVEREDABDelivered, without further confirmation
REJECTEDRERefused. recipientStatusReasons says why
ReasonMeaning
SVSyntax violation — the document does not follow the format
BVBusiness rule violation — a rule the document must satisfy was broken
BWBusiness rule warning — reported alongside, not on its own a reason to refuse
FDFailure 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.

StatusWhat it means
AS4_RECEIVED / BUSINESS_VALIDATINGArrived, being checked
VALID / UNVALIDATEDReady for you. UNVALIDATED is a document type with no rules to check
CUSTOMER_DELIVERY_QUEUED / CUSTOMER_DELIVERY_RETRYReady, and we are delivering it to your webhook or SFTP directory
CUSTOMER_DELIVERY_FAILEDWe gave up delivering it. It is still there for you to fetch and acknowledge
CUSTOMER_DELIVEREDYou have it
BUSINESS_INVALIDFailed validation. The sender has been told
VALIDATION_UNAVAILABLE / VALIDATION_ERRORCould 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.

On this page