Webhooks
Webhooks deliver events from the Peppol pipeline (sent, delivered, failed, received) to your HTTPS endpoint. Every delivery is HMAC-signed and retried with exponential backoff until you acknowledge it with HTTP 2xx.
Registering an endpoint
You register an endpoint in the portal under Webhooks, or via POST /v1/partner/webhooks. On creation you get the signing secret only once. Store it. A partner can have up to 20 endpoints; the URL must be https://. By default an endpoint receives events for allof the partner's organizations; pass an optional orgId on creation to restrict it to a single organization.
Event types
| Event | When |
|---|---|
peppol.document.sent | The document was accepted by the network (AS4 receipt). |
peppol.document.delivered | The document was delivered to the recipient (MLS/delivery receipt). |
peppol.document.failed | Sending failed (final). |
peppol.document.received | An inbound document arrived for you over Peppol. |
usage.limit_exceeded | The organization exceeded its included monthly transaction volume. |
peppol.document.status_changed, participant.activated, and participant.failed are already in the catalog (you can subscribe to them) but are not emitted yet. We'll add them without changing the payload format. Transport status changes are currently covered by the sent / delivered / failed events above.Payload format
The body is JSON with an event / timestamp (ISO 8601) / data envelope. The data.orgId field is always present and identifies the client organization. Test sends carry data.mode = "test".
{
"event": "peppol.document.delivered",
"timestamp": "2026-06-12T19:48:01.000Z",
"data": {
"invoiceId": "b1f0…",
"invoiceNumber": "2026001",
"documentType": "invoice",
"mode": "test",
"state": "DELIVERED",
"messageId": "…",
"transactionId": "…",
"orgId": "0a2c…"
}
}| Event | Fields in data (besides orgId) |
|---|---|
sent | invoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId |
delivered | invoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId |
failed | invoiceId, invoiceNumber, documentType, error |
received | senderName, senderParticipantId, documentNumber, documentType, total, currency |
Headers
| Header | Description |
|---|---|
X-Webhook-Signature | The signature in t=<unix>,v1=<hex> format (Stripe-style). |
X-Webhook-Event | The event name. |
X-Webhook-Id | A delivery UUID (idempotency key). |
X-Webhook-Timestamp | The signature's Unix time (seconds). |
Verifying the signature
The string <timestamp>.<raw body> is signed with HMAC-SHA256 and your secret; the result is lowercase hex in the v1= segment. During secret rotation the header may carry multiple v1= segments (old + new): a match on either is sufficient. Compare in constant time and reject a timestamp that's too old (replay protection).
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("="))
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// header may carry several v1=… segments during secret rotation
return header
.split(",")
.filter((kv) => kv.startsWith("v1="))
.some((kv) => {
const got = kv.slice(3);
return (
got.length === expected.length &&
timingSafeEqual(Buffer.from(got), Buffer.from(expected))
);
});
}Delivery and retries
Delivery is durable (a queue + persisted history). Your endpoint should respond with HTTP 2xx within a few seconds; do the processing asynchronously. On failure (non-2xx or timeout), delivery is retried 5× with exponential backoff (5s, 10s, 20s, 40s, 80s): 6 attempts total.
| Mechanism | Behavior |
|---|---|
| Retries | Initial attempt + 5 retries (5/10/20/40/80 s). |
| Idempotency | Deduplicate by X-Webhook-Id: a replay sends the same ID. |
| Auto-disable | After 15 consecutive failed deliveries the endpoint is deactivated. |
| Secret rotation | The old secret verifies for another 24 h (dual v1= signature). |
Testing
In the portal you can send a test event to an endpoint, view the delivery history (payload + response), and replay any past delivery. In the sandbox (test key), a real peppol.document.delivered with data.mode = "test" fires after delivery between registered test participants.