Payloads and delivery
What an event carries, and how it is retried when your endpoint fails.
What an event looks like
A payload carries the identity of what happened, and any fact that was true at the moment it happened and cannot later become untrue. Anything that can change between the send and your handler is read back through the API.
{
"id": "48211",
"event": "order.paid.v1",
"storeId": "b3f1c0d2-...",
"createdAt": "2026-08-05T14:02:11.482Z",
"data": {
"orderId": "9f2a...",
"orderNumber": "1043",
"amount": 24495,
"currency": "GBP",
"buyerEmail": "ada@example.com",
"sessionId": "7c31..."
}
}createdAt is when the thing happened, not when this delivery was sent, so a
retry carries the same timestamp as the first attempt.
The rule matters most where it bites. An order total is read back, because a
refund landing between the send and your handler would leave you emailing a
customer about money they no longer paid. The amount above is what was charged
at the moment it was charged, and it is there so you can log the sale without a
round trip; anything you are going to show a customer, read back.
An email address travels, because it is the routing key. Requiring a
read-back to discover who a message is about means a failed read turns into a
message not sent, and for customer.consent_withdrawn.v1 that is not a
commercial problem but a legal one.
A basket travels on checkout.started.v1 in summary form, because the whole
event is about a moment. Read the session back for the line detail; it is still
there for seven days.
Delivery, retries and what to do about failures
Deliveries come from the transactional outbox, so an event survives a crash
between the thing happening and the send. A failed delivery is retried with
backoff and every attempt is recorded in webhook_deliveries, which the
merchant can read in the admin under Apps, and which you can read for your own
app in the developer portal.
Six attempts over about nine hours: a minute, five minutes, half an hour, two hours, then six. The tail is that long deliberately, so an endpoint that was broken overnight receives the evening's events rather than only whatever happened after somebody fixed it. After the last attempt the delivery is given up on and says so on the merchant's screen, where there is also a button to try it again immediately once you have deployed a fix.
Deliveries go only to https addresses on the public internet, on the
standard port. Redirects are never followed: a 3xx counts as a failure, so a
signed payload cannot be bounced to somewhere you did not declare. Move your
endpoint by changing webhooks.url in your manifest instead.
Two consequences worth designing for:
At least once, not exactly once. A network timeout after your handler
succeeded looks identical to a failure, so it will be retried. Key your side on
something derived from the payload, as unique_id does above.
Order is not guaranteed. order.paid.v1 and fulfillment.created.v1 are
delivered independently, and a slow retry can arrive after a later event. If
sequence matters to you, read the current state rather than accumulating deltas.