Sapa Moto

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.

Order statuses and transitionsreassignmentcancel before pickupcreatedsearchingassignedto_pickupat_pickupscheduledcancelledpicked_upto_dropoffat_dropoffdeliveredtransferfailedreturningreturnedpickup: the courier has the orderhandover
Solid arrows are the main path; dashed — scheduled orders, reassignment, cancellation and handover.

Statuses

StepStatusWhat happensFinal
1createdCreated
1scheduledScheduled for a time or a slot
2searchingLooking for a courier
3assignedCourier assigned
4to_pickupCourier on the way to the pickup
5at_pickupCourier at the pickup (waiting for assembly is counted)
6picked_upOrder picked up
7to_dropoffOn the way to the recipient
8at_dropoffAt the recipient
9deliveredDeliveredyes
9failedNot delivered (with a reason code)
9returningReturning to the pickup point
9returnedReturnedyes
7transferHandover to another courier (breakdown, accident, battery)
—cancelledCancelled (before pickup)yes
—lostLost (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 to searching with a reason code and priority, and the ETA is recalculated (order.eta_changed if 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; then to_dropoff again.

Changing an order

PATCH /v1/orders/{id} changes only the fields you send.

FieldUntil picked_upAfter pickup
dropoff (address, entrance, floor, apartment, comment)yesno
recipient_name, scheduled_at, codyesno
recipient_phone, commentyesyes, 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):

WhenResultFee
created, scheduled, searchingcancelled0
assigned, to_pickup, at_pickupcancelled; the courier is notified and compensatedby tariff, in cancellation_fee
picked_up, to_dropoff, at_dropoffthe order returns to the pickup point (returning → returned)the return by tariff
final, failed, returning422 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.

These docs are built from the platform's OpenAPI specification. Download OpenAPI