Order lifecycle
One status model for the courier app, the cabinet, the API and the webhooks — with the rules for changes and cancellation.
Statuses follow the Onfleet / Deliverect convention and are the same everywhere: in the courier app, your cabinet,
our back-office, GET /v1/orders/{id} and the webhooks.
Statuses
| Step | Status | What happens | Final |
|---|---|---|---|
| 1 | created | Created | |
| 1 | scheduled | Scheduled for a time or a slot | |
| 2 | searching | Looking for a courier | |
| 3 | assigned | Courier assigned | |
| 4 | to_pickup | Courier on the way to the pickup | |
| 5 | at_pickup | Courier at the pickup (waiting for assembly is counted) | |
| 6 | picked_up | Order picked up | |
| 7 | to_dropoff | On the way to the recipient | |
| 8 | at_dropoff | At the recipient | |
| 9 | delivered | Delivered | yes |
| 9 | failed | Not delivered (with a reason code) | |
| 9 | returning | Returning to the pickup point | |
| 9 | returned | Returned | yes |
| 7 | transfer | Handover to another courier (breakdown, accident, battery) | |
| — | cancelled | Cancelled (before pickup) | yes |
| — | lost | Lost (after an incident) | yes |
history[] of the order keeps every transition with its time, reason_code and the order version. The version
grows with every change of the order — webhooks carry it as order_version, so you can always drop an event older
than the state you already have.
Reassignment and handover
An order can change its courier at any step; you receive order.courier_assigned with replaced: true.
- Before pickup (
assigned,to_pickup,at_pickup) — the courier declined, lost connection, the battery ran low, or a closer courier was found: the order goes back tosearchingwith a reason code and priority, and the ETA is recalculated (order.eta_changedif it moved more than your threshold). - After pickup — breakdown, accident or a discharged battery with no station nearby: a second courier takes the
order at a meeting point, status
transfer, confirmed by codes and photos on both sides; thento_dropoffagain.
Changing an order
PATCH /v1/orders/{id} changes only the fields you send.
| Field | Until picked_up | After pickup |
|---|---|---|
dropoff (address, entrance, floor, apartment, comment) | yes | no |
recipient_name, scheduled_at, cod | yes | no |
recipient_phone, comment | yes | yes, until the order is final |
A refused change is 422 order_not_editable with details[].field and the still editable fields. A new address
outside the zones is 422 out_of_zone.
Ready signal
POST /v1/orders/{id}/ready tells us the order is packed. Without it the courier search starts by the assembly norm
of the pickup point (assembly_minutes), so the courier does not wait at the counter.
Cancellation
POST /v1/orders/{id}/cancel with a reason_code (see the reason codes):
| When | Result | Fee |
|---|---|---|
created, scheduled, searching | cancelled | 0 |
assigned, to_pickup, at_pickup | cancelled; the courier is notified and compensated | by tariff, in cancellation_fee |
picked_up, to_dropoff, at_dropoff | the order returns to the pickup point (returning → returned) | the return by tariff |
final, failed, returning | 422 order_not_cancellable | — |
Not delivered
When the courier cannot hand the order over (the recipient does not answer, wrong address, refusal, no ID for 18+,
no payment), the order goes failed with a reason_code, then returning → returned. You get order.failed and
order.returned.