Orders — customer reads + transfers

The authenticated customer's order surface: list, detail (items + fulfillments with tracking + addresses) and order transfers. There is no anonymous order read — every endpoint on this page requires a customer session (authorization: Bearer <jwt>, obtained via the passwordless code flow — see auth.md); a guest's only order handle is the completeCart() response (checkout.md). Missing/invalid JWT → 401 {code: "unauthenticated"} — demonstrated executably below; happy-path shapes are shown as jsonc (a customer session is admin-owned state on the shared tenant; the authenticated paths are proven by tests/store/customer-accounts-ownership.test.ts and the transfer suite).

Auth for every endpoint on this page: x-client-id + Bearer JWT.

SDK module: @barter/storefront/api/orders.


GET /api/store/orders/display/:displayId — my order by its human number

  • Purpose — resolve "order #42" (the number customers see in emails and confirmations) to the full order detail; account order pages and support links use this instead of the opaque id.
  • Auth — Bearer JWT required, same ownership scope as the id read: another customer's number is 404 not_found. display_id is a guessable autoincrement — that is exactly why there is no anonymous lookup.
  • RequestGET, no query. A numeric segment matches the autoincrement display_id OR a purely-numeric custom_display_id (custom wins ties — it is the number the store actually showed); any other segment matches custom_display_id only.
  • Response — identical {order} detail shape to GET /api/store/orders/:id above.
  • Errors — 401 unauthenticated; 404 not_found.
  • SDKorders.retrieveOrderByDisplayId(client, displayId).
  • Components — order-confirmation deep links, account order detail.
  • Settingscustom_display_id is set by merchants/imports (admin surface); absent it, only the autoincrement resolves.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/orders/display/42" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 401

GET /api/store/orders — list my orders

  • Purpose — the account "My orders" list, newest first.
  • Auth — Bearer JWT required. Scoped to the session customer AND the tenant — another customer's orders are invisible (not 403).
  • RequestGET ?limit=&offset=&status=limit 1–200 (default 20), offset ≥ 0 (default 0), status exact-match filter (pending, completed, canceled, …).
  • Response — plain order rows (NO embeds — fetch the detail for items/tracking):
{
  "orders": [{
    "id": "order_…",
    "display_id": 42,              // human-facing autoincrement
    "status": "pending",
    "email": "jane@example.com",
    "currency_code": "eur",
    "customer_id": "cus_…",
    "sales_channel_id": null,
    "region_id": "reg_…",
    "metadata": {},                // internal staff notes NEVER appear here
    "created_at": "2026-07-20T…", "updated_at": "2026-07-20T…"
  }],
  "count": 1, "offset": 0, "limit": 20
}
  • Errors — 401 unauthenticated; 400 validation_failed (bad limit/offset).
  • SDKorders.listOrders(client, {limit, offset, status}).
  • Components — account order-history page.
  • Settings — none (ownership is structural).
# Executable auth contract: no Bearer → 401 unauthenticated.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/orders" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 401

GET /api/store/orders/:id — my order, in full

  • Purpose — the order-detail page: items, fulfillment lifecycle + tracking, both addresses.
  • Auth — Bearer JWT required. Ownership is part of the lookup: an unknown id and another customer's order are BOTH 404 not_found (indistinguishable — no existence oracle).
  • RequestGET, no query.
  • Response — a store-safe subset of the admin order select (no customer row, no payment internals, no internal timeline comments):
{
  "order": {
    "id": "order_…", "display_id": 42, "status": "pending",
    "email": "jane@example.com", "currency_code": "eur",
    "items": [{                       // version pivot + embedded line item
      "id": "oi_…", "order_id": "order_…", "quantity": 1,
      "line_item": {
        "id": "oli_…", "title": "M", "product_title": "Linen Shirt",
        "product_handle": "linen-shirt", "thumbnail": "https://…",
        "variant_id": "variant_…", "variant_sku": "LIN-SHIRT-M",
        "unit_price": 45, "metadata": null
      }
    }],
    "fulfillments": [{                // couriers surface
      "fulfillment": {
        "id": "ful_…",
        "packed_at": "2026-07-20T…", "shipped_at": null,
        "delivered_at": null, "canceled_at": null,
        "labels": [{ "tracking_number": "…", "tracking_url": "https://…" }]
      }
    }],
    "shipping_address": { "first_name": "Jane", "city": "Sofia", "…": "order_addresses row" },
    "billing_address":  { "…": "order_addresses row" }
  }
}
  • Errors — 401 unauthenticated; 404 not_found.
  • SDKorders.retrieveOrder(client, orderId).
  • Components — order-detail page, tracking widget.
  • Settings — fulfillment/tracking data appears as the merchant packs and ships (admin fulfillment flow + carrier labels).
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/orders/order_any" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 401

Order transfers — claim a (guest) order into my account

Flow: the claiming customer calls request on an order that is not theirs → a one-time token is stored for the order's email holder (email dispatch pending server-side) → the claimer, whose account email must EQUAL the order's email, calls accept with the token. decline (the recipient rejects, token required) or cancel (the requester withdraws) end a pending transfer. One pending transfer per order — a duplicate request acknowledges with the same {transfer: {requested: true}} shape without creating another. The token never crosses this surface — it is email-delivered only (pinned by tests/store/order-transfer-token-leak.test.ts).

POST /api/store/orders/:id/transfer/request

  • Request{description?}.
  • Response200 {"order":{"id":"order_…","transfer":{"requested":true}}} (or the existing pending action object on a duplicate request).
  • Errors — 401 unauthenticated; 404 not_found; 400 already_owned (the order already belongs to the caller).
  • SDKorders.requestOrderTransfer(client, orderId, {description}).

POST /api/store/orders/:id/transfer/accept

  • Request{token} (≥16 chars, from the transfer email).
  • Response200 {order} — the full updated order row, now owned by the caller.
  • Errors — 401 unauthenticated; 404 not_found (order / no pending transfer); 403 invalid_token | email_mismatch (the caller's email must match the order's original email — a leaked token alone is not enough); 400 validation_failed.
  • SDKorders.acceptOrderTransfer(client, orderId, {token}).

POST /api/store/orders/:id/transfer/decline

  • Request{token}.
  • Response200 {"order":{"id":"order_…","transfer":{"declined":true}}}.
  • Errors — 401 unauthenticated; 404 not_found; 403 invalid_token; 400 validation_failed.
  • SDKorders.declineOrderTransfer(client, orderId, {token}).

POST /api/store/orders/:id/transfer/cancel

  • Request — no body.
  • Response200 {"order":{"id":"order_…","transfer":{"canceled":true}}}.
  • Errors — 401 unauthenticated; 404 not_found; 403 forbidden (only the requester may cancel).
  • SDKorders.cancelOrderTransfer(client, orderId).
  • Components — account "claim this order" flow + the transfer email's accept/decline landing page.
# Executable auth contract for the transfer family (all four Bearer-gated).
# Note the ordering nuance: accept/decline validate the BODY before auth,
# so a missing token 400s even for guests; request/cancel hit auth first.
for EP in transfer/request transfer/cancel; do
  STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
    "$BASE/api/store/orders/order_any/$EP" \
    -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
  test "$STATUS" = 401
done
for EP in transfer/accept transfer/decline; do
  RES=$(curl -s -X POST "$BASE/api/store/orders/order_any/$EP" \
    -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
  echo "$RES" | grep -q '"code":"validation_failed"'
  STATUS=$(curl -s -o /dev/null -w '%{http_code}' -X POST \
    "$BASE/api/store/orders/order_any/$EP" \
    -H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
    -d '{"token":"0123456789abcdef0123456789abcdef"}')
  test "$STATUS" = 401
done

Cleanup / accretion note

This page creates nothing — every executable block is a read/auth-contract probe against nonexistent ids.