Outcome reasons
status_reason, status_description and actual_amounts are on every order, and the API
validates them. You set canceled.* when you cancel; a stop completed or failed through the
Routes API carries its reason to the order. Drivers choosing a reason in the driver app is
coming — until then, driver completions arrive with status_reason: null.
status tells you an order was completed, failed or canceled. status_reason tells you
why, from a closed vocabulary.
The two are not interchangeable and the reason is the one most integrations actually act on: "delivered" and "delivered to the neighbour" are the same lifecycle status and very different outcomes for whoever is waiting for the parcel.
The rules
- A reason is namespaced by the status it qualifies.
completed.*only ever appears withCOMPLETED,failed.*withFAILED,canceled.*withCANCELED. Nothing qualifiesRESERVED,ROUTEDorONGOING— those are not outcomes. nullis not a reason. It means none was recorded: the work predates this vocabulary, or nobody was asked. It is notcompleted.clean, which is a positive statement that someone checked and there was no caveat. Treat "no deviation" and "no data" as different answers — the thing legacy could not do.*.otheralways carries astatus_description. It is the only code that may not stand alone, and the only place free text is meaningful. Do not pattern-match the other codes out of that field; that is what the codes are for.- The vocabulary is closed and additive within a contract version. New codes may appear. Existing ones are never renamed or removed without a version bump — so match on the exact string, and treat an unknown code as "something happened we have not seen before" rather than as an error.
The codes
COMPLETED
| Code | Means |
|---|---|
completed.clean | Handed over with no caveat, and somebody recorded that. Not the same as no reason at all — see the note below the tables. |
completed.partial_quantity | Some of the goods were handed over, not all. Read actual_amounts for what actually moved. |
completed.item_withheld | A specific item was deliberately not handed over. |
completed.alternative_recipient | Given to a neighbour, a reception desk, or another third party. |
completed.left_unattended | Left at the door or at an agreed drop point. |
completed.no_signature | Delivered, but a signature could not be obtained. |
completed.outside_window | Delivered outside the agreed time window, and accepted anyway. |
completed.damaged | Delivered, and the goods were damaged. |
FAILED
| Code | Means |
|---|---|
failed.recipient_absent | Nobody was there. |
failed.recipient_refused | The recipient refused to take it. |
failed.access_denied | Could not reach the place — site closed, gate locked, tipping place unreachable. |
failed.goods_unavailable | Nothing to collect — not in stock, not ready. |
failed.goods_damaged | Damaged before handover, so it was not handed over. |
failed.address_problem | The address was wrong, incomplete, or could not be found. |
failed.vehicle_problem | Breakdown, or the wrong vehicle for the load. |
failed.time_exhausted | The driver ran out of legal hours. |
failed.rescheduled | Moved at the customer’s request. |
failed.returned_to_origin | Taken back to where it came from. |
failed.other | None of the above. Requires status_description. |
CANCELED
| Code | Means |
|---|---|
canceled.by_customer | The customer called it off. |
canceled.by_operator | We called it off. |
canceled.duplicate | The same work exists on another order. |
canceled.other | None of the above. Requires status_description. |
Partial deliveries are not a separate status
A short delivery is COMPLETED with completed.partial_quantity and an
actual_amounts object saying what really moved, against the amounts that were planned.
There is deliberately no PARTIAL status. It matches how legacy recorded the same thing —
delivered, qualified by a deviation — and it means a partner polling us sees a delivery
that happened, with a caveat, rather than a fourth state to special-case.
FAILED is not the end
Worth repeating from the order lifecycle: a failed stop
is work that still needs doing. It returns to the unplanned queue and may be re-planned, so
the same order can be FAILED with failed.recipient_absent today and COMPLETED with
completed.clean tomorrow.
Only COMPLETED and CANCELED are final. An integration that closes its own record on
FAILED, or treats the second transition as a duplicate, loses the real outcome.
Validation
A reason is checked against the status the order ends up in, and refused with 400 when it
does not fit:
| Message | Cause |
|---|---|
unknown status_reason '<code>' | Not a code from the tables above |
status_reason '<code>' cannot qualify status '<status>' | Wrong family, for example failed.* on a COMPLETED order |
status_reason '<code>' requires a non-empty status_description | *.other without a description |
A reason stays on the order until it is replaced or cleared. To move an order out of
CANCELED or FAILED by PATCH, send "status_reason": null in the same request.
Mapping to your own codes
The vocabulary is deliberately generic — it has to describe flowers, pallets and bulk haulage with the same words. If your system has its own status or deviation codes, map them on the reason rather than on the lifecycle status, and map by family first:
- anything in
completed.*is a delivery that happened - anything in
failed.*is a delivery that did not happen yet - anything in
canceled.*is a delivery that will not happen
Then map the individual codes you care about and let the rest fall back to a family default. That way a new code added within the contract version does not break your mapping — it lands in the right bucket until you choose to handle it.
Coming from the legacy platform's driver deviations? See Migrating from the legacy platform.