# Payloads and delivery

> What an event carries, and how it is retried when your endpoint fails.

Source: https://storeware.ai/docs/webhooks/delivery

## 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.

```json
{
  "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.
