# Order lifecycle

> Every status, event and reason an order can have, and how to handle them.

Source: https://docs.delivr.no/guides/order-lifecycle/

An order's `status` is one of a small, stable set. Each change sends a webhook and increases the order's `version`.

**On demand:** placed → awaiting_goods → searching → driver_assigned → picked_up → delivered (or it ends cancelled or failed).

**Window:** placed → scheduled → picked_up → delivered (or it ends cancelled or failed).

## Statuses

| Status            | Type      | Meaning                                                                              | Webhook                 |
| ----------------- | --------- | ------------------------------------------------------------------------------------ | ----------------------- |
| `placed`          | both      | Booked. Transport is being arranged.                                                 | `order.placed`          |
| `awaiting_goods`  | on demand | Waiting for your goods. The driver search has not started.                           | `order.placed`          |
| `searching`       | on demand | Goods released; Delivr is finding a driver.                                          | `order.searching`       |
| `scheduled`       | window    | Booked on its window's route.                                                        | `order.scheduled`       |
| `driver_assigned` | on demand | A driver accepted and is on the way to the store.                                    | `order.driver_assigned` |
| `picked_up`       | both      | The driver has the goods.                                                            | `order.picked_up`       |
| `delivered`       | both      | Handed over. `proofOfDelivery` has `deliveredAt` and, when recorded, `receiverName`. | `order.delivered`       |
| `cancelled`       | both      | Cancelled, with a `reason`.                                                          | `order.cancelled`       |
| `failed`          | both      | Could not be delivered, with a `reason`.                                             | `order.failed`          |

`placed` and `awaiting_goods` both send `order.placed`: that is the moment to create the delivery in your system. An on-demand order usually shows `awaiting_goods` from the start, while it waits for the goods. You are [billed](/guides/billing/) for delivered orders and for cancellation fees, not for placing.

## Reasons

`cancelled` and `failed` orders carry a `reason`:

| Reason                  | Ends as     | Meaning                                                                                                     |
| ----------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- |
| `no_driver`             | `failed`    | No driver accepted the on-demand order in time. Your [fallback](/guides/fallbacks/) may follow.             |
| `goods_not_ready`       | `failed`    | The goods were never reported ready, and the ready deadline and its grace passed. Your fallback may follow. |
| `no_capacity`           | `failed`    | The transport could not be booked, for example the window filled up.                                        |
| `undeliverable`         | `failed`    | The transport partner could not complete the delivery.                                                      |
| `cancelled_by_merchant` | `cancelled` | You cancelled.                                                                                              |
| `cancelled_by_delivr`   | `cancelled` | Delivr's operators cancelled.                                                                               |
| `other`                 | either      | Anything else.                                                                                              |

New reasons may be added. Treat an unknown one like `other`.

## Other events

Some webhooks are not status changes:

| Event                      | When                                                                                       |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| `order.ready_at_changed`   | You reported when the goods will be ready (`readyAt`).                                     |
| `order.ready_overdue`      | The goods are due (`readyBy`) and not reported ready.                                      |
| `order.window_changed`     | Booked on another window than quoted (`previousWindow`), after `onWindowFull: reschedule`. |
| `order.awaiting_recipient` | A failed order waits for the recipient to choose a window, until `choiceDeadline`.         |
| `order.successor_created`  | A failed order got a successor order (`successorId`).                                      |
| `order.fallback_cancelled` | No successor follows (`fallbackReason`: `expired` or `no_window`).                         |
| `order.held`               | An assisted checkout order is held for a pending sale.                                     |

See [Event types](/webhooks/events/) for the payloads.

## Reading orders

- [`GET /v1/merchant/orders/{order}`](/api/operations/getorder/) takes Delivr's `id` or the `DD-` reference. It is the source of truth.
- [`GET /v1/merchant/orders`](/api/operations/listorders/) is a change feed. Page through `nextCursor` to see every change once, and filter by `updatedSince`, `status` or `externalRef`. A nightly reconciliation with `updatedSince` catches anything a webhook missed.
- `version` increases with every change. Keep the highest you have seen, and ignore older data.

## Reconciling

Webhooks can be missed, for example while your endpoint was down for longer than the retries last. Run a job, nightly or hourly, that reads every order changed since its last run and applies it with the same `version` check as your webhook handler.

`updatedSince` counts from when Delivr recorded a change, and `nextCursor` is absent on the last page, so keep the time each run started and overlap the next run a little:

```js
const startedAt = new Date();
const since = new Date(lastRunStartedAt.getTime() - 10 * 60_000); // overlap: applying twice is harmless
let cursor;
do {
  const url = new URL("/v1/merchant/orders", process.env.DELIVR_API_URL);
  url.searchParams.set("updatedSince", since.toISOString());
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.DELIVR_API_KEY}` } });
  if (!res.ok) throw new Error(`Delivr answered ${res.status}`);
  const page = await res.json();
  for (const order of page.items) await applyIfNewer(order); // update only when order.version is higher
  cursor = page.nextCursor;
} while (cursor);
await saveLastRunStartedAt(startedAt);
```

Held assisted checkout orders are not listed until their sale completes; read them by id.

## Times on an order

| Field                   | Meaning                                                              |
| ----------------------- | -------------------------------------------------------------------- |
| `createdAt`, `placedAt` | When the order was created and placed.                               |
| `readyBy`               | On demand. When the goods are due.                                   |
| `readyAt`               | On demand. When the goods are (or are assumed to be) ready.          |
| `window`                | Window orders. The delivery window.                                  |
| `confirmedAt`           | A driver accepted (on demand), or the transport was booked (window). |
| `updatedAt`             | The last change.                                                     |