SUMU
Receiving

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.

POST/v1/api/webhooks
FieldType Description
urlstringrequiredHTTPS endpoint. Must respond 2xx within 10 seconds
eventsstring[]requiredWhich events to receive. At least one
secretstringrequiredShared secret for signature verification. Shown once
taxpayer_iduuidoptionalOnly events for one taxpayer. Omit for all of them
activebooleanoptionalDefault 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

EventFires whenAct on it by
document.receivedAn inbound document is stored for one of your taxpayersFetching it by document_id and posting into your ledger
transaction.deliveredThe buyer's Access Point signed for the documentMarking the invoice as sent
transaction.failedDelivery abandoned after all retriesAlerting someone — this needs a human
transaction.rejectedThe receiver refused the documentReading delivery.error and correcting
tax_report.acknowledgedThe Tax Authority accepted the reportRecording compliance
tax_report.rejectedThe Authority refused itEscalating — 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.

SUMU

Sumu — an e-invoicing platform owned and operated by OceanGate for Smart Technologies LLC, a FinTech and SaaS solutions company. All rights reserved © 2026.

joinsumu.com ↗