Skip to main content
Each PUSH delivery is signed so you can prove it came from Endl. Always verify before acting on a webhook.

Delivery headers

Three things to get right. It is SHA-512, not SHA-256. The hex is lowercase. And you must HMAC the raw received body bytes — parsing the JSON and re-serializing it changes those bytes and breaks the signature.

Example delivery

The body is a single compact line, and the signature covers exactly those bytes. Its top level is always eventId, eventType, eventCategory, eventCreatedAt, version and data — six keys, frozen across schema versions. Only data is version-specific. Inside data come two metadata keys, then the producer’s own fields in sorted key order. data.id is prefixed — txn_, acct_, cus_ — and that prefix is the only thing that says what the event is about. data.customerId is omitted entirely when the event carries no customer. Every leaf value is a string, amounts included, and null-valued fields are omitted rather than sent as null. No subscription id is sent. Four more rules govern how data is rendered, and all four matter if you are generating types from a delivery rather than reading this page:
Because there is no allowlist, treat the event catalogue as the current shape rather than a closed schema. Handle an unrecognised eventType — or an extra field inside data — gracefully instead of rejecting the delivery.

Verify the signature

All three examples compare in constant time and accept a list of secrets. That list is what makes a secret cutover safe — see below.

Replay and secret cutovers

The timestamp is covered by the signature, so you can trust it: reject any delivery whose X-WEBHOOK-TIMESTAMP is more than about five minutes old. A captured request cannot be replayed with a fresh timestamp, because rewriting it invalidates the signature. There is no rotate endpoint. The signing secret is returned once, when the subscription is created, and cannot be read back afterwards. If it is lost, delete the subscription and create a new one — that mints a fresh secret bound to a new subscription id, and leaves the old one’s delivery history intact. Each subscription has its own secret, so moving traffic to a new receiver means both secrets are briefly live while the old subscription drains. Write your verifier against a list of secrets from the start, as the examples above do, and drop the retired one once nothing signed under it is still in flight.
secretHint is a masked form of the current secret — whsec_… plus its last 4 characters. It exists so a support ticket can answer “which secret are you signing with?” without either side handling the secret itself: you compare the hint against your stored copy, and it either matches or you are verifying with the wrong one. Comparing hints is safe to paste into a ticket. Comparing secrets is not.A PULL subscription has neither a webhook_secret nor a secretHint — a PULL feed is never signed.
Deduplicate on X-WEBHOOK-EVENT-ID. Retries and manual sends can deliver the same event more than once, and a receiver that is idempotent on the event id handles all of it for free.