Webhooks
Get told the moment a document arrives, instead of polling for it.
A webhook is an HTTPS address of yours that we call when something happens. It is the difference between finding out about an invoice within seconds and finding out on your next scheduled check.
It works with both the API and SFTP.
Setting one up
curl -sS -X POST https://api.businessgateway.se/v1/webhooks \
-H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{
"endpoint": "https://example.com/hooks/peppol",
"secret": "a long random string you generate",
"timeoutMs": 10000
}'The address must be https:// and reachable from the internet — not localhost, not an address inside your own network. The secret is yours; we never show it again, and it is what lets you prove a call really came from us.
If your credentials do not allow creating webhooks, whoever gave them to you can set this up for you.
What you receive
{
"id": "evt_…",
"type": "message.inbound.received",
"accountId": "…",
"companyId": "…",
"documentId": "…",
"status": "CUSTOMER_DELIVERY_QUEUED"
}type | When |
|---|---|
message.inbound.received | A document arrived and is ready for you |
message.inbound.validation_failed | An inbound document failed validation |
message.outbound.as4_accepted | Something you sent reached the recipient's provider |
message.outbound.failed | Something you sent failed for good |
message.mls | The recipient's system reported back on a document |
The event tells you a document exists. Fetch the document itself — over the API, or let it appear in in/ if you use SFTP. Treating the webhook as a notification rather than as the data means a missed call costs you nothing.
Checking it really came from us
Every call carries these headers:
| Header | Contents |
|---|---|
X-Peppol-Event-ID | Unique id for this event |
X-Peppol-Event-Type | Same as type in the body |
X-Peppol-Timestamp | When we sent it, RFC 3339 in UTC |
X-Peppol-Signature | sha256= + HMAC-SHA256 of timestamp + "." + body, keyed with your secret |
To verify: take the raw request body exactly as it arrived, join it to the timestamp with a ., compute HMAC-SHA256 with your secret, and compare.
Two things to get right. Use the raw bytes — if you parse the JSON and re-serialise it before hashing, the bytes differ and the signature will never match. And compare with a constant-time comparison, not ==.
Reject anything whose timestamp is far from now, so an old call cannot be replayed at you later.
Retries
If your endpoint answers with an error or does not answer within the timeout, we retry. After enough failures the delivery is set aside and needs someone to release it, so an endpoint that is down for a long weekend is worth knowing about.
Answer quickly. Return 2xx as soon as you have accepted the event and do the real work afterwards — if you fetch and process the whole document before replying, a slow import turns into a timeout and a retry, and then you process it twice.
Be ready to receive the same event more than once. Retries and network failures both cause it. Keying on X-Peppol-Event-ID, or on the documentId, makes that harmless.