What a delivery looks like
Each delivery is onePOST to your target_url with a JSON body (one event,
at most 64 KiB) and these headers:
Every delivery carries signed context, including Free, paid, test and replayed
deliveries. Only the body, the timestamp and the context are signed: trust the
other headers only through the signed context.
Respond
Your response decides the outcome of the attempt:
Verify the request, store the event and enqueue your work, then answer
204
with an empty body. Do slow work after you answer: deliveries to one endpoint
are sequential, so a slow receiver delays every later event for that endpoint.
Lumeus connects over HTTPS (TLS 1.2 or later, HTTP/1.1, keep-alive, no
compression) and checks that the target resolves to a public address on every
connection.
Retries and timeouts
These values are the same on every plan. A dead delivery is not retried again
automatically. Find it with
GET /v1/subscriptions/{id}/deliveries
and read the event from the Activity API; see
Coverage and recovery.
Verify the signature
- Node.js (Express)
- Python
- Go
Download verify-webhook.mjs next to your app.
express.raw keeps the body as a Buffer.Delivery context
The decodedX-Webhook-Delivery-Context is a small JSON object signed with
the same key. It binds routing facts to this exact body, so a captured request
cannot be replayed into a different lane or network:
The verifiers check
domain, the event ID, the network, the body hash and the
lane. Also check that subscription_id and network are the ones this
receiver expects.
Deduplicate
Delivery is at least once. Deduplicate on the event, not the delivery: the unique key is(network, id), with network from the signed context and id
from the body. The context says mainnet where event bodies and Activity say
pubnet, so store one spelling (normalize pubnet to mainnet). X-Webhook-Id names one delivery and differs between a live
delivery and a redelivery of the same event; the event id does not. Use a database constraint, not an
in-memory set:
- Same ids as Activity. Webhooks and the Activity API
use the same decoder, so one on-chain event has the same
idin both. Rows you fill from Activity after a gap land on the same key and are ignored as duplicates. When you compare bodies, ignore the Activity-only fieldspaging_tokenandrpc_event_id.balance.changedandwebhook.testevents exist only in Webhooks. Ids stay stable within a decoder release; a change that renumbers events will be announced. - Leave
generationout of the key. It is product-specific: the signed context carries the webhook chain generation, while Activity reports its own index generation inread_view.generation, and the two differ. Usegenerationonly with the Webhooks recovery APIs and Activity pagination. - After a Testnet reset, ledger numbers start again and ids repeat. Start a new store (or add the reset date to the key) for the new Testnet. Mainnet never resets.
Test deliveries
POST /v1/subscriptions/{id}/tests
sends one signed synthetic event so you can check your receiver:
X-Webhook-Test: true, lane: "test" in the signed context and a
negative X-Webhook-Id. Verify it like any delivery, then keep it out of your
balances and workflows. Tests are sent once (no retries), cost no credits and
are limited to 12 per organization per hour.
Key rotation
After you rotate the signing key, deliveries are signed with both keys for 24 hours:X-Webhook-Signature and
X-Webhook-Context-Signature with the old key, the X-Webhook-Next-* headers
with the new key. Configure both secrets (WEBHOOK_SECRET and
WEBHOOK_NEXT_SECRET in the examples); the verifiers accept either. Remove the
old secret after the overlap ends.