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

EventWhen
peppol.document.sentThe document was accepted by the network (AS4 receipt).
peppol.document.deliveredThe document was delivered to the recipient (MLS/delivery receipt).
peppol.document.failedSending failed (final).
peppol.document.receivedAn inbound document arrived for you over Peppol.
usage.limit_exceededThe organization exceeded its included monthly transaction volume.
The events 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…"
  }
}
EventFields in data (besides orgId)
sentinvoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId
deliveredinvoiceId, invoiceNumber, documentType, mode, state, messageId, transactionId
failedinvoiceId, invoiceNumber, documentType, error
receivedsenderName, senderParticipantId, documentNumber, documentType, total, currency

Headers

HeaderDescription
X-Webhook-SignatureThe signature in t=<unix>,v1=<hex> format (Stripe-style).
X-Webhook-EventThe event name.
X-Webhook-IdA delivery UUID (idempotency key).
X-Webhook-TimestampThe 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))
      );
    });
}
Use the raw request body (byte-for-byte), not re-serialized JSON, otherwise the signature won't match.

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 with exponential backoff (5s, 10s, 20s, 40s, 80s): 6 attempts total.

MechanismBehavior
RetriesInitial attempt + 5 retries (5/10/20/40/80 s).
IdempotencyDeduplicate by X-Webhook-Id: a replay sends the same ID.
Auto-disableAfter 15 consecutive failed deliveries the endpoint is deactivated.
Secret rotationThe 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.