Webhooks
Register an HTTPS endpoint and get called as documents arrive and transactions progress.
Register an HTTPS endpoint and SUMU calls it as documents arrive and transactions progress. Preferable to polling — inbound documents have no other push mechanism.
/v1/api/webhooks| Field | Type | Description | |
|---|---|---|---|
| url | string | required | HTTPS endpoint. Must respond 2xx within 10 seconds |
| events | string[] | required | Which events to receive. At least one |
| secret | string | required | Shared secret for signature verification. Shown once |
| taxpayer_id | uuid | optional | Only events for one taxpayer. Omit for all of them |
| active | boolean | optional | Default true |
GET /v1/api/webhooks lists them, PATCH /v1/api/webhooks/{id} updates url, events or active state,
and DELETE removes one.
Events
| Event | Fires when | Act on it by |
|---|---|---|
document.received | An inbound document is stored for one of your taxpayers | Fetching it and posting into your ledger |
transaction.delivered | The buyer's Access Point signed for the document | Marking the invoice as sent |
transaction.failed | Delivery abandoned after all retries | Alerting someone — this needs a human |
transaction.rejected | The receiver refused the document | Reading delivery.error and correcting |
tax_report.acknowledged | The Tax Authority accepted the report | Recording compliance |
tax_report.rejected | The Authority refused it | Escalating — the invoice is issued but unreported |
If you're on the Platform track, decide between one unscoped webhook
(everything, one endpoint) and several taxpayer_id-scoped ones (routing events to different
downstream systems per client) — both are supported.
Payload
POST https://your-app.com/hooks/peppol
Content-Type: application/json
X-Sumu-Signature: t=1755081045,v1=5257a869e7ecebeda32affa62cdca3fa...
X-Sumu-Delivery-Attempt: 1
{
"id": "evt_9c4f2a1b",
"type": "document.received",
"created_at": "2026-08-13T10:30:45Z",
"data": {
"document_id": "d51c8b3a-...",
"direction": "inbound",
"recipient_taxpayer_id": "625919e2-...",
"sender": { "scheme": "0248", "identifier": "OM1100446274", "name": "Seller LLC" },
"received_at": "2026-08-13T10:30:45Z",
"summary": {
"invoice_number": "EINV-0001",
"issue_date": "2026-08-12",
"currency": "OMR",
"total_including_tax": "443.89"
}
}
}Verifying the signature
The header carries a timestamp and an HMAC-SHA256 of "{timestamp}.{raw body}", keyed with your
secret. Compare using a constant-time function, and reject anything with a timestamp older than five
minutes.
const [t, v1] = header.split(",").map(p => p.split("=")[1]);
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) reject();
if (Date.now() / 1000 - Number(t) > 300) reject();Retries
Any response other than 2xx, or a timeout, is retried five times: after 1 minute, 5 minutes, 30
minutes, 2 hours and 12 hours. After that the webhook is parked and shown as failing in the dashboard.
Events may arrive more than once — make your handler idempotent on id.