Skip to main content

Outcome reasons

What is live

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​

  1. A reason is namespaced by the status it qualifies. completed.* only ever appears with COMPLETED, failed.* with FAILED, canceled.* with CANCELED. Nothing qualifies RESERVED, ROUTED or ONGOING — those are not outcomes.
  2. null is not a reason. It means none was recorded: the work predates this vocabulary, or nobody was asked. It is not completed.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.
  3. *.other always carries a status_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.
  4. 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​

CodeMeans
completed.cleanHanded over with no caveat, and somebody recorded that. Not the same as no reason at all — see the note below the tables.
completed.partial_quantitySome of the goods were handed over, not all. Read actual_amounts for what actually moved.
completed.item_withheldA specific item was deliberately not handed over.
completed.alternative_recipientGiven to a neighbour, a reception desk, or another third party.
completed.left_unattendedLeft at the door or at an agreed drop point.
completed.no_signatureDelivered, but a signature could not be obtained.
completed.outside_windowDelivered outside the agreed time window, and accepted anyway.
completed.damagedDelivered, and the goods were damaged.

FAILED​

CodeMeans
failed.recipient_absentNobody was there.
failed.recipient_refusedThe recipient refused to take it.
failed.access_deniedCould not reach the place — site closed, gate locked, tipping place unreachable.
failed.goods_unavailableNothing to collect — not in stock, not ready.
failed.goods_damagedDamaged before handover, so it was not handed over.
failed.address_problemThe address was wrong, incomplete, or could not be found.
failed.vehicle_problemBreakdown, or the wrong vehicle for the load.
failed.time_exhaustedThe driver ran out of legal hours.
failed.rescheduledMoved at the customer’s request.
failed.returned_to_originTaken back to where it came from.
failed.otherNone of the above. Requires status_description.

CANCELED​

CodeMeans
canceled.by_customerThe customer called it off.
canceled.by_operatorWe called it off.
canceled.duplicateThe same work exists on another order.
canceled.otherNone 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:

MessageCause
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.