State reference

Order states

An order has one lifecycle status and one payment status. The lifecycle status answers "how far along is this order?"; the payment status answers "how much of the money has arrived?". Nothing else.

Why two fields and not one

Delivery only moves forwards: once an item ships, it has shipped. Money moves both ways — a captured payment can be refunded, and a refund is not a step backwards through the lifecycle, it is a separate fact about the same order. Folding the two into a single field would mean either losing the refund or corrupting the sequence.

So status is a single ordered sequence, and paymentStatus sits alongside it. Read them together and you have the whole picture; that combination is exactly what displayStatus gives you.

There is no fulfillmentStatus
An order used to carry a third field, fulfillmentStatus, alongside status. It was removed: the two encoded one fact, had to be kept in sync by hand, and drifted. Delivery progress now lives in status itself, with per-item counts in fulfillmentSummary.

What each field owns

  • status — the lifecycle. Moved automatically by payments and fulfillments, and settable by hand via POST /orders/{id}/status.
  • paymentStatus — how much of the money has arrived. Rolled up across every payment on the order. Never set directly.
  • displayStatus — a human-readable label combining the two, e.g. "Processing — Awaiting payment". Derived on every read, never stored, not filterable. Display it; branch on status.
  • fulfillmentSummary { itemsTotal, itemsFulfilled, itemsRemaining }. Items with nothing to deliver (a donation, a fee) are excluded from the total.
  • payments[].status — what happened on one transaction. Never a summary of the order.

State machine

stateDiagram-v2
  [*] --> draft: order created
  draft --> pending: INITIATE_CHECKOUT
  pending --> processing: PAYMENT_AUTHORIZED / PAYMENT_CAPTURED
  processing --> partially_fulfilled: FULFILLMENT_PARTIAL
  processing --> fulfilled: FULFILLMENT_COMPLETE
  partially_fulfilled --> fulfilled: FULFILLMENT_COMPLETE
  fulfilled --> closed: CLOSE
  draft --> cancelled: CANCEL
  pending --> cancelled: CANCEL
  processing --> cancelled: CANCEL
  partially_fulfilled --> cancelled: CANCEL
  cancelled --> closed: CLOSE
  closed --> [*]
The automatic transitions. A merchant can also move an order to any status directly — see Setting a status by hand.

The seven statuses

  • draft — created, checkout not started. No payment expected.
  • pending — placed, awaiting payment.
  • processing — payment authorized or captured, nothing delivered yet. Fulfillments can now be created.
  • partially_fulfilled — some deliverable items delivered, some outstanding.
  • fulfilled — everything deliverable has been delivered. This is the happy-path end state. An order with nothing to deliver at all reaches it as soon as payment lands.
  • cancelled — called off. Terminal. Authorized payments are voided; captured money is refunded only on explicit opt-in.
  • closed — archived out of active views. Terminal. Reachable from fulfilled or cancelled: closing is bookkeeping, not completion.
completed is retired
completed meant "delivered and paid" — two facts in one word. It is gone; read status === 'fulfilled' together with paymentStatus instead. Orders that held it were migrated to fulfilled.

Setting a status by hand

POST /orders/{id}/status with { "status": "fulfilled", "reason": "shipped manually" }. Any status is reachable from any other, in both directions. Real orders go wrong in ways a state machine cannot anticipate, and a merchant correcting a mistake needs to move an order back.

Moving backwards does not undo anything
Setting a status changes the label and records who changed it. It never rewinds a side effect: captured money stays captured, sent emails stay sent, shipped fulfillments stay shipped. To reverse money, refund the payment; to reverse a shipment, cancel the fulfillment.
Moving to cancelled is the one exception

cancelled is the same terminal state POST /orders/{id}/cancel reaches, so setting it by hand does the same money-side work: every authorized payment on the order is voided, because a cancelled order must never capture. Captured payments are left alone — refunding them is an explicit refundCapturedPayments: true opt-in on /cancel, and editing a status is not consent to move money back.

For the same reason, a request whose target is cancelled additionally requires the order_cancellations:write scope and returns 403 insufficient_scopes without it. Every other target status needs only orders:write.

Every change — manual or automatic — appends a row to the order's history, readable at GET /orders/{id}/status-history. Each row carries fromStatus, toStatus, actorType ( user | system | integration), reason, and source ( manual | state_machine | fulfillment | payment | migration).

Endpoint per transition

  • POST /orders/{id}/checkoutdraftpending
  • POST /orders/{id}/authorize and POST /orders/{id}/capturependingprocessing
  • Creating and completing fulfillments moves the order toward partially_fulfilled and then fulfilled, recomputed from the remaining item quantities on every fulfillment change.
  • POST /orders/{id}/status — any status, either direction.
  • POST /orders/{id}/closefulfilled or cancelledclosed
  • POST /orders/{id}/cancel — → cancelled, with payment settlement

POST /orders/{id}/complete has been removed. Use POST /orders/{id}/status with "fulfilled", or let the fulfillment roll-up do it.

What you cannot do

  • PATCH /orders/{id} still does not touch status. It accepts only billingAddress, shippingAddress, notes and metadata. Status changes go through /status so that every one of them is recorded.
  • You cannot set paymentStatus. It is rolled up from the payments on the order.
  • You cannot filter or sort on displayStatus. It does not exist in the database.
  • You cannot make a manual status change undo a side effect. See the warning above.

Outbound events

  • order.created — emitted exactly once, by whichever path inserts the order.
  • order.updated — every status change that does not have its own event, including every paymentStatus change.
  • order.fulfilled — on entering fulfilled. Replaces order.completed, which is retired: nothing emits it, and endpoints and extension manifests that named it were rewritten to the new type in place, so existing subscribers kept receiving fulfillment notifications without changing anything. The old name is still accepted on write requests (and dropped from what gets stored) so a read-modify-write on an old endpoint definition does not fail.
  • order.cancelled — on entering cancelled.
  • order.closed — on entering closed.

A manual change emits the same event its target status would emit automatically, with manual: true and an actor on the payload, and action: "SET_STATUS".

A status change ingested from a connected store emits the same event too, with source: "provider" on the payload and action: "fulfillment_sync" (or "cancel"). Exactly one event per change, the same as an automatic transition — so a store fulfilment that lands the order on fulfilled fires order.fulfilled, and one that lands it on partially_fulfilled fires a single order.updated.