Delivery headers
Example delivery
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 whoseX-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.