Shipment lifecycle

Updated May 29, 20264 min read

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 in error_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 in error (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 for error like confirm does, 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.