Business Gateway
For platforms

Sending

Submit documents on behalf of the companies under your account.

Sending is one call. You do not name the company: the sender participant determines it, and we refuse a sender that is not under a company your credential reaches.

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\"?>…"
  }'

Exactly one of payload (the XML as a string) and payloadObjectKey (see below). type is a short alias; pass documentTypeId and processId instead if you need a document type that has no alias.

Both participants matter. The sender must be READY or ACTIVE with a matching SEND capability. The receiver is looked up on the network, and if it is not registered for that document type the send fails — usually the recipient's fault, but it will surface as your customer's problem.

202 does not mean valid

The response is 202 with status QUEUED_VALIDATION for any document that has validation rules. Peppol validation runs after your request returns, not inside it, so a slow validator never becomes your latency.

What you get back is a document id and a promise of a verdict. A passing document moves on to QUEUED; a failing one stops at VALIDATION_FAILED with validationErrors set. Read it on a later GET /v1/messages/{id} or the event stream. A validation failure raises message.outbound.failed with status: VALIDATION_FAILED on your webhooks, and is also emailed to the company's notification contacts.

StatusWhere it is
QUEUED_VALIDATIONAccepted, waiting for validation. If the validator is briefly down, documents wait here rather than failing
VALIDATION_FAILEDRejected by the Peppol rules. validationErrors says what failed
VALIDATION_ERRORValidation ran but its answer could not be read. Terminal, and not the document's fault; we follow these up
VALIDATION_UNAVAILABLENo rules exist for the document type and its capability does not allow unvalidated documents. Refused with 400 at submission
UNVALIDATEDNo rules exist for the type, and its capability allows sending unvalidated. Waiting to go out
QUEUEDValid, waiting to go to the network
SENDING / SEND_RETRYOn its way, or being retried
AS4_ACCEPTEDThe recipient's access point took it. This is delivery
DISCOVERY_FAILEDTerminal. The recipient could not be found on the network, or is not registered for this document type
REJECTEDTerminal. The recipient's access point refused it
SEND_FAILEDTerminal. lastErrorCode and lastErrorMessage say why

The terminal failures — VALIDATION_FAILED included — raise message.outbound.failed, and AS4_ACCEPTED raises message.outbound.as4_accepted, on your webhooks.

A SEND_FAILED document with deliveryUnconfirmed: true is one whose outcome we could not learn — the recipient may already have it. It is never resent automatically; show it as unconfirmed rather than failed, and do not resend it without checking with the recipient.

AS4_ACCEPTED means the other access point accepted the document — that is as far as the transport guarantees. Whether the recipient's system then took it is what the recipient status says.

Recipient status

Every document we send asks the receiving provider for a Peppol Message Level Status (MLS). When one arrives, GET /v1/messages/{id} on the outbound document carries it in one word:

recipientStatusMLS codeMeaning
CONFIRMEDAPDelivered, with confirmation
DELIVEREDABDelivered, without confirmation
REJECTEDRERefused, with recipientStatusReasons
ReasonMeaning
SVSyntax violation
BVBusiness rule violation (fatal)
BWBusiness rule warning
FDFailure of delivery to the recipient

An absent recipientStatus means no answer, not a failure: a receiving provider that does not support MLS sends none, and an answer can take time. The document's status does not change when one arrives.

mlsEvents holds the full answer. The entry with direction: "INBOUND" and status: "RECEIVED" is the receiving provider's, with responseCode, description, issues ([{location, reasonCode, description}]), reasons, receivedAt, mlsId and sender (the provider's own Peppol id). Entries with direction: "OUTBOUND" appear only on older documents, carry none of these fields, and can be ignored.

A message.mls webhook announces each one as it arrives, with the same responseCode, reasons and issues.

Before you send

Two checks are worth building into an onboarding or invoicing flow.

Validate without sending. POST /v1/validate takes {type?, documentTypeId?, processId?, payload} — or {companyId?, payloadObjectKey} for an uploaded document, which stays in place so the same key can then be sent — and answers {valid, type, documentTypeId, processId, findings} with every finding, warnings included. Nothing is stored or sent. valid is false only on a fatal finding; without type the document type is read off the document. It runs the same checks as a send, so a document it passes will not stop at VALIDATION_FAILED.

Check the receiver. GET /v1/network/participants/{peppolId} answers whether an address is registered (exists) and the document types it can receive (capabilities); POST /v1/network/participants/lookup does up to 100 at once. GET /v1/network/participants/search?q= finds a counterparty by organisation number or, through the Peppol Directory, by name — confirmed hits only, so an empty answer is "not found this way" rather than "not on Peppol". More in Finding a trading partner's address.

Full request and response schemas are in the API reference.

Idempotency

Idempotency-Key is optional and worth setting on every send. Same key with the same body returns the original document and 200 instead of creating a second one; same key with a different body is 409 IDEMPOTENCY_CONFLICT. Keys are unique per account forever, up to 128 characters.

Without the header, a retried request creates a second document — and your customer's trading partner gets the invoice twice.

Large documents

Inline payload is capped at about 1 MB. Above that, upload the bytes separately:

# 1. ask for a URL
curl -sS -X POST https://api.businessgateway.se/v1/messages/uploads \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"companyId":"'"$COMPANY_ID"'","bytes":4200000}'
# → {"payloadObjectKey":"…","uploadUrl":"https://…","expiresAt":"…"}

# 2. PUT the bytes straight at uploadUrl

# 3. submit, naming the key instead of the payload
curl -sS -X POST https://api.businessgateway.se/v1/messages \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"sender":"…","receiver":"…","type":"invoice","payloadObjectKey":"…"}'

The hard ceiling for a document is about 40 MB, inbound and outbound alike.

Checking on a document

curl -sS -H "X-API-Key: $KEY" \
  "https://api.businessgateway.se/v1/messages/$ID"

Or list them. GET /v1/messages takes direction, status, companyId, from/to, q, and cursor pagination; format=csv gives the same filter as a download, and counts=true adds a breakdown by status. Across your whole account, without companyId, you see every company at once — which is the view worth putting in front of your support team.

On this page