Shipment lifecycle
Understanding the shipment lifecycle helps you build reliable integrations and diagnose unexpected states. Every Shipment moves through a defined sequence of statuses from creation through delivery. The status field is the single source of truth for where a shipment is. Downstream systems (notifications, reporting, customer-facing tracking, your OMS) react to each change.
This page describes the model. The full state machine, with every status code and every transition, is in Reference → shipment status flow.
The lifecycle in shape
draft
│ confirm (or create directly, confirmed mode)
▼
pending ──▶ error ──(fix, then confirm / reprocess)──▶ pending
│
▼ carrier accepts
booked
│
▼
ready_to_ship
│
▼
shipped ──▶ in_transit ──▶ out_for_delivery ──▶ delivered
│
└─ failed_delivery_attempt ──(retry)──▶ out_for_delivery
cancelled pre-shipping only (draft … ready_to_ship)
returned return-to-origin when delivery fails
Booking is asynchronous. Creating a shipment (or confirming a
draft) submits it to the carrier and leaves it in pending; Carriyo
doesn't wait for the carrier's answer. The shipment then moves to
booked (tracking number and label attached) or error, and that
outcome arrives on a webhook. The exception is a
pre-booked shipment, which
is registered directly as booked because Carriyo never calls the
carrier.
The path from pending to delivered is the happy path. The branches
matter: most operational complexity is in the error states, the retry
loops, and the cancellation / return paths.
Drafts and confirmation
draft is the only state in which a shipment exists without
having been booked with a carrier. No label, no tracking number,
no fees, just a record of intent. Drafts are useful for flows
where the OMS pre-creates ahead of fulfillment, or where a
deliberate confirmation step is required.
Confirming a draft submits it for booking: it moves to pending,
then to booked once the carrier accepts (or error if it doesn't).
See Draft vs confirmed for
the full distinction.
Errors and retries
A shipment lands in error in one of two ways, and
post_shipping_info.error_details[].source tells you which:
CARRIYO— Carriyo's own validation rejected the shipment up front (synchronously, in the create or confirm response), for example when no carrier account could be assigned.CARRIER— a carrier was assigned but rejected the booking (asynchronously, reported on a webhook); the carrier's own message is passed through inerror_details.
Two endpoints move an errored shipment back into the booking pipeline:
POST /shipments/{id}/confirm. Re-runs the booking step. Supported when the shipment is inerror(the typical retry case) and works the same way as confirming a draft.POST /shipments/{id}/reprocess. Fetches the current data, applies any overrides you supply, and resubmits. Works forerrorlikeconfirmdoes, but is also the retry path for a wider range of states, including post-shipping corrections (for example, re-booking after a cancellation, return, or delivery exception). See the Reprocess shipment API reference for the supported states.
Some errors are recoverable (transient carrier outage, minor address fix); others aren't (restricted commodity, permanently invalid address). The error response carries the cause; your ops flow should triage by error type.
Failed delivery
A shipment can be out_for_delivery and not actually deliver:
the customer wasn't home, the address was wrong, the shipment
was refused. That's failed_delivery_attempt. From there, the
carrier re-attempts (typically up to a configured number of
retries), returning to out_for_delivery. After exhausted
retries, the shipment may end up returned to the merchant.
Cancellation
Cancellation is a pre-shipping operation. It's allowed from
draft through to ready_to_ship, before the shipment has
physically left the origin. Once a shipment has moved to
shipped or beyond, it can no longer be cancelled directly.
Reverse shipments vs return-to-origin
These are two different things and they're often confused.
A reverse shipment is a brand-new Shipment that flows from
the end customer back to the merchant, typically created as part
of a return workflow. It uses the same Shipment object with
entity_type=REVERSE and runs through the same status set as a
forward shipment. The key difference: when a reverse shipment
reaches delivered, it means delivered to the merchant, not
to the customer.
A return-to-origin (RTO) flow is what happens to the original forward shipment when delivery fails. The carrier brings it back without a new Shipment being created. The same forward Shipment progresses through dedicated statuses, typically:
failed_delivery_attempt → ready_for_return → return_in_transit → returned
Not all of these statuses occur in every flow. Some carriers
skip intermediate states and move straight to returned. RTO
is a continuation of the forward shipment, not a new entity.
How status updates flow
Two sources of status change:
- Carrier callbacks. As shipments move, the carrier reports events back to Carriyo. Carriyo translates each carrier's own status set into a Carriyo-canonical event and publishes it.
- Workflow actions. Operations performed in Carriyo itself (marking a shipment ready to ship, cancelling it, reprocessing it) each produce their own status transitions.