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 — 200 OK, { "data": { "deleted": true } }.
Events
| Event | Fires when | Act on it by |
|---|---|---|
document.received | An inbound document is stored for one of your taxpayers | Fetching it by document_id 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
X-Sumu-Event-Id: evt_9c4f2a1b
{
"id": "evt_9c4f2a1b",
"type": "document.received",
"created_at": "2026-08-13T10:30:45Z",
"data": {
"document_id": "d51c8b3a-...",
"direction": "inbound",
"recipient_taxpayer_id": "625919e2-...",
"sender": { "participant_id": "0248:OM1100446274", "vat_id": "OM1100446274" },
"received_at": "2026-08-13T10:30:45Z",
"summary": {
"invoice_number": "EINV-0001",
"issue_date": "2026-08-12",
"invoice_type_code": "380",
"currency": "OMR",
"total_excluding_tax": "422.75",
"total_tax": "21.14"
}
}
}sender carries participant_id and vat_id, not a party name. summary carries the tax-exclusive
amount and the tax amount separately rather than a precomputed gross total — add them yourself if you
need it.
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 delivery is marked failed on SUMU's side — there's no
dashboard today that surfaces this to you (see the roadmap), so build your own alerting on
the transaction.failed/tax_report.rejected events rather than expecting to notice a stuck webhook
by checking somewhere in SUMU. Events may arrive more than once — make your handler idempotent on id.