State reference

Fulfillment states

Fulfillment lifecycles are shared across all 5 fulfillment types (shipment, digital, access, service, in_person). The terminal definition varies by type.

State machine

stateDiagram-v2
  [*] --> pending: POST /fulfillments
  pending --> in_progress: shipment label created / digital prepared / access pending
  in_progress --> fulfilled: shipped+delivered / file delivered / access granted / service complete / in-person handoff
  pending --> cancelled: merchant cancels before in_progress
  in_progress --> cancelled: merchant cancels mid-fulfillment
  cancelled --> [*]
  fulfilled --> [*]
Fulfillment lifecycle (uniform across all 5 fulfillment types).

States

  • pending — fulfillment row created, no provider activity yet.
  • in_progress — provider has acknowledged the work (label printed, digital file prepared, access pending). Fires fulfillment.shipment.shipped for shipment type — on the first tracking number, whether it arrives with POST /orders/{orderId}/fulfillments (shipment.trackingNumber) or a later PATCH .../fulfillments/{id}/shipment.
  • fulfilled — terminal. Per type:
    • shipment — carrier marks delivered ( fulfillment.delivered).
    • digital — file generated and a presigned URL was returned at least once.
    • access — access grant row inserted and active.
    • service / in_person — merchant marks complete via API.
  • cancelled — terminal. Reachable from pending or in_progress. For shipment, cancelling after label creation requires merchant void via the carrier.

Type-specific details

Each fulfillment carries exactly one detail record, decided by its type, and each detail endpoint only accepts a fulfillment of its own type:

  • shipmentPATCH /api/v1/orders/{orderId}/fulfillments/{id}/shipment
  • digital…/digital
  • access_grant…/access
  • service…/service

Pointing one at a fulfillment of another type returns 409 fulfillment_type_mismatch and writes nothing. digital and access_grant line items are fulfilled automatically when the payment captures, so their fulfillment already exists — creating a second one returns 400 over_fulfillment. Patch the existing one instead.

When the order is considered fulfilled

Per-item, not per-row
The parent order reaches fulfilled when every deliverable line item's quantity is covered by completed fulfillments — not when every fulfillment row happens to be terminal. Line items with nothing to deliver (a donation, a fee) are excluded from the count, so an order made entirely of them is fulfilled as soon as it is paid. Progress is readable on the order as fulfillmentSummary.