Orders & fulfillment
An order is created from the public surface (your storefront's checkout) and
managed from the merchant surface. Examples reuse the
api() helper.
Legacy order creation remains live via
POST /public/orders. New BYO checkout builds should prefer checkout sessions, which bind the server-authoritative total and payment handoff before the order is created.
Create an order (public)
POST /api/v1/public/orders — tenant header, no auth.
const order = await api("/public/orders", {
method: "POST",
body: JSON.stringify({
customerName: "Dana Reyes", // required, ≤200
customerEmail: "dana@example.com",// required, valid email
items: [ // required, ≥1 line
{ itemId: "…", variantId: "…", name: "Trail Pack 38L", quantity: 1, unitPriceInCents: 18900 },
],
customerPhone: "+1…", // optional, ≤50
notes: "Leave at the side door", // optional, ≤2000
}),
});What the legacy public order path captures.
POST /public/ordersrecords line items only. The persisted order'ssubtotalInCentsis the sum of the lines,taxInCentsis0, andtotalInCentsequals the subtotal — there's no shipping line, and a coupon you previewed at the cart is not attached. Tax, shipping, and discounts are display-only estimates unless the storefront uses the M3 checkout-session flow. If your legacy cart UI shows a Total that includes tax or shipping, it will be higher than the order the API stores — reconcile your displayed total tosubtotalInCents, or clearly label the extras "estimated, finalized at checkout."
The M3 litecheckout/BYO checkout path adopts checkout sessions instead of this
legacy endpoint. Once a storefront adopts that flow, a bound checkout session
can return computed tax as public taxInCents / totalInCents values when a
tax provider is explicitly enabled; the confirmed order then receives that
server-authoritative total.
Public order reads — GET /public/orders, /public/orders/:id,
/public/orders/number/:num — strip the merchant notes field (it's an
internal scratchpad, below).
Manage an order (merchant)
Reads (GET /merchant/orders, /:id, /number/:num) are open to any member.
The three mutations require orders:operate; Owner/Admin retain broad access,
and Staff needs that group:
// Order status
await api(`/merchant/orders/${id}/status`, { method: "PATCH", token,
body: JSON.stringify({ status: "CONFIRMED" }) });
// Fulfillment status (separate axis)
await api(`/merchant/orders/${id}/fulfillment-status`, { method: "PATCH", token,
body: JSON.stringify({ fulfillmentStatus: "FULFILLED" }) });
// Internal notes (merchant-only; send null to clear)
await api(`/merchant/orders/${id}/notes`, { method: "PATCH", token,
body: JSON.stringify({ notes: "Refunded shipping as a courtesy" }) });Status state machines
OrderStatus — PENDING → CONFIRMED → PROCESSING → COMPLETED, with
CANCELLED reachable from any non-terminal state. COMPLETED and CANCELLED
are terminal.
FulfillmentStatus — UNFULFILLED → PARTIALLY_FULFILLED → FULFILLED,
forward-only (reversals route through Returns, below). Fulfillment is a
distinct axis from order status, so a PROCESSING order can be
PARTIALLY_FULFILLED.
Invalid transitions are rejected — you can't jump PENDING → COMPLETED.
Wire casing. Both enums serialize as
UPPER_SNAKEon the wire — the exact strings shown above (CONFIRMED,UNFULFILLED, …) — for both the values you read back and thestatusyou send in PATCH bodies. Compare against those literal values; don't assume lower-case.
notesis a merchant-internal field, never returned on public order reads. A customer-facing order message channel is a later epic.
Related
- Returns / RMA — the post-fulfillment path
- BYO checkout API — server-authoritative checkout sessions and payment handoff
- Shipping — rates quoted at the cart
- Pricing rules — discounts applied before checkout