Skip to main content

Shipment Lifecycle

Every shipment moves through a state machine. The same statuses cover both same-city and regional deliveries — only the path differs.

State machine

PENDING
│ (transient — only seen if you use gateway payment instead of wallet)

CONFIRMED payment cleared, dispatch starts


PICKUP_DISPATCHED first-mile rider assigned, heading to sender


PICKED_UP rider has the package

├── (same-city) ──────────┐
│ │
▼ (regional) │
IN_TRANSIT │ hub-to-hub transit leg
│ │
▼ ▼
OUT_FOR_DELIVERY last-mile rider heading to receiver


DELIVERED ✅

Terminal states: DELIVERED, CANCELLED, FAILED.

Status reference

StatusMeaningWhat happens
pendingJust created, awaiting paymentFor wallet shipments, this exists only for a fraction of a second before flipping to confirmed
confirmedPayment receivedAuto-dispatch begins — engine picks the nearest available rider
pickup_dispatchedRider assignedRider has the job in their app and is heading to the sender
picked_upRider has the packageSender's part is done
in_transitRegional only — hub-to-hubPackage is on our transit network between cities
out_for_deliveryLast-mile rider heading to receiverFinal leg
deliveredSuccessfully deliveredTerminal — proof-of-delivery captured
cancelledCancelled before pickupWallet refunded
failedDispatch or delivery attempts exhaustedReturned or held for resolution. Wallet refunded if the package never moved.
Stuck in confirmed?

If no rider is in range at the time of payment, the shipment stays at confirmed and our dispatcher retries automatically on a schedule for a while before giving up and marking the shipment failed. You don't need to poll or re-create — just listen for delivery.picked_up or delivery.failed. If you see frequent failed results from no-rider situations, talk to support about expanding coverage in that area.

Same-city vs regional

StatusSame-cityRegional
pending
confirmed
pickup_dispatched
picked_up
in_transit(skipped)
out_for_delivery
delivered

Same-city shipments go directly from picked_upout_for_delivery. Regional shipments pass through in_transit while the package moves between sorting hubs.

Journey legs

Each shipment has 1 or 3 legs — segments handled by different actors:

LegDirectionHandled byUsed in
first_mileSender → origin hub (or full route for same-city)RiderBoth
transitOrigin hub → destination hubDodo transit networkRegional only
last_mileDestination hub → receiverRiderRegional only

Legs appear in the legs array of every shipment response. Each leg has its own status (pending, in_progress, completed, failed) so you can show progress per segment in your UI.

Hub sub-events (regional)

Regional shipments pass through sorting hubs. These don't change the top-level status but are recorded in the events array:

Event descriptionWhen
Shipment received at {hub_name}Package checked in at origin hub
Package containerized for transitLoaded onto an outgoing container
Container dispatched from {hub_name}Transit container left origin hub
Container arrived at {hub_name}Transit container arrived at destination hub
Last-mile rider assignedLocal rider picked up from destination hub

Use the events array on GET /merchant/shipments/{id} to surface fine-grained progress.

Webhook mapping

Each status transition fires a corresponding webhook. See Webhooks for payloads.

TransitionEvent
confirmedshipment.created, shipment.paid
pickup_dispatcheddelivery.rider_assigned
(rider moving — no status change)delivery.en_route_to_pickup
picked_updelivery.picked_up
delivereddelivery.delivered
cancelledshipment.cancelled
faileddelivery.failed

Cancellation rules

Cancellable from: pending, confirmed, pickup_dispatched. Cancellation fees are state-graded — see Manage Shipments → Cancel a shipment for the full fee table and the two-step preview/commit API.

Not cancellable from: picked_up, in_transit, out_for_delivery, delivered, cancelled, failed.

Once the rider has the package, create a return-to-sender shipment instead of cancelling.