Skip to main content
This page is the contract between Lumeus and your receiver: what each delivery contains, how to verify it, and how your response decides between success and retry. Start from one of the ready receivers, which do all of this for you: Node.js (with verify-webhook.mjs), Python or Go.

What a delivery looks like

Each delivery is one POST 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

Reject the request when no secret produces the signature, or when the timestamp is more than 300 seconds from your clock. Compare in constant time. Verify the raw bytes you received: parsing and re-serializing the JSON changes them. Then verify the context signature the same way over the decoded context bytes, with the same timestamp.
Download verify-webhook.mjs next to your app. express.raw keeps the body as a Buffer.
The verifiers return these error codes. Log the code, never the headers, secret or payload.

Delivery context

The decoded X-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:
Event ids have a fixed form:
  • Same ids as Activity. Webhooks and the Activity API use the same decoder, so one on-chain event has the same id in 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 fields paging_token and rpc_event_id. balance.changed and webhook.test events exist only in Webhooks. Ids stay stable within a decoder release; a change that renumbers events will be announced.
  • Leave generation out of the key. It is product-specific: the signed context carries the webhook chain generation, while Activity reports its own index generation in read_view.generation, and the two differ. Use generation only 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:
It has 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.

Troubleshooting