{
  "openapi": "3.1.0",
  "info": {
    "title": "Stixel Commerce API",
    "description": "Orders and money for Stixel Maker (epic BRC-61). Postgres-backed Rust service; the authority on order lifecycle from payment through delivery. Stripe is the financial source of truth \u2014 this API records and orchestrates, it never computes money it didn't get from Stripe.\nAn order carries two independent lifecycle tracks (BRC-75): `payment_status` (where the money is, written by Stripe events) and `fulfillment_state` (where the print is, written by the packer, the operator, and Gooten). A partial refund is a fact about the money and leaves the print side alone.\n",
    "version": "0.1.0"
  },
  "servers": [
    {
      "url": "https://commerce.stixelmaker.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:8002",
      "description": "Local development"
    }
  ],
  "paths": {
    "/checkout/session": {
      "post": {
        "operationId": "createCheckoutSession",
        "summary": "Create a hosted Stripe Checkout Session for a draft project",
        "description": "The web checkout head (BRC-64). The server re-derives the authoritative quote from the sticker API (`GET /projects/{id}/quote`) and prices it from the commerce table \u2014 nothing the client sends influences the price. A `pending` order row is written before the Stripe call, so an abandoned checkout is a visible fact; the webhook flips its `payment_status` to `paid`. Stripe owns everything after the redirect: card entry, address, shipping choice, promotion codes, tax.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutSessionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session created; redirect the customer to `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSessionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing project id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The sticker API knows no such project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Either the project already has a live (paid, unrefunded) order \u2014 buying it again would double-charge \u2014 or it has no quotable sticker lines (`error` distinguishes the two).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Storage failure, or catalog drift between services.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "The sticker API or Stripe is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/checkout/mobile-intent": {
      "post": {
        "operationId": "createMobileIntent",
        "summary": "Create a PaymentIntent for the iOS PaymentSheet",
        "description": "The native counterpart of `/checkout/session` (BRC-66). Same server-derived quote, same pricing table, same pending order, same webhook pipeline \u2014 only the Stripe object differs: a bare PaymentIntent the app presents via PaymentSheet (Apple Pay, cards). The app collects email, shipping choice and address in its own UI and sends them here; the server states the amount (lines + flat shipping). `tax_cents` is 0 until Stripe Tax is wired to this path (decided in BRC-66; tightened with BRC-30) \u2014 the field exists so the shape won't change.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MobileIntentRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Intent created; present PaymentSheet with `client_secret`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MobileIntentResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing project id or unknown shipping code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The sticker API knows no such project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Live order exists, or no quotable lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Storage failure, or catalog drift between services.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "The sticker API or Stripe is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/orders/{id}/status": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getOrderStatus",
        "summary": "Customer-facing order status (capability-scoped)",
        "description": "Polled by the post-checkout success page (BRC-70). The order UUID is the capability \u2014 unguessable, handed only to the checkout's creator in the success URL, the same model as project ids. Customer-safe on purpose: no email, address, or Stripe ids.\n",
        "responses": {
          "200": {
            "description": "Where the order is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderStatus"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or malformed order id."
          },
          "500": {
            "description": "Storage failure."
          }
        }
      }
    },
    "/admin/orders": {
      "get": {
        "operationId": "adminListOrders",
        "summary": "Recent orders, newest first (operator)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Order summaries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AdminOrderSummary"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/queue": {
      "get": {
        "operationId": "adminOrdersQueue",
        "summary": "The worklist \u2014 every paid, unshipped, living order (operator)",
        "description": "Oldest debt first: money held (`paid` or `partially_refunded`) and not yet shipped. The fulfillment state is the reason a row is listed \u2014 `awaiting_layout` needs a human most urgently, `none` means the post-payment pipeline stalled. An unreadable row fails the request loudly rather than presenting an empty desk.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Outstanding orders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AdminOrderSummary"
                  }
                }
              }
            }
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "adminOrderDetail",
        "summary": "One order in full \u2014 row, charged lines, timeline (operator)",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The order with its lines and audit events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "order",
                    "items",
                    "events"
                  ],
                  "properties": {
                    "order": {
                      "$ref": "#/components/schemas/AdminOrder"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdminOrderItem"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdminOrderEvent"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such order."
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`)."
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/{id}/approve": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "adminApproveOrder",
        "summary": "Approve for fulfillment \u2014 gated by the live tally (operator)",
        "description": "`in_review \u2192 approved`. The tally is fetched from the sticker API at approval time, never from a cached value: a layout that would deliver fewer stickers than were paid for is refused with 409 `layout_under_delivers` + `short_lines`. No tally, no approval (502 `tally_unavailable`) \u2014 refusing beats approving blind. What was approved (sheets + per-line tally) is snapshotted onto the order as `approved_layout`; submit compares against it.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Approved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LifecyclePosition"
                }
              }
            }
          },
          "404": {
            "description": "No such order."
          },
          "409": {
            "description": "Layout under-delivers, or illegal transition."
          },
          "502": {
            "description": "Tally or fulfillment info unavailable from the sticker API."
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`)."
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/{id}/submit": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "adminSubmitOrder",
        "summary": "Submit an approved order to Gooten (operator)",
        "description": "One recipe minted per sheet (an order packed across sheets prints a fraction of itself if only the first is submitted), then the Gooten order, then fulfillment `approved \u2192 submitted` \u2014 with an audit event carrying the external id, exactly which sheets shipped, and what was charged (the packer makes those genuinely different). Dry-run (no GOOTEN_API_KEY, or GOOTEN_DRY_RUN=1) returns a synthetic external id and skips the unrendered-sheet guard so dev flows complete without a rendered master. Live submissions send IsTest until GOOTEN_LIVE_ORDERS=1 opts in.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Submitted (or dry-run recorded).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/LifecyclePosition"
                    },
                    {
                      "type": "object",
                      "required": [
                        "external_id",
                        "dry_run",
                        "submitted_sheets"
                      ],
                      "properties": {
                        "external_id": {
                          "type": "string"
                        },
                        "dry_run": {
                          "type": "boolean"
                        },
                        "submitted_sheets": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "sheet_id",
                              "medium",
                              "pack",
                              "print_asset_url"
                            ],
                            "properties": {
                              "sheet_id": {
                                "type": "string"
                              },
                              "medium": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "pack": {
                                "type": "integer"
                              },
                              "print_asset_url": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "Not approved (`not_approved`); money no longer held \u2014 refunded or cancelled (`payment_not_held`); the layout changed since it was approved or now under-delivers (`layout_changed` + `changes` + `short_lines` \u2014 the approval is withdrawn and the order is back in `in_review`); sheets missing print masters (`sheets_unrendered` + `sheet_ids`); or nothing to print.\n"
          },
          "500": {
            "description": "Storage failure, catalog drift (`catalog_drift`), or \u2014 worst case \u2014 the Gooten order was placed but the row did not move (`submitted_but_not_recorded`; the timeline carries the external id, reconcile by hand, do not resubmit).\n"
          },
          "502": {
            "description": "Sticker API (tally or fulfillment info) or Gooten unreachable."
          },
          "404": {
            "description": "No such order."
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`)."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/{id}/layout": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "adminSetLayout",
        "summary": "Record a layout hand-off on the print track (operator)",
        "description": "The design work happens on the board; this tells the order what it meant. `needs_manual_layout`: the pack is no good \u2014 `in_review` or `approved` \u2192 `awaiting_layout`. `ready_for_review`: a layout wants proofing \u2014 `awaiting_layout` or `approved` \u2192 `in_review`. Leaving `approved` withdraws the approval and its snapshot. Illegal from anywhere else (e.g. once submitted).\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "outcome"
                ],
                "properties": {
                  "outcome": {
                    "type": "string",
                    "enum": [
                      "needs_manual_layout",
                      "ready_for_review"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LifecyclePosition"
                }
              }
            }
          },
          "409": {
            "description": "Illegal transition from the current state."
          },
          "404": {
            "description": "No such order."
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`)."
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/{id}/deliver": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "adminDeliverOrder",
        "summary": "Confirm delivery \u2014 `shipped \u2192 delivered` (operator)",
        "description": "Manual for v1; the operator confirms from carrier tracking.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LifecyclePosition"
                }
              }
            }
          },
          "409": {
            "description": "Not shipped."
          },
          "404": {
            "description": "No such order."
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`)."
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/admin/orders/{id}/retry-followups": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "adminRetryFollowups",
        "summary": "Re-run the post-payment follow-ups for a stalled order (operator)",
        "description": "For an order that is paid but whose print track is still `none`: the webhook's hand-off to the sticker API (`mark_ordered`) failed and nothing else will move it \u2014 Stripe does not resend the payment event. Runs the same phase-2 steps the webhook ran (confirmation email only if none was sent, mark-ordered, the first print transition, render trigger). Annotates `followups_retried` with the operator.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The lifecycle after the attempt. `followup_error` is null when the pipeline advanced; otherwise the step that still fails and why.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RetryFollowupsResult"
                }
              }
            }
          },
          "409": {
            "description": "Not stalled \u2014 unpaid",
            "already past `none`": null,
            "or a duplicate payment (`not_stalled`).": null
          },
          "404": {
            "description": "No such order."
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`)."
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          }
        }
      }
    },
    "/admin/orders/{id}/tracking": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "adminSetTracking",
        "summary": "Record carrier + tracking number \u2014 ships the order (operator)",
        "description": "Fulfillment `submitted \u2192 shipped`, with the tracking fields and the transition in one transaction so a shipped order always has its number.\n",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "carrier",
                  "tracking_number"
                ],
                "properties": {
                  "carrier": {
                    "type": "string"
                  },
                  "tracking_number": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Shipped.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/LifecyclePosition"
                    },
                    {
                      "type": "object",
                      "required": [
                        "carrier",
                        "tracking_number"
                      ],
                      "properties": {
                        "carrier": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "tracking_number": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed order id (`invalid_order_id`) or empty carrier / tracking number (`missing_tracking_fields`).\n"
          },
          "409": {
            "description": "Illegal transition (not yet submitted)."
          },
          "404": {
            "description": "No such order."
          },
          "500": {
            "description": "Storage failure."
          },
          "401": {
            "description": "Missing or invalid operator token."
          },
          "403": {
            "description": "Authenticated but not a Stixelize operator."
          }
        }
      }
    },
    "/webhooks/stripe": {
      "post": {
        "operationId": "stripeWebhook",
        "summary": "Stripe event delivery (Stripe-to-server only)",
        "description": "Consumed by Stripe, never by our clients \u2014 documented for completeness. Verifies the `Stripe-Signature` header (HMAC-SHA256, 5-minute tolerance) against `STRIPE_WEBHOOK_SECRET`. Handles `checkout.session.completed` and `payment_intent.succeeded` (record Stripe's charged amounts and shipping address, flip `payment_status` to `paid`, and run the sticker pipeline \u2014 but only when the session's `payment_status` is `paid` or `no_payment_required`; a delayed-notification method's `unpaid` completion records the facts and waits for `checkout.session.async_payment_succeeded` / `\u2026_failed`), `charge.refunded` (records the cumulative `refunded_cents`; `payment_status` \u2192 `partially_refunded` or `refunded`, the print side untouched), and `refund.updated` with a failed or canceled status (the money is back on the charge: `refunded_cents` drops and `payment_status` returns to `paid` / `partially_refunded`). Idempotent by event id: the processed-event ledger commits in the same transaction as the effects, so at-least-once delivery is safe. Non-2xx responses make Stripe redeliver with backoff \u2014 returned deliberately when an event can't yet be attributed to an order.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "A Stripe event envelope (opaque to this spec)."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Processed, duplicate, or ignored \u2014 do not redeliver."
          },
          "400": {
            "description": "Bad signature or unparseable body."
          },
          "500": {
            "description": "Not yet processable \u2014 redeliver later."
          }
        }
      }
    },
    "/prices": {
      "get": {
        "operationId": "getPrices",
        "summary": "The public price table (sheet prices per medium \u00d7 pack, flat shipping)",
        "description": "What the storefront shows beside a quote before any order exists (BRC-81) \u2014 the role Medusa's variant list used to play. Static constants (`pricing.rs`), so the response carries `Cache-Control: public, max-age=300`. Display only: checkout re-prices server-side from the same table and Stripe records what was charged.\n",
        "responses": {
          "200": {
            "description": "Every sellable (medium \u00d7 pack) and both shipping rates.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "description": "`public, max-age=300`"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceList"
                }
              }
            }
          }
        }
      }
    },
    "/me/orders": {
      "get": {
        "operationId": "getMyOrders",
        "summary": "The signed-in customer's orders, newest first",
        "description": "Requires the customer's Stytch **consumer** session JWT as `Authorization: Bearer`. Returns orders stamped with their Stytch id at checkout **and** orders bought anonymously with the address Stytch verified for this session \u2014 the common path, because the storefront sells without an account. Unpaid, abandoned checkouts are omitted. Customer-safe: no Stripe ids, no operator annotations.\n",
        "security": [
          {
            "CustomerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Up to 100 orders, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomerOrder"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid customer session token."
          },
          "500": {
            "description": "Storage failure."
          }
        }
      }
    },
    "/me/orders/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getMyOrder",
        "summary": "One of the signed-in customer's orders",
        "description": "Same ownership rule as the list. An order that is not theirs answers 404, not 403 \u2014 it is not theirs to know about.\n",
        "security": [
          {
            "CustomerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerOrder"
                }
              }
            }
          },
          "404": {
            "description": "No such order for this customer."
          },
          "401": {
            "description": "Missing or invalid customer session token."
          },
          "500": {
            "description": "Storage failure."
          }
        }
      }
    },
    "/me/orders/link": {
      "delete": {
        "operationId": "unlinkMyOrders",
        "summary": "Forget the customer behind their orders",
        "description": "Called by the sticker API when someone deletes their account\n(App Store Review Guideline 5.1.1(v)).\n\nAn order row is an accounting record and survives; what a deleted\ncustomer is entitled to is that it stops being *theirs*. The Stytch\nid, the email and the shipping address come off; the money, the line\nitems and the lifecycle stay exactly as they were.\n\nStripping the email matters as much as the id: `/me/orders` matches\non either, so an order that kept its address would be handed to\nwhoever signs up with that address next.\n\nAuthenticated as the customer, with the same consumer session token\nevery other `/me` route takes \u2014 the sticker API forwards the\ncaller's own token rather than acting on their behalf with operator\ncredentials.\n",
        "tags": [
          "Customer"
        ],
        "security": [
          {
            "CustomerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "What was forgotten, and what must survive it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "unlinked",
                    "retained_projects"
                  ],
                  "properties": {
                    "unlinked": {
                      "type": "integer",
                      "description": "Orders that were this customer's and no longer are."
                    },
                    "retained_projects": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Projects the sticker API must keep rather than purge with the rest of the customer's design data: an order has been paid for and not yet delivered, and still has to be printed from them. Destroying that artwork leaves a paid order unfulfillable, with no recovery but asking a customer who no longer has an account to send their photos again.\n"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid customer session token."
          },
          "500": {
            "description": "Storage failure."
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Liveness, including a live Postgres round trip",
        "description": "Returns 200 only when the service can actually reach its database \u2014 a commerce API that can't reach Postgres can't take an order, and load balancers should treat it as down.\n",
        "responses": {
          "200": {
            "description": "Service and database are reachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Postgres is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "CustomerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "The customer's Stytch **consumer** session JWT (a different project, issuer and JWKS from the operators' B2B tokens).\n"
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Stytch B2B session JWT for a Stixelize Operator \u2014 the same token stixel_admin sends admin-api; verification is the shared stytch-auth guard.\n"
      }
    },
    "schemas": {
      "PaymentStatus": {
        "type": "string",
        "description": "Where the money is. Written by Stripe events (and by checkout supersession for `cancelled`). `pending \u2192 paid`, then refunds move it to `partially_refunded` (still live: the order still ships and still blocks a second checkout for the project) or `refunded`. Both refunded statuses move back toward `paid` if Stripe later reports the refund failed. `cancelled` is an abandoned or superseded checkout, reachable only from `pending`; a superseded session Stripe still let the customer pay becomes `paid` (and a duplicate, if another order is live).\n",
        "enum": [
          "pending",
          "paid",
          "partially_refunded",
          "refunded",
          "cancelled"
        ]
      },
      "FulfillmentState": {
        "type": "string",
        "description": "Where the print is. Only advances while money is held. `none` before payment, and after payment until the packer reports; then awaiting_layout / in_review \u2192 approved \u2192 submitted \u2192 shipped \u2192 delivered. in_review \u21c4 awaiting_layout and approved \u2192 in_review are the proofing regressions.\n",
        "enum": [
          "none",
          "awaiting_layout",
          "in_review",
          "approved",
          "submitted",
          "shipped",
          "delivered"
        ]
      },
      "LifecyclePosition": {
        "type": "object",
        "description": "The two tracks, as every mutating operator route reports them back.",
        "required": [
          "payment_status",
          "fulfillment_state"
        ],
        "properties": {
          "payment_status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "fulfillment_state": {
            "$ref": "#/components/schemas/FulfillmentState"
          }
        }
      },
      "CustomerOrderItem": {
        "type": "object",
        "required": [
          "title",
          "description",
          "quantity",
          "unit_amount_cents",
          "image_url"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "quantity": {
            "type": "integer",
            "format": "int32"
          },
          "unit_amount_cents": {
            "type": "integer",
            "format": "int64"
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The line's cut-out, captured at checkout."
          }
        }
      },
      "CustomerOrder": {
        "type": "object",
        "description": "One of the customer's own orders \u2014 safe to show them in full.",
        "required": [
          "order_number",
          "id",
          "created_at",
          "payment_status",
          "fulfillment_state",
          "duplicate_payment",
          "currency",
          "subtotal_cents",
          "shipping_cents",
          "tax_cents",
          "total_cents",
          "refunded_cents",
          "carrier",
          "tracking_number",
          "shipped_at",
          "receipt_url",
          "ship_name",
          "ship_line1",
          "ship_line2",
          "ship_city",
          "ship_state",
          "ship_postal_code",
          "ship_country",
          "project_id",
          "items"
        ],
        "properties": {
          "order_number": {
            "type": "string",
            "description": "`#3F2A9C1B` \u2014 what the emails call it."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string"
          },
          "payment_status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "fulfillment_state": {
            "$ref": "#/components/schemas/FulfillmentState"
          },
          "duplicate_payment": {
            "type": "boolean",
            "description": "A second payment for a design already bought; it is refunded and nothing prints."
          },
          "currency": {
            "type": "string"
          },
          "subtotal_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "shipping_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "tax_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "total_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "refunded_cents": {
            "type": "integer",
            "format": "int64"
          },
          "carrier": {
            "type": [
              "string",
              "null"
            ]
          },
          "tracking_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "shipped_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "receipt_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Stripe's own receipt for the charge."
          },
          "ship_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_line1": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_city": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_state": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_postal_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_country": {
            "type": [
              "string",
              "null"
            ]
          },
          "project_id": {
            "type": "string",
            "description": "The design, so \"Order again\" can rebuild the basket from it."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomerOrderItem"
            }
          }
        }
      },
      "RetryFollowupsResult": {
        "type": "object",
        "required": [
          "payment_status",
          "fulfillment_state",
          "followup_error"
        ],
        "properties": {
          "payment_status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "fulfillment_state": {
            "$ref": "#/components/schemas/FulfillmentState"
          },
          "followup_error": {
            "type": [
              "object",
              "null"
            ],
            "description": "The step that stopped the pipeline and its error; null when it advanced.",
            "required": [
              "step",
              "error"
            ],
            "properties": {
              "step": {
                "type": "string",
                "enum": [
                  "mark_ordered",
                  "transition"
                ]
              },
              "error": {
                "type": "string"
              }
            }
          }
        }
      },
      "OrderStatus": {
        "type": "object",
        "required": [
          "order_number",
          "payment_status",
          "fulfillment_state",
          "duplicate_payment",
          "project_name",
          "total_cents",
          "currency",
          "carrier",
          "tracking_number"
        ],
        "properties": {
          "order_number": {
            "type": "string",
            "description": "Customer-facing number (`#3F2A9C1B`), matching the emails."
          },
          "payment_status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "fulfillment_state": {
            "$ref": "#/components/schemas/FulfillmentState"
          },
          "duplicate_payment": {
            "type": "boolean",
            "description": "True when this payment duplicated an order that already existed for the design (two checkouts completed). Nothing prints for it; it is refunded.\n"
          },
          "project_name": {
            "type": "string"
          },
          "total_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "carrier": {
            "type": [
              "string",
              "null"
            ]
          },
          "tracking_number": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "AdminOrderSummary": {
        "type": "object",
        "required": [
          "id",
          "order_number",
          "created_at",
          "updated_at",
          "payment_status",
          "fulfillment_state",
          "source",
          "project_id",
          "project_name",
          "email",
          "total_cents",
          "refunded_cents",
          "currency",
          "tracking_number",
          "duplicate_of"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "order_number": {
            "type": "string",
            "description": "What the customer sees \u2014 `#0DA89668`, the first eight characters of the id, and the same string the emails, the post-checkout page and the iOS app quote. The id is the capability; this is the thing a customer reads out over support.\n"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "payment_status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "fulfillment_state": {
            "$ref": "#/components/schemas/FulfillmentState"
          },
          "source": {
            "type": "string"
          },
          "project_id": {
            "type": "string"
          },
          "project_name": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "total_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "refunded_cents": {
            "type": "integer",
            "format": "int64"
          },
          "currency": {
            "type": "string"
          },
          "tracking_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "duplicate_of": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "AdminOrder": {
        "type": "object",
        "description": "The full order row as stored, plus `order_number` \u2014 the customer-facing string, which is derived rather than stored.\n",
        "required": [
          "id",
          "order_number",
          "created_at",
          "updated_at",
          "payment_status",
          "fulfillment_state",
          "source",
          "project_id",
          "project_name",
          "email",
          "stytch_user_id",
          "receipt_url",
          "stripe_checkout_session_id",
          "stripe_payment_intent_id",
          "currency",
          "subtotal_cents",
          "shipping_cents",
          "tax_cents",
          "discount_cents",
          "total_cents",
          "refunded_cents",
          "promo_code",
          "ship_name",
          "ship_line1",
          "ship_line2",
          "ship_city",
          "ship_state",
          "ship_postal_code",
          "ship_country",
          "carrier",
          "tracking_number",
          "shipped_at",
          "duplicate_of",
          "approved_layout"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "order_number": {
            "type": "string",
            "description": "The customer-facing number, e.g. `#0DA89668`."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          },
          "payment_status": {
            "$ref": "#/components/schemas/PaymentStatus"
          },
          "fulfillment_state": {
            "$ref": "#/components/schemas/FulfillmentState"
          },
          "source": {
            "type": "string"
          },
          "project_id": {
            "type": "string"
          },
          "project_name": {
            "type": "string"
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "stytch_user_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The customer's Stytch id when they were signed in at checkout."
          },
          "receipt_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "stripe_checkout_session_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "stripe_payment_intent_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "currency": {
            "type": "string"
          },
          "subtotal_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "shipping_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "tax_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "discount_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "total_cents": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "refunded_cents": {
            "type": "integer",
            "format": "int64",
            "description": "Cumulative amount returned via Stripe; 0 until a refund."
          },
          "promo_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_line1": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_city": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_state": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_postal_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "ship_country": {
            "type": [
              "string",
              "null"
            ]
          },
          "carrier": {
            "type": [
              "string",
              "null"
            ]
          },
          "tracking_number": {
            "type": [
              "string",
              "null"
            ]
          },
          "shipped_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "duplicate_of": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Set when this order's payment arrived for a project that already had a live order: the id of the order it lost to. Money taken, not honoured \u2014 `paid`, on the worklist to be refunded, never printed, never blocking a checkout (BRC-76).\n"
          },
          "approved_layout": {
            "description": "What approval saw \u2014 sheets (id, medium, pack, print master) and per-line tally \u2014 while the order is `approved`; null otherwise. Submit compares the live layout to it (BRC-77).\n"
          }
        }
      },
      "AdminOrderItem": {
        "type": "object",
        "required": [
          "id",
          "order_id",
          "position",
          "title",
          "description",
          "quantity",
          "unit_amount_cents",
          "image_url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "order_id": {
            "type": "string",
            "format": "uuid"
          },
          "position": {
            "type": "integer"
          },
          "title": {
            "type": "string",
            "description": "Customer-facing summary, e.g. `6 stickers \u00b7 2.9\u2033 each`."
          },
          "description": {
            "type": "string",
            "description": "What was bought, in human terms \u2014 medium and pack, e.g. `5.5 x 5.5\" (140x140mm) \u00b7 2-pack`. Gooten's variant identifier is never stored; it is derived from (medium, pack) at submit time.\n"
          },
          "quantity": {
            "type": "integer"
          },
          "unit_amount_cents": {
            "type": "integer",
            "format": "int64"
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The line's cut-out, captured at checkout."
          }
        }
      },
      "AdminOrderEvent": {
        "type": "object",
        "required": [
          "id",
          "order_id",
          "created_at",
          "kind",
          "actor",
          "from_state",
          "to_state",
          "detail"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "order_id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "`created`, `payment_changed`, `fulfillment_changed` (the two tracks' transitions; `from_state`/`to_state` name the edge), `refund_recorded`, `refund_failed`, plus annotations such as `checkout_session_created`, `awaiting_async_payment`, `async_payment_failed`, `supersede_failed`, `followup_failed`, `email_sent`, `duplicate_payment` (on the live order), `duplicate_payment_received` (on the duplicate).\n"
          },
          "actor": {
            "type": "string"
          },
          "from_state": {
            "type": [
              "string",
              "null"
            ]
          },
          "to_state": {
            "type": [
              "string",
              "null"
            ]
          },
          "detail": {}
        }
      },
      "CheckoutSessionRequest": {
        "type": "object",
        "required": [
          "project_id"
        ],
        "properties": {
          "project_id": {
            "type": "string",
            "description": "The draft project holding the order's sticker lines."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Prefill for the Stripe page when the storefront knows it (logged-in customers). Stripe validates it; omit when unknown.\n"
          }
        }
      },
      "CheckoutSessionResponse": {
        "type": "object",
        "required": [
          "url",
          "order_id"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Stripe's hosted Checkout page \u2014 redirect here."
          },
          "order_id": {
            "type": "string",
            "format": "uuid",
            "description": "The pending commerce order created for this attempt."
          }
        }
      },
      "CheckoutErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "missing_project_id",
              "unknown_shipping_code",
              "project_not_found",
              "project_already_ordered",
              "no_quotable_lines",
              "catalog_drift",
              "storage_failure",
              "sticker_api_unavailable",
              "stripe_unavailable"
            ]
          }
        }
      },
      "ShipTo": {
        "type": "object",
        "required": [
          "line1",
          "city",
          "state",
          "postal_code",
          "country"
        ],
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "line1": {
            "type": "string"
          },
          "line2": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "postal_code": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2; `US` is the only shippable value today."
          }
        }
      },
      "MobileIntentRequest": {
        "type": "object",
        "required": [
          "project_id",
          "email",
          "shipping_code",
          "ship_to"
        ],
        "properties": {
          "project_id": {
            "type": "string",
            "description": "The draft project created via `/drafts` at checkout open."
          },
          "email": {
            "type": "string",
            "description": "Typed once at checkout; required on mobile."
          },
          "shipping_code": {
            "type": "string",
            "enum": [
              "standard",
              "priority"
            ]
          },
          "ship_to": {
            "$ref": "#/components/schemas/ShipTo"
          }
        }
      },
      "AmountBreakdown": {
        "type": "object",
        "required": [
          "subtotal_cents",
          "shipping_cents",
          "tax_cents",
          "total_cents"
        ],
        "properties": {
          "subtotal_cents": {
            "type": "integer",
            "format": "int64"
          },
          "shipping_cents": {
            "type": "integer",
            "format": "int64"
          },
          "tax_cents": {
            "type": "integer",
            "format": "int64",
            "description": "Zero until Stripe Tax is wired to this path (BRC-30)."
          },
          "total_cents": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "MobileIntentResponse": {
        "type": "object",
        "required": [
          "client_secret",
          "publishable_key",
          "order_id",
          "amounts"
        ],
        "properties": {
          "client_secret": {
            "type": "string",
            "description": "PaymentIntent client secret for PaymentSheet."
          },
          "publishable_key": {
            "type": "string",
            "description": "Stripe publishable key for this environment."
          },
          "order_id": {
            "type": "string",
            "format": "uuid"
          },
          "amounts": {
            "$ref": "#/components/schemas/AmountBreakdown"
          }
        }
      },
      "PriceEntry": {
        "type": "object",
        "required": [
          "medium_title",
          "pack",
          "unit_amount_cents"
        ],
        "properties": {
          "medium_title": {
            "type": "string",
            "description": "`CanvasPreset.title` exactly as the sticker API's quotes carry it (`layout.medium_title`), e.g. `3 x 4\" (76x102mm)`.\n"
          },
          "pack": {
            "type": "integer",
            "format": "int32",
            "description": "Sheets in the pack \u2014 one of the Gooten pack sizes we sell."
          },
          "unit_amount_cents": {
            "type": "integer",
            "format": "int64",
            "description": "Price of that pack of sheets, in cents."
          }
        }
      },
      "ShippingRate": {
        "type": "object",
        "required": [
          "code",
          "display_name",
          "amount_cents"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "The `shipping_code` a checkout request may name (`standard`, `priority`)."
          },
          "display_name": {
            "type": "string"
          },
          "amount_cents": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "PriceList": {
        "type": "object",
        "required": [
          "currency",
          "prices",
          "shipping"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "description": "ISO 4217, lower-case (`usd`)."
          },
          "prices": {
            "type": "array",
            "description": "Every (medium \u00d7 pack) we sell \u2014 media in catalog order, packs ascending.",
            "items": {
              "$ref": "#/components/schemas/PriceEntry"
            }
          },
          "shipping": {
            "type": "array",
            "description": "Both flat rates, cheapest first.",
            "items": {
              "$ref": "#/components/schemas/ShippingRate"
            }
          }
        }
      },
      "Health": {
        "type": "object",
        "required": [
          "status",
          "db"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "db": {
            "type": "string",
            "enum": [
              "ok",
              "unreachable"
            ]
          }
        }
      }
    }
  }
}
