# Receiving webhooks

> Get a signed HTTPS call for every change to your orders.

Source: https://docs.delivr.no/webhooks/

Delivr posts an event to your HTTPS endpoint whenever one of your orders changes. Webhooks are how your order system stays in step without polling.

## Set up

Delivr registers your endpoint URL and gives you its signing secret (`whsec_…`). Then you manage it through the API:

| Call                                            | Does                                                                      |
| ----------------------------------------------- | ------------------------------------------------------------------------- |
| `GET /v1/merchant/webhook`                      | Your endpoint, the subscribed events and the secret's prefix.             |
| `PUT /v1/merchant/webhook/events`               | Choose the events you want. An empty list means all.                      |
| `POST /v1/merchant/webhooks/test`               | Send a `webhook.test` event.                                              |
| `GET /v1/merchant/webhooks/deliveries`          | The delivery log: every attempt, its status code and timing, for 30 days. |
| `POST /v1/merchant/webhooks/events/{id}/replay` | Send an event again, with the same id.                                    |

## The event

```http
POST /hooks/delivr HTTP/1.1
Content-Type: application/json
X-Delivr-Event: order.picked_up
X-Delivr-Event-Id: 6b0f4f1e-2b7a-4d4e-9a6b-1f3e5d2c7a90
X-Delivr-Signature: t=1762175400,v1=5f2b…
Delivr-Api-Version: 2026-11-01
```

```json
{
  "id": "6b0f4f1e-2b7a-4d4e-9a6b-1f3e5d2c7a90",
  "type": "order.picked_up",
  "apiVersion": "2026-11-01",
  "createdAt": "2026-11-03T13:30:00Z",
  "data": {
    "orderId": "3f0e9a52-8a7e-4f8e-9a51-2c0f3c9d1b7a",
    "reference": "DD-482913",
    "externalRef": "order-1001",
    "type": "on_demand",
    "status": "picked_up",
    "version": 9
  }
}
```

`data` is the order's state when the event was queued. It carries the order id, `reference`, `externalRef`, `type`, `status`, `version` and, when they apply, `reason`, `readyAt`, `window`, `previousWindow`, `predecessorId`, `successorId`, `choiceDeadline`, `fallbackReason`, `proofOfDelivery` and `sale`. Door codes and driver notes are never included.

## Handling events correctly

1. **Verify the signature** before you trust anything. See [Verifying signatures](/webhooks/signatures/).
2. **Answer `2xx` within 15 seconds.** Queue the work and answer first. Redirects are not followed.
3. **Deduplicate on `id`.** An event keeps its `id` across retries and replays. Process each id once.
4. **Expect any order.** Events for one order can arrive late or out of order, for example `order.driver_assigned` before `order.held`.
5. **Apply `data` only if it is newer.** Store each order's `version`. Apply `data` as state only when its `version` is higher than yours, but still act on every new event id: some events share a version, such as `order.window_changed` with its `order.placed`.
6. **Create orders on `order.placed` only.** Never on `order.held` or a status event. A status event for an id you do not know, or with `sale.status` `pending` or `abandoned`, is an assisted checkout hold: ignore it or read the order.
7. **Ignore unknown event types.** New ones may be added.

When in doubt, `GET /v1/merchant/orders/{id}` is the source of truth.

```js
// Express, with the raw body kept for the signature.
app.post("/hooks/delivr", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verify(process.env.DELIVR_WEBHOOK_SECRET, req.get("X-Delivr-Signature"), req.body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  if (await seen(event.id)) return res.sendStatus(200);
  await queue.add("delivr-event", event); // do the work after answering
  res.sendStatus(200);
});
```

## Retries

Any answer other than `2xx`, no answer within 15 seconds, or a redirect is a failure. Delivr retries after 30 s, 2 m, 10 m, 30 m, 1 h, 3 h, 6 h, 6 h and 6 h: 10 attempts over about 23 hours. Then it gives up, and you can still replay the event for 30 days.

Replays send the event as it was first queued, to your endpoint as it is now, whatever your subscription. One replay per event per minute.