{"openapi":"3.1.0","info":{"title":"PrintedSimply API","version":"2026-07-26","description":"Print-on-demand fulfillment API. Submit orders programmatically, track fulfillment in real time, and let us handle printing and shipping.\n\n## Authentication\nAll endpoints require an `x-api-key` header. Generate keys from the [dashboard](https://printedsimply.com/dashboard/api-keys). Test keys (`ps_test_*`) run a sandbox: their orders go through the same validation, routing and pricing as live ones and are refused where a live order would be, with three exceptions: an address the check finds undeliverable is reported in `address_check` rather than refused, per-unit variable data is taken even where a live order is refused it, and an order nothing could price is placed at $0 with `pricing_degraded` saying why (a live one is refused). They are charged $0, never sent to production, and move through production, shipment and delivery on their own in minutes (or at once, with `POST /order/{id}/simulate`). Each webhook about a test order or another test object carries `test_mode: true`; an event about the account itself, such as `product.updated`, `catalog.synced` or `webhook.disabled`, carries none. Live keys (`ps_live_*`) place, charge and produce real orders. Keys created before the PrintedSimply rename start with `mf_test_` or `mf_live_` and keep working, in the same mode.\n\n## Scopes\nEvery operation publishes the scopes it requires as `x-required-scopes` (and as the `OAuth2` alternative under `security`). A key created without a scope list keeps full access; a scoped key must hold **every** listed scope or the call returns `403 insufficient_scope` naming the ones it is missing. Platform OAuth tokens (`oat_`) are checked against the same lists.\n\n## Rate Limits\nRate limits are plan-based and enforced **per account, not per API key** — every key on an account draws from one budget, so issuing more keys does not raise it. Two windows apply to every request, and both must pass:\n\n- **Per minute**, over a sliding window (requests): Free 60.\n- **Per day**, resetting daily (requests): Free 10,000.\n\nAn account on a plan this portal does not sell keeps that plan's limits; `GET /v1/me` reports the account's own.\n\nEvery response the limiter counted carries the per-minute window, twice:\n- `X-RateLimit-Limit` — requests allowed per window\n- `X-RateLimit-Remaining` — requests remaining\n- `X-RateLimit-Reset` — timestamp (ms since epoch) when the window resets\n- `RateLimit-Policy: \"minute\";q=60;w=60` and `RateLimit: \"minute\";r=59;t=42` — the same window in the IETF RateLimit fields (draft-ietf-httpapi-ratelimit-headers): `q` the quota, `w` the window in seconds, `r` what is left and `t` the seconds until it comes back. A generic HTTP client parses these without knowing our units; `t` is a delta, so no clock skew. A `daily_quota_exceeded` 429 adds a `\"day\"` item to both, since the day is the window that refused.\n\nThe two windows refuse with **different error codes**, and the code is how you tell them apart — the status is 429 either way:\n- `rate_limit_exceeded` — the per-minute window. `Retry-After` carries the seconds to wait, usually under a minute.\n- `daily_quota_exceeded` — the daily quota. `Retry-After` carries the seconds until it resets, and `X-Daily-Limit` / `X-Daily-Remaining` carry that window's figures. Retrying inside the same day will not clear it, so back off to the reset rather than looping.\n\n`GET /v1/me` reports both windows with what is left of each — call it before planning a run instead of discovering either ceiling as a 429.\n\n## Pagination\nThese lists page one way, by an opaque cursor: GET /orders, GET /order/{id}/events, GET /order/{id}/timeline, GET /products, GET /assets, GET /events, GET /alerts, GET /webhooks, GET /webhooks/deliveries and GET /webhooks/inbox. Each answer's `meta` (the `ListMeta` schema) carries:\n\n- `has_more` — whether another page follows this one.\n- `next_cursor` — send it back as `cursor` for that page; `null` exactly when `has_more` is false.\n\nTo read a whole list, ask for it without `cursor`, then repeat the request with `cursor` set to the last answer's `meta.next_cursor` until `meta.has_more` is false. Send the same query parameters on every page; only `limit` may change. A page that follows `has_more: true` can come back empty if rows were deleted in between; the walk still ends at the first `has_more: false`. Both SDKs do this for you (`listAll` in TypeScript, `list_all` in Python).\n\n`limit` sets the page size: 20 by default and 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused. Two lists answer a request without `cursor` at the size their page walk always had, because a client paging them by number computes its next request from that first answer: GET /order/{id}/events (500 events by default and 1000 at most, nearly every order's whole history in one answer) and GET /products (every match by default and 200 at most). Every page fetched with `cursor` takes 20 and 100.\n\nA cursor is opaque: pass it back unchanged. It holds the position of the last row you were given (its time and id, or a catalog row's place and SKU), never an offset, so rows written while you page never make you repeat or skip one. It is good only on the list that returned it, with a key of the same account and the same mode (test or live), and with the same filters: a cursor that was altered, or that came from another list, account, mode or set of filters, is refused with `400 validation_error` and `details.reason`. A GET /products cursor is also bound to the source the catalog was served from (live, its last-good snapshot, its local copy or our price book, each another list): a walk that crosses an outage, either way, is refused with `409 cursor_source_changed`, and starts again without `cursor`.\n\n`page` (and `offset`, a row count) are still accepted where an operation took them before — GET /orders, GET /order/{id}/events, GET /assets and GET /products — deprecated, each answered as it was, with `has_more` and `next_cursor` added. A request that sends `cursor` as well walks by the cursor and ignores them.\n\nAny other list that takes a `cursor` pages the older way: it answers `next_cursor` beside its rows, `null` on the last page, and no `meta.has_more`. Walk it the same way, until `next_cursor` is null.\n\n## Idempotency\nPOST, PUT, and DELETE endpoints accept an `Idempotency-Key` header. If you retry a request with the same key within 24 hours, you'll receive the original response. This prevents duplicate charges.\n\nA key identifies one specific request: it is bound to the payload it was first used with. Reusing a key for a *different* payload returns `422 idempotency_key_reuse` instead of the original response, so a changed request can never be silently dropped. Generate a fresh key for each new request. A request that is still in flight returns `409 idempotency_conflict` — retry shortly to get its result.\n\nAn operation that moves money (an order, a batch, an item, a shipping or address change, a cancellation) never runs unchecked: when the idempotency ledger cannot be reached it answers `503 idempotency_unavailable` with `Retry-After`, runs nothing and charges nothing — retry with the same key. Its claim on a key also holds for the full 24 hours, so a request that ended without its answer being recorded keeps answering 409 rather than running again.\n\n## Response Format\nAll responses use a standard envelope:\n```json\n{\n  \"status\": \"success\",\n  \"data\": { ... },\n  \"request_id\": \"uuid\"\n}\n```\nError responses carry the code, a message, where the code is documented and what to do about it:\n```json\n{\n  \"status\": \"error\",\n  \"error\": {\n    \"code\": \"...\",\n    \"message\": \"...\",\n    \"doc_url\": \"...\",\n    \"suggestion\": \"...\",\n    \"details\": {}\n  },\n  \"request_id\": \"uuid\"\n}\n```\nBranch on `error.code`, never on `message`. `details` is present when the code has structured context to give.\n\n### Problem details (RFC 9457)\nSend `Accept: application/problem+json` and an error comes back as an RFC 9457 problem document instead, with `Content-Type: application/problem+json`: `type` (where the code is documented, an absolute URI), `title` (the code's short title), `status`, `detail` (the message) and `instance` (`urn:uuid:<request_id>`), with `code`, `request_id`, `doc_url`, `suggestion` and `details` beside them as extension members (the `ProblemDetails` schema). An `Accept` that names `application/problem+json` and weights it above `application/json` selects it; `*/*`, `application/*`, `application/json`, no `Accept` at all, and `application/json` and `application/problem+json` at the same weight all keep the envelope, byte for byte. A success is never changed. Every response an operation answers in the envelope says `Vary: Accept`, success or error, and so does every error response below that offers `application/problem+json`; every header such a response carries (`X-Request-Id`, the rate-limit fields, `Retry-After`) is the same either way.\n\nA path no operation is at answers `404 not_found`, and a path an operation is at, asked with a method it does not take, answers `405 method_not_allowed` with `Allow` naming the methods it does (the `MethodNotAllowed` response): both in the envelope or as problem details, with a `request_id`. A method no path takes at all (`PROPFIND`, `PURGE`, anything but `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH` and `DELETE`) answers `501 method_not_implemented`, with `Allow` where the path exists. A family this deployment does not offer still answers `404 not_available` first, and `TRACE` and `CONNECT` are refused by the web server before the API sees them. Every path that takes `GET` also answers `HEAD`, and every path answers `OPTIONS`.\n\n## Tracing\nSend a W3C Trace Context `traceparent` header (https://www.w3.org/TR/trace-context/) and the request joins your trace; send none, or one that is not valid, and a trace is started for it. Every response an operation answers, success or error, carries `traceparent` naming the trace and the request's own span, beside `X-Request-Id`, and so does the error for a path or method no operation takes. The answer to `OPTIONS` carries no `traceparent`. The trace id is kept beside the request id in your request log, where either finds the request, and a webhook delivery the request caused carries the same trace id in its own `traceparent`.\n\n## Versioning\nThe API is versioned by date. `info.version` on this document is the current version; send `X-API-Version: <date>` to pin an integration to one. Unpinned requests get the current version.\n\nEvery response echoes the version it was actually served under, as `MF-Version` (and, under the same name as the request header, `X-API-Version` — the two always carry the same value). The echo is on **every** response, including errors and idempotent replays, and it is the resolved version rather than the current one.\n\n**Compare the echo against your pin.** A pin is not always honoured, and the echo is how you find out:\n- A version id we do not recognise resolves *down* to the nearest older supported version.\n- A version id older than `x-api-versions.minimum` resolves *up* to the current version.\n\nNeither case is an error response, so a client that does not read `MF-Version` cannot tell it is being served a contract it did not ask for. If `MF-Version` does not equal the `X-API-Version` you sent, correct your pin against the supported list below.\n\nThe full supported list — including deprecation status and sunset dates — is published as `x-api-versions` on this document, so a client can check its pin without scraping the docs. Every change to the operation set below is written up at https://printedsimply.com/docs/api-changelog before it ships.","contact":{"name":"PrintedSimply Developer Support","url":"https://printedsimply.com"},"license":{"name":"PrintedSimply Terms of Service","url":"https://printedsimply.com/terms"}},"x-api-versions":{"current":"2026-07-26","minimum":"2026-02-15","header":"X-API-Version","response_header":"MF-Version","response_headers":["MF-Version","X-API-Version"],"changelog_url":"https://printedsimply.com/docs/api-changelog","supported":[{"id":"2026-07-26","status":"current"},{"id":"2026-03-14","status":"stable"},{"id":"2026-03-01","status":"stable"},{"id":"2026-02-15","status":"deprecated","sunset_date":"2026-06-15"}]},"servers":[{"url":"https://printedsimply.com/api/v1","description":"Production"}],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"tags":[{"name":"Meta","description":"The API itself: its service descriptor, what the calling key may do (scopes, request limits and spend controls), and what the account still needs before a live order is accepted."},{"name":"Orders","description":"Place, read, change, cancel and list orders: one at a time, many in one call, or one order split across addresses. On a test key an order runs the whole lifecycle for $0, and you can move it to any state yourself."},{"name":"Items","description":"Add, change and remove the lines of an order before it goes into production. Each change is re-priced, and charged or refunded, as it is made."},{"name":"Pricing","description":"What an order will cost before it is placed: a signed quote that POST /order charges exactly, an estimate, and the plan tiers with their limits."},{"name":"Products","description":"The catalog: every product, its print specs and its price, and recommendations for what you are making."},{"name":"Shipping","description":"The shipping methods on offer for a basket and a destination, with each method's rate and delivery window, and the days no delivery estimate counts."},{"name":"Artwork","description":"Check a print file before you order: format, resolution at the print size, dimensions and bleed."},{"name":"Mockups","description":"Render a product mockup from your artwork, as an SVG or a PNG or JPEG for a store listing."},{"name":"Assets","description":"Upload artwork once and order it by id: an asset's `ast_…` id is accepted wherever an order line takes `print_file`."},{"name":"Events","description":"The account's event log: every order event recorded, filterable by name and time, whether or not a webhook received it; and the integration-health alerts its alert events announce."},{"name":"Webhooks","description":"Subscriptions that deliver signed events to your endpoint, with every delivery attempt, redelivery, replay of a stored event, and secret rotation with an overlap window."}],"paths":{"/":{"get":{"operationId":"getApiIndex","summary":"API index (public discovery)","description":"Unauthenticated service descriptor: the API's name and version plus absolute links to the OpenAPI spec, the API reference, the API changelog and the MCP client config. Returns a plain JSON object, not the success envelope.","tags":["Meta"],"security":[],"responses":{"200":{"description":"The service descriptor. NOT wrapped in the success envelope — this route answers before any of that machinery, which is also why it carries no request_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceDescriptor"},"example":{"name":"PrintedSimply API","version":"1.0","api_version":"2026-07-26","minimum_api_version":"2026-02-15","description":"Route print orders to PrintedSimply's US plant by API.","openapi_spec":"https://printedsimply.com/api/openapi.json","documentation":"https://printedsimply.com/docs/api-reference","api_changelog":"https://printedsimply.com/docs/api-changelog","mcp_config":"https://printedsimply.com/mcp-config.json"}}}}},"x-required-scopes":[]}},"/me":{"get":{"operationId":"getMe","summary":"Describe the calling credential","description":"What this API key or OAuth token is allowed to do, from the perspective of the credential itself: its prefix, name, test/live mode, scopes, and expiry; the account's plan; the per-minute and per-day request limits with what is left of each (both enforced per ACCOUNT, not per key); whether this request's RFC 9421 signature verified and the rate multiplier it bought; and the spend controls — per-order cap, daily cap, and how much of today's is already committed. Read-only: it reserves nothing and moves no tally. Call it before planning a run rather than discovering the same limits as a 403, a 429, or a spend_cap_exceeded on an order you had already decided to place. Requires no scope — a credential can always describe itself — but does consume one request from both rate windows, which is why the numbers it reports are the ones remaining after this call. Fields that cannot be read (a spend ledger blip) come back null rather than zero.","tags":["Meta"],"responses":{"200":{"description":"Credential scopes, limits, and spend controls","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CredentialDescriptor"}}}]},"example":{"status":"success","data":{"key":{"prefix":"mf_test_xxxxxxxx","name":"Examples","mode":"test","scopes":["all"],"expires_at":null},"account":{"plan":"starter","plan_label":"Free"},"rate_limits":{"scope":"account","requests_per_minute":{"limit":60,"remaining":60,"reset":1792076462000},"requests_per_day":{"limit":10000,"remaining":10000,"reset":1792108800000}},"verified_agent":{"signed":false,"verified":false,"key_id":null,"agent_name":null,"rate_multiplier":1,"reason":null},"spend_controls":{"per_order_cap_cents":null,"daily_cap_cents":null,"day":"2026-10-15","spent_today_cents":null,"remaining_today_cents":null},"api_version":"2026-07-26"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":[],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}]}},"/setup":{"get":{"operationId":"getSetup","summary":"Describe what this account still needs","description":"What the account behind this credential has, what it still needs, and the one thing to do next — so an agent setting up a merchant learns it up front instead of as a refusal on the call it was making. `ready` says whether each way of selling would be accepted now: `live_orders` (a live key, a card on file, and an account not held to sandbox; otherwise a live order is refused with 402 no_payment_method or 403 sandbox_only); `hosted_sales` and `store_orders` are always false here, where orders arrive through this API and its dashboard only. `steps` lists each step with whether it is done, measured from the account's own rows rather than ticked by hand, and the dashboard page that completes it; `next_action` is the first required step not yet done, or null. Optional steps (the webhook, and a live key of your own) are ones the first live order does not need, and never the next action. Which steps are required depends on the credential asking: on a live one (a live key, or an OAuth sign-in, which is always live) the only required step is a card on file and `live_key` reads done; on a test key every step is optional and `next_action` is null, since its $0 orders need nothing. Requires no scope, like GET /me: the narrowly scoped key that was just refused is the one that most needs to learn why. Reports booleans and dashboard links only — no card, payout or customer detail — and changes nothing.","tags":["Meta"],"responses":{"200":{"description":"Readiness by way of selling, every setup step, and the next action","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccountSetup"}}}]},"example":{"status":"success","data":{"ready":{"live_orders":false,"hosted_sales":false,"store_orders":false},"sandbox_only":false,"steps":[{"key":"test_key","label":"Create your test key","hint":"It starts with ps_test_ and places $0 orders that are never printed or charged. It is made on the API keys page and shown in full once, when it is made.","done":true,"optional":true,"dashboard_url":"https://printedsimply.com/dashboard/api-keys"},{"key":"sandbox_order","label":"Send a $0 test order","hint":"Send POST /v1/order with a test key. The order runs to delivered on a compressed clock, with a tracking link; nothing is printed or charged.","done":false,"optional":true,"dashboard_url":"https://printedsimply.com/dashboard"},{"key":"webhook","label":"Add a webhook and watch the order move","hint":"We send a signed event to your URL as each order moves, test orders included, and keep a delivery log you can replay. No server yet? A hosted test inbox receives your test events for you, with nothing to install and no tunnel.","done":false,"optional":true,"dashboard_url":"https://printedsimply.com/dashboard/webhooks"},{"key":"payment_method","label":"Add a card","hint":"A live order is charged to the card on file when it is placed; without one it is refused with no_payment_method. Test orders never need one.","done":false,"optional":true,"dashboard_url":"https://printedsimply.com/dashboard/billing#payment-method"},{"key":"live_key","label":"Create a live key","hint":"It starts with ps_live_ and places real orders. Your code does not change, only the key.","done":true,"optional":true,"dashboard_url":"https://printedsimply.com/dashboard/api-keys"},{"key":"first_sale","label":"Send your first live order","hint":"Send POST /v1/order with your live key, your own artwork (an https link or an uploaded asset id) and a real address, and it is printed in our US plant and shipped.","done":false,"optional":true,"dashboard_url":"https://printedsimply.com/dashboard/orders"}],"next_action":null,"complete":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"},"500":{"description":"internal_error — the account's setup could not be read. Nothing is reported rather than a list of wrong claims; retry shortly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}}},"x-required-scopes":[],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}]}},"/orders":{"get":{"operationId":"listOrders","summary":"List orders","description":"Retrieve a paginated list of your orders, newest first, with optional filters. Pages by cursor (see Pagination): send back `meta.next_cursor` as `cursor` until `meta.has_more` is false, a keyset walk that cannot repeat or skip a row while orders are being written. Reconciliation should use the cursor. The deprecated `page` walk is still answered as before.","tags":["Orders"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting","pending","processing","on-hold","completed","refunded","failed"]},"description":"Filter by order status. An unknown value is refused with 400 validation_error, never ignored — an unrecognised status would otherwise match no row and answer 200 with an empty page, which is indistinguishable from having no such orders. Current statuses: pending_approval, pending_payment, paid, accepted, in_production, quality_check, shipped, delivered, cancelled, issue_flagged, rerouting. Historical values, accepted so orders older than this vocabulary stay reconcilable but no longer written: pending, processing, on-hold, completed, refunded, failed."},{"name":"reference","in":"query","schema":{"type":"string"},"description":"Exact match on the `reference` you supplied at create. Use it to reconcile your own order ids against ours — including after a create response you never received."},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter orders created on or after this date. A value that is not a date, or a `from` later than `to`, returns 400 validation_error — it is never dropped, because an unfiltered page is indistinguishable from a windowed one."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"Filter orders created on or before this date. A value that is not a date returns 400 validation_error."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Orders per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"page","in":"query","deprecated":true,"schema":{"type":"integer","minimum":1,"default":1},"description":"Deprecated: walk with `cursor`. The page number of the offset walk this operation always had, answered as before (`limit` 20 by default, 100 at most; `offset`, a row count, is accepted in its place). An order written while you page shifts every offset behind it; a cursor cannot be shifted. Ignored when `cursor` is sent, and a cursor walk's `meta` carries no `page` or `pages`."}],"responses":{"200":{"description":"Paginated list of orders. meta carries has_more, next_cursor (null on the last page), total and limit — plus page and pages except on a cursor walk. Each row carries the `reference` you supplied at create (null if you supplied none) and the `guarantee` in force on that order (null when it carries none).","content":{"application/json":{"schema":{"type":"object","required":["status","data","request_id"],"properties":{"status":{"type":"string","enum":["success"]},"request_id":{"type":"string","format":"uuid"},"meta":{"$ref":"#/components/schemas/ListMeta"},"data":{"type":"array","items":{"type":"object","required":["order_id","portal_order_id","provider","made_by","guarantee","reference","vat","landed_cost","status","issue_reason","total","shipping","total_cents","shipping_cents","is_test","test_mode","items","line_items","actions","tracking_token","tracking_number","tracking_carrier","tracking_url","tracking","estimated_delivery","reordered_from","created_at","updated_at","channel","drop_id"],"properties":{"order_id":{"type":"string","description":"The fulfilment network's order id — the one `GET /v1/order/{id}` and every other order operation take."},"portal_order_id":{"type":"string","description":"This platform's own id for the order."},"provider":{"type":"string","description":"The fulfilment lane that holds the order."},"made_by":{"$ref":"#/components/schemas/OrderProvenance"},"guarantee":{"$ref":"#/components/schemas/OrderGuarantee"},"reference":{"type":["string","null"],"description":"The `reference` you supplied at creation, echoed back. Null on orders placed without one, and on every order that predates the column."},"vat":{"type":["string","null"],"description":"The tax-registration number declared on this order; null when none was supplied."},"landed_cost":{"$ref":"#/components/schemas/OrderLandedCost"},"status":{"type":"string","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting","pending","processing","on-hold","completed","refunded","failed"],"description":"The order's lifecycle status, from the same vocabulary the `status` filter above accepts."},"issue_reason":{"type":["string","null"],"enum":["artwork_unprintable","address_undeliverable","shipment_exception","on_hold","cancellation_pending",null],"description":"Why the order is `issue_flagged`, as `GET /v1/order/{id}` reports it; null when it is not flagged, or no reason fits."},"total":{"type":"string","description":"What the order was charged, formatted for display — a dollar string such as `$42.00`, NOT an integer of cents. `total_cents` beside it carries the same money as an integer, under the name `GET /v1/order/{id}` uses; read that one. Kept because the shape is published and changing it would break every caller reading it."},"shipping":{"type":"string","description":"The shipping charge, formatted the same way as `total`; `shipping_cents` is the same money in cents."},"total_cents":{"type":"integer","description":"What the order was charged, in cents — the same money as `total`, and the same field `GET /v1/order/{id}` carries."},"shipping_cents":{"type":"integer","description":"The shipping charge, in cents — the same money as `shipping`."},"is_test":{"type":"boolean","description":"True on a sandbox order. The same value as `test_mode` beside it; kept because it is published."},"test_mode":{"type":"boolean","description":"True on a sandbox order, false on a live one — `is_test` under the name the create response and `GET /v1/order/{id}` use. Present on every row here (the order read omits it on a live order)."},"channel":{"type":"string","enum":["acp","ucp","mcp","verified_agent","oauth_app","api","dashboard","autopilot","hosted","storefront","unknown"],"description":"The surface that placed the order. On `hosted`, `acp` and `ucp` a shopper paid your store, and `total` is that sale; on every other channel you placed the order and `total` is what it cost you. `unknown` is an order older than the record."},"drop_id":{"type":["string","null"],"description":"The drop a shopper bought this order on; null on an order no drop sold."},"items":{"type":"array","description":"The order's lines. Each carries the id our item and return operations resolve — see OrderLine.","items":{"$ref":"#/components/schemas/OrderLine"}},"line_items":{"type":"array","description":"The same lines as `items`, under the name `GET /v1/order/{id}` publishes them as, so one name reads the lines from either.","items":{"$ref":"#/components/schemas/OrderLine"}},"actions":{"$ref":"#/components/schemas/OrderActions"},"tracking_token":{"type":["string","null"],"description":"The opaque token for this order's public tracking page; null until one is issued."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number; null until the order ships."},"tracking_carrier":{"type":["string","null"],"description":"The carrier that holds the shipment; null until the order ships."},"tracking_url":{"type":["string","null"],"description":"The carrier's own tracking URL; null until the order ships."},"tracking":{"$ref":"#/components/schemas/OrderTracking"},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery"},"reordered_from":{"type":["string","null"],"description":"On an order POST /order/{id}/reorder placed: the source order's `portal_order_id`, the value the reorder's own answer stated. Null on any other order."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"example":{"status":"success","data":[{"order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","provider":"printedsimply","made_by":null,"guarantee":null,"reference":null,"vat":null,"landed_cost":null,"status":"paid","issue_reason":null,"total":"$76.50","shipping":"$6.50","total_cents":7650,"shipping_cents":650,"is_test":true,"test_mode":true,"channel":"api","drop_id":null,"items":[{"id":"itm_fowT5-1iZwSnQcGX0ByoA64l","sku":"sticker-die-cut-3x3","quantity":100,"unit_price_cents":70,"line_total_cents":7000}],"line_items":[{"id":"itm_fowT5-1iZwSnQcGX0ByoA64l","sku":"sticker-die-cut-3x3","quantity":100,"unit_price_cents":70,"line_total_cents":7000}],"actions":{"cancel":true,"update_address":true,"update_shipping":true,"upgrade_shipping":true,"add_item":true,"update_item":true,"cancel_item":true},"tracking_token":"trk_AquJFqDLDkisK6Jh-HABHz9L","tracking_number":null,"tracking_carrier":null,"tracking_url":null,"tracking":{"number":null,"carrier":null,"url":null,"page_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIn0.199c0c58721512fcccd784902c422ca4e827c6d09b7e495bbb4db9b06ae62003"},"estimated_delivery":{"earliest":"2026-10-20","latest":"2026-10-26","ships_by":"2026-10-14","is_estimate":true},"reordered_from":null,"created_at":"2026-10-10T08:09:14.588Z","updated_at":"2026-10-10T08:09:14.588Z"}],"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"page":1,"pages":1,"limit":20,"total":1,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"description":"Unauthorized — invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["orders:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:read"]}]}},"/order":{"post":{"operationId":"createOrder","summary":"Create an order","description":"Submit a print-on-demand order. Charges the default payment method, then submits to the production system. Returns order ID and charge breakdown.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderRequest"},"example":{"items":[{"sku":"sticker-die-cut-3x3","quantity":100,"artwork_url":"https://printedsimply.com/samples/sticker-3x3.png"}],"address_to":{"name":"Jane Doe","address1":"123 Main St","city":"Austin","state":"TX","zip":"78701","country":"US"},"shipping":{"method":"standard"}}}}},"responses":{"200":{"description":"Order created successfully","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"traceparent":{"$ref":"#/components/headers/traceparent"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CreateOrderResponse"}}}]},"example":{"status":"success","data":{"order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","provider":"merchfarm","tracking_token":"trk_AquJFqDLDkisK6Jh-HABHz9L","tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIn0.199c0c58721512fcccd784902c422ca4e827c6d09b7e495bbb4db9b06ae62003","test_mode":true,"lifecycle":"simulated","charged":"$0.00","artwork_check":{"lines":[{"item_index":0,"sku":"sticker-die-cut-3x3","status":"passed","width_px":1200,"height_px":1200,"print_size_in":[3,3],"effective_dpi":400,"min_dpi":300,"findings":[]}],"would_refuse":false},"breakdown":{"items":"$70.00","shipping":"$6.50","processing_fee":"$0.00","platform_fee":"$0.00","card_fee":"$0.00","total":"$76.50"},"shipping":{"method":"standard","surcharges":[]},"estimated_delivery":{"earliest":"2026-10-20","latest":"2026-10-26","ships_by":"2026-10-14","is_estimate":true}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"402":{"description":"The order was refused before anything was charged or placed. `no_payment_method`, `payment_failed` or `authentication_required` on the card; `fraud_review` from the risk gate; `spend_cap_exceeded` when the key's per-order or daily cap would be passed, with the cap and the day's spend in the message. The cap counts what this API charges for the order: here every order is made, and charged, at our own plant. A test key meets the spend-cap refusal exactly as a live key would.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key. Use a test key (ps_test_*) or contact support to enable live orders.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"409":{"description":"duplicate_reference — an order of your account already carries this `reference` (placed in the last 30 days with a key of the same mode, not cancelled); nothing was charged. `details.order_id` and `details.portal_order_id` name it; read it instead of placing it again. `details.in_flight: true` instead means another request with this reference is being placed right now. Send `allow_duplicate_reference: true` to place a second order under the same reference on purpose. Also idempotency_conflict, while a request with the same Idempotency-Key is still in flight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"$ref":"#/components/responses/OrderRefused"},"429":{"$ref":"#/components/responses/RateLimitError"},"500":{"description":"order_error — the order was placed and CHARGED but the confirmation could not be returned or the record could not be written. Do NOT retry: a retry places and charges a second one. Look the order up, or quote the request_id to support. The one exception carries its own message — \"Payment refunded. Retry with a new Idempotency-Key.\"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"503":{"description":"provider_unavailable — the facility that produces these items is not accepting orders right now (unconfigured, or its circuit breaker is open) or (`details.reason: plant_unreachable`, with `Retry-After`) our plant gave no answer about a SKU — the request did not reach it, timed out, or came back 5xx — which is a fact about the plant and not the SKU: a definite answer that it does not carry the SKU is still a 400 pricing_error. Nothing stands charged: the pre-charge refusal never took money, and the post-charge one refunded before answering. Retry later; a pre-charge 503 may be retried with the same Idempotency-Key, a post-charge 503 needs a new one. A test key is not refused for an unanswering plant: its order is recorded at $0 with `pricing_degraded: pricing_unavailable`. Only for a SKU the catalog has, though: one the price book does not price, our plant's products do not name and the synced catalog does not hold is refused 400 unknown_sku with `details.skus`, as it is on POST /order, POST /orders/quote and POST /pricing/estimate alike. reference_check_unavailable — the order carries a `reference` and whether another order already holds it could not be checked, so it was not placed and nothing was charged; retry the same request after `Retry-After`. tax_unavailable — sales tax collection is on and Stripe Tax could not calculate this live order's tax, so it was refused before anything was charged; retry the same request shortly. idempotency_unavailable — the request carried an `Idempotency-Key` and the ledger that makes it mean at-most-once could not be reached, so nothing ran and nothing was charged; retry the same request with the same key after `Retry-After`.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Sent with `details.reason: plant_unreachable` (the catalog breaker's own cooldown), with reference_check_unavailable and with idempotency_unavailable: seconds to wait before retrying."},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/order/{id}":{"get":{"operationId":"getOrder","summary":"Get order details","description":"Retrieve one order by its `order_id` or its `portal_order_id` — either id resolves the same order, and the response names it by its `order_id`. The read carries where the parcel is: `tracking` (number, carrier, carrier link and the hosted tracking page) and the flat `tracking_number`, `tracking_carrier` and `tracking_url`, the same values `GET /v1/orders` and the `order.shipped` webhook report, with `created_at` and `updated_at`. Alongside the fulfilment record, the response carries the fields this API is authoritative for: the `reference` and `vat` you supplied, `made_by` (the network shop that produced it), the `guarantee` in force on the order — what you were promised at quote time, whether it held, and any remedy recorded — `artwork_access`, the record of which independent shops were given this order's print files, how many times each fetched them, and whether they can still be fetched, and `line_items`, the order's lines with the id of each. That id is the `{itemId}` in `PUT /v1/{id}/item/{itemId}` and `DELETE /v1/{id}/item/{itemId}/cancel`, and the `order_item_id` a `size_exchange` return requires. It is deliberately NOT the `items[].id` in the fulfilment record beside it: that one is the facility's own line id, in a different namespace, and none of those three operations accept it. `actions` says which changes the order still takes (cancel, address, shipping, and adding, changing or cancelling a line), from the same checks those operations refuse on, so a client can offer an action without trying it first.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"}],"responses":{"200":{"description":"Order details","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"traceparent":{"$ref":"#/components/headers/traceparent"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"guarantee":{"$ref":"#/components/schemas/OrderGuarantee"},"order_id":{"type":"string","description":"The fulfilment network's order id: the one every order operation takes, whichever id this request was made with. Present on every order; on a live one the facility's own body supplies it when it carries one."},"portal_order_id":{"type":"string","description":"This platform's own id for the order (`ord_…`), the id in the dashboard's order page. `GET /v1/order/{id}` and every other order operation accept it in place of `order_id`. Present on every order; on a live one the facility's own body supplies it when it carries one."},"reference":{"type":["string","null"],"description":"The `reference` you supplied at creation, echoed back."},"vat":{"type":["string","null"],"description":"The `vat` you supplied at creation, echoed back."},"status":{"type":"string","description":"The order's lifecycle status. The published vocabulary is the one `GET /v1/orders` filters on. Always on a sandbox order; on a live one it comes from the facility's own body. `pending_approval` on an order held for a person's approval (see `approval`), which is served from our own record, as a sandbox order is, until it is placed."},"approval":{"$ref":"#/components/schemas/OrderApproval","description":"On an order placed with `approval: \"required\"` only: whether a person has approved it, the link they decide at while it still decides, and their decision. A hold whose link has expired is closed by this read (`expired`, the order `cancelled`)."},"issue_reason":{"type":["string","null"],"enum":["artwork_unprintable","address_undeliverable","shipment_exception","on_hold","cancellation_pending",null],"description":"Why the order is `issue_flagged`, when the facility's status said: the same code `order.issue` carries. Null when the order is not flagged, or is flagged without a reason that fits one of these. Always on a sandbox order (a simulation names it); on a live one it is filled from our record wherever the facility's own body carries no key of that name."},"provider":{"type":"string","description":"The fulfilment network that holds the order. Always on a sandbox order; on a live one it comes from the facility's own body."},"made_by":{"$ref":"#/components/schemas/OrderProvenance"},"total_cents":{"type":"integer","description":"What the order was charged, in cents. Sandbox orders only — a live response reports money the way the facility does."},"shipping_cents":{"type":"integer","description":"The shipping charge, in cents. Sandbox orders only, like `total_cents`."},"tax_cents":{"type":"integer","description":"The sales tax charged on top of the order's total, in cents, from our own record. Present only on a live order that was taxed: while sales tax collection is on, Stripe Tax calculates it on the goods and shipping for the ship-to address. Absent on a sandbox order, which is never taxed, and on every order placed while tax collection is off."},"total_with_tax_cents":{"type":"integer","description":"What the card was charged for the order, tax included, in cents. Present only with `tax_cents`."},"tracking_token":{"type":["string","null"],"description":"The opaque token for this order's hosted tracking page, as `GET /v1/orders` reports it; null on an order with none."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number; null until the order ships. The same value as `tracking.number`, the list's `tracking_number` and the `order.shipped` webhook's."},"tracking_carrier":{"type":["string","null"],"description":"The carrier that holds the shipment; null until the order ships."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking link; null until the order ships. (The create response's `tracking_url` is the hosted tracking page, which this read publishes as `tracking.page_url`.)"},"tracking":{"$ref":"#/components/schemas/OrderTracking"},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"When the order should arrive, as an estimate counted from placement; null where no honest date exists (see OrderEstimatedDelivery). Ours on a live order too: a key of that name in the facility's body is replaced."},"reordered_from":{"type":["string","null"],"description":"On an order POST /order/{id}/reorder placed: the source order's `portal_order_id` (this operation takes it), as the order's row records it and the reorder's own answer stated it. Null on any other order. Ours on a live order too: a key of that name in the facility's body is replaced."},"created_at":{"type":"string","format":"date-time","description":"When the order was placed. On a live order the facility's own body supplies it when it carries one."},"updated_at":{"type":"string","format":"date-time","description":"When the order last changed. On a live order the facility's own body supplies it when it carries one."},"test_mode":{"type":"boolean","description":"Present, and true, only on a sandbox order. Its absence is how a live order is told apart."},"artwork_access":{"type":"array","description":"Every print-file grant ever issued on this order, oldest first. Empty when no independent shop has been given this order's artwork — which is every order produced in-house, and every sandbox order.","items":{"$ref":"#/components/schemas/ArtworkAccessGrant"}},"landed_cost":{"$ref":"#/components/schemas/OrderLandedCost"},"line_items":{"type":"array","description":"The order's lines, each with the id our own item and return operations resolve. Present on live and sandbox orders alike.","items":{"$ref":"#/components/schemas/OrderLine"}},"actions":{"$ref":"#/components/schemas/OrderActions"}}}}}]},"example":{"status":"success","data":{"order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","reference":null,"vat":null,"status":"paid","issue_reason":null,"provider":"merchfarm","made_by":null,"guarantee":null,"artwork_access":[],"line_items":[{"id":"itm_fowT5-1iZwSnQcGX0ByoA64l","sku":"sticker-die-cut-3x3","quantity":100,"unit_price_cents":70,"line_total_cents":7000}],"actions":{"cancel":true,"update_address":true,"update_shipping":true,"upgrade_shipping":true,"add_item":true,"update_item":true,"cancel_item":true},"total_cents":7650,"shipping_cents":650,"landed_cost":null,"tracking_token":"trk_AquJFqDLDkisK6Jh-HABHz9L","tracking_number":null,"tracking_carrier":null,"tracking_url":null,"tracking":{"number":null,"carrier":null,"url":null,"page_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIn0.199c0c58721512fcccd784902c422ca4e827c6d09b7e495bbb4db9b06ae62003"},"estimated_delivery":{"earliest":"2026-10-20","latest":"2026-10-26","ships_by":"2026-10-14","is_estimate":true},"reordered_from":null,"created_at":"2026-10-10T08:09:14.588Z","updated_at":"2026-10-10T08:09:14.588Z","test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"},"502":{"$ref":"#/components/responses/UpstreamError"}},"x-required-scopes":["orders:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:read"]}]}},"/order/{id}/cancel":{"delete":{"operationId":"cancelOrder","summary":"Cancel an order","description":"Cancel an order and refund all successful charges. The order is cancelled in the production system first, then all charges are refunded. `refund_status` says what happened to the money: `refunded`, `not_owed`, or `pending` when the refund failed at the card processor and is being retried; the order is cancelled either way. An order held for a person's approval (`pending_approval`) was never charged or sent: it is cancelled on our record alone, `refund_status: not_owed`, and its approval link stops deciding; while an approval is placing it, the cancel is 409 concurrent_modification.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Order cancelled and refunded","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CancelOrderResponse"}}}]},"example":{"status":"success","data":{"order_id":"test_ord_JyZLq7sUxTZxc7qjzI76ZQYU","refunded":"$0.00","refunded_cents":0,"refund_status":"not_owed","owed_cents":0,"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"already_cancelled — the order was cancelled before; nothing more is refunded or sent. Read it with GET /order/{id}.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"not_cancellable — the order has gone into production (or shipped, been delivered, been flagged or is being re-routed): an order can be cancelled only before production, while it is pending_payment, paid or accepted. Not a retry: the window does not reopen. An order already cancelled answers 400 already_cancelled instead. production_reconciliation_required — this order has an unresolved production submission. Nothing is cancelled or refunded until support reconciles the actual plant order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"},"502":{"$ref":"#/components/responses/UpstreamError"},"503":{"$ref":"#/components/responses/IdempotencyUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/order/{id}/address":{"put":{"operationId":"updateOrderAddress","summary":"Update shipping address","description":"Update the shipping address for an order that has not yet gone into production. On a live order the new address is checked as POST /order checks one (POST /addresses/validate's check), and an address that cannot be right as written is refused 422 address_invalid before the facility or the order is touched; a test order is never refused for it. Our own facilities ship to US addresses only, so an address outside the US is refused 422 destination_unavailable before anything is changed.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Address"},"example":{"name":"Jane Doe","address1":"456 Oak Ave","city":"Austin","state":"TX","zip":"78702","country":"US"}}}},"responses":{"200":{"description":"Address updated","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AddressAmendment"}}}]},"example":{"status":"success","data":{"order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","status":"paid","provider":"merchfarm","ship_to":{"name":"Jane Doe","address1":"456 Oak Ave","city":"Austin","state":"TX","zip":"78702","country":"US"},"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"order_not_modifiable — the order has gone into production (or later), past the point where its address can change: an order takes a change only while it is pending_payment, paid or accepted. Terminal for this order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"$ref":"#/components/responses/AddressChangeRefused"},"429":{"$ref":"#/components/responses/RateLimitError"},"501":{"description":"not_supported_by_provider — the order was produced by an independent network shop, which does not accept amendments at all. Not a timing problem and never retryable, which is what separates it from the 409.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"description":"fulfillment_error — the facility refused the change, answered while still holding the previous address, or did not confirm it (unreachable, timed out, or a reply that could not be read). The order's ship_to and its webhooks are unchanged either way. When the facility refused, or is known to hold the previous address, the order still ships there. When the facility did not confirm, the order is read back from it: if it already holds the new address the request answers 200 as if it had confirmed, and if that cannot be told the message says the change may not have been applied. Retrying the same request is safe in every case: it sends the same address again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"503":{"$ref":"#/components/responses/IdempotencyUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/order/{id}/shipping":{"put":{"operationId":"updateOrderShipping","summary":"Update shipping method","description":"Change the shipping method for an order that has not yet gone into production.\n\n**This call moves money.** The difference between the order's current shipping rate and the new one is settled immediately: an upgrade charges the delta to your card on file, a downgrade refunds it across the order's outstanding charges. The new method is priced as the order path prices it: by the parcel's weight class, and for items made at the partner facility at no less than the partner's own estimate. Naming the method the order is recorded as shipping by is not a change: it moves no money, whatever the rates have done since the order was placed. An order placed before methods were recorded carries none, so a change on it is priced; asking such an order for `standard`, the cheapest parcel, never charges: it refunds only what the order paid above today's standard rate. That makes it subject to the same controls as any other charge — the API key's per-order and daily spend caps are checked *before* the fulfillment system is told anything, so an over-cap upgrade returns 402 `spend_cap_exceeded` with the cap and the day's spend so far, and neither charges the card nor changes the shipping method. An upgrade with no payment method on file returns 402 `no_payment_method`; a declined card returns 402 `payment_failed`, and neither consumes the key's daily budget. The account-level sandbox switch applies here as it does to every charging endpoint: a live key on an account restricted to sandbox returns 403 `sandbox_only`, and neither the card nor the fulfillment system is touched.\n\nA sandbox order settles nothing: the change is reflected on the order record and Stripe is never called. An order produced by an independent shop cannot have its shipping changed after placement (501 `not_supported_by_provider`) — the published provider contract has no endpoint for it.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Shipping"},"example":{"method":"standard"}}}},"responses":{"200":{"description":"Shipping updated","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShippingAmendment"}}}]},"example":{"status":"success","data":{"order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","status":"paid","provider":"merchfarm","shipping_cents":1399,"total_cents":8399,"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"402":{"$ref":"#/components/responses/PaymentError"},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"tax_not_supported_on_path — sales tax collection is on and this is a live order: a shipping change that charges, or that refunds part of an order that was taxed, is refused, because this operation does not recalculate the order's tax. Cancel the order and place it again with the method you want. Nothing was charged or changed. order_not_modifiable — the order has gone into production (or later), past the point where its shipping can change. shopper_paid_order — a shipping upgrade on an order a shopper paid for (a drop, a checkout link or an agent checkout); it would be charged to your card and refunded to the shopper. A downgrade is allowed and refunds the shopper's own charge.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"},"501":{"description":"not_supported_by_provider — the order was produced by an independent network shop, which does not accept amendments at all. Never retryable, unlike the 409. Live PrintedSimply paid shipping upgrades are also refused before any charge because the plant handler is not confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"description":"fulfillment_error — the facility could not be reached or refused the change. Where an upgrade had already been charged the message says so and the payment was refunded; retry with a NEW Idempotency-Key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"503":{"$ref":"#/components/responses/IdempotencyUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/order/{id}/events":{"get":{"operationId":"getOrderEvents","summary":"Get order events","description":"Retrieve the event history for an order including status changes and tracking updates, oldest first. Pages by cursor (see Pagination): `data` stays a bare array, and `meta` carries `has_more`, `next_cursor` and the full event count (`total`). A request without `cursor` answers up to 500 events (1,000 with `limit`), which is nearly every order's whole history in one answer. To follow an order without a webhook address: keep `meta.since` from each answer and send it back as `since` with `wait_seconds` (at most 25). The answer comes as soon as the order has an event after `since`; when it has none yet, the request waits for one, up to `wait_seconds`, and then answers with an empty `data` and the same `meta.since`, so the next request asks from the same place. One request, counted once by the rate limit.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and only for this order, and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":500},"description":"Events per page. Without `cursor`: 500 by default and 1000 at most, which is nearly every order's whole history in one answer — the size this list's page walk always had, so a first answer and `page` agree on it. With `cursor`: 20 by default and 100 at most, as on every list. Either way a value above the ceiling is clamped to it and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value takes the default; none is refused."},{"name":"page","in":"query","deprecated":true,"schema":{"type":"integer","minimum":1,"default":1},"description":"Deprecated: walk with `cursor`. The page number of the offset walk this operation always had, answered as before (`offset`, a row count, is accepted in its place), at the page size a request without `cursor` takes. Ignored when `cursor` or `since` is sent."},{"name":"since","in":"query","schema":{"type":"string"},"description":"The position to read on from: `meta.since` from an earlier answer for this order (a `meta.next_cursor` is accepted too), sent back unchanged. Only events after it are returned, oldest first, at a cursor page's size (`limit` 20 by default, 100 at most). Good only for this order, with a key of the same account and mode; an altered or foreign value is refused with 400 validation_error and `details.parameter: \"since\"`, and so is a request that sends `cursor` as well. Omit it to read from the first event."},{"name":"wait_seconds","in":"query","schema":{"type":"integer","minimum":0,"maximum":25,"default":0},"description":"How long to wait for an event when there is none to return yet (a long poll). 0, the default, answers at once. Otherwise the request answers as soon as an event is found — the order's events are re-read every 2 seconds — or after this many seconds with an empty `data`. A value above 25 is clamped to 25, a fraction is truncated, and a missing, negative or non-numeric value is 0; none is refused."}],"responses":{"200":{"description":"The order's history, oldest first. `data` is a BARE ARRAY — the count, `has_more` and `next_cursor` live in `meta`, because there is nowhere in the body to hang them without breaking every existing client.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrderEvent"}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":[{"action":"order_submitted","details":"A test order: nothing is charged, printed or sent. The total is what a live order would pay.","timestamp":"2026-10-15T15:00:10.000Z"},{"action":"artwork_checked","details":"The artwork prints well (1200×1200 px, 400 DPI at 3×3 in).","timestamp":"2026-10-15T15:00:10.000Z"}],"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"page":1,"pages":1,"limit":500,"total":2,"has_more":false,"next_cursor":null,"since":"eyJyIjoib3JkZXJfZXZlbnRzIiwiayI6WyIyMDI2LTEwLTE1VDE1OjAwOjEwLjAwMFoiLCJjdnEwNHlsNjQ5bzd0M283YWJpa3Y5NzE0Il19.9452219bf0c9044af2bbb15342e6bd3d"}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["orders:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:read"]}]}},"/order/{id}/timeline":{"get":{"operationId":"getOrderTimeline","summary":"Get an order's timeline","description":"One order's API calls, events and webhook delivery attempts in one list, oldest first, as GET /order/{id}/events orders its events. Each entry has a `type` (`request`, `event` or `delivery`), its own `id`, the time it happened (`at`) and one object named after its type. A `request` is a call that placed this order (POST /order, a reorder, a batch) or changed it: one that named it in its path with any method but GET (a cancel, an address, shipping or item change, a simulated step). A read is not listed, this timeline included: it acts on no order. It carries the method, the operation's path template, the status, `request_id`, `trace_id`, the duration, the prefix of the key that made it (never more of the key) and the error code; no request or response body, which the request log does not keep. Its `at` is when it was answered, so at one instant an event comes first, then a request, then a delivery: the events a call wrote come before it. An `event` is the order's event exactly as GET /order/{id}/events returns it. A `delivery` is one attempt at sending a webhook about this order to one of your endpoints: the endpoint's id and the host the attempt was sent to (never its path or query), the event and its `event_id` (the `X-PS-Event-Id` that all attempts at one event carry), the rung of the retry ladder, whether it was delivered, the status your endpoint answered, the latency, and when the next attempt is due while one is (none while the endpoint is paused or disabled, which holds its attempts). Never a payload or a signing secret: GET /webhooks/deliveries/{id} returns an attempt's payload. What a credential reads: delivery attempts need `webhooks:manage` as well, the scope GET /webhooks/deliveries needs, and a connected app reads only the attempts to the endpoints it subscribed. A key created with a list of scopes, or a connected app, reads only the calls it made itself; a key created without one reads the calls of all the account's credentials. A test key lists only calls made with test keys and only sandbox deliveries. Calls and delivery attempts are kept for 90 days, as the request log and the delivery log keep them; events are kept with the order, so an older order's timeline lists its events alone. Calls answered before the request log recorded the order a call reached are listed when their path named this order; a call that placed an order before then is not. Pass `type` to list one kind only (only the calls, only the deliveries). Pages by cursor (see Pagination): `data` is a bare array and `meta` carries `has_more` and `next_cursor`. A walk lists what had happened when its first page was read, so it ends whatever is written meanwhile; the next walk lists what was. An order of another account, or a live order asked for with a test key, is 404 order_not_found.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Entries per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and only for this order, and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."},{"name":"type","in":"query","schema":{"type":"string","enum":["request","event","delivery"]},"description":"Only entries of this `type`: `request` (the calls that placed or changed the order), `event` (its events) or `delivery` (the attempts at delivering webhooks about it, which still need `webhooks:manage`: without it a `delivery` timeline is empty). Leave it out for every kind, merged, as before. Any other value is 400 validation_error. The cursor is bound to it: send the same `type` (or none) with every page of a walk; a cursor sent with another is 400 `other_filters`."}],"responses":{"200":{"description":"The order's requests, events and delivery attempts, oldest first. `data` is a bare array, as on GET /order/{id}/events; `has_more` and `next_cursor` live in `meta`.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrderTimelineEntry"}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":[{"type":"event","id":"c57yvwpkmchhnkd7fa6p20ued","at":"2026-10-15T15:00:47.000Z","event":{"action":"artwork_checked","details":"The artwork prints well (1200×1200 px, 400 DPI at 3×3 in).","timestamp":"2026-10-15T15:00:47.000Z"}},{"type":"event","id":"cfbgb3yt0b96cqzl7lijwdpe0","at":"2026-10-15T15:00:47.000Z","event":{"action":"order_submitted","details":"A test order: nothing is charged, printed or sent. The total is what a live order would pay.","timestamp":"2026-10-15T15:00:47.000Z"}},{"type":"request","id":"c64ztkjlkujdlmlc4sviofcv6","at":"2026-10-15T15:00:47.000Z","request":{"method":"POST","path":"/v1/order","status":200,"request_id":"a40f913f-9b81-49ea-8d9c-c03bd20ad9b2","trace_id":"4e2da63b674690074ebd7d6a9629312d","duration_ms":0,"key_prefix":"mf_test_xxxxxxxx","oauth_client_id":null,"mode":"test","error_code":null}},{"type":"delivery","id":"cgd0j2lh07ssq8ifc9xvozi8z","at":"2026-10-15T15:00:47.000Z","delivery":{"webhook_id":"whk_4czoxnKiV1lGoLIP_yodB3xT","endpoint_host":"example.com","event":"order.created","event_id":"evt_nyPRpD32bgnJc6D8T0TE6Y89","attempt":1,"outcome":"delivered","status_code":200,"failure_cause":null,"latency_ms":0,"next_retry_at":null}}],"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["orders:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:read"]}]}},"/orders/quote":{"post":{"operationId":"quoteOrder","summary":"Get a signed, expiring price quote","description":"Prices an order and returns a signed quote_token (24h TTL). Pass the token to POST /order to charge exactly the quoted total — no re-pricing surprise. May include a non-binding delivery_risk advisory and cross-border presentment/duties blocks.","tags":["Pricing"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":100,"description":"At most 100 lines of at most 10,000 units each, the bounds POST /order takes; a cart beyond either is 400 validation_error, as the order would refuse it.","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","minimum":1,"maximum":10000},"variable_data":{"$ref":"#/components/schemas/VariableData","description":"The same per-unit field POST /order takes, preflighted the same way, so an overflow or a count mismatch is refused here before you order. Never priced: the quote is what it is without it. The response's `variable_data` carries the variant proofs."}}}},"shipping":{"type":"object","properties":{"method":{"type":"string","enum":["standard","priority","express","letter"],"description":"standard (default); priority or express once they are priced — until then a request for either is 400 shipping_method_unavailable. Any other value is 400 validation_error, as on POST /order."}}},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"Optional presentment currency (ISO 4217); adds a presentment block when non-USD. The block's amount_cents is in that currency's own minor units and minor_units carries the exponent (0 for JPY, 2 for EUR). An unsupported currency is rejected with unsupported_currency rather than quoted at a fallback rate."},"destination_country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2. Our own plant ships to US addresses only, so any other country is refused 422 destination_unavailable. `address_to.country`, when sent, is the one checked."},"deadline":{"type":"string","format":"date-time","description":"Arrival deadline — delivery_risk warns when it's tight."},"deliver_by":{"type":"string","format":"date","description":"A day the order should arrive by, YYYY-MM-DD (UTC). Answered in `deliver_by_check`, judged against the estimate's latest day rather than its earliest, from the facility's production estimate and the method's transit window. Optional; a day before today, or anything but a real date, is 400 validation_error."},"provider":{"type":"string","enum":["printedsimply"],"default":"printedsimply","description":"The lane to price: always our own plant here, which is where POST /order makes the order. Omit it, or send `printedsimply`; any other value is refused with 422 `provider_not_available` before anything is priced."},"address_to":{"$ref":"#/components/schemas/Address","description":"The destination. Optional: our own plant prices shipping from its own table, so only its country is read, and it ships to US addresses only (any other country is refused 422 destination_unavailable)."}}},"example":{"items":[{"sku":"sticker-die-cut-3x3","quantity":100}],"shipping":{"method":"standard"}}}}},"responses":{"200":{"description":"Signed quote, priced at our own plant: `quote_id`, `quote_token` (pass it to POST /order to be charged exactly this total), `expires_at`, `lines`, `breakdown`, `margin` and `shipping` (its method, and the peak-season surcharge the shipping figure already holds, itemised); plus `estimated_delivery`, `deliver_by_check` when the request named a deliver_by day, `delivery_risk` when the order carries a known risk, `dispatch_promise` (switched off today), `presentment` when a currency other than USD was asked for, `variable_data` when a line carries it, and, on a test key, `pricing_degraded`.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrderQuote"}}}]},"example":{"status":"success","data":{"quote_id":"qt_EMyYo3DyOSrd7siDgh2niPvP","quote_token":"eyJxdW90ZV9pZCI6InF0X0VNeVlvM0R5T1NyZDdzaURnaDJuaVB2UCJ9.3b71988dd79915637f2e65aeddd8893705bb2140ef57a64dcf71aa67148bef1e","expires_at":"2026-10-11T08:09:14.578Z","lines":[{"sku":"sticker-die-cut-3x3","quantity":100,"unit_cents":70,"line_total_cents":7000}],"breakdown":{"items_cents":7000,"shipping_cents":650,"processing_fee_cents":0,"platform_fee_cents":0,"card_fee_cents":0,"total_cents":7650},"margin":{"suggested_retail_cents":15666,"margin_pct":50},"estimated_delivery":{"earliest":"2026-10-20","latest":"2026-10-26","ships_by":"2026-10-14","is_estimate":true},"shipping":{"method":"standard","surcharges":[]}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"$ref":"#/components/responses/QuoteRefused"},"429":{"$ref":"#/components/responses/RateLimitError"},"503":{"description":"provider_unavailable — the facility that produces these items is not accepting orders right now, so there is no price to quote. Nothing was charged; a quote never charges. With `details.reason: plant_unreachable` and `Retry-After`, our plant gave no answer about a SKU (the request did not reach it, timed out, or came back 5xx) — about the plant, not the SKU; the catalog says the same on the row (`not_orderable_code: plant_unreachable`). A test key gets a $0 quote marked `pricing_degraded: pricing_unavailable` instead. Only for a SKU the catalog has, though: one the price book does not price, our plant's products do not name and the synced catalog does not hold is refused 400 unknown_sku with `details.skus`, as it is on POST /order, POST /orders/quote and POST /pricing/estimate alike. tax_unavailable — sales tax collection is on, the quote has an `address_to`, and Stripe Tax could not calculate its tax, so nothing was quoted; retry shortly, or quote without `address_to` to have the tax calculated when the order is placed.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Sent with `details.reason: plant_unreachable`: seconds to wait before retrying, the catalog breaker's own cooldown."},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"x-required-scopes":["pricing:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["pricing:read"]}]}},"/order/{id}/missing-letter":{"post":{"operationId":"reportMissingLetter","summary":"Report a missing letter and reship it once","description":"For an order that shipped by `letter` — untracked letter mail, which carries no carrier scans. Once its expected window has passed (the plant's production estimate plus the slowest letter transit, stated on the order's `letter_mail` event and its tracking page), reporting it missing creates ONE free replacement: the same lines at $0, to the same address, by letter, produced where the original was. Nothing is charged. The original fires `order.updated` with `changes.action: \"letter_reshipped\"` and the replacement's ids.\n\nOnce per letter: a second report — on the original or on the replacement — is refused 409 `letter_report_refused`, and two racing reports produce one replacement. `details.reason` is `not_a_letter` (it shipped as a parcel; read its tracking), `not_shipped` (it has not left the plant), `not_yet_due` (with `details.expected_by`), `already_reshipped` (with `details.replacement_order_id`) or `is_a_reship`. A sandbox letter may be reported as soon as it has shipped — the sandbox compresses time — and its replacement runs the sandbox lifecycle. Letter mail exists only where the platform prices it; on a deployment that does not, no order is a letter and every report is `not_a_letter`.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The letter was reported missing and its one free replacement created","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MissingLetterReport"}}}]},"example":{"status":"success","data":{"order_id":"test_ord_rzbohZBiqt6XnFMC5RazH_R5","portal_order_id":"ord_rzbohZBiqt6XnFMC5RazH_R5","replacement":{"order_id":"rmk_KEFW7K8h6mUCGRWlDC","portal_order_id":"ord_FvjgWGKEFW7K8h6mUCGRWlDC","status":"paid","shipping_method":"letter","total_cents":0,"tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfMkJMeUh2TVd1RS1MbDc5QlFEU050dHdFIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDo0MS4wMDBaIn0.d6738ba42dc0cc76aa05ceb30c4b0f02fae1f278f1dbd4b69434d65549551044"},"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"letter_report_refused — `details.reason` says why: not_a_letter, not_shipped, not_yet_due (with expected_by), already_reshipped (with replacement_order_id) or is_a_reship.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/order/{id}/reorder":{"post":{"operationId":"reorderOrder","summary":"Place an order again","description":"Place the same order again in one call. The new order is built from what the source order stored: each line's SKU, quantity, artwork (the `print_file` it was sent) and variable data, its `address_to`, its shipping method and its `vat`; a line's quantity, the address, the method and the VAT id can be changed in the body, and a return address (`address_from`) sent. It is then placed exactly as POST /order places that body, at today's price: the same validation, artwork check, address check, price floor, spend caps, charge on a live key and $0 sandbox order on a test key, and the same webhooks. The answer is POST /order's, with `reordered_from` naming the source. Not copied: the `reference` (send a new one), a quote and the return address. POST /order does not store a line's `size`, `color`, `product_id`, `design_name`, `notes` or `mock_image_url`, or any file but the one it prints, so a line that carried them goes again without them: place such an order with POST /order. A line that kept no artwork at all is refused 422 artwork_required with `details.items[]`, on either key, and nothing is charged. The source must be an order of your account in the key's mode: a test key reorders a test order and a live key a live one, and any other id is 404 order_not_found. A SKU no longer sold and artwork that can no longer be fetched or no longer passes the check are refused with the codes POST /order uses.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderRequest"},"example":{"items":[{"sku":"sticker-die-cut-3x3","quantity":200}]}}}},"responses":{"200":{"description":"The new order, as POST /order answers it, with `reordered_from`","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"traceparent":{"$ref":"#/components/headers/traceparent"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/CreateOrderResponse"},{"type":"object","required":["reordered_from"]}]}}}]},"example":{"status":"success","data":{"order_id":"test_ord_3WDuvjlojmq4wMObnNbeIMyk","portal_order_id":"ord_3WDuvjlojmq4wMObnNbeIMyk","provider":"merchfarm","tracking_token":"trk__bBBdLzQVxbyNDvtBMfw9N32","tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfX2JCQmRMelFWeGJ5TkR2dEJNZnc5TjMyIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDo0NS4wMDBaIn0.9b18151b2dd9ce9ff005f798c145ddf495f7f532bfa31354836e1b3cfd3c4d9a","test_mode":true,"lifecycle":"simulated","charged":"$0.00","reordered_from":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","artwork_check":{"lines":[{"item_index":0,"sku":"sticker-die-cut-3x3","status":"passed","width_px":1200,"height_px":1200,"findings":[],"print_size_in":[3,3],"effective_dpi":400,"min_dpi":300}],"would_refuse":false},"address_check":{"valid":true,"checked":true,"normalized":{"name":"Jane Doe","address1":"456 OAK AVE","city":"AUSTIN","state":"TX","zip":"78702","country":"US"},"findings":[]},"estimated_delivery":{"earliest":"2026-10-23","latest":"2026-10-29","ships_by":"2026-10-19","is_estimate":true},"breakdown":{"items":"$103.89","shipping":"$13.99","processing_fee":"$0.00","platform_fee":"$0.00","card_fee":"$0.00","total":"$117.88"},"shipping":{"method":"standard","surcharges":[]}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"validation_error — the body failed validation (a field POST /order takes and a reorder does not, such as `provider` or `quote_token`, is one), an `items` entry names no line of the source or a sku on more than one (`details.lines` lists the lines), or the source holds no address or shipping method an order takes (send `address_to` or `shipping.method`). Otherwise any 400 POST /order answers for the order built, such as pricing_error or unknown_sku.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"402":{"description":"As on POST /order: refused before anything was charged (`no_payment_method`, `payment_failed`, `authentication_required`, `fraud_review`, `spend_cap_exceeded`, `spend_cap_unenforceable`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"403":{"description":"sandbox_only, as on POST /order; or insufficient_scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"404":{"description":"order_not_found — no order of this account has this id in the key's mode.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"409":{"description":"invalid_state — every line of the source was cancelled, so there is nothing to place again; or the source is a team-store member order, or has a line whose stored personalization POST /order has no field for (`details.items[].fields`). duplicate_reference — as on POST /order, for the `reference` you sent. Also idempotency_conflict, while a request with the same Idempotency-Key is still in flight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"$ref":"#/components/responses/OrderRefused"},"429":{"$ref":"#/components/responses/RateLimitError"},"500":{"description":"order_error — as on POST /order: the order was placed and charged but the confirmation could not be returned or the record written. Do not retry; look the order up first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"503":{"description":"provider_unavailable or reference_check_unavailable, as on POST /order. idempotency_unavailable — the request carried an `Idempotency-Key` and the ledger that makes it mean at-most-once could not be reached, so nothing ran and nothing was charged; retry the same request with the same key after `Retry-After`.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/order/{id}/simulate":{"post":{"operationId":"simulateOrder","summary":"Force a sandbox order to a lifecycle state","description":"Test keys only. Deterministically advances a sandbox order to in_production, shipped, delivered, or cancelled, firing the same signed webhooks the timed sandbox sweep would — so integration tests can drive the full lifecycle without waiting on the clock. `issue_flagged` rehearses the unhappy path: the order takes the status and the `issue_reason` you name, as a live order does when the facility flags it, and `order.issue` is enqueued carrying the same `issue_reason`. Omit `issue_reason` to rehearse a flag the facility gave no reason for. A flag stops the sandbox clock; simulate a later state to move the order on, which clears it.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["target_state"],"properties":{"target_state":{"type":"string","enum":["in_production","shipped","delivered","cancelled","issue_flagged"]},"issue_reason":{"type":"string","enum":["artwork_unprintable","address_undeliverable","shipment_exception","on_hold","cancellation_pending"],"description":"Only with `target_state: issue_flagged`: why the rehearsed flag was raised — the same codes a live order's `issue_reason` carries on its reads and on `order.issue`. Sent with any other target_state, it is a 400 validation_error."}}},"example":{"target_state":"shipped"}}}},"responses":{"200":{"description":"Order advanced; webhooks fired","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"order_id":{"type":"string"},"portal_order_id":{"type":"string"},"status":{"type":"string","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"],"description":"The status the order was moved to. A simulation only ever moves an order forward through the statuses the platform writes, never into a legacy one."},"issue_reason":{"type":["string","null"],"enum":["artwork_unprintable","address_undeliverable","shipment_exception","on_hold","cancellation_pending",null],"description":"Present only when `status` is `issue_flagged`: the `issue_reason` the flag was raised with, or null when none was named."},"simulated":{"type":"boolean","enum":[true]}}}}}]},"example":{"status":"success","data":{"order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","status":"shipped","simulated":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"Live key (test_mode_only), invalid target_state or issue_reason (validation_error), or order already terminal (already_terminal)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/{id}/item":{"post":{"operationId":"addItem","summary":"Add item to order","description":"Add a new item to an existing order. Charges the price delta to the default payment method: the line's goods, and what the line adds to the order's shipping. The line rides in the parcel the order already ships, so the parcel is re-priced for its new lines by the method the order is recorded as shipping by (standard when it recorded none), on its own lane, folded as POST /order folds it: the lines before and after are priced on today's terms and the difference is charged, so a line that moves the parcel into a heavier class pays for that and nothing else. `shipping` says what moved; `total_delta_cents` is the whole charge, the figure the key's spend caps see. On an order shipping by letter, a line the letter cannot take (a parcel-only item, too many units, goods over the ceiling) is refused 400 `letter_mail_unavailable` with `details.reason`, as POST /order refuses the same lines, and nothing is charged: change the order's method with PUT /order/{id}/shipping first. A parcel method no longer offered is refused 400 `shipping_method_unavailable`. The order's size limits hold here as on POST /order: a line past an order's 100th, or a quantity over 10,000, is refused 400 `validation_error` and nothing is charged. Add-items on one order are taken one at a time, so two sent together cannot pass the line limit between them. On a test order the line is priced $0, marked `pricing_degraded`, when its price cannot be had just now (the facility unconfigured, or not answering — `pricing_unavailable` for our plant not answering, which a live key is answered 503 provider_unavailable for); a SKU the catalog does not have is refused 400 unknown_sku with `details.skus`, and our plant's answer that it does not carry the SKU is the 400 pricing_error a live key gets, as on POST /order.","tags":["Items"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddItemRequest"},"example":{"sku":"magnet-4x4","quantity":25,"print_file":"https://example.com/magnet-design.png"}}}},"responses":{"200":{"description":"Item added","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AddItemResponse"}}}]},"example":{"status":"success","data":{"item":{"id":"itm_vNx36IWd8kx2dCQqkFbsTVg3","sku":"magnet-4x4","quantity":25,"unit_price":"$2.07","charged":"$51.85"},"shipping":{"method":"standard","shipping_class":{"name":"parcel_5lb","weight_oz":35},"delta_cents":1200,"shipping_cents":2099},"total_delta_cents":6385,"test_mode":true,"artwork_check":{"lines":[{"item_index":0,"sku":"magnet-4x4","status":"passed","width_px":1200,"height_px":1200,"findings":[],"print_size_in":[4,4],"effective_dpi":300,"min_dpi":300}],"would_refuse":false}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"402":{"$ref":"#/components/responses/PaymentError"},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"tax_not_supported_on_path — sales tax collection is on and this is a live order: adding a line would charge it without its sales tax, so it is refused. Cancel the order and place it again with the line. Nothing was charged or changed. order_not_modifiable — the order has gone into production (or later), past the point where items can be added, or was cancelled while this request waited for another change to the order to finish; the order is judged as it stands once that change is done, and nothing was charged. shopper_paid_order — a shopper paid for this order (a drop, a checkout link or an agent checkout), so it takes no charge on your card: its refunds go back to the shopper. Place a separate order instead. concurrent_modification — another line was being added to this order and did not finish within a few seconds: add-items on one order are taken one at a time, so the 100-line limit holds, and nothing was charged. Retry in a moment. Also answered when the order moved to another facility while the line was being added: nothing was charged; read the order and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"description":"artwork_not_printable — the order is made at our own plant and the added line's artwork (print_file) cannot be printed as it is (unreachable, not_an_image, low_resolution). Checked before the line is priced or the card is charged: nothing was charged and the Idempotency-Key is free for the corrected line. `details.problems[]` and `details.artwork_check` are shaped as on POST /order's 422. artwork_required — the order is made at our own plant and the added line has no print_file, so there is nothing to print; refused before anything is read or charged, with `details.items[]` as on POST /order's 422. A test key is never refused for artwork; its 200 carries `artwork_check.would_refuse` (and `artwork_check.missing_artwork` for a line with none). sku_not_orderable_yet — live keys only: a line names a modeled product our catalog lists that our plant has not confirmed it makes yet. `details.skus` names them as sent, and `details.plant_skus` the spelling our plant adopts for one sent another way. Refused before anything is asked of the plant or charged; a test key orders a modeled listing at its listed price. catalog_only — a flag or Santa hat is a catalog reference whose artwork, production cost and shipping weight are unconfirmed. Automatic quotes and orders are refused in both test and live mode before pricing, routing or charging, including requests carrying an earlier quote; `details.skus`, `details.readiness` and `details.reason` explain the refusal. shipment_capacity_exceeded — a basket of sized own-plant products exceeds the modeled 70 lb single-parcel limit, including stickers, magnets, posters, decals, packs and rolls. Both test and live requests are refused before charging; `details.skus`, `details.weight_oz` and `details.max_weight_oz` state the affected SKUs, whole-basket estimated weight and limit. Reduce the quantity or split the order. sku_not_available — the added line is a product our own plant does not make; refused before it is priced or charged, and `details.skus` names it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"},"501":{"description":"not_supported_by_provider — the order was produced by an independent network shop, which does not accept amendments at all. Never retryable, unlike the 409. Live PrintedSimply add-item calls are also refused before any charge because the plant handler is not confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"503":{"description":"provider_unavailable (`details.reason: plant_unreachable`, with `Retry-After`) — the order is made at our own plant and the plant gave no answer about the added SKU (the request did not reach it, timed out, or came back 5xx), which is about the plant and not the SKU: a definite answer that it does not carry the SKU is still the 400 pricing_error. Asked before the card is charged, so nothing was charged and the same Idempotency-Key may be retried. A test order is not refused: its line is priced $0 with `pricing_degraded: pricing_unavailable`, as on POST /order. idempotency_unavailable — the request carried an `Idempotency-Key` and the ledger that makes it mean at-most-once could not be reached, so nothing ran and nothing was charged; retry the same request with the same key after `Retry-After`.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Sent with `details.reason: plant_unreachable` (the catalog breaker's own cooldown) and with idempotency_unavailable: seconds to wait before retrying."},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/{id}/item/{itemId}":{"put":{"operationId":"updateItem","summary":"Update item quantity","description":"Change the quantity of an item. Charges the delta if increasing, refunds if decreasing: the line's goods, and what the new quantity does to the order's shipping. The order ships one parcel, so it is re-priced for the new quantity by the method the order is recorded as shipping by (standard when it recorded none), on its own lane, folded as POST /order folds it: the lines before and after are priced on today's terms and the difference moves with the goods, in the same direction. An increase never refunds shipping and a decrease never charges it; a decrease never refunds the shipping line below what the smaller parcel costs today. `shipping` says what moved; `total_delta_cents` is the whole charge or (negative) refund, the figure the key's spend caps see on an increase. On an order shipping by letter, an increase the letter cannot take is refused 400 `letter_mail_unavailable` with `details.reason`, as POST /order refuses the same lines, and nothing moves; a parcel method no longer offered refuses an increase with 400 `shipping_method_unavailable`. A decrease is never refused for either: its goods are refunded and its shipping stays as charged.","tags":["Items"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/ItemId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateItemRequest"},"example":{"quantity":5}}}},"responses":{"200":{"description":"Item updated","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UpdateItemResponse"}}}]},"example":{"status":"success","data":{"item":{"id":"itm_vNx36IWd8kx2dCQqkFbsTVg3","sku":"magnet-4x4","previous_quantity":25,"new_quantity":5,"unit_price":"$2.07","delta":"$-34.85"},"shipping":{"method":"standard","shipping_class":{"name":"parcel_2lb","weight_oz":17.1},"delta_cents":-700,"shipping_cents":1399},"total_delta_cents":-4185,"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"402":{"$ref":"#/components/responses/PaymentError"},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"tax_not_supported_on_path — sales tax collection is on and this is a live order: a quantity change that charges, or that refunds part of an order that was taxed, is refused, because this operation does not recalculate the order's tax. Cancel the order and place it again with the quantity you want. Nothing was charged or changed. order_not_modifiable — the order has gone into production (or later), past the point where its items can change. shopper_paid_order — a quantity increase on an order a shopper paid for (a drop, a checkout link or an agent checkout); it would be charged to your card and refunded to the shopper. A decrease is allowed and refunds the shopper's own charge. concurrent_modification — another change to this order's lines (a quantity change, a cancel, an added line) did not finish within a few seconds, or, where this change has moved nothing yet (a test key, or a change that restates the quantity), landed on a line this change rewrites while it ran: nothing was written, charged, refunded or sent to the plant. Read the order and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"description":"variable_data_invalid — the line was placed with per-unit variable data, which fixes its quantity to the units it carries (`details.problems[0].code` is `count_mismatch`). Nothing was charged or refunded; cancel the line and order it again with the units you want. artwork_required — a new quantity for a line with no artwork on record, on a live order at our own plant (`details.item_id`, `details.sku`): the change is sent to the plant with the line's SKU and its file, and the plant prints every line from a file, so it is refused before anything is charged or refunded. Cancel the line and add it again with a print_file. A test key is not refused, and a PUT that restates the current quantity sends the plant nothing. below_cost_floor — on our own plant, a decrease that would leave an order costing more to make and ship than it was charged, at worst-case postage on today's season (two 8×10 posters taken down to one in the carriers' peak window: one 8×10 or 9×11 poster alone, which checkout refuses too). Nothing was written, charged or refunded, and a test order is held to it as a live one is; `details.item_id` names the line, `details.skus` the lines the order would hold and `details.contribution_cents` how far short they fall. Keep the quantity, or cancel the whole order (DELETE /v1/order/{id}/cancel). An increase is never refused for it. sku_not_available — a quantity increase on a line our own plant does not make (one placed before this portal sold only its own products); refused before anything is charged, and `details.skus` names it. A decrease is allowed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"},"500":{"description":"refund_failed — the quantity decrease was applied but the refund for it could not be processed. The money is owed and support is alerted; do not retry, which would decrease the quantity again. settlement_recording_failed — the charge or refund went through, or the plant made the change, but the order's record could not follow it (among the reasons, another change landed on a line this one rewrites while it ran, and this one was not written over it): do not retry; read the order and quote the request_id to support.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"501":{"description":"not_supported_by_provider — the order was produced by an independent network shop, which does not accept amendments at all. Never retryable, unlike the 409. Live PrintedSimply quantity changes are also refused before any charge or refund because the plant handler is not confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"503":{"$ref":"#/components/responses/LineAmendmentUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/{id}/item/{itemId}/cancel":{"delete":{"operationId":"cancelItem","summary":"Remove item from order","description":"Remove an item from an order and refund its full cost.","tags":["Items"],"parameters":[{"$ref":"#/components/parameters/OrderId"},{"$ref":"#/components/parameters/ItemId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Item removed and refunded","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CancelItemResponse"}}}]},"example":{"status":"success","data":{"item_id":"itm_vNx36IWd8kx2dCQqkFbsTVg3","refunded":"$0.00","refunded_cents":0,"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"order_not_modifiable — the order has gone into production (or later), past the point where items can be removed. concurrent_modification — another change to this order's lines (a quantity change, a cancel, an added line) did not finish within a few seconds, or, on a test order (which the plant never sees), changed this line while it ran: nothing was removed or refunded. Read the order and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"422":{"description":"below_cost_floor — on our own plant, the order this cancel would leave costs more to make and ship than it was charged, at worst-case postage on today's season (one 8×10 or 9×11 poster alone in the carriers' peak window, which checkout refuses too). Nothing was cancelled or refunded; `details.skus` names the lines that would be left and `details.contribution_cents` how far short they fall. Cancel the whole order instead (DELETE /v1/order/{id}/cancel).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"},"500":{"description":"refund_failed — the item was removed at the plant and from the order, but its refund went through only in part or not at all (`details.refunded_cents` of `details.owed_cents`); the rest is retried automatically. Do not retry the cancellation. settlement_recording_failed — the item was removed at the plant and its refund, if any, was sent, but the order's record could not follow it (among the reasons, another change landed on this line while the cancel ran, and the line was not removed over it): do not retry; read the order and quote the request_id to support.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"501":{"description":"not_supported_by_provider — the order was produced by an independent network shop, which does not accept amendments at all. Never retryable, unlike the 409.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"502":{"$ref":"#/components/responses/UpstreamError"},"503":{"$ref":"#/components/responses/LineAmendmentUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/products":{"get":{"operationId":"listProducts","summary":"List products","description":"Browse the product catalog. Supports filtering by category, size, finish, and free-text search. Pages by cursor (see Pagination): the catalog is not a table, so it is walked in the order the catalog reports it, and a cursor names the last row's place and SKU, and the SKU after it — a product added or removed elsewhere in the catalog while you page moves nothing you have read, and if the last row you were given leaves, the walk resumes at the row that followed it. A request without `cursor` answers every match unless `limit` caps it, as it always has. Every price field on a row (`price`, `regular_price`, `sale_price`, `base_price`) is the price an order charges, the same on every plan: there is no platform fee (`platform_fee_pct` on GET /v1/pricing/tiers reads 0). For a product our own plant makes that our price book prices (a sticker, a vinyl decal up to 5×5 in, a poster, a magnet or a wall decal, at a size where one unit pays its own handling) the list price is the book's, a base plus a rate per square inch of the size the SKU names, and every price field on its row shows it — the plant's own catalog figure is a cross-check, never charged; a sticker our sticker catalog lists is priced by the catalog's own sheet instead, its one-unit price on the row and its pack totals in `quantity_breaks`, a wall decal our catalog lists by its own sheet's price, and a poster it lists at its listed price, each one price each at any quantity, and a magnet our catalog lists by its own price list, a total at every quantity (`price_interpolation: formula`); for anything else (unlisted sticker sheets and rolls, larger vinyl decals, smaller sizes, and every other facility's goods) it is the facility's list price. POST /v1/order charges that list price, with only shipping and card processing added. On a row the price book does not price, a `sale_price` is the catalog's own and is never what an order charges; on a row it prices, `sale_price` shows the book's price like every other field. Every row our own plant makes also says what it sells and whether it can be ordered now: `size_in`, `finish`, `family`, `minimum_quantity` (1: a run of one is an order), `quantity_breaks` (what an order charges at each quantity where it changes, or at each pack quantity of our sticker catalog or each published quantity of our magnet catalog, from the function the quote and the order charge with) with `price_interpolation` saying how to read between its entries, and `orderable` with `not_orderable_code` and `not_orderable_reason` (the quote's own refusals, asked of the SKU first, as a code and as a sentence). Our catalog (die-cut and kiss-cut stickers in white, clear, glitter and holographic; three single bumper stickers and six single-sheet variants in white, clear (Transparent), and holographic, each at its own flat single-unit price; wall decals in 36 sizes of Removable Wall Film and Polyester Fabric, each priced by its own sheet, posters in 46 sizes of matte paper, each one price at any quantity, and magnets in 16 sizes, priced by their own price list) is listed whole: our plant's rows come first, then each row of the catalog they do not hold, `source: \"listed\"`, each with its `material` (and a sticker's `options`); a row our plant's catalog does not carry yet says `not_orderable_code: plant_unmapped`.","tags":["Products"],"parameters":[{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and only on the catalog it was returned for, served from the source it was returned under (live, the last-good snapshot, the local synced copy or our price book): a cursor read against a page served from another source is refused with 409 cursor_source_changed, and the walk starts again without `cursor`, and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200},"description":"Products per page, applied after the filters (`total` still counts every match). Without `cursor`: no default (every match comes back) and 200 at most — the size this list's page walk always had, so a first answer and `page` agree on it. With `cursor`: 20 by default and 100 at most, as on every list. Without `cursor` a value is read by its leading whole number, as it always was: above 200 it is clamped to 200, and one below 1, or with no leading number, is read as 1. With `cursor` a value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value takes the default. None is refused."},{"name":"page","in":"query","deprecated":true,"schema":{"type":"integer","minimum":1,"default":1},"description":"Deprecated: walk with `cursor`. The facility's own page of the catalog, passed through and answered as before, with `limit` as a request without `cursor` takes it; `meta.has_more` and `meta.next_cursor` say where the cursor walk continues after this page's last row. Ignored when `cursor` is sent."},{"name":"category","in":"query","schema":{"type":"string"},"description":"Filter by product category (e.g. 'stickers', 'magnets', 'decals'). Matched generously: a row is kept if its DERIVED category equals this exactly, or if the value appears anywhere in its raw category, SKU or name. So the result can hold rows whose `category` field is not this string."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Free-text search across product name, SKU, and description"},{"name":"size","in":"query","schema":{"type":"string"},"description":"Filter by size (e.g. '3x3', '4x4', '6x6')"},{"name":"finish","in":"query","schema":{"type":"string"},"description":"Filter by finish (e.g. 'glossy', 'matte', 'holographic')"},{"name":"provider","in":"query","schema":{"type":"string","enum":["printedsimply"],"default":"printedsimply"},"description":"Which catalog to browse: `printedsimply`, our own plant's — the stickers, decals, labels, magnets and posters it makes and prices. It is the default and the only value; anything else is a 400."},{"name":"dedupe","in":"query","schema":{"type":"boolean","default":false},"description":"Collapse the OMS's two-SKU-per-product rows to one canonical row per product. Opt-in: `true` enables it, anything else leaves the catalog as the facility reports it. `total` still counts the rows actually returned."}],"responses":{"200":{"description":"The matching catalog rows. May have been served out of the last-good snapshot — read `X-Catalog-Stale` on every facility answer, which is sent both ways. When our own plant's catalog (`printedsimply`) has nothing of its own to serve — no live answer, no snapshot, no local copy — its rows are our price book's: the sizes the book prices, at its list prices (the price an order is charged), each row carrying `source: \"price_book\"`, with `X-Catalog-Stale: true` and `X-Catalog-Degraded` naming why. A size or product the book does not list is missing from that answer, so do not republish it as the plant's whole catalog. Each of those rows says `orderable: false` and why (`facility_not_configured` or `plant_unreachable`). Our catalog's rows (its stickers, wall decals, posters and magnets) the book does not hold follow them, `source: \"listed\"`, as on a fresh answer. A stale answer is sent `Cache-Control: private, no-store`, so no shared cache keeps serving an outage's availability after the plant answers again.","headers":{"X-Catalog-Stale":{"$ref":"#/components/headers/X-Catalog-Stale"},"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Warning":{"$ref":"#/components/headers/Warning"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProductListResponse"},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"products":[{"sku":"sticker-die-cut-3x3","name":"Die-Cut Sticker 3\" x 3\"","price":"0.70","regular_price":"0.70","category":"stickers"}],"total":1,"provider":"printedsimply","facets":{"category":{"stickers":1}},"filters":{"category":"stickers","search":null,"size":"3x3","finish":null,"dedupe":false}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"total":1,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"409":{"description":"cursor_source_changed — `cursor` was returned while the catalog was served from one source (live, the last-good snapshot, the local synced copy or our price book; `details.issued_under`) and this page is served from another (`details.source`, with `X-Catalog-Stale` and `X-Catalog-Degraded` saying why). Each is another list, so the walk is refused rather than continued with rows skipped or repeated: leave `cursor` out and start again from the first page. A cursor returned before cursors named their source (`details.issued_under: null`) is refused the same way.","headers":{"X-Catalog-Stale":{"$ref":"#/components/headers/X-Catalog-Stale"},"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"$ref":"#/components/responses/RateLimitError"},"502":{"description":"Nothing was servable — no live catalog, no snapshot, no local copy (never on our own plant's catalog, `printedsimply`, whose last resort is our price book: see 200). Deliberately loud: this endpoint never answers a catalog outage with an empty 200. `X-Catalog-Degraded` carries the reason, so a failure is diagnosable rather than indistinguishable from an empty catalog.","headers":{"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["products:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["products:read"]}]}},"/products/{sku}":{"get":{"operationId":"getProduct","summary":"Get product by SKU","description":"Retrieve details and pricing for a specific product — its price fields as on GET /v1/products: the list price (our price book's, for a product of our own plant's that the book prices), the same on every plan since there is no platform fee — from the catalog of the facility whose capabilities cover it (a sticker SKU is answered by the flat-goods facility; apparel by the default facility). Pass `provider` to ask one facility's catalog explicitly. Returns 404 when that catalog has no such SKU — 'does this SKU exist?' is answerable here before ordering. When the facility gives no answer about the SKU (5xx, a refused credential, a timeout, a rate limit, or an open circuit), the row is served out of the last-good snapshot with `X-Catalog-Stale: true`, as `GET /v1/products` does — so the list and this read agree during an outage. A SKU the snapshot does not hold is a 502, never a 404: a stale list missing a row says nothing about a product added since. On our own plant's catalog, a SKU our price book lists is served from the book instead, as the list serves it (`source: \"price_book\"`, `X-Catalog-Stale: true`), and so is another size of a product it lists, spelled as the list spells it (sticker-die-cut-2x2 beside the listed 3×3), which a test key's quote and order take at $0 while the plant does not answer. A SKU our catalog lists (a sticker's, a wall decal's, a poster's or a magnet's, in any canonical spelling of a row or of its shapes and finishes) is never a 404: where our plant's catalog has no row for it, it is the catalog's row under the SKU's canonical spelling, `source: \"listed\"`, with `not_orderable_code: plant_unmapped`; where the plant gives no answer, the same row, stale. A stale answer is sent `Cache-Control: private, no-store`, as on the list.","tags":["Products"],"parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string"},"description":"Product SKU identifier"},{"name":"provider","in":"query","schema":{"type":"string","enum":["printedsimply"],"default":"printedsimply"},"description":"Which catalog to ask: `printedsimply`, our own plant's. It is the default and the only value; anything else is a 400."}],"responses":{"200":{"description":"The catalog row for this SKU. `data` is the facility's own row, unwrapped — the same shape as one element of `GET /v1/products`, and open for the same reason. Note that `category` appears here only if the facility publishes one: the derivation that fills it runs on the list, not on this route. May have been served out of the last-good snapshot — read `X-Catalog-Stale`, which is sent both ways; a stale row from the local synced catalog carries only `sku`, `name` and `priceCents`, and one from our price book `sku`, `name`, `base_price`, `category` and `source: \"price_book\"` (with `alt_skus` where the book knows the product by another SKU too). Every row our own plant makes, whatever its source, also carries `size_in`, `finish`, `family`, `minimum_quantity`, `quantity_breaks`, `price_interpolation`, `min_unit_cents`, `material`, `options`, `orderable`, `not_orderable_code` and `not_orderable_reason`, as on the list; a row the catalog built carries `sku`, `name`, `base_price`, `category` and `source: \"listed\"`.","headers":{"X-Catalog-Stale":{"$ref":"#/components/headers/X-Catalog-Stale"},"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Warning":{"$ref":"#/components/headers/Warning"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CatalogProduct"}}}]},"example":{"status":"success","data":{"sku":"sticker-die-cut-3x3","name":"Die-Cut Sticker, 3×3 in","price":"0.70","regular_price":"0.70","category":"stickers","purchasable":true,"stock_status":"instock","size_in":[3,3],"finish":"glossy","family":"stickers","minimum_quantity":1,"quantity_breaks":[{"min_qty":1,"unit_cents":70,"total_cents":70,"list_total_cents":70},{"min_qty":5,"unit_cents":70,"total_cents":350,"list_total_cents":350},{"min_qty":10,"unit_cents":70,"total_cents":700,"list_total_cents":700},{"min_qty":25,"unit_cents":70,"total_cents":1750,"list_total_cents":1750},{"min_qty":50,"unit_cents":70,"total_cents":3500,"list_total_cents":3500},{"min_qty":100,"unit_cents":70,"total_cents":7000,"list_total_cents":7000},{"min_qty":250,"unit_cents":48,"total_cents":12083,"list_total_cents":12083},{"min_qty":500,"unit_cents":37,"total_cents":18667,"list_total_cents":18667},{"min_qty":1000,"unit_cents":30,"total_cents":30083,"list_total_cents":30083},{"min_qty":2500,"unit_cents":24,"total_cents":59167,"list_total_cents":59167}],"price_interpolation":"linear_total","min_unit_cents":null,"material":"white","options":{"shapes":["contour","circle","square"],"finishes":["gloss","matte"],"default_shape":"contour","default_finish":"gloss","variant_skus":["sticker-die-cut-3x3","sticker-die-cut-3x3-matte","sticker-die-cut-3x3-circle","sticker-die-cut-3x3-circle-matte","sticker-die-cut-3x3-square","sticker-die-cut-3x3-square-matte"]},"orderable":true,"not_orderable_code":null,"not_orderable_reason":null},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"},"502":{"description":"The facility catalog gave no answer and no snapshot holds the SKU (nor, on our own plant's catalog, our price book) — or, with not_configured, the facility has no credentials in this environment, which is an operator fact rather than an outage. `X-Catalog-Degraded` carries the reason on every 502, so a failure is diagnosable rather than indistinguishable from an empty catalog. This is not a verdict on the SKU: retry, or read `GET /v1/products`.","headers":{"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["products:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["products:read"]}]}},"/products/options":{"get":{"operationId":"listProductOptions","summary":"List what a custom size may be","description":"For each product our own plant makes to any size (die-cut and kiss-cut stickers, vinyl and wall decals, magnets, posters), what an order of a custom size needs, from the rules the order itself applies: the SKU grammar (`sku_template`, a stem and `{width}x{height}` in decimal inches, case aside), the largest size (`max_width_in` × `max_height_in`, either way round), the smallest (`min_area_sq_in`, the least width × height the price book prices: below it one unit does not pay what it costs the plant to make and ship alone; null when every size is priced), the step (none, so a side is any decimal number of inches), the quantity a line may carry, the DPI the order grades artwork at (`min_dpi` recommended, refused below `refuse_below_dpi` on a live order) and the bleed, the finishes on offer (a sticker's: gloss and matte) and the price book's list price for one unit (`base_cents` + width × height × `cents_per_sq_in`, rounded, before quantity breaks, any plan discount and the unit floor: POST /orders/quote is the price an order is charged). For every other product, a size inside these bounds is one the book prices, and a test key's quote and order take it. A live quote or order also asks our plant for that exact SKU (`plant_confirms_sku`), and a size its catalog does not carry is refused 400 `pricing_error`; GET /products lists the sizes it carries now. Stickers are sold at the sizes and materials our sticker catalog lists (`sizes`, `materials`, `shapes`) and no other (`price_book` null: custom sticker sizes are retired), each priced by its own sheet: a pack total at each of its tiers, linear between them (each row's `quantity_breaks` on GET /products). Wall decals also list the sizes and materials our wall-decal catalog sells (`sizes`, `materials`), each priced by its own sheet, one price each at any quantity; a wall-decal size it does not list is still one the book prices. Posters also list the sizes our poster catalog sells (`sizes`, `materials`: matte paper), each at its listed price, one price each at any quantity; a poster size it does not list is still one the book prices. Magnets list sixteen purchasing-list sizes through 18×24 in. Square sizes offer custom contour, circle and square cuts; rectangular sizes offer custom contour, oval and rectangle cuts, with no finish choice. Every shape shares its size's price key and bulk quantity. Goods are the lower of 85% of the buying-list unit price times quantity and 85% of the historical bulk profile, with no discount expiry at 250 (each row's `quantity_breaks`, `price_interpolation` `formula`). A magnet size not listed is still one the book prices. A live quote or order of a size and material one of those catalogs lists that our plant does not carry yet is refused 422 `sku_not_orderable_yet` before anything is charged, and a test key orders it now. Sticker sheets and rolls are not here: their SKUs name no printed area, so they are ordered at the sizes GET /products lists. One bounded document, not a page; it asks nothing of the plant, so it is never stale. Read-only.","tags":["Products"],"responses":{"200":{"description":"Every configurable product, stickers first, then decals, magnets and posters.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProductOptions"}}}]},"example":{"status":"success","data":{"products":[{"product":"sticker-die-cut","name":"Die-Cut Sticker","family":"stickers","sku_template":"sticker-die-cut-{width}x{height}","example_sku":"sticker-die-cut-3x3","size":{"unit":"in","max_width_in":17,"max_height_in":17,"either_orientation":true,"min_area_sq_in":1.55,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":300,"refuse_below_dpi":225,"bleed_in":0.125},"sizes":[{"sku":"sticker-die-cut-2x2","width_in":2,"height_in":2,"materials":[{"material":"white","sku":"sticker-die-cut-2x2"},{"material":"clear","sku":"sticker-die-cut-clear-2x2"},{"material":"glitter","sku":"sticker-die-cut-glitter-2x2"},{"material":"holographic","sku":"sticker-die-cut-holographic-2x2"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-2.5x2.5","width_in":2.5,"height_in":2.5,"materials":[{"material":"white","sku":"sticker-die-cut-2.5x2.5"},{"material":"clear","sku":"sticker-die-cut-clear-2.5x2.5"},{"material":"glitter","sku":"sticker-die-cut-glitter-2.5x2.5"},{"material":"holographic","sku":"sticker-die-cut-holographic-2.5x2.5"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-3x3","width_in":3,"height_in":3,"materials":[{"material":"white","sku":"sticker-die-cut-3x3"},{"material":"clear","sku":"sticker-die-cut-clear-3x3"},{"material":"glitter","sku":"sticker-die-cut-glitter-3x3"},{"material":"holographic","sku":"sticker-die-cut-holographic-3x3"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-4x4","width_in":4,"height_in":4,"materials":[{"material":"white","sku":"sticker-die-cut-4x4"},{"material":"clear","sku":"sticker-die-cut-clear-4x4"},{"material":"glitter","sku":"sticker-die-cut-glitter-4x4"},{"material":"holographic","sku":"sticker-die-cut-holographic-4x4"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-5x5","width_in":5,"height_in":5,"materials":[{"material":"white","sku":"sticker-die-cut-5x5"},{"material":"clear","sku":"sticker-die-cut-clear-5x5"},{"material":"glitter","sku":"sticker-die-cut-glitter-5x5"},{"material":"holographic","sku":"sticker-die-cut-holographic-5x5"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-6x6","width_in":6,"height_in":6,"materials":[{"material":"white","sku":"sticker-die-cut-6x6"},{"material":"clear","sku":"sticker-die-cut-clear-6x6"},{"material":"glitter","sku":"sticker-die-cut-glitter-6x6"},{"material":"holographic","sku":"sticker-die-cut-holographic-6x6"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-8x8","width_in":8,"height_in":8,"materials":[{"material":"white","sku":"sticker-die-cut-8x8"},{"material":"clear","sku":"sticker-die-cut-clear-8x8"},{"material":"glitter","sku":"sticker-die-cut-glitter-8x8"},{"material":"holographic","sku":"sticker-die-cut-holographic-8x8"}],"shapes":["contour","circle","square"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-3x2","width_in":3,"height_in":2,"materials":[{"material":"white","sku":"sticker-die-cut-3x2"},{"material":"clear","sku":"sticker-die-cut-clear-3x2"},{"material":"glitter","sku":"sticker-die-cut-glitter-3x2"},{"material":"holographic","sku":"sticker-die-cut-holographic-3x2"}],"shapes":["contour","rectangle","oval"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-4x3","width_in":4,"height_in":3,"materials":[{"material":"white","sku":"sticker-die-cut-4x3"},{"material":"clear","sku":"sticker-die-cut-clear-4x3"},{"material":"glitter","sku":"sticker-die-cut-glitter-4x3"},{"material":"holographic","sku":"sticker-die-cut-holographic-4x3"}],"shapes":["contour","rectangle","oval"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-5x3","width_in":5,"height_in":3,"materials":[{"material":"white","sku":"sticker-die-cut-5x3"},{"material":"clear","sku":"sticker-die-cut-clear-5x3"},{"material":"glitter","sku":"sticker-die-cut-glitter-5x3"},{"material":"holographic","sku":"sticker-die-cut-holographic-5x3"}],"shapes":["contour","rectangle","oval"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-6x4","width_in":6,"height_in":4,"materials":[{"material":"white","sku":"sticker-die-cut-6x4"},{"material":"clear","sku":"sticker-die-cut-clear-6x4"},{"material":"glitter","sku":"sticker-die-cut-glitter-6x4"},{"material":"holographic","sku":"sticker-die-cut-holographic-6x4"}],"shapes":["contour","rectangle","oval"],"finishes":["gloss","matte"]},{"sku":"sticker-die-cut-8x5","width_in":8,"height_in":5,"materials":[{"material":"white","sku":"sticker-die-cut-8x5"},{"material":"clear","sku":"sticker-die-cut-clear-8x5"},{"material":"glitter","sku":"sticker-die-cut-glitter-8x5"},{"material":"holographic","sku":"sticker-die-cut-holographic-8x5"}],"shapes":["contour","rectangle","oval"],"finishes":["gloss","matte"]}],"materials":["white","clear","glitter","holographic"],"shapes":["contour","circle","square","rectangle","oval"],"finishes":["glossy","matte"],"price_book":null,"plant_confirms_sku":true},{"product":"sticker-kiss-cut","name":"Kiss-Cut Sticker","family":"stickers","sku_template":"sticker-kiss-cut-{width}x{height}","example_sku":"sticker-kiss-cut-3x3","size":{"unit":"in","max_width_in":17,"max_height_in":17,"either_orientation":true,"min_area_sq_in":1.55,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":300,"refuse_below_dpi":225,"bleed_in":0.125},"sizes":[{"sku":"sticker-kiss-cut-2x2","width_in":2,"height_in":2,"materials":[{"material":"white","sku":"sticker-kiss-cut-2x2"},{"material":"clear","sku":"sticker-kiss-cut-clear-2x2"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-2x2"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-2x2"}],"shapes":[],"finishes":["gloss","matte"]},{"sku":"sticker-kiss-cut-2.5x2.5","width_in":2.5,"height_in":2.5,"materials":[{"material":"white","sku":"sticker-kiss-cut-2.5x2.5"},{"material":"clear","sku":"sticker-kiss-cut-clear-2.5x2.5"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-2.5x2.5"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-2.5x2.5"}],"shapes":[],"finishes":["gloss","matte"]},{"sku":"sticker-kiss-cut-3x3","width_in":3,"height_in":3,"materials":[{"material":"white","sku":"sticker-kiss-cut-3x3"},{"material":"clear","sku":"sticker-kiss-cut-clear-3x3"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-3x3"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-3x3"}],"shapes":[],"finishes":["gloss","matte"]},{"sku":"sticker-kiss-cut-4x4","width_in":4,"height_in":4,"materials":[{"material":"white","sku":"sticker-kiss-cut-4x4"},{"material":"clear","sku":"sticker-kiss-cut-clear-4x4"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-4x4"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-4x4"}],"shapes":[],"finishes":["gloss","matte"]},{"sku":"sticker-kiss-cut-5x5","width_in":5,"height_in":5,"materials":[{"material":"white","sku":"sticker-kiss-cut-5x5"},{"material":"clear","sku":"sticker-kiss-cut-clear-5x5"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-5x5"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-5x5"}],"shapes":[],"finishes":["gloss","matte"]},{"sku":"sticker-kiss-cut-6x6","width_in":6,"height_in":6,"materials":[{"material":"white","sku":"sticker-kiss-cut-6x6"},{"material":"clear","sku":"sticker-kiss-cut-clear-6x6"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-6x6"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-6x6"}],"shapes":[],"finishes":["gloss","matte"]},{"sku":"sticker-kiss-cut-8x8","width_in":8,"height_in":8,"materials":[{"material":"white","sku":"sticker-kiss-cut-8x8"},{"material":"clear","sku":"sticker-kiss-cut-clear-8x8"},{"material":"glitter","sku":"sticker-kiss-cut-glitter-8x8"},{"material":"holographic","sku":"sticker-kiss-cut-holographic-8x8"}],"shapes":[],"finishes":["gloss","matte"]}],"materials":["white","clear","glitter","holographic"],"shapes":[],"finishes":["glossy","matte"],"price_book":null,"plant_confirms_sku":true},{"product":"sticker-bumper","name":"Bumper Sticker","family":"stickers","sku_template":"sticker-bumper-{width}x{height}","example_sku":"sticker-bumper-11x3","size":{"unit":"in","max_width_in":15,"max_height_in":3.75,"either_orientation":true,"min_area_sq_in":null,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":300,"refuse_below_dpi":225,"bleed_in":0.125},"sizes":[{"sku":"sticker-bumper-11x3","width_in":11,"height_in":3,"materials":[{"material":"white","sku":"sticker-bumper-11x3"}],"shapes":[],"finishes":[]},{"sku":"sticker-bumper-15x3.75","width_in":15,"height_in":3.75,"materials":[{"material":"white","sku":"sticker-bumper-15x3.75"}],"shapes":[],"finishes":[]},{"sku":"sticker-bumper-7.5x3.75","width_in":7.5,"height_in":3.75,"materials":[{"material":"white","sku":"sticker-bumper-7.5x3.75"}],"shapes":[],"finishes":[]}],"materials":["white"],"shapes":[],"finishes":[],"price_book":null,"plant_confirms_sku":true},{"product":"sticker-sheet","name":"Sticker Sheet","family":"stickers","sku_template":"sticker-sheet-{width}x{height}","example_sku":"sticker-sheet-6x4","size":{"unit":"in","max_width_in":8.5,"max_height_in":11,"either_orientation":true,"min_area_sq_in":null,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":300,"refuse_below_dpi":225,"bleed_in":0.125},"sizes":[{"sku":"sticker-sheet-6x4","width_in":6,"height_in":4,"materials":[{"material":"white","sku":"sticker-sheet-6x4"},{"material":"holographic","sku":"sticker-sheet-holographic-6x4"},{"material":"clear","sku":"sticker-sheet-clear-6x4"}],"shapes":[],"finishes":[]},{"sku":"sticker-sheet-8.5x11","width_in":8.5,"height_in":11,"materials":[{"material":"white","sku":"sticker-sheet-8.5x11"},{"material":"holographic","sku":"sticker-sheet-holographic-8.5x11"},{"material":"clear","sku":"sticker-sheet-clear-8.5x11"}],"shapes":[],"finishes":[]}],"materials":["white","holographic","clear"],"shapes":[],"finishes":[],"price_book":null,"plant_confirms_sku":true},{"product":"decal-vinyl","name":"Vinyl Decal","family":"decals","sku_template":"decal-vinyl-{width}x{height}","example_sku":"decal-vinyl-3x3","size":{"unit":"in","max_width_in":5,"max_height_in":5,"either_orientation":true,"min_area_sq_in":1.55,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":150,"refuse_below_dpi":112.5,"bleed_in":0.25},"sizes":null,"materials":[],"shapes":[],"finishes":[],"price_book":{"base_cents":177,"cents_per_sq_in":10},"plant_confirms_sku":true},{"product":"decal-wall","name":"Wall Decal","family":"decals","sku_template":"wall-decal-{width}x{height}","example_sku":"wall-decal-24x36","size":{"unit":"in","max_width_in":96,"max_height_in":96,"either_orientation":true,"min_area_sq_in":80.17,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":150,"refuse_below_dpi":112.5,"bleed_in":0.25},"sizes":[{"sku":"wall-decal-2x10","width_in":2,"height_in":10,"materials":[{"material":"film","sku":"wall-decal-2x10"},{"material":"fabric","sku":"wall-decal-fabric-2x10"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-4x14","width_in":4,"height_in":14,"materials":[{"material":"film","sku":"wall-decal-4x14"},{"material":"fabric","sku":"wall-decal-fabric-4x14"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x12","width_in":6,"height_in":12,"materials":[{"material":"film","sku":"wall-decal-6x12"},{"material":"fabric","sku":"wall-decal-fabric-6x12"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x18","width_in":6,"height_in":18,"materials":[{"material":"film","sku":"wall-decal-6x18"},{"material":"fabric","sku":"wall-decal-fabric-6x18"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-8x8","width_in":8,"height_in":8,"materials":[{"material":"film","sku":"wall-decal-8x8"},{"material":"fabric","sku":"wall-decal-fabric-8x8"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-8x10","width_in":8,"height_in":10,"materials":[{"material":"film","sku":"wall-decal-8x10"},{"material":"fabric","sku":"wall-decal-fabric-8x10"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x10","width_in":10,"height_in":10,"materials":[{"material":"film","sku":"wall-decal-10x10"},{"material":"fabric","sku":"wall-decal-fabric-10x10"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x12","width_in":10,"height_in":12,"materials":[{"material":"film","sku":"wall-decal-10x12"},{"material":"fabric","sku":"wall-decal-fabric-10x12"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-11x14","width_in":11,"height_in":14,"materials":[{"material":"film","sku":"wall-decal-11x14"},{"material":"fabric","sku":"wall-decal-fabric-11x14"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x12","width_in":12,"height_in":12,"materials":[{"material":"film","sku":"wall-decal-12x12"},{"material":"fabric","sku":"wall-decal-fabric-12x12"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x24","width_in":6,"height_in":24,"materials":[{"material":"film","sku":"wall-decal-6x24"},{"material":"fabric","sku":"wall-decal-fabric-6x24"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x36","width_in":6,"height_in":36,"materials":[{"material":"film","sku":"wall-decal-6x36"},{"material":"fabric","sku":"wall-decal-fabric-6x36"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x18","width_in":10,"height_in":18,"materials":[{"material":"film","sku":"wall-decal-10x18"},{"material":"fabric","sku":"wall-decal-fabric-10x18"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x24","width_in":10,"height_in":24,"materials":[{"material":"film","sku":"wall-decal-10x24"},{"material":"fabric","sku":"wall-decal-fabric-10x24"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x18","width_in":12,"height_in":18,"materials":[{"material":"film","sku":"wall-decal-12x18"},{"material":"fabric","sku":"wall-decal-fabric-12x18"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x24","width_in":12,"height_in":24,"materials":[{"material":"film","sku":"wall-decal-12x24"},{"material":"fabric","sku":"wall-decal-fabric-12x24"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-16x16","width_in":16,"height_in":16,"materials":[{"material":"film","sku":"wall-decal-16x16"},{"material":"fabric","sku":"wall-decal-fabric-16x16"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-16x20","width_in":16,"height_in":20,"materials":[{"material":"film","sku":"wall-decal-16x20"},{"material":"fabric","sku":"wall-decal-fabric-16x20"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-18x24","width_in":18,"height_in":24,"materials":[{"material":"film","sku":"wall-decal-18x24"},{"material":"fabric","sku":"wall-decal-fabric-18x24"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-18x30","width_in":18,"height_in":30,"materials":[{"material":"film","sku":"wall-decal-18x30"},{"material":"fabric","sku":"wall-decal-fabric-18x30"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x48","width_in":6,"height_in":48,"materials":[{"material":"film","sku":"wall-decal-6x48"},{"material":"fabric","sku":"wall-decal-fabric-6x48"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x60","width_in":6,"height_in":60,"materials":[{"material":"film","sku":"wall-decal-6x60"},{"material":"fabric","sku":"wall-decal-fabric-6x60"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-6x72","width_in":6,"height_in":72,"materials":[{"material":"film","sku":"wall-decal-6x72"},{"material":"fabric","sku":"wall-decal-fabric-6x72"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x36","width_in":10,"height_in":36,"materials":[{"material":"film","sku":"wall-decal-10x36"},{"material":"fabric","sku":"wall-decal-fabric-10x36"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x48","width_in":10,"height_in":48,"materials":[{"material":"film","sku":"wall-decal-10x48"},{"material":"fabric","sku":"wall-decal-fabric-10x48"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x60","width_in":10,"height_in":60,"materials":[{"material":"film","sku":"wall-decal-10x60"},{"material":"fabric","sku":"wall-decal-fabric-10x60"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-10x72","width_in":10,"height_in":72,"materials":[{"material":"film","sku":"wall-decal-10x72"},{"material":"fabric","sku":"wall-decal-fabric-10x72"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x36","width_in":12,"height_in":36,"materials":[{"material":"film","sku":"wall-decal-12x36"},{"material":"fabric","sku":"wall-decal-fabric-12x36"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x48","width_in":12,"height_in":48,"materials":[{"material":"film","sku":"wall-decal-12x48"},{"material":"fabric","sku":"wall-decal-fabric-12x48"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x60","width_in":12,"height_in":60,"materials":[{"material":"film","sku":"wall-decal-12x60"},{"material":"fabric","sku":"wall-decal-fabric-12x60"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-12x72","width_in":12,"height_in":72,"materials":[{"material":"film","sku":"wall-decal-12x72"},{"material":"fabric","sku":"wall-decal-fabric-12x72"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-24x24","width_in":24,"height_in":24,"materials":[{"material":"film","sku":"wall-decal-24x24"},{"material":"fabric","sku":"wall-decal-fabric-24x24"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-24x36","width_in":24,"height_in":36,"materials":[{"material":"film","sku":"wall-decal-24x36"},{"material":"fabric","sku":"wall-decal-fabric-24x36"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-24x48","width_in":24,"height_in":48,"materials":[{"material":"film","sku":"wall-decal-24x48"},{"material":"fabric","sku":"wall-decal-fabric-24x48"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-30x60","width_in":30,"height_in":60,"materials":[{"material":"film","sku":"wall-decal-30x60"},{"material":"fabric","sku":"wall-decal-fabric-30x60"}],"shapes":[],"finishes":["matte"]},{"sku":"wall-decal-36x48","width_in":36,"height_in":48,"materials":[{"material":"film","sku":"wall-decal-36x48"},{"material":"fabric","sku":"wall-decal-fabric-36x48"}],"shapes":[],"finishes":["matte"]}],"materials":["film","fabric"],"shapes":[],"finishes":["matte"],"price_book":{"base_cents":0,"cents_per_sq_in":3},"plant_confirms_sku":true},{"product":"magnet","name":"Magnet","family":"magnets","sku_template":"magnet-{width}x{height}","example_sku":"magnet-3x3","size":{"unit":"in","max_width_in":18,"max_height_in":24,"either_orientation":true,"min_area_sq_in":6.55,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":300,"refuse_below_dpi":225,"bleed_in":0.125},"sizes":[{"sku":"magnet-2x2","width_in":2,"height_in":2,"materials":[],"shapes":["contour","circle","square"],"finishes":[]},{"sku":"magnet-3x3","width_in":3,"height_in":3,"materials":[],"shapes":["contour","circle","square"],"finishes":[]},{"sku":"magnet-4x4","width_in":4,"height_in":4,"materials":[],"shapes":["contour","circle","square"],"finishes":[]},{"sku":"magnet-5x5","width_in":5,"height_in":5,"materials":[],"shapes":["contour","circle","square"],"finishes":[]},{"sku":"magnet-6x6","width_in":6,"height_in":6,"materials":[],"shapes":["contour","circle","square"],"finishes":[]},{"sku":"magnet-2x3.5","width_in":2,"height_in":3.5,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-3x4","width_in":3,"height_in":4,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-4x6","width_in":4,"height_in":6,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-4x8","width_in":4,"height_in":8,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-8x8","width_in":8,"height_in":8,"materials":[],"shapes":["contour","circle","square"],"finishes":[]},{"sku":"magnet-8.5x11","width_in":8.5,"height_in":11,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-5.5x3.5","width_in":5.5,"height_in":3.5,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-2.7x6","width_in":2.7,"height_in":6,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-12x24","width_in":12,"height_in":24,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-10x20","width_in":10,"height_in":20,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]},{"sku":"magnet-18x24","width_in":18,"height_in":24,"materials":[],"shapes":["contour","oval","rectangle"],"finishes":[]}],"materials":[],"shapes":["contour","circle","square","oval","rectangle"],"finishes":[],"price_book":{"base_cents":130,"cents_per_sq_in":10},"plant_confirms_sku":true},{"product":"poster","name":"Poster","family":"posters","sku_template":"poster-{width}x{height}","example_sku":"poster-18x24","size":{"unit":"in","max_width_in":36,"max_height_in":54,"either_orientation":true,"min_area_sq_in":null,"step_in":null},"quantity":{"minimum":1,"maximum":10000},"artwork":{"min_dpi":150,"refuse_below_dpi":112.5,"bleed_in":0.125},"sizes":[{"sku":"poster-8x10","width_in":8,"height_in":10,"materials":[{"material":"paper","sku":"poster-8x10"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-8x12","width_in":8,"height_in":12,"materials":[{"material":"paper","sku":"poster-8x12"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-9x11","width_in":9,"height_in":11,"materials":[{"material":"paper","sku":"poster-9x11"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-10x10","width_in":10,"height_in":10,"materials":[{"material":"paper","sku":"poster-10x10"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-10x13","width_in":10,"height_in":13,"materials":[{"material":"paper","sku":"poster-10x13"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-12x12","width_in":12,"height_in":12,"materials":[{"material":"paper","sku":"poster-12x12"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-11x14","width_in":11,"height_in":14,"materials":[{"material":"paper","sku":"poster-11x14"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-8x20","width_in":8,"height_in":20,"materials":[{"material":"paper","sku":"poster-8x20"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-10x17","width_in":10,"height_in":17,"materials":[{"material":"paper","sku":"poster-10x17"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-11x16","width_in":11,"height_in":16,"materials":[{"material":"paper","sku":"poster-11x16"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-11x17","width_in":11,"height_in":17,"materials":[{"material":"paper","sku":"poster-11x17"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-12x16","width_in":12,"height_in":16,"materials":[{"material":"paper","sku":"poster-12x16"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-14x14","width_in":14,"height_in":14,"materials":[{"material":"paper","sku":"poster-14x14"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-10x20","width_in":10,"height_in":20,"materials":[{"material":"paper","sku":"poster-10x20"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-12x18","width_in":12,"height_in":18,"materials":[{"material":"paper","sku":"poster-12x18"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-10x24","width_in":10,"height_in":24,"materials":[{"material":"paper","sku":"poster-10x24"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-11x22","width_in":11,"height_in":22,"materials":[{"material":"paper","sku":"poster-11x22"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-13x19","width_in":13,"height_in":19,"materials":[{"material":"paper","sku":"poster-13x19"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-14x18","width_in":14,"height_in":18,"materials":[{"material":"paper","sku":"poster-14x18"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-16x16","width_in":16,"height_in":16,"materials":[{"material":"paper","sku":"poster-16x16"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-16x20","width_in":16,"height_in":20,"materials":[{"material":"paper","sku":"poster-16x20"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-14x23","width_in":14,"height_in":23,"materials":[{"material":"paper","sku":"poster-14x23"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-18x18","width_in":18,"height_in":18,"materials":[{"material":"paper","sku":"poster-18x18"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-17x20","width_in":17,"height_in":20,"materials":[{"material":"paper","sku":"poster-17x20"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-16x24","width_in":16,"height_in":24,"materials":[{"material":"paper","sku":"poster-16x24"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-20x20","width_in":20,"height_in":20,"materials":[{"material":"paper","sku":"poster-20x20"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-18x24","width_in":18,"height_in":24,"materials":[{"material":"paper","sku":"poster-18x24"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-15x30","width_in":15,"height_in":30,"materials":[{"material":"paper","sku":"poster-15x30"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-20x24","width_in":20,"height_in":24,"materials":[{"material":"paper","sku":"poster-20x24"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-23x23","width_in":23,"height_in":23,"materials":[{"material":"paper","sku":"poster-23x23"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-20x28","width_in":20,"height_in":28,"materials":[{"material":"paper","sku":"poster-20x28"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-24x24","width_in":24,"height_in":24,"materials":[{"material":"paper","sku":"poster-24x24"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-20x30","width_in":20,"height_in":30,"materials":[{"material":"paper","sku":"poster-20x30"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-22x28","width_in":22,"height_in":28,"materials":[{"material":"paper","sku":"poster-22x28"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-22x30","width_in":22,"height_in":30,"materials":[{"material":"paper","sku":"poster-22x30"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-22x34","width_in":22,"height_in":34,"materials":[{"material":"paper","sku":"poster-22x34"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-24x32","width_in":24,"height_in":32,"materials":[{"material":"paper","sku":"poster-24x32"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-28x28","width_in":28,"height_in":28,"materials":[{"material":"paper","sku":"poster-28x28"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-20x40","width_in":20,"height_in":40,"materials":[{"material":"paper","sku":"poster-20x40"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-24x36","width_in":24,"height_in":36,"materials":[{"material":"paper","sku":"poster-24x36"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-27x40","width_in":27,"height_in":40,"materials":[{"material":"paper","sku":"poster-27x40"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-28x40","width_in":28,"height_in":40,"materials":[{"material":"paper","sku":"poster-28x40"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-24x48","width_in":24,"height_in":48,"materials":[{"material":"paper","sku":"poster-24x48"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-30x40","width_in":30,"height_in":40,"materials":[{"material":"paper","sku":"poster-30x40"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-36x48","width_in":36,"height_in":48,"materials":[{"material":"paper","sku":"poster-36x48"}],"shapes":[],"finishes":["matte"]},{"sku":"poster-36x54","width_in":36,"height_in":54,"materials":[{"material":"paper","sku":"poster-36x54"}],"shapes":[],"finishes":["matte"]}],"materials":["paper"],"shapes":[],"finishes":["matte"],"price_book":{"base_cents":300,"cents_per_sq_in":2},"plant_confirms_sku":true}]},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["products:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["products:read"]}]}},"/orders/export":{"get":{"operationId":"exportOrders","summary":"Export orders as CSV","description":"The account's orders as a text/csv artifact, streamed newest first — the bookkeeping export. Every order in the window is a row, including cancelled and never-paid ones (visible with their status, never silently absent); money columns are CENTS — gross_cents is what the order charged before any refund, sales tax excluded; refunded_cents is what its charges have refunded, attributed to the order they refund; net_cents is gross_cents less refunded_cents, so a refund that also lowered the order's total (a quantity decrease, an item cancel, a shipping downgrade) is subtracted once — and counts_as_revenue carries the platform's own revenue basis so the sheet filters to exactly the number the dashboard reports. The ops columns ride beside the money ones: an items summary and tracking. The key's test/live side selects the rows, as on GET /v1/orders; an unknown status filter is refused rather than answered with an empty file. This endpoint serves the file itself rather than a JSON envelope — agents wanting rows use GET /v1/orders.","tags":["Orders"],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"},"description":"Inclusive lower bound on created_at."},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"},"description":"Exclusive upper bound."},{"name":"status","in":"query","schema":{"type":"string","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting","pending","processing","on-hold","completed","refunded","failed"]},"description":"One order status; unknown values are refused."}],"responses":{"200":{"description":"The CSV, streamed. Content-Disposition names the file.","content":{"text/csv":{"schema":{"type":"string"},"example":"\"order_id\",\"portal_order_id\",\"reference\",\"status\",\"test_mode\",\"provider\",\"created_at\",\"items\",\"gross_cents\",\"refunded_cents\",\"net_cents\",\"counts_as_revenue\",\"tracking_number\",\"tracking_url\"\n\"test_ord__w1OSQwtOy5IVUF4Z7Zx0gyn\",\"ord__w1OSQwtOy5IVUF4Z7Zx0gyn\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:19.000Z\",\"sticker-die-cut-3x3 x50\",\"4399\",\"0\",\"4399\",\"false\",\"\",\"\"\n\"test_ord_8zJ_6hfCTgPoJN4qw3R7zXKn\",\"ord_8zJ_6hfCTgPoJN4qw3R7zXKn\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:19.000Z\",\"sticker-die-cut-3x3 x50\",\"4399\",\"0\",\"4399\",\"false\",\"\",\"\"\n\"test_ord_qbpi4ALIbckcSICFfIPmmOKl\",\"ord_qbpi4ALIbckcSICFfIPmmOKl\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:18.000Z\",\"sticker-die-cut-3x3 x100\",\"7899\",\"0\",\"7899\",\"false\",\"\",\"\"\n\"test_ord_JyZLq7sUxTZxc7qjzI76ZQYU\",\"ord_JyZLq7sUxTZxc7qjzI76ZQYU\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:18.000Z\",\"sticker-die-cut-3x3 x100\",\"7899\",\"0\",\"7899\",\"false\",\"\",\"\"\n\"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk\",\"ord_wVK9ZiG-VYiPrlSeBUBmrUVk\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:10.000Z\",\"sticker-die-cut-3x3 x100\",\"8399\",\"0\",\"8399\",\"false\",\"\",\"\"\n"}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["orders:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:read"]}]}},"/orders/bulk":{"post":{"operationId":"createBulkOrders","summary":"Create bulk orders","description":"Submit up to 50 orders in a single API call. Each order is processed independently — partial success is possible. Returns per-order results. An order may carry a quote_token from POST /orders/quote and is then charged that quote's locked total instead of being re-priced; the honored quote_id comes back on the result. A token that is expired, issued to another key, or does not match the order's line items and shipping method fails that one order, not the batch. With a test key the whole batch is validated exactly as a live batch and every order is persisted as a real sandbox order (no charge, not sent to production), so the returned ids work with GET /order/{id} and run the time-compressed lifecycle. An order whose `reference` another order already carries — placed earlier, or another entry of this batch — fails that one order, uncharged, with an error starting `duplicate_reference:` that names the order holding it, as POST /order refuses it with 409; `allow_duplicate_reference: true` on the entry places it anyway. An entry whose reference could not be checked fails alone, uncharged, with an error starting `reference_check_unavailable:`.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["orders"],"properties":{"orders":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/CreateOrderRequest"}}}},"example":{"orders":[{"items":[{"sku":"sticker-die-cut-3x3","quantity":100,"artwork_url":"https://printedsimply.com/samples/sticker-3x3.png"}],"address_to":{"name":"Jane Doe","address1":"123 Main St","city":"Austin","state":"TX","zip":"78701","country":"US"},"shipping":{"method":"standard"}},{"items":[{"sku":"sticker-die-cut-3x3","quantity":100,"artwork_url":"https://printedsimply.com/samples/sticker-3x3.png"}],"address_to":{"name":"Sam Lee","address1":"420 Pine St","city":"Seattle","state":"WA","zip":"98101","country":"US"},"shipping":{"method":"standard"}}]}}}},"responses":{"200":{"description":"Bulk order results — one row per entry, partial success inside a 200. An entry our plant gave no answer about (the request did not reach it, timed out, or came back 5xx — POST /order's 503 provider_unavailable with `details.reason: plant_unreachable`) fails alone, uncharged, with an error starting `plant_unreachable:`, and the 200 carries `Retry-After`; a test key's entry is recorded at $0 instead, as on POST /order. Place the failed entries again in a new request: a replay of this one under the same Idempotency-Key returns these same rows.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Sent when at least one row failed with an error starting `plant_unreachable:` (our plant gave no answer about a SKU): seconds to wait before placing those rows again, the catalog breaker's own cooldown."}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkOrdersResponse"}}}]},"example":{"status":"success","data":{"total":2,"succeeded":2,"failed":0,"results":[{"index":0,"status":"success","order_id":"test_ord_qbpi4ALIbckcSICFfIPmmOKl","portal_order_id":"ord_qbpi4ALIbckcSICFfIPmmOKl","provider":"merchfarm","charged":"$0.00","tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfckVOcGFBcXFhY3ZWWUZubVdZcGhYUGdiIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDoxOC4wMDBaIn0.1ff490e6f8eebee240ec6a31f301d2bb46d857291a20c15e906488dc38e80ed8","test_mode":true,"artwork_check":{"lines":[{"item_index":0,"sku":"sticker-die-cut-3x3","status":"passed","width_px":1200,"height_px":1200,"findings":[],"print_size_in":[3,3],"effective_dpi":400,"min_dpi":300}],"would_refuse":false},"estimated_delivery":{"earliest":"2026-10-23","latest":"2026-10-29","ships_by":"2026-10-19","is_estimate":true},"address_check":{"valid":true,"checked":true,"normalized":{"name":"Jane Doe","address1":"123 MAIN ST","city":"AUSTIN","state":"TX","zip":"78701","country":"US"},"findings":[]},"error_code":null},{"index":1,"status":"success","order_id":"test_ord_JyZLq7sUxTZxc7qjzI76ZQYU","portal_order_id":"ord_JyZLq7sUxTZxc7qjzI76ZQYU","provider":"merchfarm","charged":"$0.00","tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfaklVQXZFTTl6UXZMank4d3lWUmZwOUpvIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDoxOC4wMDBaIn0.c342585c817ecadf771a6b3c3a8c0b0af13404c28dd1bca7129bd38c102ec43f","test_mode":true,"artwork_check":{"lines":[{"item_index":0,"sku":"sticker-die-cut-3x3","status":"passed","width_px":1200,"height_px":1200,"findings":[],"print_size_in":[3,3],"effective_dpi":400,"min_dpi":300}],"would_refuse":false},"estimated_delivery":{"earliest":"2026-10-23","latest":"2026-10-29","ships_by":"2026-10-19","is_estimate":true},"address_check":{"valid":true,"checked":true,"normalized":{"name":"Sam Lee","address1":"420 PINE ST","city":"SEATTLE","state":"WA","zip":"98101","country":"US"},"findings":[]},"error_code":null}]},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"402":{"$ref":"#/components/responses/PaymentError"},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key. The whole batch is refused before any row is placed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"description":"variable_data_invalid — an entry's `items[].variable_data` failed the preflight; the whole batch is refused before any row is placed or charged, and each of `details.problems[]` carries the entry's `order_index` beside its item, unit and slot. variable_data_unavailable — a live batch carries variable data while production is switched off; refused whole, nothing charged. artwork_not_printable — a live batch has a line made at our own plant whose artwork cannot be printed as it is (unreachable, not_an_image, low_resolution); the whole batch is refused before any row is placed or charged, each of `details.problems[]` names the line by `order_index` and `item_index`, and `details.artwork_checks[]` carries every checked order's verdicts. artwork_required — a live batch has a line made at our own plant with no artwork at all (no artwork_url, no print_file); the whole batch is refused before any file is read or any row is placed or charged, and `details.items[]` names each line by `order_index`, `item_index` and `sku`. A test key is never refused for artwork; each row's `artwork_check.would_refuse` says what a live batch would do, and `artwork_check.missing_artwork` lists a row's lines with none. See POST /order's 422. sku_not_available or provider_not_available — an entry names a product our own plant does not make, or a lane other than our plant: the whole batch is refused before anything is priced or charged, and `details.errors[]` names each refused entry by `index` with its `code` and its `skus`, or its `provider` and `available`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"},"503":{"$ref":"#/components/responses/IdempotencyUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/shipping/estimate":{"post":{"operationId":"estimateShipping","summary":"Estimate shipping costs","description":"Calculate shipping options with costs and estimated delivery dates for a set of items to a destination. Does not create an order. Each option's `cost_cents` is the figure the order path will charge for that method: shipping is charged per parcel, never per item, at the class the items' estimated weight puts the parcel in (`shipping_class`). Standard shipping uses the configured light-parcel classes and published whole-pound weight rows above five pounds, with the applicable tube addition. The existing single-sticker cap and exact flag/hat fulfillment benchmarks are separate policies. There is no customer peak surcharge. Sized own-plant baskets above the estimated70lb single-parcel limit are refused before ordering; no multi-parcel plan is implied. Weight is an estimate and does not establish measured packed mass or dimensional-weight eligibility. Items made at the partner facility (apparel, mugs) ship at the larger of that class rate and the partner's own estimate for the parcel, and the option quotes that same figure: the partner's answer for a basket is reused for five minutes, so an order placed in that window is charged what this quoted. The returned shipping cost carries no platform fee and is the same on every plan, as the order path charges it. `shipping_address` decides which destination the estimate is echoed for; `address_to`, the name every order operation gives the destination, is accepted in its place, so the address you will order to can be sent as it is. One of the two is required, and both is a 400 validation_error when they name different destinations. Our own facilities ship to US addresses only, so a country other than US is refused 422 destination_unavailable. `options` lists only the methods on offer: standard always, priority and express once they are priced.","tags":["Shipping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"product_id":{"type":"integer","description":"Catalog product ID"},"quantity":{"type":"integer","minimum":1},"size":{"type":"string","description":"Product size (e.g. S, M, L, XL)"},"color":{"type":"string","description":"Product color"}}}},"shipping_address":{"type":"object","description":"Where the parcel goes. Only `country` decides the price (the rate depends on the method and the parcel's weight class, not the zone), so a country-only address is a complete request; `zip` is echoed back in `destination` when given. `state` and `zip` also decide which carrier advisories touch the destination (`delivery_risk`, `deliver_by_check.status`): a destination given by country alone is touched by every one. Required unless `address_to` is sent.","properties":{"state":{"type":"string"},"zip":{"type":"string","minLength":1,"description":"Optional. Echoed back as `destination.zip`; null in the response when omitted."},"country":{"type":"string","default":"US"}}},"address_to":{"type":"object","description":"The same destination under the name POST /order gives it, so the order's own `address_to` can be sent as it is: only `state`, `zip` and `country` are read, and the rest is ignored. Send this or `shipping_address`; both are accepted only when every field they both give agrees (the country defaults to US), and otherwise refused 400 validation_error.","properties":{"state":{"type":"string"},"zip":{"type":"string","minLength":1,"description":"Optional. Echoed back as `destination.zip`; null in the response when omitted."},"country":{"type":"string","default":"US"}}},"deliver_by":{"type":"string","format":"date","description":"A day the order should arrive by, YYYY-MM-DD (UTC). Answered in `deliver_by_check`, judged against the estimate's latest day rather than its earliest, from the facility's production estimate and the method's transit window. Optional; a day before today, or anything but a real date, is 400 validation_error."}}},"example":{"items":[{"sku":"sticker-die-cut-3x3","quantity":100}],"shipping_address":{"zip":"78701","country":"US"}}}}},"responses":{"200":{"description":"Every method, priced per PARCEL rather than per item, with business-day windows.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ShippingEstimate"}}}]},"example":{"status":"success","data":{"destination":{"zip":"78701","country":"US"},"total_items":100,"shipping_class":{"name":"parcel_1lb","weight_oz":12.6},"options":[{"method":"standard","label":"Standard Shipping","cost":"$6.50","cost_cents":650,"estimated_delivery":{"earliest":"2026-10-20","latest":"2026-10-26","ships_by":"2026-10-14","is_estimate":true,"transit_days":"4-8 business days"},"surcharges":[]}]},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"$ref":"#/components/responses/ShippingEstimateRefused"},"429":{"$ref":"#/components/responses/RateLimitError"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"x-required-scopes":["pricing:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["pricing:read"]}]}},"/calendar":{"get":{"operationId":"getCalendar","summary":"List the days no delivery estimate counts","description":"The weekdays every `estimated_delivery` skips, for the twelve months from today: the federal holidays of 5 U.S.C. 6103 (each on the weekday it is observed: a Saturday holiday on the Friday before, a Sunday one on the Monday after) and the plant's own closure days, each with its date, kind, reason and source. Saturdays and Sundays are never counted either (`weekly`). One bounded document, not a page: `from` is today (UTC) and `through` the day before the same date next year, or the last day the table covers when that is sooner. These are the days POST /orders/quote and POST /shipping/estimate skip when they date an order, so `estimated_delivery` and `deliver_by_check` already account for them; read this to show a buyer why a date moved, or to plan around a closure. Buying for a date, read this first. `order_by` answers the last day to order: for each parcel method on offer and each of Christmas Eve, Christmas Day, New Year's Eve (each in the next year it falls on or after today) and the next federal holiday, the last UTC day an order of our own plant's goods is estimated to arrive by that day, computed by the same estimate POST /orders/quote dates an order with. Each day is dated for the goods the plant takes longest to make (wall decals, 3 production days), so a quote of any of our own plant's goods placed that day answers `meets: true`. Goods made sooner can often be ordered later, even once a day here reads null: send `deliver_by` on POST /orders/quote, as for any other day, and read `deliver_by_check.latest_order_day` for the goods quoted. `peak` is the carrier's peak postage window as the cost book holds it: its dates, the most it adds to one parcel's rate in cents (bare), the methods it can apply to, the carrier's zones and weight, and the carrier's announcement; null once the window has ended. PrintedSimply absorbs this carrier cost; it is not added as a customer surcharge. `advisories` lists every carrier advisory not yet over: a delay a carrier posted, as the plant entered it from the carrier's notice, with the carrier, the states and ZIP prefixes it touches (neither means every US destination), its first and last day, the carrier's estimate of the business days it adds, one sentence and the notice itself. A quote or shipping estimate into a destination one touches reports it in `delivery_risk`, and its `deliver_by_check` answers `at_risk` rather than `meets`. Requires an API key and no scope: it describes the platform, the same for every caller. Read-only.","tags":["Shipping"],"responses":{"200":{"description":"The non-operating days from today for twelve months","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PlantCalendar"}}}]},"example":{"status":"success","data":{"from":"2026-10-15","through":"2027-10-14","timezone":"UTC","weekly":["saturday","sunday"],"days":[{"date":"2026-11-11","kind":"federal_holiday","reason":"Veterans Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2026-11-26","kind":"federal_holiday","reason":"Thanksgiving Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2026-12-25","kind":"federal_holiday","reason":"Christmas Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-01-01","kind":"federal_holiday","reason":"New Year's Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-01-18","kind":"federal_holiday","reason":"Birthday of Martin Luther King, Jr.","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-02-15","kind":"federal_holiday","reason":"Washington's Birthday","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-05-31","kind":"federal_holiday","reason":"Memorial Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-06-18","kind":"federal_holiday","reason":"Juneteenth National Independence Day (observed)","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-07-05","kind":"federal_holiday","reason":"Independence Day (observed)","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-09-06","kind":"federal_holiday","reason":"Labor Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"},{"date":"2027-10-11","kind":"federal_holiday","reason":"Columbus Day","source":"https://www.law.cornell.edu/uscode/text/5/6103"}],"order_by":[{"deliver_by":"2026-11-11","occasion":"Veterans Day","method":"standard","latest_order_day":"2026-10-26","is_estimate":true},{"deliver_by":"2026-12-24","occasion":"Christmas Eve","method":"standard","latest_order_day":"2026-12-09","is_estimate":true},{"deliver_by":"2026-12-25","occasion":"Christmas Day","method":"standard","latest_order_day":"2026-12-09","is_estimate":true},{"deliver_by":"2026-12-31","occasion":"New Year's Eve","method":"standard","latest_order_day":"2026-12-15","is_estimate":true}],"peak":{"from":"2026-10-04","through":"2027-01-16","in_window":true,"cents":55,"methods":["standard"],"carrier_zones":{"from":5,"through":9},"carrier_max_weight_lb":3,"source":"https://about.usps.com/newsroom/national-releases/2026/0825-usps-announces-temporary-price-change-for-2026-holiday-shipping-season.pdf"},"advisories":[]},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":[],"security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}]}},"/addresses/validate":{"post":{"operationId":"validateAddress","summary":"Check a shipping address","description":"Check a US shipping address before quoting or ordering against it. Creates no order or shipping label. Local checks cover the ZIP's shape (five digits or ZIP+4), the state against the USPS state, territory and military codes (a state's full name is accepted), and whether the ZIP's first three digits belong to that state, and flag a PO Box or military (APO/FPO/DPO) address. `valid` is false when any finding is an `error` — `zip_format`, `state_unknown`, or `zip_state_mismatch` (the ZIP belongs to a state that does not border the one named) — and POST /order and POST /orders/quote refuse such an address with a live key (422 `address_invalid`) and return the same findings as `address_check`. A ZIP from a neighbouring state is a `warning` (`zip_state_border`): a few ZIPs straddle a state line. A state written in a way the check does not know (it takes the codes, the full names, the usual abbreviations such as Calif. and W.Va., and names such as Washington, D.C. or USVI) is `state_unknown`, an `error` only when the ZIP does not settle it: when the ZIP's first three digits belong to one state it is a `warning` and `normalized.state` carries that state's code. `normalized` is the address in USPS style: upper case, no punctuation, ZIP+4 hyphenated, a unit typed into address1 moved to address2. Any country other than US is passed through with `checked: false`. Live keys also use the configured USPS or ShipStation street-verification service, as the quote and final order do. Definite invalid streets make `valid: false`; unit or partial findings remain warnings. Test keys make no external call. Provider outages preserve the local checks and never produce a confirmed finding.","tags":["Shipping"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressValidationRequest"},"example":{"address1":"123 Main St. Apt 4","city":"Austin","state":"Texas","zip":"787011234","country":"US"}}}},"responses":{"200":{"description":"The verdict, whatever it is: `valid: false` is an answer, not an error.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AddressCheck"}}}]},"example":{"status":"success","data":{"valid":true,"checked":true,"normalized":{"address1":"123 MAIN ST","city":"AUSTIN","state":"TX","zip":"78701-1234","country":"US","address2":"APT 4"},"findings":[{"code":"address2_split","severity":"info","field":"address2","message":"Moved \"APT 4\" from address1 to address2."}]},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"x-required-scopes":["pricing:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["pricing:read"]}]}},"/artwork/validate":{"post":{"operationId":"validateArtwork","summary":"Validate artwork file","description":"The order's own artwork check, before the order. Reads the file (fetched server-side through the same SSRF-guarded, size-capped reader POST /v1/order uses; stores nothing unless `auto_fix` is sent) and grades it with the order's grader against `sku`: `findings` carries the order's codes (low_resolution, below_recommended_resolution, cmyk, no_transparency, touches_edge, no_bleed, content_in_bleed, no_cutline, unreachable, not_an_image, …), the bleed ones measured from the file's own pixels at the SKU's print size, `effective_dpi` and `print_size_in` are what the order measures, and `would_refuse` says whether a live POST /v1/order of this SKU with this file would be refused 422 artwork_not_printable. A test key gets the same verdict. Only when the file cannot be read just now (a timeout, a 5xx, a host the guard will not fetch) is the answer built from what you declared (`width_px`, `height_px`, `dpi`, the URL's extension), with `verified: false` and `status: unverified` — the order is not refused for an unread file either. Name the file as exactly one of `file_url` or `asset_id`, as an order line names it. Send neither `sku` nor `product_type` and the one read of the file is graded at a fixed set of sizes in each family (stickers, decals, magnets, posters), from a small size up to the largest an order of it takes (114 today, the bound on the list): `fits` holds, for each, the `status`, `effective_dpi` and `would_refuse` the call naming that `sku` answers, and `largest_passing` names, per family, the largest of those sizes whose `status` is `passed`. Without a `sku`, the top-level `valid`, `status` and `would_refuse` are measured at no size: read `fits`.","tags":["Artwork"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Exactly one of `file_url` or `asset_id` (a 400 otherwise).","anyOf":[{"required":["file_url"]},{"required":["asset_id"]}],"properties":{"file_url":{"type":"string","format":"uri","description":"URL of the artwork file to validate: an order line's `artwork_url`. Send this or `asset_id`."},"asset_id":{"type":"string","pattern":"^ast_[A-Za-z0-9_-]+$","description":"An asset you uploaded (POST /v1/assets), as an order line's `print_file` names it. Send this or `file_url`; an id that is not yours is a 400, as on the order."},"sku":{"type":"string","minLength":1,"maxLength":200,"description":"The SKU the file is for (e.g. sticker-die-cut-3x3): its print size and format are what the file is graded against, as the order grades its line. Without it, `product_type` is read as one."},"size":{"type":"string","minLength":1,"maxLength":100,"description":"The line's free-text size (e.g. 4 x 2 in), read for a print size when the SKU names none, as on an order line."},"product_type":{"type":"string","minLength":1,"description":"Product type for spec matching (e.g. 'sticker-die-cut', 'decal-vinyl', 'poster'), or a SKU. Read as the SKU when `sku` is not sent."},"width_px":{"type":"integer","minimum":1,"description":"Image width in pixels. Read only when the file cannot be: a file that is read is measured."},"height_px":{"type":"integer","minimum":1,"description":"Image height in pixels. Read only when the file cannot be."},"dpi":{"type":"number","exclusiveMinimum":0,"description":"Image resolution in dots per inch. Any positive number. A read file's resolution is measured at the SKU's print size; this is graded instead when the file cannot be read, or when neither the SKU nor `size` names a print size (a product type such as `poster`), against that product's minimum."},"file_size_bytes":{"type":"integer","minimum":1,"description":"File size in bytes"},"auto_fix":{"type":"boolean","description":"Fix what the bleed findings name and store the result as a new asset on your account: a rectangle with `no_bleed` gets its edge pixels extended past the trim by the product's bleed; a die-cut with `content_in_bleed` or `touches_edge` gets a wider transparent canvas, and one with `no_cutline` (separate shapes) a white border that joins them. The fixed file is graded again and `auto_fix` in the response says what changed and its verdict; order it by the new asset id. Your file is never changed. Needs `sku`, a file that could be read whole, and the `assets:write` scope as well as `artwork:write` (a 403 insufficient_scope otherwise). Default false: nothing is stored."}}},"example":{"file_url":"https://example.com/logo.png","sku":"sticker-die-cut-3x3"}}}},"responses":{"200":{"description":"Artwork validation result","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ArtworkValidation"}}}]},"example":{"status":"success","data":{"valid":true,"file_url":"https://example.com/logo.png","product_type":null,"resolved_product_type":"sticker-die-cut","product_specs":{"name":"Die-Cut Sticker","min_dpi":300,"max_size":"17\" x 17\"","bleed":"0.125\"","formats":["png","pdf","svg"]},"checks":[{"name":"file_format","status":"pass","message":"PNG, 1200×1200 px, read from the file."},{"name":"resolution","status":"pass","message":"400 DPI at 3×3 in, at or above the 300 recommended."},{"name":"color_space","status":"pass","message":"SRGB, the colours previews and mockups show."},{"name":"cut_line","status":"pass","message":"Transparent background with a margin around the design: the cut follows its shape."}],"warnings":[],"suggestions":["Leave 0.125\" of bleed on every side; the file check cannot see it."],"sku":"sticker-die-cut-3x3","asset_id":null,"verified":true,"status":"passed","would_refuse":false,"findings":[],"width_px":1200,"height_px":1200,"print_size_in":[3,3],"effective_dpi":400,"min_dpi":300,"fits":null,"largest_passing":null},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"403":{"description":"insufficient_scope — `auto_fix: true` on a key whose scopes leave out `assets:write`: the fixed file would be stored as an asset. Nothing was read or stored; validate without `auto_fix`, or grant the scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"x-required-scopes":["artwork:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["artwork:write"]}]}},"/orders/multi-ship":{"post":{"operationId":"createMultiShipOrder","summary":"Create multi-address order","description":"Place an order whose items ship to several addresses. Give the full item list once, then a shipment per destination naming the item indices that go there — every item in exactly one shipment. Each shipment is placed as its own order through the same path POST /orders/bulk uses (priced, charged, routed, and tracked independently, with quote honoring, spend caps, fraud gating, and webhooks identical to a batch), and the response groups them under group_id with order_ids and one row per shipment: shipment_id, index, status, order_id, portal_order_id, tracking_url, charged, error, address, items. Partial success is a 200: a shipment the platform refuses fails its own row and charges nothing while the others proceed; on a live key that includes a shipment whose address cannot be right as written (POST /addresses/validate's check), whose error starts `address_invalid:`. A test key places real sandbox orders that run the time-compressed lifecycle; a live key charges and routes to production — the same code path in both modes. At most 50 shipments per call. Counts as one request, against your rate limit and in your usage.","tags":["Orders"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items","shipments"],"properties":{"items":{"maxItems":100,"type":"array","minItems":1,"items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"product_id":{"type":"integer","minimum":1,"description":"Catalog product ID"},"quantity":{"type":"integer","maximum":10000,"minimum":1},"size":{"type":"string","description":"Product size (e.g. S, M, L, XL)"},"color":{"type":"string","description":"Product color"},"design_name":{"type":"string","description":"Design/artwork name"},"artwork_url":{"type":"string","format":"uri","description":"URL to artwork file"},"print_file":{"type":"string","description":"URL or asset ID (ast_*)"},"notes":{"type":"string","description":"Item-level notes"},"artwork":{"$ref":"#/components/schemas/ArtworkDeclaration"}}}},"shipments":{"type":"array","minItems":1,"items":{"type":"object","required":["address","item_indices"],"properties":{"address":{"$ref":"#/components/schemas/Address"},"item_indices":{"type":"array","minItems":1,"items":{"type":"integer","minimum":0},"description":"Indices into the items array for this shipment"}}}}}},"example":{"items":[{"sku":"sticker-die-cut-3x3","quantity":50,"artwork_url":"https://example.com/logo.png"},{"sku":"sticker-die-cut-3x3","quantity":50,"artwork_url":"https://example.com/logo.png"}],"shipments":[{"address":{"name":"Jane Doe","address1":"123 Main St","city":"Austin","state":"TX","zip":"78701","country":"US"},"item_indices":[0]},{"address":{"name":"Sam Lee","address1":"420 Pine St","city":"Seattle","state":"WA","zip":"98101","country":"US"},"item_indices":[1]}]}}}},"responses":{"200":{"description":"Multi-ship group placed — one row per shipment, partial success inside a 200. A shipment our plant gave no answer about fails alone, uncharged, with POST /v1/orders/bulk's error starting `plant_unreachable:`, and the 200 carries `Retry-After`.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Sent when at least one shipment failed with an error starting `plant_unreachable:` (our plant gave no answer about a SKU): seconds to wait before placing those shipments again, the catalog breaker's own cooldown."}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MultiShipResponse"}}}]},"example":{"status":"success","data":{"group_id":"msh_4uWll5So4Wdjsd0Duh7TQtBN","order_ids":["test_ord_8zJ_6hfCTgPoJN4qw3R7zXKn","test_ord__w1OSQwtOy5IVUF4Z7Zx0gyn"],"shipments":[{"shipment_id":"shp_IBpsk1RQj3kzZYkcBgSLl2Zo","index":0,"status":"success","order_id":"test_ord_8zJ_6hfCTgPoJN4qw3R7zXKn","portal_order_id":"ord_8zJ_6hfCTgPoJN4qw3R7zXKn","tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfbTVNUUN4eXlyWWVXMDdXN1Q2RWh2WXc4IiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDoxOS4wMDBaIn0.68435d27ec0cb6ce5420fe82993e831b58dadf2b86100d464195bf6c8489e93e","charged":"$0.00","error":null,"error_code":null,"address_check":{"valid":true,"checked":true,"normalized":{"name":"Jane Doe","address1":"123 MAIN ST","city":"AUSTIN","state":"TX","zip":"78701","country":"US"},"findings":[]},"estimated_delivery":{"earliest":"2026-10-23","latest":"2026-10-29","ships_by":"2026-10-19","is_estimate":true},"address":{"name":"Jane Doe","address1":"123 Main St","city":"Austin","state":"TX","zip":"78701","country":"US"},"items":[{"sku":"sticker-die-cut-3x3","artwork_url":"https://example.com/logo.png","quantity":50}]},{"shipment_id":"shp_K44HhNfmUG5JUKJz2R9iXbbk","index":1,"status":"success","order_id":"test_ord__w1OSQwtOy5IVUF4Z7Zx0gyn","portal_order_id":"ord__w1OSQwtOy5IVUF4Z7Zx0gyn","tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfVzVWdDk4NTlUc3JvZWt1QkptRm5tYnAyIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDoxOS4wMDBaIn0.01b69cca187803b0988f73da9cfc9548469566673537466b5a1360a35fed516c","charged":"$0.00","error":null,"error_code":null,"address_check":{"valid":true,"checked":true,"normalized":{"name":"Sam Lee","address1":"420 PINE ST","city":"SEATTLE","state":"WA","zip":"98101","country":"US"},"findings":[]},"estimated_delivery":{"earliest":"2026-10-23","latest":"2026-10-29","ships_by":"2026-10-19","is_estimate":true},"address":{"name":"Sam Lee","address1":"420 Pine St","city":"Seattle","state":"WA","zip":"98101","country":"US"},"items":[{"sku":"sticker-die-cut-3x3","artwork_url":"https://example.com/logo.png","quantity":50}]}],"total_items":2,"total_shipments":2,"succeeded":2,"failed":0,"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"402":{"$ref":"#/components/responses/PaymentError"},"403":{"description":"sandbox_only — the account is restricted to sandbox mode and the request used a live key. Every shipment is placed through POST /v1/orders/bulk, which refuses the whole batch before any shipment is placed, and that refusal is answered here unchanged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"description":"variable_data_invalid or variable_data_unavailable — an item's `variable_data` was refused by POST /v1/orders/bulk, which answers before any shipment is placed or charged; the refusal is answered here unchanged, and each problem's `order_index` is the shipment's position in `shipments`. sku_not_available — an item is a product our own plant does not make; refused before any shipment is placed or charged, and `details.skus` names each one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"429":{"$ref":"#/components/responses/RateLimitError"},"502":{"description":"internal_error — the placement answered without per-shipment results, so this response cannot say which shipments were placed. Check GET /v1/orders before retrying.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"503":{"$ref":"#/components/responses/IdempotencyUnavailable"}},"x-required-scopes":["orders:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["orders:write"]}]}},"/pricing/tiers":{"get":{"operationId":"getPricingTiers","summary":"Get plan tiers","description":"The plan catalog and the plan your account is on. No plan is gated on monthly order volume — there are no volume discounts on monthly order counts, and min_orders/max_orders are null for that reason. Our own plant's products priced by our price book carry quantity breaks (vinyl decals up to 5×5 in 40% off from 10, 61% off from 25, 73% off from 50, 76% off from 100; magnets 15% off from 10, 30% off from 50, 40% off from 100; posters 8% off from 5, 15% off from 10, 18% off from 25; wall decals 8% off from 5, 15% off from 10), on the order's whole quantity of a SKU and after any plan discount; a negotiated price takes none. Our die-cut and kiss-cut stickers are priced by their own sheet instead, the same on every plan: a pack total at 1, 5, 10, 25, 50, 100, 250, 500, 1,000 and 2,500 units (each row's quantity_breaks, with total_cents), the total running in a straight line between two of those (price_interpolation linear_total), the 2,500-unit rate held above 2,500, and never below the size's unit floor, which holds the largest sizes at volume (min_unit_cents). Our wall decals are priced by their own sheet too, the same on every plan: one price each, at any quantity (each row's one quantity_breaks entry, price_interpolation none); the wall decals' breaks above are for a size the sheet does not list. Our posters are priced by their own price list too, the same on every plan: one price each, at any quantity (each row's one quantity_breaks entry, price_interpolation none); the posters' breaks above are for a size the list does not hold. Our magnets are priced by their own price list, the same on every plan: a total at every quantity, published at 1, 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000 and 10,000 units (each row's quantity_breaks, with total_cents; price_interpolation formula, so between two of those the quote gives the figure). The total never falls as the quantity grows. The bulk discount continues at every quantity; rounding can move the unit price slightly; the magnets' breaks above are for a size the list does not include. No plan carries a platform fee: platform_fee_pct is 0 on every plan since 2026-10-17, as is pricing_model.platform_fee_pct, the rate your account pays today. catalog_discount_pct is the percentage the plan takes off what our own plant makes other than die-cut and kiss-cut stickers and the wall decals, posters and magnets our catalog lists; no plan sold on this portal takes one. current.entitled_catalog_discount_pct is what your account may take today. Tiers with offered: false are not sold on this portal; accounts already on them keep them. The response is account-specific and served Cache-Control: private.","tags":["Pricing"],"responses":{"200":{"description":"Every plan, plus the one this key's account is on. Account-specific, so `Cache-Control: private` — a shared cache must not hold it. The example lists the plans sold here; an answer also lists, with `offered: false`, each plan an account may already be on.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PricingTiersResponse"}}}]},"example":{"status":"success","data":{"note":"Order pricing is the published price for products our facilities make and their shipping, with no platform fee. No card-processing or seasonal peak fee is added. No plan is gated on monthly order volume — there are no volume discounts on monthly order counts, and min_orders/max_orders are null for that reason. Our own plant's products priced by our price book carry quantity breaks (vinyl decals up to 5×5 in 40% off from 10, 61% off from 25, 73% off from 50, 76% off from 100; magnets 15% off from 10, 30% off from 50, 40% off from 100; posters 8% off from 5, 15% off from 10, 18% off from 25; wall decals 8% off from 5, 15% off from 10), on the order's whole quantity of a SKU and after any plan discount; a negotiated price takes none. Our die-cut and kiss-cut stickers are priced by their own sheet instead, the same on every plan: a pack total at 1, 5, 10, 25, 50, 100, 250, 500, 1,000 and 2,500 units (each row's quantity_breaks, with total_cents), the total running in a straight line between two of those (price_interpolation linear_total), the 2,500-unit rate held above 2,500, and never below the size's unit floor, which holds the largest sizes at volume (min_unit_cents). Our wall decals are priced by their own sheet too, the same on every plan: one price each, at any quantity (each row's one quantity_breaks entry, price_interpolation none); the wall decals' breaks above are for a size the sheet does not list. Our posters are priced by their own price list too, the same on every plan: one price each, at any quantity (each row's one quantity_breaks entry, price_interpolation none); the posters' breaks above are for a size the list does not hold. Our magnets are priced by their own price list, the same on every plan: a total at every quantity, published at 1, 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000 and 10,000 units (each row's quantity_breaks, with total_cents; price_interpolation formula, so between two of those the quote gives the figure). The total never falls as the quantity grows. The bulk discount continues at every quantity; rounding can move the unit price slightly; the magnets' breaks above are for a size the list does not include. Plans marked offered: false are not sold on PrintedSimply; accounts already on them keep them.","pricing_model":{"volume_discounts":false,"platform_fee_pct":0,"card_fee_pct":2.9,"card_fee_fixed_cents":30,"processing_fee_pct":0,"catalog_discount_pct":0},"tiers":[{"tier":1,"plan":"starter","name":"Free","offered":true,"monthly_price_cents":0,"catalog_discount_pct":0,"discount_percent":0,"platform_fee_pct":0,"requests_per_minute":60,"requests_per_day":10000,"recommended_monthly_orders":"0–99","min_orders":null,"max_orders":null,"bands":[],"label":"Free · 60 requests/min","features":["No platform fee — the price shown is the price","No added card-processing or seasonal peak fees","60 API requests/min","REST API","$0 time-compressed sandbox","Test + live API keys, HMAC webhooks","Community support"]}],"current":{"tier":1,"plan":"starter","name":"Free","offered":true,"catalog_discount_pct":0,"discount_percent":0,"entitled_catalog_discount_pct":0,"monthly_orders":null},"next_tier":null},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["pricing:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["pricing:read"]}]}},"/pricing/estimate":{"post":{"operationId":"estimatePricing","summary":"Get pricing estimate","description":"Calculate the product and shipping total for a set of items, with no added platform, card-processing or seasonal peak-postage fee. Does not create an order or charge anything. Priced through the same facility routing POST /order uses, so the estimate consults the catalog of the facility that would actually produce and charge the order (a sticker estimates at the flat-goods facility, a tee at the apparel facility). When no configured facility can produce the items, a live key gets 503 provider_unavailable — the same answer the order would give — and a test key gets a $0 breakdown flagged pricing_degraded: \"provider_unavailable\", matching the sandbox order path. This prices our own facilities' lanes, which ship to US addresses only: a destination_country other than US is refused 422 destination_unavailable, as the order would refuse it.","tags":["Pricing"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingEstimateRequest"},"example":{"items":[{"sku":"sticker-die-cut-3x3","quantity":100}],"shipping_method":"standard"}}}},"responses":{"200":{"description":"Pricing breakdown","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PricingEstimateResponse"}}}]},"example":{"status":"success","data":{"items":"$70.00","shipping":"$8.99","processing_fee":"$0.00","platform_fee":"$0.00","card_fee":"$0.00","total":"$78.99","total_cents":7899},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"422":{"$ref":"#/components/responses/EstimateRefused"},"429":{"$ref":"#/components/responses/RateLimitError"},"503":{"description":"provider_unavailable — no configured facility can produce these items, or (`details.reason: plant_unreachable`, with `Retry-After`) our plant gave no answer about a SKU — the request did not reach it, timed out, or came back 5xx, which is about the plant and not the SKU. Live keys; a test key gets a $0 estimate flagged `pricing_degraded` instead (`provider_unavailable` for an unconfigured facility, `pricing_unavailable` for an unanswering plant). Only for a SKU the catalog has, though: one the price book does not price, our plant's products do not name and the synced catalog does not hold is refused 400 unknown_sku with `details.skus`, as it is on POST /order, POST /orders/quote and POST /pricing/estimate alike.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Sent with `details.reason: plant_unreachable`: seconds to wait before retrying, the catalog breaker's own cooldown."},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"x-required-scopes":["pricing:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["pricing:read"]}]}},"/products/recommend":{"post":{"operationId":"recommendProducts","summary":"Get product recommendations","description":"Rank the live catalog against what you are trying to make. Deterministic — a keyword and use-case scorer over the same catalog GET /v1/products serves, with no model anywhere in it, so the same query over the same catalog always returns the same ranking. Every recommendation is priced through the same path POST /v1/order charges from (your negotiated overrides, then your plan's catalog discount), so price_cents is what you would actually pay for one unit; a SKU the catalog cannot price comes back price_cents: null with priced_from: \"unavailable\" rather than a guess. Provide at least one of query or use_case.","tags":["Products"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"Free text describing what you need. Each word scores a point against a product's name, SKU, description and derived category — the same fields GET /v1/products?search= matches."},"use_case":{"type":"string","enum":["branding","packaging","events","marketing","decor"],"description":"Intended use case; scores products in the catalog categories and keywords that serve it."},"size":{"type":"string","enum":["small","medium","large"],"description":"Desired size, matched as a substring of the SKU or name — the same way GET /v1/products?size= matches it."},"indoor_outdoor":{"type":"string","enum":["indoor","outdoor","both"],"description":"Indoor, outdoor, or both. The catalog has no such field, so this is matched against product copy: a product that says nothing scores nothing here and is never excluded for it."},"quantity":{"minimum":1,"maximum":100000,"type":"integer","description":"Units you expect to order. Prices each recommendation at that quantity and adds estimated_items_total_cents (what an order of that quantity is charged for the goods: the unit price times the quantity, or a sticker's pack total, which its rounded unit times the quantity need not be). Our own plant's products priced by our price book carry quantity breaks (vinyl decals up to 5×5 in 40% off from 10, 61% off from 25, 73% off from 50, 76% off from 100; magnets 15% off from 10, 30% off from 50, 40% off from 100; posters 8% off from 5, 15% off from 10, 18% off from 25; wall decals 8% off from 5, 15% off from 10), on the order's whole quantity of a SKU and after any plan discount; a negotiated price takes none. Our die-cut and kiss-cut stickers are priced by their own sheet instead, the same on every plan: a pack total at 1, 5, 10, 25, 50, 100, 250, 500, 1,000 and 2,500 units (each row's quantity_breaks, with total_cents), the total running in a straight line between two of those (price_interpolation linear_total), the 2,500-unit rate held above 2,500, and never below the size's unit floor, which holds the largest sizes at volume (min_unit_cents). Our wall decals are priced by their own sheet too, the same on every plan: one price each, at any quantity (each row's one quantity_breaks entry, price_interpolation none); the wall decals' breaks above are for a size the sheet does not list. Our posters are priced by their own price list too, the same on every plan: one price each, at any quantity (each row's one quantity_breaks entry, price_interpolation none); the posters' breaks above are for a size the list does not hold. Our magnets are priced by their own price list, the same on every plan: a total at every quantity, published at 1, 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000 and 10,000 units (each row's quantity_breaks, with total_cents; price_interpolation formula, so between two of those the quote gives the figure). The total never falls as the quantity grows. The bulk discount continues at every quantity; rounding can move the unit price slightly; the magnets' breaks above are for a size the list does not include. It excludes shipping and the processing fee; POST /v1/pricing/estimate quotes an order total."},"budget":{"type":"string","enum":["low","medium","premium"],"description":"Price preference, relative to the products that matched rather than to a fixed dollar band: low favours the cheapest third of the matches, premium the dearest third. Scores one point, the same as every other signal, and breaks remaining ties in that direction. Products the catalog could not price take no budget point and rank last."}}},"example":{"query":"logo stickers for a trade show","use_case":"events","quantity":100}}}},"responses":{"200":{"description":"Up to 5 ranked recommendations, plus a `catalog` block saying what they were ranked over and whether it was stale.","headers":{"X-Catalog-Stale":{"$ref":"#/components/headers/X-Catalog-Stale"},"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Warning":{"$ref":"#/components/headers/Warning"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProductRecommendationsResponse"}}}]},"example":{"status":"success","data":{"recommendations":[{"sku":"sticker-sheet-clear-6x4","name":"Sticker Sheet, Transparent, 6×4 in","category":"stickers","price":"$4.10","price_cents":410,"priced_from":"catalog","estimated_items_total_cents":41000,"estimated_items_total":"$410.00","score":2,"match_reasons":["keyword: stickers","keyword: a"]},{"sku":"sticker-sheet-clear-8.5x11","name":"Sticker Sheet, Transparent, 8.5×11 in","category":"stickers","price":"$6.10","price_cents":610,"priced_from":"catalog","estimated_items_total_cents":61000,"estimated_items_total":"$610.00","score":2,"match_reasons":["keyword: stickers","keyword: a"]},{"sku":"sticker-sheet-holographic-6x4","name":"Sticker Sheet, Holographic, 6×4 in","category":"stickers","price":"$4.10","price_cents":410,"priced_from":"catalog","estimated_items_total_cents":41000,"estimated_items_total":"$410.00","score":2,"match_reasons":["keyword: stickers","keyword: a"]},{"sku":"sticker-sheet-holographic-8.5x11","name":"Sticker Sheet, Holographic, 8.5×11 in","category":"stickers","price":"$6.10","price_cents":610,"priced_from":"catalog","estimated_items_total_cents":61000,"estimated_items_total":"$610.00","score":2,"match_reasons":["keyword: stickers","keyword: a"]},{"sku":"magnet-10x20","name":"Magnet, 10×20 in","category":"magnets","price":"$6.42","price_cents":642,"priced_from":"catalog","estimated_items_total_cents":64175,"estimated_items_total":"$641.75","score":1,"match_reasons":["keyword: a"]}],"quantity":100,"budget":null,"catalog":{"stale":false,"reason":null,"source":"catalog","considered":31,"matched":28}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"},"502":{"description":"The catalog could not be read at all, so there was nothing to rank. Deliberately an error rather than an empty `recommendations` list, which would read as \"nothing suits you\". `X-Catalog-Degraded` carries the reason. Never for our own plant's catalog, which falls back to its price book (`catalog.source: \"price_book\"`), as GET /v1/products does.","headers":{"X-Catalog-Degraded":{"$ref":"#/components/headers/X-Catalog-Degraded"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["products:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["products:read"]}]}},"/mockups/generate":{"post":{"operationId":"generateMockup","summary":"Generate product mockup","description":"Composite artwork onto a product and get the picture back. `format: \"svg\"` (the default) answers a data URI, the same in test and live mode, with the artwork's own bytes inlined (`artwork_inlined: true`) so it paints anywhere, inside an `<img>` included: the artwork is read server-side through the same guarded, size-capped fetch the raster formats use. The inlined artwork is fitted to the print area's own pixels first (a PNG where it has transparency, else a JPEG), so the answer stays a bounded size; artwork that cannot be read safely, or is still over 512 KB once fitted, is referenced by its URL instead (`artwork_inlined: false`, with `artwork_inline_reason`), and an SVG loaded as an image does not load a referenced file. `thumbnail_url` is its own 300 px render, never a second copy of `mockup_url`. A product type that is one of our own plant's SKUs naming its size (poster-18x24, decal-wall-24x36) is drawn at that size's proportions, in every format (`size_in`). `format: \"png\"` or `\"jpeg\"` answers `mockup_url` as a STORED, publicly fetchable image plus `content_type`, `width` and `height`: that is the form a storefront can publish, because a data URI is not something Shopify or TikTok will fetch and an SVG loaded as an image may not load the artwork it references. If storage is not configured the render still comes back, inline as a data URI, with `stored: false` and `storage_reason` saying why. A stored render also answers `asset_id` — the `Asset` row `DELETE /v1/assets/{id}` takes — so a caller can delete its own render without matching it by URL in `GET /v1/assets`; it is `null`, not absent, when `stored` is false. With `cut_line: true` on a die-cut SKU naming its size (sticker-die-cut-3x3), the mockup also shows the cut line, in every format, for a print proof: the outline of the artwork's transparent background, offset outward by the SKU's bleed, as a thin dashed magenta line (`cut_line` in the response says whether it was drawn; off by default, so a product photograph is unchanged). SVG artwork is drawn at the size it is placed at, and has 10 seconds to finish drawing: one that takes longer (heavy filters and effects) is a 400 validation_error with `details.artwork_url: [\"artwork_too_complex\"]` in the raster formats, and is referenced rather than inlined in the SVG format (`artwork_inline_reason: artwork_too_complex`); send such artwork as a PNG. In the raster formats, artwork over 50 megapixels is a 400 with `details.artwork_url: [\"artwork_too_large\"]` and its size in the message (a 24×36 in poster at 300 DPI is 77.8 MP: use a smaller copy for the mockup), and a file that does not decode (cut short or damaged) is a 400 with `artwork_unreadable`. Renders run a few at a time: a request that finds none free within 20 seconds is a 503 render_busy with `Retry-After`, nothing rendered or stored, and the same request is safe to send again. One account's renders take one render at a time, so another account's slow artwork does not hold yours up: an account's further requests wait for that one, and with four already waiting, one more is a 503 render_busy at once.","tags":["Mockups"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["artwork_url","product_type"],"properties":{"artwork_url":{"type":"string","format":"uri","description":"URL of the artwork file, or a data: URI (the form /designs/generate answers). For a raster format an https URL is fetched and the bytes must be a PNG, JPEG, WebP or SVG — the extension and the served content type are not trusted."},"product_type":{"type":"string","description":"Product type for the mockup (sticker, poster, magnet, …) or a SKU. A SKU of our own plant's that names its size is drawn at that size's proportions (`size_in`). Where the portal sells only our own plant's products, a product type it does not make (a tee, a mug) is a 400 validation_error with `details.product_type: [\"not_offered\"]`."},"background":{"type":"string","enum":["white","transparent","lifestyle"],"default":"white","description":"Background style for the mockup"},"angle":{"type":"string","enum":["front","angled","flat-lay"],"default":"front","description":"Viewing angle for the mockup"},"format":{"type":"string","enum":["svg","png","jpeg"],"default":"svg","description":"svg returns a data URI (unchanged default). png and jpeg return a stored, publicly fetchable image — the form a store listing can carry."},"size":{"type":"integer","minimum":200,"maximum":2000,"default":1200,"description":"Square edge in pixels for the raster formats. The SVG is always 1200."},"cut_line":{"type":"boolean","default":false,"description":"On a die-cut SKU (sticker-die-cut-3x3), draw the cut line over the artwork: a thin dashed magenta line along the outline of the artwork's transparent background, offset outward by the SKU's bleed, where the plant's blade goes. Opt-in, for a print proof: off by default, because a mockup is what a seller publishes as a product photograph. No effect on a product cut along a rectangle. The response's `cut_line` says whether it was drawn."}}},"example":{"artwork_url":"https://example.com/logo.png","product_type":"sticker","background":"white","format":"png","size":1200}}}},"responses":{"200":{"description":"Mockup generated","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MockupResponse"}}}]},"example":{"status":"success","data":{"mockup_url":"https://assets.example.com/assets/1792076422000-945098792998-mockup-sticker-front.png","thumbnail_url":"https://assets.example.com/assets/1792076422000-945098792998-mockup-sticker-front.png","asset_id":"ast_MeOk7RY2Euc22EZ15k_2agc2","product_type":"sticker","background":"white","angle":"front","format":"png","content_type":"image/png","stored":true,"width":1200,"height":1200,"dimensions":{"width":1200,"height":1200},"size_in":null,"cut_line":null,"generated_at":"2026-10-15T15:00:22.000Z"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"},"503":{"description":"render_busy — every render slot stayed taken for the whole wait (20 seconds), or this account already had four renders waiting behind its one in progress, so nothing was rendered or stored; retry after `Retry-After`.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["mockups:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["mockups:write"]}]}},"/events":{"get":{"operationId":"listEvents","summary":"List account events","description":"The same events your webhooks receive, pulled instead of pushed — for a client with no public URL to POST to (an agent, a notebook, a job behind NAT), and as the catch-up path when your endpoint was down longer than the retry ladder. `event_id` equals the `X-PS-Event-Id` header on a delivered copy and the `id` inside the webhook body, so a receiver that both subscribes and polls dedupes on it. `data` is the webhook body's `data` verbatim. Newest first, paged by cursor (see Pagination); `next_cursor` is also beside the rows, where this list has always carried it. A test key sees test events, a live key sees live ones, and account-level events (catalog syncs, design proofs) are returned to both.","tags":["Events"],"parameters":[{"name":"event","in":"query","schema":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","test.ping"]},"description":"One event name, e.g. `order.shipped`. An unknown name is refused rather than ignored. The list is the events this portal offers; a name outside it that an event in your history carries (one received before the switch) is still accepted."},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Events per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."}],"responses":{"200":{"description":"events (id, event, event_id, test_mode, created_at, data) + next_cursor + retention_days; meta carries has_more and next_cursor","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/AccountEvent"}},"next_cursor":{"type":["string","null"],"description":"The same as `meta.next_cursor`: send it back as `cursor` for the next page. Null on the last."},"retention_days":{"type":"integer","description":"How long an event stays readable here."}}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"events":[{"id":"c3sx7f4gaslmi2lt9d4gu6cca","event":"order.cancelled","event_id":"evt_F5S_u7olBRx4IBTVZNXoH-8d","test_mode":true,"created_at":"2026-10-15T15:00:21.000Z","data":{"status":"cancelled","event_id":"evt_PA21dQPJmlEc0sS9bhCLg5l_","order_id":"test_ord_JyZLq7sUxTZxc7qjzI76ZQYU","provider":"printedsimply","test_mode":true,"portal_order_id":"ord_JyZLq7sUxTZxc7qjzI76ZQYU"}},{"id":"cp9t18xiy0nkptsc4f0dfu16r","event":"order.updated","event_id":"evt_T6LdoYs-r7UK7oBoe6PVn6dG","test_mode":true,"created_at":"2026-10-15T15:00:17.000Z","data":{"changes":{"action":"address_changed","ship_to":{"zip":"78702","city":"Austin","name":"Jane Doe","state":"TX","country":"US","address1":"456 Oak Ave"},"total_cents":8399,"total_delta_cents":0},"event_id":"evt_ORmnOh7KCgTFDahmHVzDhdg9","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","test_mode":true,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}},{"id":"cawnkjmeihmmx7cxd53rufekc","event":"order.updated","event_id":"evt_Ze8JBOtDDevHmgQRbq6jY6TC","test_mode":true,"created_at":"2026-10-15T15:00:14.000Z","data":{"changes":{"item":{"id":"itm_vNx36IWd8kx2dCQqkFbsTVg3","sku":"magnet-4x4","quantity":5},"action":"item_updated","total_cents":10099,"total_delta_cents":-4185},"event_id":"evt_gwqAUgOZe7itGr-n6zvxYDi0","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","test_mode":true,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}},{"id":"cyriilhobcg82vqjt9iuchpnk","event":"order.paid","event_id":"evt_NPH58Tcs_h19qfEXVf9xMang","test_mode":true,"created_at":"2026-10-15T15:00:10.000Z","data":{"status":"paid","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","provider":"merchfarm","test_mode":true,"amount_cents":7899,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}},{"id":"cfdz97bh79wppvqvf6rcpzeyj","event":"order.created","event_id":"evt_pfvUVLY9cBbI0i5Mv1kauVL-","test_mode":true,"created_at":"2026-10-15T15:00:10.000Z","data":{"items":[{"sku":"sticker-die-cut-3x3","quantity":100}],"status":"paid","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","provider":"merchfarm","test_mode":true,"created_at":"2026-10-15T15:00:10.000Z","total_cents":7899,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}}],"next_cursor":null,"retention_days":30},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["events:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["events:read"]}]}},"/alerts":{"get":{"operationId":"listAlerts","summary":"List integration-health alerts","description":"The account's integration-health alerts, newest first: every open one and those resolved in the last 30 days. Three kinds, each read every 5 minutes from live traffic in the request log and the delivery log: `webhook_failing` (more than half of an endpoint's attempts in the last hour failed on its side for events that have not arrived since, five at least, with two such events 23 minutes past their first attempt, or three events in a row were lost after their whole retry ladder and one in the hour has not arrived; a failure the retry recovered is not counted, and it resolves once two events arrived and none is missing; failures on our side are counted apart as `platform_failed`; an endpoint that takes test events only is not read, nor a sandbox event sent to one that takes both; it stays open, saying so, once the endpoint is switched off after its consecutive failures, which `webhook.disabled` announces), `error_rate_spike` (live calls failing well above the account's own rate over the 24 hours before, or the 7 days before when that day held fewer than 20 live calls: at least 20 calls and 10 failures in the hour, at least 10% of them and three times that rate, or 30% when even the week held fewer than 20; failures are 5xx answers and 401, 402, 403 and 429, not 400, 404, 409 or 422 answers about one request; while it is open, an hour with fewer than 10 live calls is read over the account's newest 10 instead, and fewer than that keeps it open, since no calls is no recovery) and `key_expiring` (a live key that stops working within 7 days, by its expiry or because it was rolled, that answered at least 3 calls in the last day and one in the last 6 hours; once the key stops, it stays open until 6 hours pass without a call since the stop; a key that went quiet and was called again before the same stop opens again without a notification or an email, and stays open until it stops). Each opens once, stays open while its condition holds, and resolves on a clearly better reading, never within 30 minutes of opening unless what it was about is gone; one with nothing to read for 7 days (no delivery attempt, or no live call) resolves as `dormant`, which is no recovery. `evidence` holds the numbers that opened it and `latest` the newest reading. Opening and resolving are the `alert.opened` and `alert.resolved` events (sent to the account's own live endpoints, an opened `webhook_failing` never to the endpoint it is about and a `key_expiring` to none; an `error_rate_spike`'s `top_errors` and `example_request_ids` are empty in them), a notification in the dashboard, and an email when it opens (at most three a day). Best-effort signals over our own logs: one may miss a problem or open on one that fixes itself. What a credential is shown: a test key, none (every alert is read from live traffic); a `webhook_failing` alert needs `webhooks:manage` too, and a connected app sees those of the endpoints it subscribed; a key created with a scope list sees the `key_expiring` alert about itself and a connected app none; and an `error_rate_spike` alert's `top_errors` and `example_request_ids` are empty but to a key with no scope list. GET /events, the delivery log and a replay show the two events to a key with no scope list only. Paged by cursor (see Pagination); `status` and `kind` filter it, and an unknown value is refused.","tags":["Events"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["open","resolved"]},"description":"`open` for the alerts still open, `resolved` for the resolved ones. Omit for both."},{"name":"kind","in":"query","schema":{"type":"string","enum":["webhook_failing","error_rate_spike","key_expiring"]},"description":"One kind of alert. Omit for every kind."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Alerts per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."}],"responses":{"200":{"description":"alerts (newest first) + history_days; meta carries has_more and next_cursor","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","required":["alerts","history_days"],"properties":{"alerts":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationAlert"}},"history_days":{"type":"integer","description":"How long a resolved alert stays listed."}}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"alerts":[{"id":"cmb4alrt0000example0001","kind":"webhook_failing","status":"open","subject_type":"webhook","subject_id":"whk_SV8d5wGm2kUnSwx_RhZfY9OE","summary":"7 of 9 deliveries to example.com failed in the last 60 minutes.","opened_at":"2026-10-10T08:15:00.000Z","resolved_at":null,"resolution":null,"last_evaluated_at":"2026-10-10T08:25:00.000Z","evidence":{"endpoint_host":"example.com","window_minutes":60,"attempts":9,"failed":7,"failure_ratio":0.778,"platform_failed":0,"newest_delivered":false,"consecutive_failures":0,"disabled":false,"top_causes":[{"cause":"http_5xx","count":6},{"cause":"timeout","count":1}],"example_order_ids":["ord_wVK9ZiG-VYiPrlSeBUBmrUVk"]},"latest":{"endpoint_host":"example.com","window_minutes":60,"attempts":9,"failed":7,"failure_ratio":0.778,"platform_failed":0,"newest_delivered":false,"consecutive_failures":0,"disabled":false,"top_causes":[{"cause":"http_5xx","count":6},{"cause":"timeout","count":1}],"example_order_ids":["ord_wVK9ZiG-VYiPrlSeBUBmrUVk"]}}],"history_days":30},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["events:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["events:read"]}]}},"/assets":{"get":{"operationId":"listAssets","summary":"List your uploaded assets","description":"Your uploaded assets, newest first. Pages by cursor (see Pagination): send back `meta.next_cursor` as `cursor` until `meta.has_more` is false. `next_cursor` and the `pagination` counts are also in the payload, where this list has always carried them. The deprecated `page` walk is still answered as before.","tags":["Assets"],"parameters":[{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Assets per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"page","in":"query","deprecated":true,"schema":{"type":"integer","minimum":1,"default":1},"description":"Deprecated: walk with `cursor`. The page number of the offset walk this operation always had, answered as before (`limit` 20 by default, 100 at most; `offset`, a row count, is accepted in its place). Ignored when `cursor` is sent, and a cursor walk's `pagination` carries no `page` or `pages`."}],"responses":{"200":{"description":"Assets, next_cursor (null on the last page), and pagination counts; meta carries has_more, next_cursor, limit and total","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"assets":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"next_cursor":{"type":["string","null"],"description":"The same as `meta.next_cursor`: send it back as `cursor` for the next page. Null on the last page."},"pagination":{"$ref":"#/components/schemas/AssetPagination"}}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"assets":[{"id":"ast_bptSUfSLn2FInuMnC5S4fVD1","filename":"logo.png","url":"https://assets.example.com/assets/1792076423000-a8e85f4abc4c-logo.png","content_type":"image/png","size_bytes":70,"source":"upload","created_at":"2026-10-15T15:00:23.000Z"},{"id":"ast_MeOk7RY2Euc22EZ15k_2agc2","filename":"mockup-sticker-front.png","url":"https://assets.example.com/assets/1792076422000-945098792998-mockup-sticker-front.png","content_type":"image/png","size_bytes":679951,"source":"mockup","created_at":"2026-10-15T15:00:22.000Z"}],"next_cursor":null,"pagination":{"page":1,"pages":1,"limit":20,"total":2}},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"total":2,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["assets:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["assets:read"]}]},"post":{"operationId":"uploadAsset","summary":"Upload an asset (base64)","description":"Uploads artwork or a document as base64. Allowed types: image/png, image/jpeg, image/svg+xml, application/pdf. 50 MB cap. Returns the hosted URL to use in print files, proofs, or mockups.","tags":["Assets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["filename","content_type","data"],"properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"content_type":{"type":"string","enum":["image/png","image/jpeg","image/svg+xml","application/pdf"]},"data":{"type":"string","minLength":1,"maxLength":69905068,"description":"Base64-encoded file contents. The 50 MB cap is on the decoded file; this bound is its base64 length (69,905,068 characters)."}}},"example":{"filename":"logo.png","content_type":"image/png","data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="}}}},"responses":{"200":{"description":"Asset stored; includes the hosted url","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Asset"}}}]},"example":{"status":"success","data":{"id":"ast_bptSUfSLn2FInuMnC5S4fVD1","filename":"logo.png","url":"https://assets.example.com/assets/1792076423000-a8e85f4abc4c-logo.png","content_type":"image/png","size_bytes":70,"source":"upload","created_at":"2026-10-15T15:00:23.000Z"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"x-required-scopes":["assets:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["assets:write"]}]}},"/assets/{id}":{"get":{"operationId":"getAsset","summary":"Get an asset","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Asset detail","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Asset"}}}]},"example":{"status":"success","data":{"id":"ast_bptSUfSLn2FInuMnC5S4fVD1","filename":"logo.png","url":"https://assets.example.com/assets/1792076423000-a8e85f4abc4c-logo.png","content_type":"image/png","size_bytes":70,"source":"upload","created_at":"2026-10-15T15:00:23.000Z"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["assets:read"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["assets:read"]}]},"delete":{"operationId":"deleteAsset","summary":"Delete an asset","description":"Removes the stored file, then its record. When storage refuses to delete the file, the record is kept and the call fails with a 500 internal_error, so the asset stays listed and the same delete is safe to retry; `deleted: true` means the file is gone.","tags":["Assets"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}]},"example":{"status":"success","data":{"deleted":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["assets:write"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["assets:write"]}]}},"/webhooks":{"get":{"operationId":"listWebhooks","summary":"List webhook subscriptions","description":"Your webhook endpoints, newest first, paged by cursor (see Pagination). Each carries its `mode` (all / test / live). Signing secrets are never returned — only rotation state (secret_rotated_at, previous_secret_expires_at, rotation_pending). Subscriptions are not deduplicated by URL: pass `url` to ask whether an endpoint is already subscribed in one call before POST /webhooks.","tags":["Webhooks"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Subscriptions per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."},{"name":"url","in":"query","schema":{"type":"string","format":"uri"},"description":"Only subscriptions at this URL — exact match on the `url` as it was registered. POST /webhooks stores the URL exactly as sent and normalises nothing, so neither does this filter: `https://Example.com/hooks/` and `https://example.com/hooks` are different subscriptions. An empty page means none of your subscriptions is at that URL. Must be an absolute http:// or https:// URL; anything else is 400 validation_error. The cursor is bound to it: send the same `url` with every page of a walk."}],"responses":{"200":{"description":"webhooks[] + next_cursor; meta carries has_more and next_cursor","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"}},"next_cursor":{"type":["string","null"],"description":"The same as `meta.next_cursor`: send it back as `cursor` for the next page. Null on the last."}}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"webhooks":[{"id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:26.000Z","secret_rotated_at":null,"previous_secret_expires_at":null,"rotation_pending":false,"inbox":false}],"next_cursor":null},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]},"post":{"operationId":"createWebhook","summary":"Create a webhook subscription","description":"Subscribe an HTTPS endpoint to events. The response carries `secret` — the signing secret, shown exactly once here and on rotation, never on a later read. Use `\"*\"` in events to receive everything, and `mode` to scope the endpoint to sandbox or live traffic. A test key subscribes only to test traffic: its default mode is test, and live or all answers 403 live_key_required. It changes only test subscriptions, and its delivery log carries sandbox and account events only.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","description":"Destination URL. It must use HTTPS, or http:// to localhost or 127.0.0.1 outside production — the hosted API is production, so it accepts HTTPS only."},"events":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Event names to receive, or [\"*\"] for all of them. See /docs/webhooks for the catalog. On this portal a subscription names `order.created`, `order.paid`, `order.in_production`, `order.shipped`, `order.delivered`, `order.cancelled`, `order.failed`, `order.updated`, `order.quality_check`, `order.issue`, `order.approval_requested`, `order.approval_approved`, `order.approval_declined`, `order.approval_expired`, `product.updated`, `catalog.synced`, `webhook.disabled`, `alert.opened`, `alert.resolved`, `*`; any other event is refused with 400 event_not_available."},"mode":{"type":"string","enum":["all","test","live"],"default":"all","description":"Which traffic this endpoint receives. \"test\" delivers only sandbox events, \"live\" only real ones, \"all\" (the default) both. Account-level events — catalog syncs, delivery-failure notices, design proofs — arrive on every subscription regardless. Every mode-bearing event carries `test_mode` in its payload, stamped from the same decision that routed it."}}},"example":{"url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered"],"mode":"test"}}}},"responses":{"200":{"description":"The subscription and its signing secret in the clear. This is one of only TWO moments the secret is ever returned — this and a rotation. It is stored sealed and no read gives it back; store it now, and if you lose it, rotate.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSubscriptionCreated"}}}]},"example":{"status":"success","data":{"id":"whk_SV8d5wGm2kUnSwx_RhZfY9OE","url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered"],"active":true,"mode":"test","created_at":"2026-10-10T08:09:14.588Z","secret_rotated_at":null,"previous_secret_expires_at":null,"rotation_pending":false,"inbox":false,"secret":"f727cc1c27bffcc21ba9b993e38905166961014d23af696723f996abd4889764"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"Invalid request body or parameters. When the request failed the operation's schema, `details` carries `formErrors` (problems with the request as a whole), `fieldErrors` (messages keyed by top-level field) and `errors`: one `{ pointer, code, message }` per refused value, `pointer` an RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) — `/items/0/sku`, with `~` written `~0` and `/` written `~1`, and `\"\"` for the request as a whole. POST /v1/orders/bulk answers `errors` as one `{ index, details }` per refused order, each `details` with its own pointers from the request body (`/orders/3/items/0/sku`); POST /v1/orders/multi-ship keeps `errors` as the `{ formErrors, fieldErrors }` it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top of `details`, with `errors` beside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it in `message` and carries no `errors`. event_not_available — `events` names an event this portal does not offer (another family's, or one it never raises: `order.rerouted`); refused before anything is saved. `details.events` names each one and `details.available` lists the events a subscription can name here.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"validation_error","message":"Request body failed validation.","details":{"formErrors":[],"fieldErrors":{"address_to":["Invalid input: expected string, received undefined"]},"errors":[{"pointer":"/address_to/zip","code":"invalid_type","message":"Invalid input: expected string, received undefined"}]}},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"$ref":"#/components/responses/AuthError"},"403":{"description":"live_key_required — a test key subscribes only to test traffic. Omit mode (a test key's subscription defaults to test) or send mode \"test\"; use a live key for live or all.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/{id}":{"get":{"operationId":"getWebhook","summary":"Retrieve a webhook subscription","description":"One subscription: url, events, active, mode, and rotation state. Never the signing secret.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The subscription. Never the secret — only create and rotate return that.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSubscription"}}}]},"example":{"status":"success","data":{"id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:26.000Z","secret_rotated_at":null,"previous_secret_expires_at":null,"rotation_pending":false,"inbox":false},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]},"patch":{"operationId":"updateWebhook","summary":"Update a webhook subscription","description":"Change the endpoint URL, the subscribed events, the test/live scope, or pause it with active: false. Send at least one field. A connected app changes, rotates and deletes only the subscriptions it created; any other answers 404, as one on another account does.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"New destination URL. It must use HTTPS, or http:// to localhost or 127.0.0.1 outside production — the hosted API is production, so it accepts HTTPS only."},"events":{"type":"array","minItems":1,"items":{"type":"string"},"description":"On this portal a subscription names `order.created`, `order.paid`, `order.in_production`, `order.shipped`, `order.delivered`, `order.cancelled`, `order.failed`, `order.updated`, `order.quality_check`, `order.issue`, `order.approval_requested`, `order.approval_approved`, `order.approval_declined`, `order.approval_expired`, `product.updated`, `catalog.synced`, `webhook.disabled`, `alert.opened`, `alert.resolved`, `*`; any other event is refused with 400 event_not_available. An event this subscription already names is kept, whatever its family."},"active":{"type":"boolean","description":"false pauses delivery: events already queued for this endpoint are held, not sent, and go out where their retry schedule left off once it is true again. Events raised while it is paused are not queued for it; replay them with POST /v1/events/{eventId}/replay after resuming."},"mode":{"type":"string","enum":["all","test","live"],"default":"all","description":"Which traffic this endpoint receives. \"test\" delivers only sandbox events, \"live\" only real ones, \"all\" (the default) both. Account-level events — catalog syncs, delivery-failure notices, design proofs — arrive on every subscription regardless. Every mode-bearing event carries `test_mode` in its payload, stamped from the same decision that routed it."}}},"example":{"events":["order.shipped","order.delivered","order.cancelled"]}}}},"responses":{"200":{"description":"The subscription after the update.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSubscription"}}}]},"example":{"status":"success","data":{"id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered","order.cancelled"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:26.000Z","secret_rotated_at":null,"previous_secret_expires_at":null,"rotation_pending":false,"inbox":false},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"Invalid request body or parameters. When the request failed the operation's schema, `details` carries `formErrors` (problems with the request as a whole), `fieldErrors` (messages keyed by top-level field) and `errors`: one `{ pointer, code, message }` per refused value, `pointer` an RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) — `/items/0/sku`, with `~` written `~0` and `/` written `~1`, and `\"\"` for the request as a whole. POST /v1/orders/bulk answers `errors` as one `{ index, details }` per refused order, each `details` with its own pointers from the request body (`/orders/3/items/0/sku`); POST /v1/orders/multi-ship keeps `errors` as the `{ formErrors, fieldErrors }` it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top of `details`, with `errors` beside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it in `message` and carries no `errors`. event_not_available — `events` names an event this portal does not offer (another family's, or one it never raises: `order.rerouted`); refused before anything is saved. `details.events` names each one and `details.available` lists the events a subscription can name here.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"validation_error","message":"Request body failed validation.","details":{"formErrors":[],"fieldErrors":{"address_to":["Invalid input: expected string, received undefined"]},"errors":[{"pointer":"/address_to/zip","code":"invalid_type","message":"Invalid input: expected string, received undefined"}]}},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"$ref":"#/components/responses/AuthError"},"403":{"description":"live_key_required — a test key cannot move a subscription to live or all traffic. A test key changes only test-mode subscriptions; any other answers 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"404":{"$ref":"#/components/responses/NotFoundError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]},"delete":{"operationId":"deleteWebhook","summary":"Delete a webhook subscription","description":"Removes the endpoint and its delivery history, including events queued for it but not yet delivered. To replace a compromised secret without losing those, rotate instead. A connected app changes, rotates and deletes only the subscriptions it created; any other answers 404, as one on another account does.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"{ id, deleted: true }","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean"}}}}}]},"example":{"status":"success","data":{"id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","deleted":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/{id}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","summary":"Rotate a webhook signing secret","description":"Issues a new signing secret and keeps the outgoing one valid for grace_hours (default 24, max 168). Deliveries in the window are signed with both, so you can deploy the new secret with no dropped events. The new secret is returned once, here. A connected app changes, rotates and deletes only the subscriptions it created; any other answers 404, as one on another account does.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"grace_hours":{"type":"integer","minimum":0,"maximum":168,"default":24,"description":"How long the outgoing secret keeps verifying. 0 cuts over immediately."}}},"example":{"grace_hours":24}}}},"responses":{"200":{"description":"The new secret in the clear — the second and last time you see it — with grace_hours and previous_secret_expires_at bounding the window in which BOTH secrets verify. Accept either until then.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSecretRotated"}}}]},"example":{"status":"success","data":{"id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered","order.cancelled"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:26.000Z","secret_rotated_at":"2026-10-15T15:00:36.000Z","previous_secret_expires_at":"2026-10-16T15:00:36.000Z","rotation_pending":true,"inbox":false,"secret":"df33a8999390b44d0827dc3897e680cce9f376136ee934601cb44cc44f0cead0","grace_hours":24},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]},"delete":{"operationId":"endWebhookSecretRotation","summary":"End an open rotation window","description":"Stops honouring the outgoing secret now, rather than at its expiry — what you call once the new secret is deployed and the old one leaked. Idempotent: closing an already-closed window succeeds. A connected app changes, rotates and deletes only the subscriptions it created; any other answers 404, as one on another account does.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The subscription with rotation_pending false: the outgoing secret stops verifying immediately. Any receiver still signing with it starts failing now.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSubscription"}}}]},"example":{"status":"success","data":{"id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","url":"https://example.com/hooks/orders","events":["order.shipped","order.delivered","order.cancelled"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:26.000Z","secret_rotated_at":"2026-10-15T15:00:36.000Z","previous_secret_expires_at":null,"rotation_pending":false,"inbox":false},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/{id}/test":{"post":{"operationId":"sendWebhookTestEvent","summary":"Send a test event to a webhook endpoint","description":"Queues one event's sample payload for this endpoint: the payload the dashboard's webhook tester sends, built by the same code as the real event and marked `test_mode: true`, so a handler can tell a rehearsal from a real event. It is delivered like any other event, signed with the endpoint's signing secrets, on the retry ladder and into the delivery log (GET /webhooks/deliveries), on the dispatcher's next pass; an endpoint that is a hosted test inbox keeps it as it keeps any delivery. Only this endpoint receives it, and nothing is added to the event feed (GET /events): a rehearsal about an order that does not exist is not an event of the account's. Test keys only, since a sample is test traffic. Refused with 409 invalid_state, and `details.reason`, when the endpoint is paused or disabled (`endpoint_inactive`), takes live traffic only (`live_only`), or is not subscribed to the event (`not_subscribed`): each a case where the queue would hold or skip it. An order's event may go to an endpoint that takes both test and live traffic, where a test order's real events already arrive; an account event (one about the account rather than an order, such as product.updated), whose real counterpart carries no `test_mode`, goes only to an endpoint that takes test traffic alone, and is otherwise refused with `takes_live_traffic`. An integration-health alert (alert.opened, alert.resolved) is refused on every endpoint with `alert_events_not_readable_by_test_key`: a test key is shown no alert delivery in the delivery log, so it could never read the sample back; the dashboard's webhook tester sends it. An account sends at most 10 test events a minute, through this operation and the dashboard's webhook tester together, on top of the plan's own limit: one budget, spent by a send that has passed every check above, so a refusal spends none of it.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"event":{"type":"string","minLength":1,"maxLength":64,"enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved"],"description":"The event to rehearse, such as `order.shipped`. Every event a subscription can name on this deployment (see /docs/webhooks#event-types) has a sample. One that is not offered here is 400 event_not_available; a name that is not an event, 400 validation_error with `details.available`."}}},"example":{"event":"order.shipped"}}}},"responses":{"202":{"description":"Queued for this endpoint, not yet sent: the delivery log shows the attempt once the dispatcher makes it.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookTestEventQueued"}}}]},"example":{"status":"success","data":{"webhook_id":"whk_SV8d5wGm2kUnSwx_RhZfY9OE","event":"order.shipped","queued":1,"test_mode":true},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"test_mode_only — sent with a live key; validation_error — no `event`, or one that is not an event (`details.available` lists the ones that are); event_not_available — an event of a family this deployment does not offer; invalid_json.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"invalid_state — nothing was queued, and `details.reason` says why: `endpoint_inactive` (re-enable the endpoint), `live_only` (let it take test traffic) or `not_subscribed` (add the event to its subscription), where a real event would not reach it either; `takes_live_traffic`, an account event for an endpoint that takes live traffic too (send it to a test-only endpoint or the hosted test inbox); or `alert_events_not_readable_by_test_key`, alert.opened or alert.resolved, which a test key is shown in no delivery log (send it from the dashboard's webhook tester).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"description":"rate_limit_exceeded — more than 10 test events a minute from this account, counting the dashboard's webhook tester, which spends the same budget (only a send that passed the endpoint's checks spends it); nothing was queued. Or the plan's own per-minute or daily limit, as on every operation (see RateLimitError). `Retry-After` says when to send again.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"queue_unavailable — the event could not be queued just now, so nothing was sent; retry after `Retry-After`.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/events/{eventId}/replay":{"post":{"operationId":"replayEvent","summary":"Replay a stored event to your webhooks","description":"Re-enqueues one event from your feed to every active subscription that accepts its mode, with the STORED envelope verbatim — same id, so a receiver that dedupes on X-PS-Event-Id sees the same logical event again rather than a new one. This is the push half of the catch-up loop GET /v1/events is the pull half of: after an outage, read what you missed, then replay what your endpoint needs to process. Visibility follows the feed exactly — an event on the other side of your key's test/live line is a 404, and account-level events replay to both sides. enqueued: 0 is an honest answer meaning no active subscription accepts this event's mode; the fix is a webhook, not a retry.","tags":["Webhooks"],"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"},"description":"The event_id from GET /v1/events — the envelope's id, the X-PS-Event-Id value."},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Replay result: the event name, its event_id, and how many queue rows were enqueued.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"replayed":{"type":"boolean"},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied","test.ping"]},"event_id":{"type":["string","null"]},"enqueued":{"type":"integer","description":"How many subscriptions the replay was queued to. Zero is an honest answer: no active subscription accepts this event’s mode, and the fix is a webhook rather than a retry."}}}}}]},"example":{"status":"success","data":{"replayed":true,"event":"order.cancelled","event_id":"evt_F5S_u7olBRx4IBTVZNXoH-8d","enqueued":1},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/deliveries":{"get":{"operationId":"listWebhookDeliveries","summary":"List webhook delivery attempts","description":"The webhook event log: recent delivery attempts across your endpoints, newest first, paged by cursor (see Pagination). Newest by when the event happened (the body's `timestamp`), then by when the attempt was made: an order's events list in the order they happened even when their attempts were recorded in the same millisecond, and the attempts at one event sit together, its latest first. Filter by webhook_id, event_id, or success. A row is one attempt; `event_id` is the logical event it was an attempt at (the value sent as `X-PS-Event-Id`), so the retry ladder's rows and every later redelivery of one event share it.","tags":["Webhooks"],"parameters":[{"name":"webhook_id","in":"query","schema":{"type":"string"}},{"name":"event_id","in":"query","schema":{"type":"string"},"description":"Every attempt at one logical event — the X-PS-Event-Id value."},{"name":"success","in":"query","schema":{"type":"string","enum":["true","false"]},"description":"Filter to successful or failed attempts. Exactly `true` or `false` — any other spelling returns 400 validation_error rather than quietly applying no filter."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Attempts per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."}],"responses":{"200":{"description":"Deliveries (id, event, event_id, status_code, success, failure_cause) + next_cursor; meta carries has_more and next_cursor","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"next_cursor":{"type":["string","null"],"description":"The same as `meta.next_cursor`: send it back as `cursor` for the next page. Null on the last."}}},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"deliveries":[{"id":"ccbkfrz5r3gcby08k4iqiuxwb","webhook_id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","event":"order.shipped","event_id":"evt_ey8SQMTGI1sCP-c0JpwB3AyX","status_code":200,"success":true,"failure_cause":null,"created_at":"2026-10-15T15:00:32.000Z"},{"id":"cs5qsuszabsomxv2bdyxe25e1","webhook_id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","event":"order.cancelled","event_id":"evt_F5S_u7olBRx4IBTVZNXoH-8d","status_code":200,"success":true,"failure_cause":null,"created_at":"2026-10-15T15:00:32.000Z"}],"next_cursor":null},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"has_more":false,"next_cursor":null}}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/AuthError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/deliveries/{id}":{"get":{"operationId":"getWebhookDelivery","summary":"Retrieve a webhook delivery","description":"One delivery attempt including the `payload` that was sent — the list endpoint omits payloads. Use this to inspect the exact body your endpoint received, or to forward it somewhere else (this is how `printedsimply listen` replays events to localhost). `event_id` is the logical event this attempt belongs to, and equals `payload.id`.","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The attempt, with the exact body that was POSTed so a receiver that lost one can replay it against its own handler.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookDeliveryDetail"}}}]},"example":{"status":"success","data":{"id":"ccbkfrz5r3gcby08k4iqiuxwb","webhook_id":"whk_hCyrY24j_gUrQWxJ7ji6s9st","event":"order.shipped","event_id":"evt_ey8SQMTGI1sCP-c0JpwB3AyX","payload":{"id":"evt_ey8SQMTGI1sCP-c0JpwB3AyX","data":{"status":"shipped","event_id":"evt_o0ONdH8-7LUjFVrEVoIyufJ0","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","provider":"printedsimply","tracking":{"url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDozMi4wMDBaIn0.50de0b297f5ed5a39f7b618a2e6f8138f8d50b31a2daabe2709af8f2d7265b25","number":"TRK-TEST-JCWX8ET5IGBT","carrier":"UPS"},"test_mode":true,"tracking_url":"https://printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDozMi4wMDBaIn0.50de0b297f5ed5a39f7b618a2e6f8138f8d50b31a2daabe2709af8f2d7265b25","portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk","tracking_number":"TRK-TEST-JCWX8ET5IGBT","tracking_carrier":"UPS","estimated_delivery":{"latest":"2026-10-29","earliest":"2026-10-23","ships_by":"2026-10-19","is_estimate":true}},"event":"order.shipped","timestamp":"2026-10-15T15:00:32.000Z"},"status_code":200,"success":true,"failure_cause":null,"created_at":"2026-10-15T15:00:32.000Z"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/deliveries/{id}/redeliver":{"post":{"operationId":"redeliverWebhook","summary":"Replay a webhook delivery","description":"Re-sends the stored payload to your endpoint with a fresh signature. Safe to retry (Idempotency-Key supported).","tags":["Webhooks"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The result of making the attempt. A 200 here means the attempt was MADE — read redelivered to learn whether your endpoint accepted it.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookRedelivery"}}}]},"example":{"status":"success","data":{"delivery_id":"ccbkfrz5r3gcby08k4iqiuxwb","redelivered":true,"status_code":200,"error":null},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/inbox":{"get":{"operationId":"getWebhookInbox","summary":"Read the hosted test inbox","description":"The account's hosted test inbox and the requests it kept, newest first, paged by cursor (see Pagination). The inbox keeps its newest 100, each for at most 7 days, so a walk is a few pages at most. Each entry is exactly what an endpoint would have received for one delivery attempt — every header as signed (`X-PS-Signature-V2` among them) and the raw `body`, byte for byte — so a signature check can be run against it with the secret POST /v1/webhooks/inbox returned. A test key only: the inbox receives test events and nothing else, and a live key is 400 `test_mode_only`. 404 `not_found` until the inbox is made.","tags":["Webhooks"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Entries per page: 20 by default, 100 at most. A value above 100 is clamped to 100 and one below 1 to 1, a fraction is truncated, and a missing, zero or non-numeric value falls back to 20; none is refused."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor: send back `meta.next_cursor` from the previous page for the page after it; omit it for the first page. Good only on this list, with a key of the same account and mode (test or live), and with the same filters (only `limit` may change); an altered or foreign cursor, or one sent with other filters, is refused with 400 validation_error. Wins over `page` when both are sent."}],"responses":{"200":{"description":"The inbox subscription (`inbox: true`) and a page of its kept requests, newest first; meta carries has_more and next_cursor","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookInbox"},"meta":{"$ref":"#/components/schemas/ListMeta"}}}]},"example":{"status":"success","data":{"webhook":{"id":"whk_Pxpo7Z0TfZI3H2ZY7yS2tukP","url":"https://printedsimply.com/dashboard/webhooks/inbox","events":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:49.000Z","secret_rotated_at":null,"previous_secret_expires_at":null,"rotation_pending":false,"inbox":true},"entries":[{"id":"ce51qmztk5pep7spdwg9rg58q","event":"order.shipped","event_id":"evt_6GV9KxH-xHgHOcS13DqpCgDS","received_at":"2026-10-15T15:00:49.000Z","headers":{"X-MF-Event":"order.shipped","X-PS-Event":"order.shipped","Content-Type":"application/json","X-MF-Event-Id":"evt_6GV9KxH-xHgHOcS13DqpCgDS","X-PS-Event-Id":"evt_6GV9KxH-xHgHOcS13DqpCgDS","X-MF-Signature":"sha256=ff059fc56400150adea84edb9f5ce87df993fa67c28c136cbfaae542c4ac05f0","X-MF-Timestamp":"1792076449","X-PS-Signature":"sha256=ff059fc56400150adea84edb9f5ce87df993fa67c28c136cbfaae542c4ac05f0","X-PS-Timestamp":"1792076449","X-MF-Signature-V2":"t=1792076449,v1=7e591194144fe43fdd5e55fbaae9a09ef3f297f62c15578829a73d4019c90ac0","X-PS-Signature-V2":"t=1792076449,v1=7e591194144fe43fdd5e55fbaae9a09ef3f297f62c15578829a73d4019c90ac0","X-MF-Delivery-Attempt":"1","X-PS-Delivery-Attempt":"1"},"body":"{\"id\":\"evt_6GV9KxH-xHgHOcS13DqpCgDS\",\"data\":{\"order_id\":\"test_ord_tVI4josqx9ZnMqHu-aBSFxsn\",\"test_mode\":true,\"tracking_number\":\"9400111899223197428490\"},\"event\":\"order.shipped\",\"timestamp\":\"2026-10-15T15:00:49.000Z\"}"}]},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a","meta":{"limit":20,"has_more":false,"next_cursor":null}}}}},"400":{"description":"A live key (test_mode_only): the inbox is read with a test key. Or a cursor this list did not issue to this account (validation_error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]},"post":{"operationId":"createWebhookInbox","summary":"Create the hosted test inbox","description":"Make the account's hosted test inbox: a test-only subscription to every event this API offers, whose deliveries are kept for you to read with GET /v1/webhooks/inbox instead of being POSTed anywhere — a test order's events, signed and in full, with no endpoint, tunnel or install of your own. Takes no body. One inbox per account: a later call answers the same inbox with `created: false` and `secret: null` (rotate its secret with POST /v1/webhooks/{id}/rotate-secret if you lost it). Its `url` is the dashboard page that shows it and is never dialled; its url and mode cannot be changed. A test key only: a live key is 400 `test_mode_only`, and a body field (a `url`, a `mode`) is 400 `validation_error` — to receive events at a URL of your own, use POST /v1/webhooks.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{}},"example":{}}}},"responses":{"200":{"description":"The inbox, and on the call that made it the signing secret in the clear — the one time it is returned, other than a rotation.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookInboxCreated"}}}]},"example":{"status":"success","data":{"id":"whk_Pxpo7Z0TfZI3H2ZY7yS2tukP","url":"https://printedsimply.com/dashboard/webhooks/inbox","events":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved"],"active":true,"mode":"test","created_at":"2026-10-15T15:00:49.000Z","secret_rotated_at":null,"previous_secret_expires_at":null,"rotation_pending":false,"inbox":true,"created":true,"secret":"95677446d44ce8482bae8540120310988cf90314cce8db3fed338d0918c5c296"},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"A live key (test_mode_only), or a field in the body (validation_error): the inbox takes none.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"413":{"$ref":"#/components/responses/PayloadTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}},"/webhooks/inbox/entries":{"delete":{"operationId":"emptyWebhookInbox","summary":"Empty the hosted test inbox","description":"Delete every request the account's hosted test inbox kept, and keep the inbox: its id, `url`, events and signing secret are unchanged, so the next test event lands in an empty inbox you can verify with the secret you saved. Only this account's inbox entries are deleted, never a subscription, its queued events or its delivery log. Takes no body. Answers the inbox's id and how many entries were removed, 0 when it was already empty (so a retry is harmless; an `Idempotency-Key` replays the first answer). A test key only: a live key is 400 `test_mode_only`. 404 `not_found` until the inbox is made with POST /v1/webhooks/inbox. Deleting the inbox itself is DELETE /v1/webhooks/{id}.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The inbox's id and the number of entries removed","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/SuccessEnvelope"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookInboxEmptied"}}}]},"example":{"status":"success","data":{"webhook_id":"whk_Pxpo7Z0TfZI3H2ZY7yS2tukP","removed":1},"request_id":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"}}}},"400":{"description":"A live key (test_mode_only): the inbox is emptied with a test key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}},"headers":{"Vary":{"$ref":"#/components/headers/Vary"}}},"401":{"$ref":"#/components/responses/AuthError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}},"x-required-scopes":["webhooks:manage"],"security":[{"ApiKeyAuth":[]},{"OAuth2":["webhooks:manage"]}]}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key from your dashboard. Test keys start with `ps_test_`, live keys with `ps_live_`. A key created without a scope list has full access; a scoped key must hold every scope in the operation's `x-required-scopes`. Keys are minted with the prefix `ps_live_` or `ps_test_`; keys created earlier carry `mf_live_` or `mf_test_` and keep working, with the same checksum. A key is its prefix and 38 base62 characters (0-9, A-Z, a-z): 32 random, then a 6-character checksum, the CRC-32 (IEEE) of the 32 random characters written in base62, most significant digit first, padded with 0 to six. A secret scanner matches a key with `\\b(?:ps|mf)_(?:live|test)_[0-9A-Za-z]{38}\\b`. A key in that shape whose checksum fails is refused with 401 `invalid_api_key` before any lookup. Keys created before the checksum was added (32 characters after the prefix, which may include `_` and `-`, no checksum) keep working, but that pattern does not match them: `\\bmf_(?:live|test)_[0-9A-Za-z_-]{32}(?![0-9A-Za-z_-])` does. Rotating an older key gives one in the new shape."},"OAuth2":{"type":"oauth2","description":"Authorization-code + PKCE flow for platform integrations. Access tokens (`oat_`) go in the same `x-api-key` header (or as a Bearer token) and are enforced by the same scope checks as API keys. Discovery: /.well-known/oauth-authorization-server","flows":{"authorizationCode":{"authorizationUrl":"https://printedsimply.com/oauth/authorize","tokenUrl":"https://printedsimply.com/api/oauth/token","refreshUrl":"https://printedsimply.com/api/oauth/token","scopes":{"orders:read":"Read orders","orders:write":"Create and modify orders","products:read":"Read products","pricing:read":"Read pricing","webhooks:manage":"Manage webhooks","assets:read":"Read assets","assets:write":"Create and modify assets","artwork:write":"Create and modify artwork","mockups:write":"Create and modify mockups","events:read":"Read events"}}}},"BearerAuth":{"type":"http","scheme":"bearer","description":"The same credential sent as `Authorization: Bearer <credential>`. Accepts a dashboard API key or a platform OAuth access token (`oat_...`), which /api/oauth/token issues as `token_type: \"Bearer\"`."}},"parameters":{"WebhookSignatureV2":{"name":"X-PS-Signature-V2","in":"header","required":true,"description":"The signature to verify: `t=<unix seconds>,v1=<hex>`, where `v1` is HMAC-SHA256 with your endpoint's signing secret over `<t>.<raw body>`. Compare in constant time against the raw bytes received, and refuse a `t` more than 300 seconds from your clock: the timestamp is inside the signed material, so a captured request cannot be replayed later. While a secret rotation is in its grace window the header carries one `v1` per live secret, the new one first; accept the request when any `v1` matches a secret you hold.","schema":{"type":"string","pattern":"^t=\\d+(,v1=[0-9a-f]+)+$"},"example":"t=1760536800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"},"WebhookSignature":{"name":"X-PS-Signature","in":"header","required":true,"description":"The original signature, kept for receivers written against it: `sha256=<hex>`, HMAC-SHA256 with the signing secret over the raw body alone. It carries no timestamp, so a captured request verifies forever; prefer `X-PS-Signature-V2`.","deprecated":true,"schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}},"WebhookSignaturePrevious":{"name":"X-PS-Signature-Previous","in":"header","required":false,"description":"Only while a secret rotation is in its grace window (POST /webhooks/{id}/rotate-secret): `X-PS-Signature` computed with the outgoing secret, for a receiver still verifying the single-valued header with it.","deprecated":true,"schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}},"WebhookTimestamp":{"name":"X-PS-Timestamp","in":"header","required":true,"description":"The `t` of `X-PS-Signature-V2`: when this attempt was signed, in Unix seconds. Read it from the signature header when you verify; this copy is not signed on its own.","schema":{"type":"string","pattern":"^\\d+$"}},"WebhookEventHeader":{"name":"X-PS-Event","in":"header","required":true,"description":"The event's name, the body's `event`, so a router can dispatch before it parses the body.","schema":{"type":"string"}},"WebhookEventId":{"name":"X-PS-Event-Id","in":"header","required":true,"description":"The event's id, the body's `id`: the same on every retry of this event and on a later redelivery or replay of it. Record it once handled and drop a delivery whose id you have handled already; the retry ladder resends an identical body.","schema":{"type":"string"}},"WebhookDeliveryAttempt":{"name":"X-PS-Delivery-Attempt","in":"header","required":true,"description":"Which attempt this is: `1` for the first, counting up on each retry; `redelivery` for one you asked for (POST /webhooks/deliveries/{id}/redeliver); `test` for the dashboard's Test button.","schema":{"type":"string","pattern":"^(\\d+|redelivery|test)$"}},"WebhookTraceparent":{"name":"traceparent","in":"header","required":false,"description":"W3C Trace Context, on a delivery an API request caused: the trace id that request's response carried in its own `traceparent`, and a span id of this attempt's own. Absent on a delivery no request caused (the sandbox's lifecycle sweep, a status update from the plant). Not part of the signature.","schema":{"type":"string","pattern":"^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$"}},"LegacyWebhookSignatureV2":{"name":"X-MF-Signature-V2","in":"header","required":true,"description":"Deprecated alias of `X-PS-Signature-V2`, with the same value. Sent on every delivery that carries `X-PS-Signature-V2`, for receivers written before the PrintedSimply rename; read `X-PS-Signature-V2`.","schema":{"type":"string","pattern":"^t=\\d+(,v1=[0-9a-f]+)+$"},"example":"t=1760536800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd","deprecated":true},"LegacyWebhookSignature":{"name":"X-MF-Signature","in":"header","required":true,"description":"Deprecated alias of `X-PS-Signature`, with the same value. Sent on every delivery that carries `X-PS-Signature`, for receivers written before the PrintedSimply rename; read `X-PS-Signature`.","deprecated":true,"schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}},"LegacyWebhookSignaturePrevious":{"name":"X-MF-Signature-Previous","in":"header","required":false,"description":"Deprecated alias of `X-PS-Signature-Previous`, with the same value. Sent on every delivery that carries `X-PS-Signature-Previous`, for receivers written before the PrintedSimply rename; read `X-PS-Signature-Previous`.","deprecated":true,"schema":{"type":"string","pattern":"^sha256=[0-9a-f]{64}$"}},"LegacyWebhookTimestamp":{"name":"X-MF-Timestamp","in":"header","required":true,"description":"Deprecated alias of `X-PS-Timestamp`, with the same value. Sent on every delivery that carries `X-PS-Timestamp`, for receivers written before the PrintedSimply rename; read `X-PS-Timestamp`.","schema":{"type":"string","pattern":"^\\d+$"},"deprecated":true},"LegacyWebhookEventHeader":{"name":"X-MF-Event","in":"header","required":true,"description":"Deprecated alias of `X-PS-Event`, with the same value. Sent on every delivery that carries `X-PS-Event`, for receivers written before the PrintedSimply rename; read `X-PS-Event`.","schema":{"type":"string"},"deprecated":true},"LegacyWebhookEventId":{"name":"X-MF-Event-Id","in":"header","required":true,"description":"Deprecated alias of `X-PS-Event-Id`, with the same value. Sent on every delivery that carries `X-PS-Event-Id`, for receivers written before the PrintedSimply rename; read `X-PS-Event-Id`.","schema":{"type":"string"},"deprecated":true},"LegacyWebhookDeliveryAttempt":{"name":"X-MF-Delivery-Attempt","in":"header","required":true,"description":"Deprecated alias of `X-PS-Delivery-Attempt`, with the same value. Sent on every delivery that carries `X-PS-Delivery-Attempt`, for receivers written before the PrintedSimply rename; read `X-PS-Delivery-Attempt`.","schema":{"type":"string","pattern":"^(\\d+|redelivery|test)$"},"deprecated":true},"OrderId":{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The order's `order_id`, or its `portal_order_id` (`ord_…`): either resolves the same order. An id the order answered to before it was re-routed resolves too."},"ItemId":{"name":"itemId","in":"path","required":true,"schema":{"type":"string"},"description":"Item ID"},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Unique key for idempotent requests. If you retry a request with the same key within 24 hours, you receive the original response. The key is bound to the request payload: reusing it for a different payload returns 422 `idempotency_key_reuse` rather than the original response, so a changed request is never silently dropped. The binding is over the JSON's content, not its bytes — keys in another order or other whitespace are the same request. Generate a new key for each new request. On an operation that moves money (an order, a batch, an item, a shipping or address change, a cancellation's refund), a key that cannot be checked because the idempotency ledger is unreachable is answered 503 `idempotency_unavailable` with `Retry-After`, and nothing runs or is charged: retry with the same key."}},"headers":{"X-Request-Id":{"schema":{"type":"string","format":"uuid"},"description":"Unique identifier for this request, useful for debugging and support."},"traceparent":{"schema":{"type":"string","pattern":"^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$","examples":["00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"]},"description":"W3C Trace Context (https://www.w3.org/TR/trace-context/): the trace this request is part of, and this request's own span. When the request sent a valid `traceparent`, its trace id is kept and its flags' `sampled` bit with it; an invalid or absent one is ignored and a new trace is started (flags `01`). On every response an operation answers, success or error, in the envelope or as problem details. The answer to `OPTIONS` carries no `traceparent`. The trace id is in your request log beside the request id, and finds the request there. A webhook delivery an API request caused carries `traceparent` with the same trace id and a span id of its own."},"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Maximum requests allowed per window."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Requests remaining in the current window."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"When the per-minute rate limit window resets, as a Unix timestamp in MILLISECONDS. On a 429, read `Retry-After` instead: it is the wait in seconds for both 429s, including the daily quota, which this header does not describe."},"RateLimit-Policy":{"schema":{"type":"string","examples":["\"minute\";q=60;w=60","\"minute\";q=60;w=60, \"day\";q=1000;w=86400"]},"description":"The windows the request was counted in, as an IETF RateLimit-Policy Structured Field (draft-ietf-httpapi-ratelimit-headers): each item a quoted policy name with `q` (the quota) and `w` (the window in seconds). Always `\"minute\"`; a `daily_quota_exceeded` 429 adds `\"day\"`."},"RateLimit":{"schema":{"type":"string","examples":["\"minute\";r=59;t=42","\"minute\";r=12;t=30, \"day\";r=0;t=3600"]},"description":"Where each window in RateLimit-Policy stands: `r` requests remaining and `t` seconds until it resets — a delta, so no clock and no unit to guess. On a 429 the exhausted item's `t` is the wait; a client that backs off reads the largest `t` among items with `r=0`, falls back to Retry-After, and never waits less than Retry-After when the response carries one (an `ai_daily_limit` 429 waits for the day's AI allowance, which no item here describes)."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying. Set on both 429s: seconds to the sliding window's reset for `rate_limit_exceeded`, seconds to the daily reset for `daily_quota_exceeded`. Set on a 503 the API expects to clear, such as `idempotency_unavailable`: seconds before retrying the same request."},"X-Daily-Limit":{"schema":{"type":"integer"},"description":"Requests allowed per day on this account's plan. Set on a `daily_quota_exceeded` 429."},"X-Daily-Remaining":{"schema":{"type":"integer"},"description":"Requests remaining in today's quota. Set on a `daily_quota_exceeded` 429."},"X-Catalog-Stale":{"schema":{"type":"string","enum":["true","false"]},"description":"Whether this answer came out of the last-good snapshot rather than a live read. Sent on every catalog response, BOTH ways, deliberately: absent cannot mean fresh, because absent is also what every non-catalog route sends. Gate anything that republishes the catalog on `false`."},"X-Catalog-Degraded":{"schema":{"type":"string"},"description":"Why the catalog is degraded, when it is: `oms_401` and the other `oms_<status>` codes (the facility answered non-2xx), `oms_bad_shape` (a 200 this API could not read as a catalog), `oms_unreachable`, `circuit_open` (the facility is known-bad and the live call was skipped), or `not_configured` (no credentials for that facility here — an operator fact, not an outage). Also set on the 502 when nothing was servable at all, and on a 200 whose rows came from our price book (`source: \"price_book\"`) because our plant's catalog had nothing of its own to serve."},"Vary":{"schema":{"type":"string","examples":["Accept"]},"description":"`Accept`, on every response an operation answers in the envelope, success or error: an error is the envelope or, when `Accept` prefers `application/problem+json`, an RFC 9457 problem document. Merged with any other field the response varies on."},"Allow":{"schema":{"type":"string","examples":["OPTIONS, POST","DELETE, GET, HEAD, OPTIONS"]},"description":"The methods the path takes, on a `405 method_not_allowed` (and on a `501 method_not_implemented` at a path that exists): the ones its operations list, `HEAD` wherever it takes `GET`, and `OPTIONS`. `details.allow` carries the same list."},"Warning":{"schema":{"type":"string"},"description":"RFC 9111 `110 - \"Response is stale (upstream catalog unavailable)\"`. Rides with `X-Catalog-Stale: true`."}},"schemas":{"ListMeta":{"type":"object","description":"How a list answer pages. To read a whole list, ask for it without `cursor`, then send each answer's `next_cursor` back as `cursor` until `has_more` is false. Keep the other query parameters the same on every page.","required":["has_more","next_cursor"],"properties":{"has_more":{"type":"boolean","description":"Whether another page follows this one. Decided from one row past this page, never from a page that came back short — so a page after `has_more: true` can come back empty only if rows were deleted in between, and the walk still ends at the first `has_more: false`."},"next_cursor":{"type":["string","null"],"description":"Send it back as `cursor` for the next page; null exactly when `has_more` is false. Opaque: pass it back unchanged. Good only on the list that returned it, with a key of the same account and mode (test or live), and with the same filters."},"limit":{"type":"integer","description":"The page size applied, which is not necessarily the one asked for: a larger `limit` is clamped. Absent only on a GET /products request without `cursor` that sent no `limit`, which answers every match."},"total":{"type":"integer","description":"Every row matching the request's filters, on the lists that count them (orders, an order's events, assets and products)."},"page":{"type":"integer","deprecated":true,"description":"The page number of the deprecated offset walk, on the lists that published it (orders and an order's events, on every request without `cursor`; products, on a request that sent `page`). Absent on a cursor walk, where no page number would be true."},"pages":{"type":"integer","deprecated":true,"description":"`total` over `limit`, rounded up, beside `page` (orders and an order's events). Absent on a cursor walk."},"since":{"type":["string","null"],"description":"On GET /order/{id}/events only: where to ask from next — the position of the last event in this answer, or, when it has none, the position the request read from. Send it back as `since` (with `wait_seconds` to wait for the next event). Unlike `next_cursor` it is not null at the end of the history; it is null only when the order has no events and the request named no position. Opaque, and bound like a cursor to this order, account and key mode."}}},"SuccessEnvelope":{"type":"object","required":["status","data","request_id"],"properties":{"status":{"type":"string","enum":["success"]},"data":{"description":"The payload. Its shape is declared on each operation; on the one operation that answers with a bare array, it is an array."},"request_id":{"type":"string","format":"uuid"},"meta":{"type":"object","properties":{"page":{"type":"integer"},"total":{"type":"integer"},"limit":{"type":"integer","description":"The page size actually applied, which is not necessarily the one asked for — the operation's own ceiling wins."},"pages":{"type":"integer","description":"`total` over `limit`, rounded up."},"next_cursor":{"type":["string","null"],"description":"On a cursor-paged operation: send this back as `cursor` for the next page. Null on the last page. Absent on operations that do not page by cursor."},"has_more":{"type":"boolean","description":"On a list operation whose `meta` is `ListMeta`: whether another page follows this one."}}}}},"ErrorEnvelope":{"type":"object","required":["status","error","request_id"],"properties":{"status":{"type":"string","enum":["error"]},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable: branch on this, never on `message`. Every code is listed, with what to do about it, at /docs/errors."},"message":{"type":"string","description":"What went wrong, for a person reading a log."},"doc_url":{"type":"string","format":"uri","description":"Where this code is documented, on this deployment's own docs."},"suggestion":{"type":"string","description":"What to do about this code: the next step a person or an agent can take."},"details":{"description":"Structured context for this code (an object), when it has any: on a validation error `fieldErrors`, and `errors` with a JSON pointer to each refused value (see the ValidationError response); the SKUs a refusal names; the problems an artwork check found."}}},"request_id":{"type":"string","format":"uuid"}}},"ProblemDetails":{"type":"object","description":"An error as an RFC 9457 problem document, answered with `Content-Type: application/problem+json` in place of the envelope when the request's `Accept` prefers `application/problem+json`. The standard members, then everything the envelope says as extension members, so nothing is lost by asking for it.","required":["type","title","status","detail","code"],"properties":{"type":{"type":"string","format":"uri","description":"Where the code is documented, as an absolute URI: the same link as `doc_url`. `about:blank` for a code with no entry."},"title":{"type":"string","description":"The code's short title, the same on every occurrence: `Order not found` for `order_not_found`."},"status":{"type":"integer","minimum":400,"maximum":599,"description":"The HTTP status: always the response's own."},"detail":{"type":"string","description":"What went wrong this time: the envelope's `error.message`."},"instance":{"type":"string","format":"uri","description":"This occurrence: `urn:uuid:<request_id>`, the id `X-Request-Id` and `request_id` carry. An idempotent replay is the original answer, so it repeats the original's `request_id` and `instance` under a new `X-Request-Id`. Absent when the answer has no request id."},"code":{"type":"string","description":"Extension. Machine-readable, as the envelope's `error.code`: branch on this."},"request_id":{"type":"string","format":"uuid","description":"Extension. The envelope's `request_id`."},"doc_url":{"type":"string","format":"uri","description":"Extension. The envelope's `error.doc_url`, the same link as `type`."},"suggestion":{"type":"string","description":"Extension. What to do about it: the envelope's `error.suggestion`."},"details":{"description":"Extension. The envelope's `error.details`, when the code has structured context to give."}},"additionalProperties":true},"AddressValidationRequest":{"type":"object","required":["address1","city","state","zip"],"description":"An address as `address_to` takes it; the name is optional and the country defaults to US.","properties":{"name":{"type":"string"},"company":{"type":"string"},"address1":{"type":"string","minLength":1},"address2":{"type":"string"},"city":{"type":"string","minLength":1},"state":{"type":"string","minLength":1,"description":"A USPS code (TX, PR, AE) or a state's full name."},"zip":{"type":"string","minLength":1},"country":{"type":"string","minLength":2,"maxLength":2,"default":"US","description":"ISO 3166-1 alpha-2. Only US addresses are checked."},"phone":{"type":"string"},"email":{"type":"string","format":"email"}}},"AddressFinding":{"type":"object","required":["code","severity","field","message"],"properties":{"code":{"type":"string","enum":["zip_format","state_unknown","zip_state_mismatch","zip_state_border","zip_unassigned","po_box","military_state","military","address2_split","not_checked","delivery_point_not_found","delivery_point_incomplete","delivery_point_confirmed"],"description":"The `delivery_point_*` codes come from the configured USPS or ShipStation service on live address-validation, quote, order, bulk and address-update requests. `delivery_point_not_found` (`error`) is a definite invalid-address verdict; `delivery_point_incomplete` (`warning`) means the unit or full address could not be confirmed; `delivery_point_confirmed` (`info`) means this address was confirmed. Test keys never call the service. Unconfigured or unavailable services leave only the local ZIP/state findings, without claiming street confirmation."},"severity":{"type":"string","enum":["error","warning","info"],"description":"`error`: the address cannot be right as written, and a live order or quote is refused with 422 address_invalid. `warning`: worth a look, never refused. `info`: what normalization did, or that nothing was checked."},"field":{"type":"string","enum":["address1","address2","city","state","zip","country"]},"message":{"type":"string"}}},"AddressCheck":{"type":"object","required":["valid","checked","normalized","findings"],"properties":{"valid":{"type":"boolean","description":"False when any finding is an `error`."},"checked":{"type":"boolean","description":"False for an address outside the US, which is passed through as written."},"normalized":{"type":"object","required":["address1","city","state","zip","country"],"description":"The address in USPS style: upper case, no punctuation, ZIP+4 hyphenated, the state as its code, a unit typed into address1 moved to address2. Name, company, phone and email are trimmed and otherwise as sent.","properties":{"name":{"type":"string"},"company":{"type":"string"},"address1":{"type":"string"},"address2":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string"}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/AddressFinding"}}}},"Address":{"type":"object","required":["name","address1","city","state","zip","country"],"properties":{"name":{"type":"string","minLength":1},"company":{"type":"string"},"address1":{"type":"string","minLength":1},"address2":{"type":"string"},"city":{"type":"string","minLength":1},"state":{"type":"string","minLength":1},"zip":{"type":"string","minLength":1},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code"},"phone":{"type":"string"},"email":{"type":"string","format":"email"}}},"Shipping":{"type":"object","properties":{"carrier":{"type":"string","description":"For PrintedSimply, omit to use the priced method's USPS service, or name USPS/the matching USPS service. A conflicting carrier is refused before payment."},"service":{"type":"string","description":"For PrintedSimply, omit for the method default. If supplied, it must match: standard usps_ground_advantage, letter usps_first_class_mail, priority usps_priority_mail, express usps_priority_mail_express. A method must also be offered; naming its service does not enable it."},"method":{"$ref":"#/components/schemas/ShippingMethod"}}},"OrderItem":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","maximum":10000,"minimum":1},"artwork_url":{"type":"string","format":"uri","description":"The artwork for this line, as a public http(s) URL. Printed when set; otherwise the line's `print_file` is. Send one of the two: a line with neither has nothing to print."},"print_file":{"type":"string","description":"The artwork for this line: a public http(s) URL, or the id of a file you uploaded with POST /assets (`ast_…`), resolved to that file's URL. Read when `artwork_url` is not set."},"variable_data":{"$ref":"#/components/schemas/VariableData"},"artwork":{"$ref":"#/components/schemas/ArtworkDeclaration"}}},"ArtworkDeclaration":{"type":"object","additionalProperties":false,"description":"Optional: how this line's artwork was made, and who approved it and when. A declaration you state, stored on the line as sent (approved_by sanitized, approved_at in UTC), echoed on the order's reads (`line_items[].artwork`) and on its `order.created` webhook. It never changes the price, the routing or the artwork check, and a line without it is placed exactly as before. Strict: an unknown key is a 400 validation_error.","properties":{"provenance":{"type":"string","enum":["human","ai_assisted","ai_generated"],"description":"How the artwork was made: `human` (made by a person), `ai_assisted` (a person made it with a model's help) or `ai_generated` (a model made it)."},"approved_by":{"type":"string","minLength":1,"maxLength":200,"description":"Who approved the artwork for print: a name or an email address, at most 200 characters. Control and invisible formatting characters are removed and whitespace collapsed before it is stored. Send it with approved_at."},"approved_at":{"type":"string","format":"date-time","description":"When the artwork was approved: an ISO 8601 date-time with a time zone, not in the future. Stored and echoed in UTC. Send it with approved_by."}}},"VariableData":{"type":"object","required":["slots"],"description":"A different name, number or QR payload on every unit of this line. Accepted only on flat goods made at our own plant (stickers, decals, magnets, posters); an order carrying it is produced there. Declare the named slots of the line's design, then give every unit a value for every slot: from `units` (one entry per unit, so its length must equal `quantity`), from a `sequence` that fills one slot, or both. An optional `qr.template` builds each unit's QR payload from its values. Preflighted before anything is priced or charged: a value longer than its slot, a `units` array of the wrong length, an undeclared slot, a missing value or another plant's SKU is refused with 422 `variable_data_invalid`, naming each problem by item, unit and slot. Never changes the price. Live orders and quotes carrying it are refused with 422 `variable_data_unavailable` until production is switched on; sandbox (ps_test_*) orders accept it end to end.","properties":{"slots":{"type":"array","minItems":1,"maxItems":10,"description":"The named text slots of the line's design, each with the most characters it holds.","items":{"type":"object","required":["name","max_length"],"properties":{"name":{"type":"string","pattern":"^[a-z][a-z0-9_]{0,39}$","description":"Lower-case letters, digits and underscores, starting with a letter, e.g. name or serial. Each name once."},"max_length":{"type":"integer","minimum":1,"maximum":200,"description":"The longest value this slot prints. A longer value is refused as `overflow`, not shrunk or cut."}}}},"units":{"type":"array","minItems":1,"maxItems":10000,"description":"One object per unit, in print order: { slot name: value }. Its length must equal the line's quantity. Values are strings or numbers, with no line breaks. Omit it when a `sequence` fills every slot.","items":{"type":"object","additionalProperties":{"type":["string","number"]}}},"sequence":{"type":"object","required":["slot"],"description":"Numbers one slot per unit: prefix + (start + (unit - 1) × step), zero-padded to `pad` digits, e.g. A-0001, A-0002. A unit may not also set that slot.","properties":{"slot":{"type":"string","minLength":1,"maxLength":40,"description":"A declared slot."},"prefix":{"type":"string","maxLength":40,"description":"Printed before the number. Default empty."},"start":{"type":"integer","minimum":0,"maximum":1000000000,"description":"The first unit's number. Default 1."},"step":{"type":"integer","minimum":1,"maximum":1000000,"description":"Added per unit. Default 1."},"pad":{"type":"integer","minimum":0,"maximum":12,"description":"Zero-pad the number to this many digits. Default 0 (no padding)."}}},"qr":{"type":"object","required":["template"],"description":"Each unit's QR payload, built from its values.","properties":{"template":{"type":"string","minLength":1,"maxLength":1000,"description":"Text with {slot} placeholders, e.g. https://example.com/t/{serial}. Every placeholder must name a declared slot."},"max_length":{"type":"integer","minimum":1,"maximum":1000,"description":"The longest payload accepted, for a small sticker whose code must stay scannable. Default 1000."}}}}},"ArtworkCheck":{"type":"object","required":["lines"],"properties":{"lines":{"type":"array","items":{"$ref":"#/components/schemas/ArtworkLineVerdict"},"description":"One entry per line made at our own plant (stickers, decals, magnets, posters) that carries artwork, in item order. A line another lane makes is graded there, not here."},"missing_artwork":{"type":"array","items":{"$ref":"#/components/schemas/MissingArtwork"},"description":"Test keys (ps_test_*) only, on POST /order, POST /orders/bulk and POST /{id}/item: the lines made at our own plant that carry no artwork at all, which a live request is refused for with 422 `artwork_required`. Absent when there are none."},"would_refuse":{"type":"boolean","description":"Test keys (ps_test_*) only: whether a live order with these lines would have been refused, with 422 `artwork_not_printable` or (for a line in `missing_artwork`) `artwork_required`. A test key is never refused for artwork, so placeholder artwork still places a sandbox order. Absent on a live order, which either was refused or was not."}}},"MissingArtwork":{"type":"object","required":["item_index","sku"],"properties":{"order_index":{"type":"integer","minimum":0,"description":"POST /orders/bulk's 422 only: the order's position in `orders`."},"item_index":{"type":"integer","minimum":0,"description":"The line's position in the order's `items`, 0-based."},"sku":{"type":"string"}}},"ArtworkLineVerdict":{"type":"object","required":["item_index","sku","status","findings"],"properties":{"item_index":{"type":"integer","minimum":0,"description":"The line's position in the request's `items`, 0-based."},"sku":{"type":"string"},"status":{"type":"string","enum":["passed","warned","refused","unverified"],"description":"passed: read, nothing to say. warned: read, at least one warning. refused: at least one error (a live order is refused). unverified: the file could not be read just now (see the `unverified` finding); the order is not refused for it."},"width_px":{"type":"integer","minimum":1,"description":"The file's width in pixels. Absent when it was not read, or is vector."},"height_px":{"type":"integer","minimum":1,"description":"The file's height in pixels."},"print_size_in":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2,"description":"The print size resolution was measured against, width and height in inches: the SKU's `WxH`, else the line's `size`. Absent when neither names one."},"effective_dpi":{"type":"integer","minimum":1,"description":"Pixels per inch with the artwork printed as large as the print size allows (turned a quarter if that fits it larger)."},"min_dpi":{"type":"integer","minimum":1,"description":"The product's recommended minimum DPI. Below 75% of it is an error."},"findings":{"type":"array","items":{"type":"object","required":["code","severity","message"],"properties":{"code":{"type":"string","enum":["unreachable","not_an_image","low_resolution","below_recommended_resolution","cmyk","no_transparency","touches_edge","no_bleed","content_in_bleed","no_cutline","size_unknown","vector","unverified","format_not_accepted","not_fetchable","design_embeds_raster","generated_design","placement_clipped","geometry_unverified"]},"severity":{"type":"string","enum":["error","warning","info"],"description":"error refuses a live order; warning and info never do."},"message":{"type":"string","description":"What was found and what to do about it, naming the line."}}}}}},"VariableDataSummary":{"type":"object","required":["item_index","sku","units","slots","proofs","live_production"],"properties":{"item_index":{"type":"integer","minimum":0,"description":"The line's position in the request's `items`."},"sku":{"type":"string"},"units":{"type":"integer","minimum":1,"description":"Units on the line, each printed with its own values."},"slots":{"type":"array","description":"Each declared slot with its `max_length` and the `longest` value any unit gives it: the headroom.","items":{"type":"object","required":["name","max_length","longest"],"properties":{"name":{"type":"string"},"max_length":{"type":"integer"},"longest":{"type":"integer","description":"Characters in this slot's longest value on the line."}}}},"qr":{"type":"object","required":["max_length","longest"],"description":"Present when the line has a QR template: the payload ceiling and the longest payload on the line.","properties":{"max_length":{"type":"integer"},"longest":{"type":"integer"}}},"proofs":{"type":"array","description":"The variant proofs: the first unit, the last, and the longest (the most printed characters, earliest on a tie), with the exact values and QR payload each will print. A unit that is more than one of the three is listed once, with each variant it stands for. Described rather than drawn: a plain line's artwork carries no slot positions to draw a value into.","items":{"type":"object","required":["unit","variants","values"],"properties":{"unit":{"type":"integer","minimum":1,"description":"The unit's position on the line, 1-based, in print order."},"variants":{"type":"array","items":{"type":"string","enum":["first","last","longest"]}},"values":{"type":"object","additionalProperties":{"type":"string"},"description":"Slot name to the text this unit prints."},"qr_payload":{"type":"string","description":"What this unit's QR code encodes. Present when the line has a QR template."}}}},"live_production":{"type":"boolean","description":"Whether a LIVE order carrying this line would be accepted now. False while variable-data production is switched off: sandbox orders run end to end, and a live order or quote is refused with 422 variable_data_unavailable."}}},"OrderActions":{"type":"object","description":"Which changes this order still takes, read from the same checks the operations refuse on, so a client can show or offer an action without trying it first. `true` means the order's own state does not refuse it; the request is still checked as usual (the key's scope; the line an item action names, which must exist and, on a live order at our own plant, carry its artwork; and on a live order the facility, which can refuse a change with 502 fulfillment_error). `false` names the refusal the operation would give. Live PrintedSimply orders have add_item, update_item and upgrade_shipping false; only supported no-cost or downward shipping requests remain requestable. On other orders a shopper paid for (a drop, a checkout link or an agent checkout), `update_item` and `update_shipping` stay true because a decrease or a downgrade is allowed, while an increase or an upgrade is refused 409 shopper_paid_order, as `add_item` is.","required":["cancel","update_address","update_shipping","upgrade_shipping","add_item","update_item","cancel_item"],"properties":{"cancel":{"type":"boolean","description":"DELETE /order/{id}/cancel. True only before production (pending_payment, paid or accepted); false once the order is cancelled (400 already_cancelled) or past that (409 not_cancellable)."},"update_address":{"type":"boolean","description":"PUT /order/{id}/address. False on an order an independent shop made (501 not_supported_by_provider) and once the order is past the point of change (409 order_not_modifiable)."},"update_shipping":{"type":"boolean","description":"PUT /order/{id}/shipping. False for the reasons `update_address` is."},"upgrade_shipping":{"type":"boolean","description":"Positive-cost PUT /order/{id}/shipping upgrades. False for live PrintedSimply orders (501 not_supported_by_provider), shopper-paid orders (409 shopper_paid_order), and when update_shipping is false. A downgrade or unchanged method can still be allowed when this is false."},"add_item":{"type":"boolean","description":"POST /{id}/item. False for the reasons `update_address` is, on live PrintedSimply orders (501 not_supported_by_provider), and on an order a shopper paid for (409 shopper_paid_order)."},"update_item":{"type":"boolean","description":"PUT /{id}/item/{itemId}. False for the reasons `update_address` is and on live PrintedSimply orders (501 not_supported_by_provider). An unchanged quantity is still an allowed no-op."},"cancel_item":{"type":"boolean","description":"DELETE /{id}/item/{itemId}/cancel. False for the reasons `update_address` is."}}},"OrderLine":{"type":"object","required":["id","sku","quantity","unit_price_cents","line_total_cents"],"properties":{"id":{"type":"string","description":"This line's id. Use it as `{itemId}` on PUT /v1/{id}/item/{itemId} and DELETE /v1/{id}/item/{itemId}/cancel."},"sku":{"type":"string"},"quantity":{"type":"integer","minimum":1},"unit_price_cents":{"type":"integer","description":"The unit price the line was placed at, in USD cents. Where a price is a pack total that does not divide into whole cents a unit, this is the line's rate rounded to the cent, for display: it stays as placed when the line's quantity changes and is shared by the lines of one SKU, so it is not necessarily `line_total_cents` over `quantity`. Read the line's money from `line_total_cents`, never as this times `quantity`."},"line_total_cents":{"type":"integer","description":"This line's goods, exact, in USD cents, at its current quantity: what the order charged for them, and what cancelling the line refunds. A quantity change moves it at the rate the line's SKU was placed at on this order, which the order's lines of one SKU share, rounded once to the cent. Equal to `unit_price_cents` × `quantity` on every line priced in whole cents a unit."},"artwork":{"$ref":"#/components/schemas/ArtworkDeclaration","description":"The line's artwork declaration, as the order accepted it: how the artwork was made, and who approved it and when. Absent on a line that declared none."}}},"OrderTracking":{"type":"object","description":"Where the parcel is. The same block on `GET /v1/order/{id}` and on every `GET /v1/orders` row. `number`, `carrier` and `url` are null until the order ships, and equal the flat `tracking_number`, `tracking_carrier` and `tracking_url` beside it. On a live order whose facility reports its own `tracking` object, that object's keys are kept and these are added where it has none.","required":["number","carrier","url","page_url"],"properties":{"number":{"type":["string","null"],"description":"The carrier's tracking number; null until the order ships."},"carrier":{"type":["string","null"],"description":"The carrier that holds the shipment; null until the order ships."},"url":{"type":["string","null"],"description":"The carrier's tracking link; null until the order ships."},"page_url":{"type":["string","null"],"format":"uri","description":"The order's hosted tracking page, the link to give your customer. It exists from the moment the order is placed and shows each step, including before the carrier has a number; the create response's `tracking_url` opens the same page. Null only on an order with no tracking token."}}},"CreateOrderRequest":{"type":"object","required":["address_to","items"],"properties":{"address_to":{"$ref":"#/components/schemas/Address"},"address_from":{"$ref":"#/components/schemas/Address"},"shipping":{"$ref":"#/components/schemas/Shipping"},"items":{"maxItems":100,"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/OrderItem"}},"provider":{"type":"string","enum":["printedsimply"],"default":"printedsimply","description":"Where the order is made: always our own plant here. Omit it, or send `printedsimply`; any other value is refused with 422 `provider_not_available` before anything is priced or charged."},"reference":{"type":"string","maxLength":200,"description":"Your own identifier for this order — a marketplace order number, a cart id, whatever you key on. At most 200 characters, and no NUL byte. Stored verbatim, echoed on the create response, on GET /orders and GET /order/{id}, and on the order webhooks, and filterable with GET /orders?reference=. One reference is one order: while an order of your account placed in the last 30 days with a key of the same mode (a test key against test orders) and not cancelled carries it, another order with the same reference is refused 409 `duplicate_reference` before anything is charged, whatever its Idempotency-Key, and `details.order_id` names the order that has it. Use Idempotency-Key for exactly-once retries of one request; this catches the duplicate sent under a new key. When whether the reference is free cannot be checked, the order is not placed: 503 `reference_check_unavailable`, nothing charged."},"allow_duplicate_reference":{"type":"boolean","default":false,"description":"Place the order even when another order already carries its `reference` (see `reference`). For a reference that is not unique on your side, such as order numbers from two stores that each count from 1. Omit it, or send false, to have a duplicate refused with 409 `duplicate_reference`."},"vat":{"type":"string","description":"Tax-registration number (VAT / IOSS / GST) to declare for this shipment. Stored, echoed on the create response and on GET /orders and GET /order/{id}, and forwarded to the fulfilling facility for the customs declaration. Supply it per order — an international run can ship to several destinations under different registrations. Omit it on domestic shipments."},"quote_token":{"type":"string","description":"A signed quote from POST /orders/quote. When present, the order is charged exactly that quote's locked total instead of being re-priced and the quoted facility routing is honored. Refused (400) when expired, issued to a different key, or when the order's items or shipping method differ from what was quoted."},"payment_intent":{"type":"string","pattern":"^pi_[A-Za-z0-9]+$","description":"Live keys only. When a live order's card needs 3D Secure, the order is answered 402 `authentication_required` with `details.payment_intent` and `details.client_secret`: the amount opened on the card as an authorization only, nothing charged. Have the cardholder approve it in their browser with Stripe.js, loaded with `details.publishable_key` (the intent is on our Stripe account), calling `handleNextAction({ clientSecret })`, then send the same order again with this field set to `details.payment_intent` and a new Idempotency-Key. The order captures that authorization instead of charging the card again. It is refused with 402 `payment_failed` unless it was opened for this account, authorizes exactly this order's total and has not been captured — `details.code` says which check failed — and one the cardholder has not finished approving answers `authentication_required` again. An approval no order captures is released by the card network on its own. Ignored on a test key, which charges nothing."},"approval":{"type":"string","enum":["required"],"description":"`required` holds the order for a person's approval before anything is charged: the order is checked and priced exactly as without it (every refusal it would get, it gets now), then recorded `pending_approval` with nothing charged, nothing counted against the key's spend caps and nothing sent to production, and answered 202 `OrderAwaitingApproval` with `approval_url`, a signed page the person opens with no account. It shows each line's artwork, product, size, quantity and price, the total, the town it ships to and the line's artwork declaration. Approving it places the order through this same operation, as the key it was held with, at the total the person was shown: the checks run again, the card is charged and the order goes to production (`order.approval_approved`, then `order.created` and `order.paid`). If the price has moved, the card is declined or a spend cap refuses it, nothing is charged, `approval.status` reads `failed` with the reason, and the person may approve it again. Declined, or not decided within 72 hours (`approval_expires_at`), the order is cancelled with nothing charged (`order.approval_declined`, `order.approval_expired`); DELETE /order/{id}/cancel cancels it too, with nothing to refund. A held order takes no address, shipping or line change. A `quote_token` is honoured at approval only while it is valid (24 hours): approved later, the order places nothing (`invalid_quote`). Taken on an API key's order only (a platform OAuth token's is 400 validation_error, `details.field: approval`) and not with `payment_intent`. On POST /orders/bulk an entry carrying it fails validation."}}},"ReorderRequest":{"type":"object","additionalProperties":false,"description":"Every field is optional: an empty body places the source order's lines again, to its address, by its shipping method, with its VAT id. The `reference` is never copied from the source; send a new one.","properties":{"items":{"type":"array","minItems":1,"maxItems":100,"description":"Quantities to change, one entry per line: name the line by its `line_item_id` (from `line_items` on GET /order/{id}) or by `sku` where exactly one line carries it (any case). A line not named keeps its quantity. A name that matches no line, or a sku on more than one, is a 400 validation_error whose `details.lines` lists the source's lines.","items":{"type":"object","additionalProperties":false,"required":["quantity"],"properties":{"line_item_id":{"type":"string","minLength":1,"description":"The line's id. Send this or `sku`, not both."},"sku":{"type":"string","minLength":1,"description":"The line's SKU, when exactly one line carries it. Send this or `line_item_id`, not both."},"quantity":{"type":"integer","minimum":1,"maximum":10000}}}},"address_to":{"$ref":"#/components/schemas/Address","description":"Ship to this address instead of the source's."},"address_from":{"$ref":"#/components/schemas/Address","description":"As on POST /order: the return address for this order. An order does not store one, so it is not copied; without it the account's brand return address applies."},"shipping":{"type":"object","additionalProperties":false,"properties":{"method":{"$ref":"#/components/schemas/ShippingMethod"}},"description":"Ship by this method instead of the source's. Without `method`, the source's."},"vat":{"type":"string","description":"As on POST /order: the tax registration number (VAT, IOSS or GST) to declare. Without it, the source's goes again."},"reference":{"type":"string","maxLength":200,"description":"Your own identifier for the new order, as on POST /order: one reference is one order, so the source's is never copied, and one another order of the account carries (last 30 days, same key mode, not cancelled) is refused 409 duplicate_reference before anything is charged."},"allow_duplicate_reference":{"type":"boolean","default":false,"description":"As on POST /order."},"payment_intent":{"type":"string","pattern":"^pi_[A-Za-z0-9]+$","description":"As on POST /order: after a 402 `authentication_required`, the approved `details.payment_intent`, sent with the same reorder and a new Idempotency-Key."}}},"WebhookSubscription":{"type":"object","required":["id","url","events","active","mode","created_at","inbox"],"properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"mode":{"type":"string","enum":["all","test","live"],"description":"Which order mode this subscription accepts. Normalised on the way out, so a stored value this build does not know reads as what it is actually routed as (`all`) rather than as the stored string."},"created_at":{"type":"string","format":"date-time"},"secret_rotated_at":{"type":["string","null"],"format":"date-time"},"previous_secret_expires_at":{"type":["string","null"],"format":"date-time","description":"During a rotation both secrets verify until this moment. Sign-check against both, or accept the older one, until it passes."},"rotation_pending":{"type":"boolean","description":"True while the previous secret is still inside its window."},"inbox":{"type":"boolean","description":"True for the account's hosted test inbox (POST /v1/webhooks/inbox): it receives test events only and keeps them for GET /v1/webhooks/inbox rather than POSTing them, its `url` is the dashboard page that shows them, and its url and mode cannot be changed."}}},"WebhookDelivery":{"type":"object","required":["id","webhook_id","event","success","created_at"],"properties":{"id":{"type":"string"},"webhook_id":{"type":"string"},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied","test.ping"]},"event_id":{"type":["string","null"],"description":"Null only for attempts recorded before the log carried the id."},"status_code":{"type":["integer","null"]},"success":{"type":"boolean"},"failure_cause":{"type":["string","null"],"enum":["dns","unsafe_url","refused","timeout","tls","reset","network","redirect","unauthorized","http_4xx","http_5xx","inbox_refused","egress","signing","internal",null],"description":"Why a failed attempt failed, and so what to fix: `dns`, `refused`, `timeout`, `tls`, `reset`, `redirect`, `unauthorized` and the other endpoint-side causes are your URL's; `egress`, `signing` and `internal` are ours, and the attempt is retried on schedule. Null when delivered, and on a failure logged with neither a cause nor a status."},"created_at":{"type":"string","format":"date-time"}}},"AccountEvent":{"type":"object","required":["id","event","test_mode","created_at"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied"]},"event_id":{"type":["string","null"]},"test_mode":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"data":{"description":"The event's payload, verbatim. Null when none was recorded."}}},"IntegrationAlert":{"type":"object","description":"An integration-health alert: what tripped it, whether it is still open, and the readings.","required":["id","kind","status","subject_type","subject_id","summary","opened_at","resolved_at","resolution","last_evaluated_at","evidence","latest"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["webhook_failing","error_rate_spike","key_expiring"],"description":"Which rule: `webhook_failing`, `error_rate_spike` or `key_expiring`."},"status":{"type":"string","enum":["open","resolved"]},"subject_type":{"type":"string","enum":["webhook","account","api_key"],"description":"What it is about: `webhook` (subject_id is the endpoint's id, as GET /webhooks lists it), `api_key` (a key's id) or `account` (subject_id `live`: the account's live calls)."},"subject_id":{"type":"string"},"summary":{"type":"string","description":"One sentence: what the newest reading says while it is open, or why it resolved."},"opened_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time","description":"Null while it is open."},"resolution":{"type":["string","null"],"enum":["cleared","subject_removed","endpoint_switched_off","key_stopped","dormant",null],"description":"Why it resolved: `cleared` (the reading got clearly better), `subject_removed` (the endpoint or key was deleted), `endpoint_switched_off` (you switched the endpoint off, or set it to take test events only), `key_stopped` (the key was revoked, or stopped working and is no longer called) or `dormant` (nothing to read for 7 days: no delivery attempt, or no live call; not a recovery). Null while it is open."},"last_evaluated_at":{"type":"string","format":"date-time","description":"When the alert was last read; it is read every few minutes while open."},"evidence":{"$ref":"#/components/schemas/IntegrationAlertEvidence","description":"The reading that opened it."},"latest":{"$ref":"#/components/schemas/IntegrationAlertEvidence","description":"The newest reading while it was open: the one it resolved on, once resolved."}}},"IntegrationAlertEvidence":{"description":"The numbers one reading took, by the alert's kind.","oneOf":[{"$ref":"#/components/schemas/WebhookFailingEvidence"},{"$ref":"#/components/schemas/ErrorRateSpikeEvidence"},{"$ref":"#/components/schemas/KeyExpiringEvidence"}]},"WebhookFailingEvidence":{"type":"object","description":"A `webhook_failing` reading: one endpoint's last hour of delivery attempts, the dashboard's Test pings left out.","additionalProperties":false,"required":["endpoint_host","window_minutes","attempts","failed","failure_ratio","platform_failed","newest_delivered","consecutive_failures","disabled","top_causes","example_order_ids"],"properties":{"endpoint_host":{"type":"string","description":"The endpoint URL's host, never its path or query."},"window_minutes":{"type":"integer"},"attempts":{"type":"integer","description":"Attempts in the window, those that failed on our side (`platform_failed`) left out."},"failed":{"type":"integer","description":"Those that failed on the endpoint's side, for events that have not arrived since: a failure the retry recovered is not counted."},"failure_ratio":{"type":"number","minimum":0,"maximum":1,"description":"`failed / attempts`, to three decimals; 0 when nothing was sent."},"platform_failed":{"type":"integer","description":"Attempts in the window that failed on our side (the `egress`, `signing` and `internal` causes): counted apart and never against the endpoint. /status counts them."},"newest_delivered":{"type":["boolean","null"],"description":"Whether the endpoint's newest attempt, at any time, was delivered; null when nothing was ever sent to it."},"consecutive_failures":{"type":"integer","description":"Events in a row lost after their whole retry ladder: the count that switches an endpoint off. An event lost on our side (the delivery log's `signing`, `egress` and `internal` causes) is not counted."},"disabled":{"type":"boolean","description":"Whether it was switched off after its consecutive failures (`webhook.disabled`)."},"top_causes":{"type":"array","description":"The window's failures by cause, either side's, most first, at most three.","items":{"type":"object","additionalProperties":false,"required":["cause","count"],"properties":{"cause":{"type":"string","enum":["dns","unsafe_url","refused","timeout","tls","reset","network","redirect","unauthorized","http_4xx","http_5xx","inbox_refused","egress","signing","internal"],"description":"One of the delivery log's causes."},"count":{"type":"integer"}}}},"example_order_ids":{"type":"array","items":{"type":"string"},"description":"Orders (`portal_order_id`) whose events failed in the window and have not arrived (lost, or still retrying), newest first, at most three."}}},"ErrorRateSpikeEvidence":{"type":"object","description":"An `error_rate_spike` reading: the account's live calls in the last hour, or, for an open alert whose hour held fewer than 10, its newest 10 over `window_minutes`; against the 24 hours before, or the 7 days before (`baseline_hours` 168) when those held fewer than 20 calls, and fewer than 20 there too is no baseline, compared as 10%. In `latest`, the baseline is the one the alert opened against, which is what it resolves against.","additionalProperties":false,"required":["window_minutes","calls","errors","error_rate","baseline_hours","baseline_calls","baseline_errors","baseline_rate","top_errors","example_request_ids"],"properties":{"window_minutes":{"type":"integer"},"calls":{"type":"integer","description":"Live calls in the window."},"errors":{"type":"integer","description":"Those answered 5xx, 401, 402, 403 or 429."},"error_rate":{"type":"number","minimum":0,"maximum":1,"description":"`errors / calls`, to three decimals."},"baseline_hours":{"type":"integer"},"baseline_calls":{"type":"integer"},"baseline_errors":{"type":"integer"},"baseline_rate":{"type":"number","minimum":0,"maximum":1},"top_errors":{"type":"array","description":"The window's failures by error code and status, most first, at most three.","items":{"type":"object","additionalProperties":false,"required":["code","status","count"],"properties":{"code":{"type":["string","null"],"description":"The answer's `error.code`; null when it carried none."},"status":{"type":"integer"},"count":{"type":"integer"}}}},"example_request_ids":{"type":"array","items":{"type":"string"},"description":"The `request_id` of the window's newest failures, at most three. Empty, as `top_errors` is, to a credential that reads only its own calls, and in the `alert.*` events."}}},"KeyExpiringEvidence":{"type":"object","description":"A `key_expiring` reading: a key about to stop working, and its calls.","additionalProperties":false,"required":["key_prefix","stop_reason","stops_at","calls","last_call_at"],"properties":{"key_prefix":{"type":"string","description":"The key's prefix, as the API keys page shows it; never more of the key."},"stop_reason":{"type":"string","enum":["rotated","expires"],"description":"`rotated` (it was rolled, and works until its grace ends) or `expires` (its own expiry)."},"stops_at":{"type":"string","format":"date-time"},"calls":{"type":"integer","description":"Calls it answered in the last day, or since it was rolled."},"last_call_at":{"type":["string","null"],"format":"date-time"}}},"ArtworkValidation":{"type":"object","required":["valid","file_url","checks"],"properties":{"valid":{"type":"boolean","description":"True when no finding refuses the file and no check fails: with a `sku`, the order's verdict at its print size. Without a `sku` (or a `product_type` that is one), nothing is measured at a size (`size_unknown`), so it does not say the file prints at any: read `fits` and `largest_passing`; `warnings` says when no size in `fits` prints the file."},"file_url":{"type":"string"},"product_type":{"type":["string","null"],"description":"As you sent it."},"resolved_product_type":{"type":["string","null"],"description":"What it resolved to — \"sticker-die-cut-3x3\" resolves to \"sticker-die-cut\" — so a normalization is visible rather than silent."},"product_specs":{"type":["object","null"],"properties":{"name":{"type":"string"},"min_dpi":{"type":"integer"},"max_size":{"type":"string"},"bleed":{"type":"string"},"formats":{"type":"array","items":{"type":"string"}}}},"checks":{"type":"array","items":{"$ref":"#/components/schemas/PreflightCheck"},"description":"The verdict as named checks. On a read file: file_format, resolution, color_space and, for a die-cut SKU, cut_line (with the margin measured around the design), or for another SKU whose bleed the file lets us measure, bleed_area, each from the order's findings (a fail is an error the order refuses for). On an unread file (`verified: false`): the checks built from what you declared. file_size when you sent `file_size_bytes`."},"warnings":{"type":"array","items":{"type":"string"}},"suggestions":{"type":"array","items":{"type":"string"}},"sku":{"type":["string","null"],"description":"The SKU the file was graded against: `sku`, else `product_type`; null when neither was sent."},"asset_id":{"type":["string","null"],"description":"The asset you named, when you named the file by `asset_id`; `file_url` is then its URL."},"verified":{"type":"boolean","description":"True when the file was read and graded; false when it could not be read just now and the answer is from what you declared."},"status":{"type":"string","enum":["passed","warned","refused","unverified"],"description":"The order's line status for this file: passed, warned, refused, or unverified (not read). Without a `sku` it is graded at no print size, so resolution plays no part in it: `fits` holds the status at each size."},"would_refuse":{"type":"boolean","description":"Whether a live POST /v1/order of this SKU with this file would be refused 422 artwork_not_printable. Only a SKU our own plant makes is graded by the order, and an unread file is never refused. Without a `sku` there is no SKU to refuse, so it is false: each entry of `fits` answers it for its own SKU."},"findings":{"type":"array","items":{"type":"object","required":["code","severity","message"],"properties":{"code":{"type":"string","enum":["unreachable","not_an_image","low_resolution","below_recommended_resolution","cmyk","no_transparency","touches_edge","no_bleed","content_in_bleed","no_cutline","size_unknown","vector","unverified","format_not_accepted","not_fetchable","design_embeds_raster","generated_design","placement_clipped","geometry_unverified"]},"severity":{"type":"string","enum":["error","warning","info"],"description":"error refuses a live order; warning and info never do."},"message":{"type":"string"}}},"description":"The order's own findings for this file (ArtworkLineVerdict's), with its codes and severities."},"width_px":{"type":["integer","null"],"description":"Measured from the file; null when it was not read or is vector."},"height_px":{"type":["integer","null"],"description":"Measured from the file; null when it was not read or is vector."},"print_size_in":{"type":["array","null"],"items":{"type":"number"},"minItems":2,"maxItems":2,"description":"The print size resolution was measured at, in inches: the SKU's, else `size`'s. Null when neither names one."},"effective_dpi":{"type":["integer","null"],"description":"Pixels per inch with the file printed as large as the print size allows, as the order measures it."},"min_dpi":{"type":["integer","null"],"description":"The product's recommended minimum."},"fits":{"type":["array","null"],"maxItems":114,"items":{"$ref":"#/components/schemas/ArtworkFit"},"description":"When neither `sku` nor `product_type` is sent: the file, read once, graded at a fixed set of sizes in each family (114 today), stickers, decals (the wall decal), magnets and posters in that order, smallest first, each family's last the largest an order of it takes. Sizes between two of them are orderable and not graded. Each entry is what the call naming that `sku` answers. Null when a `sku` or `product_type` was sent, and when the file could not be read (`verified: false`), since nothing was measured."},"largest_passing":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ArtworkLargestPassing"},"description":"One entry per family in `fits`, in its order: the largest of that family's sizes in `fits` whose `status` is `passed`, the order's \"prints well\". A `warned` size prints, with a warning, and is not counted. Null exactly when `fits` is."},"auto_fix":{"oneOf":[{"$ref":"#/components/schemas/ArtworkAutoFix"},{"type":"null"}],"description":"What `auto_fix: true` did: null when it was not sent."}}},"ArtworkAutoFix":{"type":"object","required":["applied"],"properties":{"applied":{"type":"boolean","description":"True when a fixed file was written and stored; false with `reason` when nothing was."},"reason":{"type":"string","description":"Why nothing was fixed (nothing to fix, no sku, a vector file, a file not read whole, too large to decode). Only when `applied` is false."},"asset_id":{"type":"string","description":"The new asset (`source: fixup`): order it as a line's `print_file`."},"url":{"type":"string","description":"The new asset's URL."},"content_type":{"type":"string","enum":["image/png","image/jpeg"]},"width_px":{"type":"integer"},"height_px":{"type":"integer"},"changes":{"type":"array","items":{"type":"object","required":["code","description"],"properties":{"code":{"type":"string","enum":["no_bleed","content_in_bleed","touches_edge","no_cutline"],"description":"The finding this change answers."},"description":{"type":"string","description":"What was done, in pixels and inches."}}}},"notes":{"type":"array","items":{"type":"string"},"description":"What the fix could not do, such as shapes too far apart to join with a border."},"status":{"type":"string","enum":["passed","warned","refused","unverified"],"description":"The fixed file's own verdict for the same SKU."},"would_refuse":{"type":"boolean","description":"Whether a live POST /v1/order of this SKU with the fixed file would be refused."},"findings":{"type":"array","items":{"type":"object","required":["code","severity","message"],"properties":{"code":{"type":"string","enum":["unreachable","not_an_image","low_resolution","below_recommended_resolution","cmyk","no_transparency","touches_edge","no_bleed","content_in_bleed","no_cutline","size_unknown","vector","unverified","format_not_accepted","not_fetchable","design_embeds_raster","generated_design","placement_clipped","geometry_unverified"]},"severity":{"type":"string","enum":["error","warning","info"]},"message":{"type":"string"}}},"description":"The fixed file's findings, graded by the order's grader."}}},"ArtworkFit":{"type":"object","required":["sku","family","size","size_in","status","effective_dpi","would_refuse"],"properties":{"sku":{"type":"string","description":"The size's SKU, as the price book spells it while our plant's own catalog is silent: a product GET /v1/products lists keeps the spelling listed there, and GET /v1/products/{sku} serves every one. Once the plant's catalog answers, its rows may spell or size products otherwise. The call naming this `sku` answers the same verdict."},"family":{"type":"string","enum":["stickers","decals","magnets","posters"],"description":"The SKU's family, as its catalog row's `family` names it."},"size":{"type":"string","description":"The SKU's print size, e.g. 3×3 in."},"size_in":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2,"description":"The same size in inches, [width, height]."},"status":{"type":"string","enum":["passed","warned","refused"],"description":"The order's line status for this file at this SKU: passed, warned or refused. Never unverified: an unread file has no `fits`."},"effective_dpi":{"type":["integer","null"],"description":"Pixels per inch at this size, as the order measures it; null for a PDF or SVG, which is not measured in pixels, and for a file that is not an image."},"would_refuse":{"type":"boolean","description":"Whether a live POST /v1/order of this SKU with this file would be refused 422 artwork_not_printable."}}},"ArtworkLargestPassing":{"type":"object","required":["family","sku","size","size_in","effective_dpi"],"properties":{"family":{"type":"string","enum":["stickers","decals","magnets","posters"],"description":"The family, as `fits[].family` names it."},"sku":{"type":["string","null"],"description":"The SKU of the family's largest passing size; null when no size of the family passes."},"size":{"type":["string","null"],"description":"That SKU's print size, e.g. 4×4 in; null when no size passes."},"size_in":{"type":["array","null"],"items":{"type":"number"},"minItems":2,"maxItems":2,"description":"The same size in inches, [width, height]; null when no size passes."},"effective_dpi":{"type":["integer","null"],"description":"The file's resolution at that size; null when no size passes, and for a PDF or SVG."}}},"AccountSetup":{"type":"object","required":["ready","sandbox_only","steps","next_action","complete"],"properties":{"ready":{"type":"object","required":["live_orders","hosted_sales","store_orders"],"description":"Whether each way of selling would be accepted right now — the same condition the refusing route checks.","properties":{"live_orders":{"type":"boolean","description":"A live key — the credential asking, when it is a live one (a live key or an OAuth sign-in), and otherwise one you made on the API keys page that is not revoked or expired (the key the `live_key` step counts) — a card on file, and an account not held to sandbox."},"hosted_sales":{"type":"boolean","description":"Always false here: orders reach this portal through this API and its dashboard only."},"store_orders":{"type":"boolean","description":"Always false here: orders reach this portal through this API and its dashboard only."}}},"sandbox_only":{"type":"boolean","description":"Held to test mode by the platform; live orders are refused until support lifts it."},"steps":{"type":"array","description":"Every setup step in order, each measured from the account's rows.","items":{"$ref":"#/components/schemas/SetupStep"}},"next_action":{"description":"The first required step not yet done, or null when every required step is done.","oneOf":[{"type":"object","required":["key","label","hint","dashboard_url"],"properties":{"key":{"type":"string","enum":["test_key","sandbox_order","webhook","payment_method","live_key","first_sale"]},"label":{"type":"string"},"hint":{"type":"string"},"dashboard_url":{"type":"string","format":"uri"}}},{"type":"null"}]},"complete":{"type":"boolean","description":"Every required step is done. Optional steps never hold this back."}}},"SetupStep":{"type":"object","required":["key","label","hint","done","optional","dashboard_url"],"properties":{"key":{"type":"string","enum":["test_key","sandbox_order","webhook","payment_method","live_key","first_sale"]},"label":{"type":"string"},"hint":{"type":"string","description":"What completing the step does, and what is refused until it is done."},"done":{"type":"boolean"},"optional":{"type":"boolean","description":"A step the first live order does not need: the webhook (GET /order/{id} reads the same state), and a live key of your own (a real order placed from the dashboard's order page uses the key that page makes). Asked by a live credential, every step but the card is optional; asked by a test key, every step is. Never holds back `complete`, and never the next action."},"dashboard_url":{"type":"string","format":"uri","description":"The dashboard page that completes it."}}},"CredentialDescriptor":{"type":"object","required":["key","account","rate_limits","verified_agent","spend_controls","api_version"],"properties":{"key":{"type":"object","properties":{"prefix":{"type":"string","description":"The key's public prefix. Never the key itself."},"name":{"type":["string","null"]},"mode":{"type":"string","enum":["test","live"]},"scopes":{"type":"array","items":{"type":"string"}},"expires_at":{"type":["string","null"],"format":"date-time"}}},"account":{"type":"object","properties":{"plan":{"type":"string"},"plan_label":{"type":"string"}}},"rate_limits":{"type":"object","description":"Both windows are enforced per ACCOUNT, not per key — minting more keys does not buy more throughput, and `scope` says so rather than leaving it to be inferred from a per-key-looking response.","properties":{"scope":{"type":"string","enum":["account"]},"requests_per_minute":{"$ref":"#/components/schemas/RateLimitWindow"},"requests_per_day":{"$ref":"#/components/schemas/RateLimitWindow"}}},"verified_agent":{"type":"object","description":"`signed` is what the request claimed; `verified` is what checked out. Only the second buys the rate multiplier, and an agent debugging its own signature needs to see the two disagree.","properties":{"signed":{"type":"boolean"},"verified":{"type":"boolean"},"key_id":{"type":["string","null"]},"agent_name":{"type":["string","null"]},"rate_multiplier":{"type":"number"},"reason":{"type":["string","null"],"description":"Why the signature did not verify. Null when it verified, and when the request carried none."}}},"spend_controls":{"type":"object","properties":{"per_order_cap_cents":{"type":["integer","null"]},"daily_cap_cents":{"type":["integer","null"]},"day":{"type":"string"},"spent_today_cents":{"type":["integer","null"],"description":"Reserved against the daily cap so far today. Null when the key has no daily cap, or when the day's tally could not be read."},"remaining_today_cents":{"type":["integer","null"]}}},"api_version":{"type":"string","description":"The version this response was served under — the same value as the `MF-Version` header."}}},"RateLimitWindow":{"type":"object","required":["limit","remaining","reset"],"properties":{"limit":{"type":"integer"},"remaining":{"type":"integer"},"reset":{"type":"integer","description":"When the window resets, as a Unix timestamp in milliseconds (the same value as X-RateLimit-Reset)."}}},"AddressAmendment":{"type":"object","description":"What PUT /order/{id}/address answers. A sandbox order is answered from our record. A live one is the facility's own reply, every key it sends kept as it sent it, with `order_id`, `portal_order_id` and `ship_to` filled from our record wherever the reply has none — never `status`, which on a live order is the facility's word.","required":["order_id","portal_order_id","ship_to"],"properties":{"order_id":{"type":"string","description":"The order's id, the one every order operation takes. On a live order, the facility's reply supplies it when it carries one."},"portal_order_id":{"type":"string","description":"This platform's own id for the order (`ord_…`). On a live order, the facility's reply supplies it when it carries one."},"status":{"type":"string","description":"The order's status; the amendment does not move it. Always present on a sandbox answer, one of pending_approval, pending_payment, paid, accepted, in_production, quality_check, shipped, delivered, cancelled, issue_flagged, rerouting, pending, processing, on-hold, completed, refunded, failed. On a live answer, the facility's own value when its reply carries one, and otherwise absent: it is never filled from our record (read GET /order/{id})."},"provider":{"type":"string","description":"Always `merchfarm` on a sandbox answer. On a live answer, the facility's own value when its reply carries one, and otherwise absent."},"ship_to":{"type":"object","description":"The address the order now ships to, as accepted. On a live order, the facility's own `ship_to` when its reply carries one; otherwise the address on our record, which this call has just written."},"test_mode":{"type":"boolean","enum":[true],"description":"Present and true on a sandbox order; absent on a live one, as on GET /order/{id}."}}},"ShippingAmendment":{"type":"object","description":"What PUT /order/{id}/shipping answers. A sandbox order is answered from our record. A live one is the facility's own reply, every key it sends kept as it sent it, with `order_id` and `portal_order_id` filled from our record wherever the reply has none. The status and the money are never filled on a live answer: read GET /order/{id} for `shipping_cents` and `total_cents`, and the `order.updated` webhook for what the change moved.","required":["order_id","portal_order_id"],"properties":{"order_id":{"type":"string","description":"The order's id, the one every order operation takes. On a live order, the facility's reply supplies it when it carries one."},"portal_order_id":{"type":"string","description":"This platform's own id for the order (`ord_…`). On a live order, the facility's reply supplies it when it carries one."},"status":{"type":"string","description":"The order's status; the amendment does not move it. Always present on a sandbox answer, one of pending_approval, pending_payment, paid, accepted, in_production, quality_check, shipped, delivered, cancelled, issue_flagged, rerouting, pending, processing, on-hold, completed, refunded, failed. On a live answer, the facility's own value when its reply carries one, and otherwise absent."},"provider":{"type":"string","description":"Always `merchfarm` on a sandbox answer. On a live answer, the facility's own value when its reply carries one, and otherwise absent."},"shipping_cents":{"type":"integer","description":"The order's shipping line after the change, in cents. Always present on a sandbox answer; on a live one, the facility's own figure when its reply carries one, and otherwise absent."},"total_cents":{"type":"integer","description":"The order's total after the change, in cents. Always present on a sandbox answer; on a live one, the facility's own figure when its reply carries one, and otherwise absent."},"test_mode":{"type":"boolean","enum":[true],"description":"Present and true on a sandbox order; absent on a live one, as on GET /order/{id}."}}},"MissingLetterReport":{"type":"object","required":["order_id","portal_order_id","replacement","test_mode"],"properties":{"order_id":{"type":"string","description":"The letter that was reported missing."},"portal_order_id":{"type":"string"},"replacement":{"type":"object","required":["order_id","portal_order_id","status","shipping_method","total_cents","tracking_url"],"description":"The one free replacement. An order in its own right: GET /v1/order/{id} reads it.","properties":{"order_id":{"type":"string","description":"Its public id. A placeholder until the plant accepts it, then the plant's own id; the portal id never changes."},"portal_order_id":{"type":"string"},"status":{"type":"string","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"],"description":"`paid` when it is written; it moves through the lifecycle like any order."},"shipping_method":{"type":"string","enum":["letter"],"description":"A replacement ships the way the original did."},"total_cents":{"type":"integer","enum":[0],"description":"Nothing is charged for a replacement."},"tracking_url":{"type":"string","format":"uri","description":"Its tracking page, which states the letter's expected window — there are no carrier scans."}}},"test_mode":{"type":"boolean"}}},"Asset":{"type":"object","required":["id","filename","url","content_type","size_bytes","source","created_at"],"properties":{"id":{"type":"string","description":"`ast_…`. `POST /v1/order` accepts this directly as an item's `print_file`, in place of a URL."},"filename":{"type":"string"},"url":{"type":"string","description":"Publicly reachable URL for the stored bytes."},"content_type":{"type":"string","enum":["image/png","image/jpeg","image/svg+xml","application/pdf"]},"size_bytes":{"type":"integer"},"source":{"type":"string","enum":["upload","mockup","listing","design","carryover","manifest","fixup"],"description":"Where the bytes came from: `upload` (sent through POST /v1/assets or the dashboard), `mockup` (a render from POST /v1/mockups/generate), `listing` (the photograph a checkout link gets on its first publish), `design` (artwork a design run generated), `carryover` (a listing image a listing import fetched and rehosted), `manifest` (a production batch's manifest), `fixup` (a copy of your artwork with its bleed, margin or outline fixed by POST /v1/artwork/validate with auto_fix). A fact, not a filter: every asset surface lists, exports and deletes a render exactly as an upload, and there is no `?source=` on the list. Files stored before the field existed were classified from their filenames and read `upload` where nothing said otherwise."},"created_at":{"type":"string","format":"date-time"}}},"AssetPagination":{"type":"object","required":["limit","total"],"properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"pages":{"type":"integer"}}},"MockupResponse":{"type":"object","required":["mockup_url","product_type","stored"],"properties":{"mockup_url":{"type":"string","description":"The render. A hosted URL when `stored` is true, otherwise an inline data URI."},"thumbnail_url":{"type":"string","description":"A 300 px render of the same mockup: for `svg`, its own data URI with the artwork fitted to it; for a stored raster, the render's URL; for an unstored raster, a 300 px inline copy. Never a second full-size copy of `mockup_url`."},"asset_id":{"type":["string","null"],"description":"`ast_…` for the stored render — what `DELETE /v1/assets/{id}` takes. Null when `stored` is false, because there is no row to name."},"product_type":{"type":"string"},"background":{"type":"string","enum":["white","transparent","lifestyle"]},"angle":{"type":"string","enum":["front","angled","flat-lay"]},"format":{"type":"string","enum":["svg","png","jpeg"]},"content_type":{"type":"string","enum":["image/svg+xml","image/png","image/jpeg"],"description":"One per `format`. A narrower list than `POST /v1/assets` accepts: no render produces a PDF."},"stored":{"type":"boolean","description":"False when this deployment has no asset storage; the render is then inline only."},"storage_reason":{"type":"string","description":"Why the render was not stored. Present only when `stored` is false."},"width":{"type":"integer"},"height":{"type":"integer"},"dimensions":{"type":"object","properties":{"width":{"type":"integer"},"height":{"type":"integer"}}},"size_in":{"type":["array","null"],"items":{"type":"number"},"minItems":2,"maxItems":2,"description":"The print's [width, height] in inches the product was drawn at, read from a SKU of our own plant's that names its size; null when the product type names none, and the family's own plate was drawn."},"cut_line":{"type":["object","null"],"description":"The die-cut line drawn on the mockup: null when `cut_line: true` was not sent, or the product is not cut around its artwork. On a die-cut SKU, `drawn` says whether the line is on the picture; `offset_in` is the SKU's bleed, the distance the line sits outside the artwork; `pieces` is how many separate outlines were drawn (more than one means the shapes are too far apart to cut as one sticker: see `validate_artwork`'s `no_cutline`); `not_drawn_reason`, when not drawn: artwork_unread (the file could not be read or decoded within the bound), no_print_size, no_transparency (cut as a rectangle), empty, too_complex or photo_plate.","required":["drawn","offset_in"],"properties":{"drawn":{"type":"boolean"},"offset_in":{"type":["number","null"],"description":"The SKU's bleed in inches: how far outside the artwork the line runs."},"pieces":{"type":"integer","description":"Present when `drawn`: separate outlines on the picture."},"not_drawn_reason":{"type":"string","enum":["artwork_unread","no_print_size","no_transparency","empty","too_complex","photo_plate"],"description":"Present when not `drawn`: why."}}},"artwork_inlined":{"type":"boolean","description":"SVG only: true when the artwork's bytes are inside the SVG, so it paints anywhere, an `<img>` included; false when it is referenced by URL (see `artwork_inline_reason`)."},"artwork_inline_reason":{"type":"string","description":"SVG only, when `artwork_inlined` is false: why the artwork is referenced — artwork_blocked, artwork_unreachable, artwork_too_large (over the 15 MB read limit, or still over 512 KB once fitted to the print area), artwork_unsupported_type, artwork_empty, artwork_unreadable, artwork_too_complex (SVG artwork that did not finish drawing within 10 seconds) or render_busy (no render slot came free in time, or this account already had four renders waiting; the same request is safe to send again)."},"generated_at":{"type":"string","format":"date-time"}}},"PreflightCheck":{"type":"object","required":["name","status","message"],"properties":{"name":{"type":"string","description":"Which check this is — `file_format`, `resolution`, `print_area`, `color`, `embedded_assets`. The set differs by what was checked; treat it as an open vocabulary."},"status":{"type":"string","enum":["pass","fail","warn"],"description":"`fail` blocks the print. `warn` does not — it is advice that the artwork will print better if you act on it. Only `fail` should stop a pipeline."},"message":{"type":"string","description":"The finding in prose, safe to show a human."}}},"CatalogProduct":{"type":"object","additionalProperties":true,"properties":{"fulfillment_cost_basis":{"type":"string","enum":["approved_size_shipping_benchmark"],"description":"Approved per-size US standard shipping benchmark; actual postage, manufacturing cost and profit remain unknown."},"goods_cost_cents":{"type":"null","description":"Actual manufacturing cost is unknown, never represented by zero."},"manufacturing_cost_cents":{"type":"null"},"actual_contribution_cents":{"type":"null"},"maximum_quantity":{"type":"integer","enum":[1],"description":"One product, one SKU, quantity 1 in its own order."},"shipping_constraints":{"type":"object","required":["destination_country","method","separate_order"],"properties":{"destination_country":{"type":"string","enum":["US"]},"method":{"type":"string","enum":["standard"]},"separate_order":{"type":"boolean","enum":[true]}}},"shipping_benchmark_cents":{"type":"integer","minimum":0,"description":"The approved quantity-1 US standard benchmark, packing included."},"packaging":{"type":"string","enum":["included_in_shipping_benchmark"]},"artwork_layout":{"type":"string","enum":["prepared_combined_all"]},"artwork_instructions":{"type":"string","description":"Prepare one combined ALL PNG: flag front and back, or Santa hat front and band, according to the exact guide; remove guide lines."},"artwork_guide_url":{"type":"string","description":"Local path to the layout guide SVG. This reference is not customer artwork and must not be sent as print_file."},"sku":{"type":["string","number"],"description":"The identifier every other operation takes: an order item's `sku`, the `{sku}` path parameter, a recommendation's `sku`. Numeric on some facility catalogs — compare it as a string."},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","description":"The facility's own category, where it publishes one — an open vocabulary, not this API's. `GET /v1/products` always fills this field (see `ProductListItem`); by SKU it is present only if the facility sent it."},"source":{"type":"string","enum":["price_book","listed"],"description":"Present only on a row this API built itself; absent on every row a facility sent. `price_book`: built from our own price book because our plant's catalog had nothing of its own to serve (the answer is then `X-Catalog-Stale: true`, with `X-Catalog-Degraded` naming why). Its price is the book's, which is what an order of the SKU is charged; the row carries `sku`, `name`, `base_price` and `category`. `listed`: a row of our catalog (a sticker, a wall decal, a poster or a magnet) that our plant's catalog, as this read found it, does not hold, listed after the rows read, with the same fields and its price from the catalog; `not_orderable_code` says whether our plant carries it."},"size_in":{"type":["array","null"],"items":{"type":"number"},"minItems":2,"maxItems":2,"description":"Our plant's rows only: [width, height] in inches, read from the size the SKU names (the size the price book and an order price). Null when the SKU names no size, as an unsized sticker roll does."},"finish":{"type":["string","null"],"description":"Our plant's rows only: the finish the plant's row names, else the one the SKU names (glossy, matte, holographic, clear, satin); null when neither names one. No finish changes the price. A SKU of our sticker catalog is read by its own grammar: the finish it names after the size, else the bare stem's (glossy); there `clear` and `holographic` name a `material`, not a finish. A wall decal of our catalog: its sheet's one finish, `matte`; a poster of our catalog: its one finish, `matte` (matte paper). A magnet, bumper sticker or single-sheet sticker of our catalog: null, as it names none."},"material":{"type":["string","null"],"enum":["white","clear","glitter","holographic","film","fabric","paper","transparent-film","bopp","polyester",null],"description":"Our catalog's rows only: the material the SKU is made in, as our catalog names it — an economy die-cut or kiss-cut sticker's white, clear, glitter or holographic adhesive vinyl; a bumper sticker's white surface or a single-sheet sticker's white, clear (Transparent) or holographic surface (no finish inferred); a wall decal's `film` (Removable Wall Film, the bare `wall-decal-` stem) or `fabric` (Polyester Fabric, the Textured Wall Decal), both indoor rated; a poster's `paper` (matte paper, the bare `poster-` stem and its only material); UV DTF `transparent-film`, roll labels `bopp`, and catalog-only flags and hats `polyester`. Sheet packs preserve white, clear or holographic surfaces. Null on every other row, a magnet of our catalog's included: our catalog names no material for one."},"options":{"type":["object","null"],"description":"Economy die-cut and kiss-cut stickers and listed magnets: the shapes and finishes an order line may name. Magnet cuts share their size's price and have no finish choice. Null for bumper stickers, single sticker sheets, UV DTF packs, label rolls, sheet packs, wall decals, posters, catalog-only flags and hats, and other rows with no choices. Pack count and surface are distinct SKUs.","required":["shapes","finishes","default_shape","default_finish","variant_skus"],"properties":{"shapes":{"type":"array","items":{"type":"string","enum":["contour","circle","square","rectangle","oval","kiss"]},"description":"The shapes a SKU may name after its size, the bare SKU's first. Empty for a kiss cut, whose shape is the cut."},"finishes":{"type":"array","items":{"type":"string","enum":["gloss","matte"]},"description":"The finishes a SKU may name after its size, the bare SKU's first."},"default_shape":{"type":"string","enum":["contour","circle","square","rectangle","oval","kiss"],"description":"What the bare SKU is cut to: `contour` (the artwork's outline), or `kiss` for a kiss cut. A magnet's bare SKU is custom contour."},"default_finish":{"type":["string","null"],"enum":["gloss","matte",null],"description":"What the bare SKU is finished in; null when no finish is offered, including magnets."},"variant_skus":{"type":"array","items":{"type":"string"},"description":"Every SKU an order line may name for this product, canonically spelled, the bare SKU first (`sticker-die-cut-3x3`, `sticker-die-cut-3x3-matte`, `sticker-die-cut-3x3-circle`, …): the spellings our plant is asked to carry, and so the ones to send. A shape or finish never changes the price, and an order's lines of one product count as one quantity. A test key orders any of them now. A live key orders each once our plant's catalog carries that SKU itself: the row's `orderable` speaks for the row's own SKU only, and a live quote or order of a variant the plant does not carry yet is refused 422 `sku_not_orderable_yet` before anything is charged. Any other spelling (`-gloss`, `-glossy`, the words in another order) is asked of our plant under that spelling and is not one it was asked to carry."}}},"family":{"type":"string","enum":["stickers","decals","magnets","posters","flags","headwear"],"description":"Our plant's rows only: the product family. Modeled rows read pricing and cost under their family. The approved flag and Santa hat policy has bounded fulfillment while actual manufacturing cost remains unknown."},"catalog_readiness":{"type":"string","enum":["modeled","owner_exception","catalog_only"],"description":"Present on provider-product additions. Modeled sticker goods have estimated cost/weight. owner_exception identifies only the four approved flag sizes and one Santa hat: separate quantity-1 US standard orders with prepared combined ALL artwork and explicit benchmark/unknown-cost disclosures. Both test and live requests require reviewed exact production mappings and confirmed product evidence. catalog_only references cannot be quoted or ordered automatically."},"selling_unit":{"type":"string","enum":["piece","roll","pack"],"description":"One order quantity buys this complete selling unit. A roll or pack is never priced or ordered as one individual label or sheet."},"physical_pieces":{"type":"integer","minimum":1,"description":"Physical pieces inside one selling unit: labels per roll, sheets per sheet pack, pieces per UV DTF pack, or one single product. Two ordered packs contain twice this number; order quantity remains two packs."},"minimum_quantity":{"type":"integer","enum":[1],"description":"Our plant's rows only: the fewest units of this SKU one order line takes, which is 1. A run of one is an order, priced at the first `quantity_breaks` entry; POST /v1/order refuses a quantity below it with 400 validation_error."},"quantity_breaks":{"type":"array","items":{"$ref":"#/components/schemas/CatalogQuantityBreak"},"description":"Our plant's rows only: what an order of this SKU charges, from one unit up — computed by the function POST /v1/orders/quote and POST /v1/order charge with (no platform fee is added on any plan). The first entry (`min_qty` 1) is the row's own price. `price_interpolation` says how to read between entries. With `none`, one entry per quantity where the unit price changes, each unit holding until the next entry; a single entry when the SKU climbs no ladder (a SKU our price book does not price, and a wall decal or a poster of our catalog: its listed price, one price each, at any quantity). With `linear_total` (our sticker catalog's rows), one entry at each of the catalog's pack quantities (1, 5, 10, 25, 50, 100, 250, 500, 1,000 and 2,500): an order of exactly `min_qty` is charged `total_cents`, and any other quantity follows the rule `price_interpolation` states, to the cent. With `formula` (our magnet catalog's rows), one entry at each of 1, 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000 and 10,000 units: an order of exactly `min_qty` is charged `total_cents`, and any other quantity is the price list's own total for it, which POST /v1/orders/quote gives. Counted on an order's whole quantity of the SKU, across its lines (on our sticker catalog's rows, across every shape and finish of one product). A negotiated price for your account takes no break and replaces these. Empty when the row carries no usable price (`not_orderable_code: no_price`) or is a catalog-only reference (`not_orderable_code: catalog_only`), whose displayed reference price cannot be charged."},"price_interpolation":{"type":"string","enum":["none","linear_total","formula"],"description":"Our plant's rows only: how an order's goods total moves between two `quantity_breaks` entries. `none`: each entry's `unit_cents` holds from its `min_qty` until the next entry's, so the total is that unit times the quantity (a wall decal or a poster of our catalog: its one entry, at any quantity). `linear_total` (our sticker catalog's rows, priced as a pack total): between two entries the total is the straight line between their `list_total_cents`, rounded half up to the cent — for a quantity q between entries a and b, `round_half_up(list_a + (list_b − list_a) × (q − a) ÷ (b − a))`; from the last entry on, its unit rate holds: `round_half_up(q × list_last ÷ min_qty_last)`; and never less than `min_unit_cents` × q where the row states one. That is the quote's goods total at every quantity, exactly; `unit_cents` is the total over the quantity, rounded, and is not a bound on the units between entries. `formula` (our magnet catalog's rows): the total at every quantity is the magnet price list's own, computed from the size and the quantity, which no entry restates between two of them: quote the quantity. It never falls as the quantity grows. Rounding can make a unit rise slightly between neighboring quantities, but the 85% bulk share continues at 250 and beyond; the discount no longer expires there. Its entries carry no `list_total_cents`."},"min_unit_cents":{"type":["integer","null"],"description":"Our plant's rows only: the least a unit of this SKU is charged, where that raises the goods total of some quantity a line can carry (a large sticker at volume is charged this, not less); its `quantity_breaks` entries from there on already show it as their `unit_cents`. Null where it raises no quantity, and on every row whose `price_interpolation` is `none`."},"orderable":{"type":"boolean","description":"Our plant's rows only: true when nothing POST /v1/orders/quote checks before pricing would refuse this SKU right now. False says why in `not_orderable_reason` (a sentence) and `not_orderable_code` (a code). A hint for a cart, read when the catalog was: the quote and the order decide."},"not_orderable_reason":{"type":["string","null"],"description":"Why not, in the order path's own words — the same field, meaning the same thing, as on every other row of this endpoint that says `orderable`. Null when `orderable`. On our plant's rows `not_orderable_code` is the same answer as a code."},"not_orderable_code":{"type":["string","null"],"enum":["catalog_only","facility_not_configured","plant_unreachable","plant_unmapped","plant_price_held","no_price",null],"description":"Our plant's rows only: null when `orderable`; otherwise why not, as a code, in the order the quote meets them. `catalog_only` — a flag or hat whose artwork, production cost and shipping weight remain unconfirmed: automatic test and live quotes/orders are refused, even if the plant feed carries the SKU. `facility_not_configured` — the plant has no credentials in this environment (a live quote or order answers 503 provider_unavailable; a test key's degrades to $0 marked `pricing_degraded`, except a row of our catalog, a sticker's, a wall decal's, a poster's or a magnet's, which a test key orders at its listed price). `plant_unreachable` — the plant's catalog did not answer this read (the row is a snapshot or the price book's); a quote, estimate or order asks the plant about the SKU, and answers 503 provider_unavailable with `Retry-After` (`details.reason: plant_unreachable`) until it does. `plant_unmapped` — a row of our catalog (a sticker's, a wall decal's, a poster's or a magnet's) that our plant's catalog, as this read found it, does not carry yet: a live quote or order of it is refused 422 `sku_not_orderable_yet` before anything is asked or charged, and a test key orders it now, at the listed price. `plant_price_held` — the plant's prices are held after one of its launch goods read far above its book price, and this SKU has no book price to show it is not wrong the same way; the quote refuses it until the hold lifts. `no_price` — the row carries no usable list price and our price book does not price the SKU, so the quote refuses it."}}},"CatalogQuantityBreak":{"type":"object","required":["min_qty","unit_cents"],"properties":{"min_qty":{"type":"integer","minimum":1,"description":"From this many units of the SKU on one order (counted across its lines)."},"unit_cents":{"type":"integer","description":"What an order of `min_qty` charges per unit, in cents. On a row whose `price_interpolation` is `none` it holds until the next entry's `min_qty`; on a `linear_total` or `formula` row it is `total_cents` over `min_qty`, rounded, and the units between entries follow the row's rule instead."},"total_cents":{"type":"integer","description":"What an order of exactly `min_qty` units of the SKU is charged for the goods, in cents: the quote's `line_total_cents` for that many. On every entry."},"list_total_cents":{"type":"integer","description":"Our sticker catalog's rows only (`price_interpolation: linear_total`): the catalog's own pack total for `min_qty`, before `min_unit_cents` raises it; equal to `total_cents` wherever it is not raised. The rule between entries reads these. Absent on every other row, a magnet's (`formula`) included."}}},"ProductListItem":{"allOf":[{"$ref":"#/components/schemas/CatalogProduct"},{"type":"object","required":["category"],"properties":{"category":{"type":"string","description":"The facility's category if it published one, otherwise derived from the product name: one of swimwear, apparel, outerwear, headwear, bags, drinkware, stickers, decals, magnets, posters, accessories, other — `other` when no rule matched."}}}]},"CatalogFacets":{"type":"object","required":["category"],"properties":{"category":{"type":"object","additionalProperties":{"type":"integer"},"description":"Derived category to number of matching products — always the value derived from the product name, even for a row that carries the facility's own `category`. Categories with no matches are omitted rather than sent as 0, so a missing key means none matched."}}},"ProductListResponse":{"type":"object","required":["products","total","provider","facets","filters"],"properties":{"products":{"type":"array","description":"Our own plant's catalog rows.","items":{"$ref":"#/components/schemas/ProductListItem"}},"total":{"type":"integer","description":"How many products matched the whole query. This is the match count, NOT `products.length`: `limit` truncates the array and leaves this untouched, so `total > products.length` means there is more behind the same filters."},"provider":{"type":"string","enum":["printedsimply"],"description":"Which catalog answered: always `printedsimply` here."},"facets":{"$ref":"#/components/schemas/CatalogFacets"},"filters":{"type":"object","description":"The filters this answer was computed under, echoed back. Null where you sent nothing.","required":["category","search","size","finish","dedupe"],"properties":{"category":{"type":["string","null"]},"search":{"type":["string","null"]},"size":{"type":["string","null"]},"finish":{"type":["string","null"]},"dedupe":{"type":"boolean"}}}}},"ProductRecommendation":{"type":"object","required":["sku","name","category","price","price_cents","priced_from","score","match_reasons"],"properties":{"sku":{"type":"string","description":"Pass straight to `POST /v1/pricing/estimate` or an order item."},"name":{"type":"string"},"category":{"type":"string","description":"The derived catalog category — the same vocabulary as `CatalogProduct.category`."},"price":{"type":["string","null"],"description":"The unit price formatted for display, e.g. \"$4.50\". **Null, not \"$0.00\"**, when the catalog could not price the SKU — a zero here would be a number nothing backs."},"price_cents":{"type":["integer","null"],"description":"Your unit price in cents, already through your negotiated overrides and your plan's catalog discount, with no platform fee (there is none on any plan) — the same path `POST /v1/order` charges from, so this is what you would actually pay for one. Null when unpriced."},"priced_from":{"type":"string","enum":["catalog","unavailable"],"description":"`catalog` — the number came through the order pricing path. `unavailable` — the lookup failed, so no price is reported rather than a guessed one. There is deliberately no estimate case: the SKU still matched your signals, so it is returned unpriced instead of dropped."},"estimated_items_total_cents":{"type":"integer","description":"Present only when you sent `quantity` AND the SKU is priced. Unit price x quantity — items only. Shipping and the processing fee are NOT in it; `POST /v1/pricing/estimate` quotes an order total."},"estimated_items_total":{"type":"string","description":"The same number formatted, e.g. \"$450.00\". Present under the same condition."},"score":{"type":"integer","description":"Points this SKU scored against your signals. Comparable within one response only — it is a count of matched signals, not a rating of the product, and rows scoring zero are never returned."},"match_reasons":{"type":"array","items":{"type":"string"},"description":"Why it scored, one string per point, e.g. \"keyword: waterproof\", \"use_case: branding (stickers)\". This is the whole explanation — the ranking is deterministic and has no model in it."}}},"ProductRecommendationsResponse":{"type":"object","required":["recommendations","quantity","budget","catalog"],"properties":{"recommendations":{"type":"array","maxItems":5,"items":{"$ref":"#/components/schemas/ProductRecommendation"},"description":"Best first, at most 5. Empty means nothing in the catalog matched — check `catalog.stale` before concluding that."},"quantity":{"type":["integer","null"],"description":"Echoed back; null when you sent none."},"budget":{"type":["string","null"],"enum":["low","medium","premium",null],"description":"Echoed back; null when you sent none."},"catalog":{"type":"object","required":["stale","reason","considered","matched"],"description":"What this ranking was computed over. An empty `recommendations` off a stale snapshot is a different fact from an empty one off a healthy catalog, and these four fields are how you tell.","properties":{"stale":{"type":"boolean","description":"Same signal as the `X-Catalog-Stale` header, in the body."},"reason":{"type":["string","null"],"description":"Same values as `X-Catalog-Degraded`; null when not stale."},"source":{"type":"string","enum":["catalog","price_book"],"description":"Where the ranked rows came from: `catalog` (the plant's catalog, live or its last-good copy), or `price_book` when our own plant's catalog had nothing of its own to serve and the ranking is over the sizes our price book prices, as GET /v1/products then lists them. Stale either way when `stale` is true."},"considered":{"type":"integer","description":"Sellable rows in the catalog this ranked over."},"matched":{"type":"integer","description":"Of those, how many scored above zero. Only the top few are priced and returned, so this is normally larger than `recommendations.length`."}}}}},"VolumeBand":{"type":"object","required":["min_orders","max_orders","catalog_discount_pct"],"properties":{"min_orders":{"type":"integer","description":"Inclusive lower bound, in billed orders over the trailing window."},"max_orders":{"type":["integer","null"],"description":"Inclusive upper bound; null on the open-ended top rung."},"catalog_discount_pct":{"type":"number","description":"% off what our own plant makes other than die-cut and kiss-cut stickers and the wall decals, posters and magnets our catalog lists at that volume."}}},"PricingTier":{"type":"object","required":["tier","plan","name","offered","monthly_price_cents","catalog_discount_pct","discount_percent","requests_per_minute","requests_per_day","recommended_monthly_orders","min_orders","max_orders","bands","label","features"],"properties":{"tier":{"type":"integer","description":"1-based position in this list, offered plans first. An ordering, not a ranking."},"plan":{"type":"string","description":"The plan id an upgrade would name (`starter`, …)."},"name":{"type":"string","description":"Display label (`Free`, …)."},"offered":{"type":"boolean","description":"Whether this plan can be bought on this portal today. False means it is not sold here — accounts already on it keep it, nobody new can join it."},"monthly_price_cents":{"type":"integer"},"catalog_discount_pct":{"type":"number","description":"% off what our own plant makes other than die-cut and kiss-cut stickers and the wall decals, posters and magnets our catalog lists this plan GRANTS, never below a unit's cost floor (die-cut and kiss-cut stickers and the wall decals our catalog lists are the same price on every plan since 2026-10-23, and the posters and magnets our catalog lists since 2026-10-24). No plan sold on this portal grants one. What the account may take today is `current.entitled_catalog_discount_pct`."},"platform_fee_pct":{"type":"number","description":"The platform fee this plan pays, as a percentage of items plus shipping. 0 on every plan: there is no platform fee on any order since 2026-10-17. Card processing is separate and passed through at cost."},"discount_percent":{"type":"number","description":"Deprecated alias of `catalog_discount_pct`, kept because it is the field this endpoint has always published. It used to mean a volume discount and was hardcoded to 0; it now carries the plan discount, the only discount a plan grants. The quantity breaks on our own plant's goods are per order, not per plan, and the note states them."},"requests_per_minute":{"type":"integer"},"requests_per_day":{"type":"integer"},"recommended_monthly_orders":{"type":"string","description":"The order volume this plan is SUGGESTED for. Nothing is enforced on it — exceeding it costs nothing and changes nothing."},"min_orders":{"type":["integer","null"],"description":"The volume this plan's discount ladder starts at. **Null on every plan today**, because no plan carries a ladder — not zero, which would read as one starting at the first order."},"max_orders":{"type":["integer","null"],"description":"The top of the ladder. Null when there is none, and also when its top rung is open-ended (which it always is)."},"bands":{"type":"array","items":{"$ref":"#/components/schemas/VolumeBand"},"description":"The plan's volume ladder, lowest rung first. Empty means one flat discount at every volume — which is every plan today."},"label":{"type":"string","description":"Price and the single most distinguishing benefit, prebuilt for display."},"features":{"type":"array","items":{"type":"string"}}}},"PricingTiersResponse":{"type":"object","required":["note","pricing_model","tiers","current","next_tier"],"properties":{"note":{"type":"string","description":"The pricing rules in prose, generated from the same constants that enforce them — including the sentence about plan volume discounts on monthly order counts, which changes on the day a plan has one, and the quantity breaks on one order's units of a SKU."},"pricing_model":{"type":"object","required":["volume_discounts","processing_fee_pct","catalog_discount_pct"],"properties":{"volume_discounts":{"type":"boolean","description":"Whether ANY published plan prices on order volume. False today."},"processing_fee_pct":{"type":"number","deprecated":true,"description":"Deprecated 2026-09-26: the single fee it described is now two lines. Holds `platform_fee_pct`; read that and the card fields instead."},"platform_fee_pct":{"type":"number","description":"The platform fee this account's next order pays, as a percentage of items plus shipping. 0: there is no platform fee on any order, on any plan, since 2026-10-17."},"card_fee_pct":{"type":"number","description":"Card processing's percentage, passed through at cost on everything else the order is charged."},"card_fee_fixed_cents":{"type":"integer","description":"Card processing's fixed part per charge, in cents."},"catalog_discount_pct":{"type":"number","description":"The discount that will actually apply to what our own plant makes other than die-cut and kiss-cut stickers and the wall decals, posters and magnets our catalog lists on this account's next order; die-cut and kiss-cut stickers and the wall decals, posters and magnets our catalog lists take none on any plan."}}},"tiers":{"type":"array","items":{"$ref":"#/components/schemas/PricingTier"},"description":"Every plan, offered ones first, then retired ones."},"current":{"type":"object","required":["tier","plan","name","offered","catalog_discount_pct","discount_percent","entitled_catalog_discount_pct","monthly_orders"],"description":"The plan this key's account is on.","properties":{"tier":{"type":"integer"},"plan":{"type":"string"},"name":{"type":"string"},"offered":{"type":"boolean"},"catalog_discount_pct":{"type":"number","description":"What the plan grants."},"discount_percent":{"type":"number","description":"Deprecated alias of `catalog_discount_pct`."},"entitled_catalog_discount_pct":{"type":"number","description":"What the account may take TODAY, on our own plant's goods. Lower than `catalog_discount_pct` while a subscription is past_due, canceled or unpaid — budget against this one, not the plan's headline."},"monthly_orders":{"type":"null","description":"**Always null.** This endpoint does not count your orders, and nothing prices on volume. Zero would read as \"you have shipped nothing\" to a merchant who has shipped thousands."}}},"next_tier":{"type":["object","null"],"description":"The next plan up, or null when there is none to move to.","required":["tier","plan","name","monthly_price_cents","catalog_discount_pct","discount_percent","orders_needed"],"properties":{"tier":{"type":"integer"},"plan":{"type":"string"},"name":{"type":"string"},"monthly_price_cents":{"type":"integer"},"catalog_discount_pct":{"type":"number"},"discount_percent":{"type":"number","description":"Deprecated alias of `catalog_discount_pct`."},"orders_needed":{"type":"null","description":"**Always null.** No plan is reached by placing orders, so there is no count that would get you there. Zero would read as \"you already qualify\"."}}}}},"ServiceDescriptor":{"type":"object","required":["name","version","api_version","minimum_api_version","description","openapi_spec","documentation","api_changelog","mcp_config"],"properties":{"name":{"type":"string"},"version":{"type":"string","description":"The TRANSPORT major, matching the /api/v1 prefix. Deliberately frozen at \"1.0\" — it is not the contract version and does not move."},"api_version":{"type":"string","description":"The date-based CONTRACT version, which is the one that moves. Published separately rather than overloading `version`, so a discovery agent learns which version it will be served without fetching the whole spec."},"minimum_api_version":{"type":"string","description":"The oldest version still served. A pin below this resolves upward."},"description":{"type":"string"},"openapi_spec":{"type":"string","description":"Path to this document."},"documentation":{"type":"string"},"api_changelog":{"type":"string"},"mcp_config":{"type":"string","description":"Absolute URL of the MCP client config: an mcpServers block naming this deployment's MCP server, for an agent that would rather hold tools than endpoints."}}},"WebhookSubscriptionCreated":{"allOf":[{"$ref":"#/components/schemas/WebhookSubscription"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"The signing secret, in the clear, and the only time it is returned for this subscription other than a rotation. It is stored sealed and no read gives it back. Store it now; if you lose it, rotate."}}}]},"WebhookInboxCreated":{"allOf":[{"$ref":"#/components/schemas/WebhookSubscription"},{"type":"object","required":["created","secret"],"properties":{"created":{"type":"boolean","description":"True when this call made the inbox; false when the account already had one, which is answered as it is."},"secret":{"type":["string","null"],"description":"The signing secret, in the clear, on the call that made the inbox and never again (null on every later call). It is stored sealed and no read gives it back. Store it now; if you lose it, rotate."}}}]},"WebhookInbox":{"type":"object","required":["webhook","entries"],"properties":{"webhook":{"$ref":"#/components/schemas/WebhookSubscription"},"entries":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/WebhookInboxEntry"},"description":"One page of the requests the inbox kept, newest first. The inbox keeps its newest 100, each for at most 7 days."}}},"WebhookInboxEmptied":{"type":"object","required":["webhook_id","removed"],"properties":{"webhook_id":{"type":"string","description":"The inbox's subscription id, unchanged: the inbox stays, with its url and signing secret."},"removed":{"type":"integer","minimum":0,"description":"Entries deleted: at most the 100 the inbox keeps, and 0 when it was already empty."}}},"WebhookInboxEntry":{"type":"object","required":["id","event","event_id","received_at","headers","body"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied","test.ping"],"description":"The event name, as sent in `X-PS-Event`."},"event_id":{"type":"string","description":"The logical event this attempt delivered: the `X-PS-Event-Id` value, and the body's `id`."},"received_at":{"type":"string","format":"date-time"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Every header the signer produced for this attempt, name to value: the signature headers, the event, its id, the attempt and the timestamp."},"body":{"type":"string","description":"The raw body, byte for byte as signed — a string, not parsed JSON, so a signature check runs against exactly these bytes."}}},"WebhookSecretRotated":{"allOf":[{"$ref":"#/components/schemas/WebhookSubscription"},{"type":"object","required":["secret","grace_hours"],"properties":{"secret":{"type":"string","description":"The NEW secret, in the clear. As on create, this is the last time you will see it."},"grace_hours":{"type":"integer","description":"How long the outgoing secret keeps verifying, measured from `secret_rotated_at` and restated here so a receiver knows its window without arithmetic. Both secrets verify until `previous_secret_expires_at`; accept either until then. Zero means cut over now, with no overlap written at all."}}}]},"WebhookTestEventQueued":{"type":"object","required":["webhook_id","event","queued","test_mode"],"properties":{"webhook_id":{"type":"string","description":"The endpoint the sample was queued for."},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied"],"description":"The event whose sample was queued, as sent."},"queued":{"type":"integer","minimum":1,"description":"Queue rows written: one, for this endpoint."},"test_mode":{"type":"boolean","enum":[true],"description":"Always true: the sample's payload carries `test_mode: true` too."}}},"WebhookDeliveryDetail":{"allOf":[{"$ref":"#/components/schemas/WebhookDelivery"},{"type":"object","required":["payload"],"properties":{"payload":{"type":"object","description":"The exact body that was POSTed, so a receiver that lost one can replay it against its own handler. Its `id` is the same value as `event_id` beside it — surfaced as its own key so reading the log does not mean reaching into an opaque body for the dedupe key."}}}]},"WebhookRedelivery":{"type":"object","required":["delivery_id","redelivered","status_code","error"],"properties":{"delivery_id":{"type":"string","description":"The attempt that was replayed. A redelivery does not create a new id here."},"redelivered":{"type":"boolean","description":"Whether YOUR endpoint accepted it this time. False is a successful call to this operation reporting a failed delivery — the 200 is about the attempt being made, not about it landing."},"status_code":{"type":["integer","null"],"description":"What your endpoint answered. Null when it could not be reached at all."},"error":{"type":["string","null"],"description":"Why it did not land. Null when `redelivered` is true."}}},"OrderEvent":{"type":"object","required":["action","details","timestamp"],"properties":{"action":{"type":"string"},"details":{"type":["string","null"]},"timestamp":{"type":"string","format":"date-time"}}},"OrderTimelineEntry":{"description":"One entry of an order's timeline. `type` says which object it carries: `request`, `event` or `delivery`.","oneOf":[{"$ref":"#/components/schemas/OrderTimelineRequestEntry"},{"$ref":"#/components/schemas/OrderTimelineEventEntry"},{"$ref":"#/components/schemas/OrderTimelineDeliveryEntry"}]},"OrderTimelineRequestEntry":{"type":"object","description":"An API call that placed or changed the order.","required":["type","id","at","request"],"properties":{"type":{"type":"string","enum":["request"]},"id":{"type":"string","description":"The request log row's id."},"at":{"type":"string","format":"date-time","description":"When the call was answered."},"request":{"$ref":"#/components/schemas/OrderTimelineRequest"}}},"OrderTimelineEventEntry":{"type":"object","description":"One of the order's events, as GET /order/{id}/events returns it.","required":["type","id","at","event"],"properties":{"type":{"type":"string","enum":["event"]},"id":{"type":"string","description":"The event's id."},"at":{"type":"string","format":"date-time","description":"The event's timestamp."},"event":{"$ref":"#/components/schemas/OrderEvent"}}},"OrderTimelineDeliveryEntry":{"type":"object","description":"One attempt at delivering a webhook about the order to one of your endpoints.","required":["type","id","at","delivery"],"properties":{"type":{"type":"string","enum":["delivery"]},"id":{"type":"string","description":"The delivery attempt's id, as GET /webhooks/deliveries/{id} takes it."},"at":{"type":"string","format":"date-time","description":"When the attempt was made."},"delivery":{"$ref":"#/components/schemas/OrderTimelineDelivery"}}},"OrderTimelineRequest":{"type":"object","required":["method","path","status","request_id","trace_id","duration_ms","key_prefix","oauth_client_id","mode","error_code"],"properties":{"method":{"type":"string","description":"The HTTP method the call was made with."},"path":{"type":"string","description":"The operation's path template, such as `/v1/order/{id}/events`: the ids the call named are not repeated."},"status":{"type":"integer","description":"The HTTP status the call was answered with."},"request_id":{"type":["string","null"],"description":"The `X-Request-Id` (and `request_id`) the call was answered with: what the dashboard's request log finds it by."},"trace_id":{"type":["string","null"],"description":"The W3C trace id the response's `traceparent` carried; null on calls logged before traces were kept."},"duration_ms":{"type":"integer","minimum":0,"description":"How long the call took to answer, in milliseconds."},"key_prefix":{"type":["string","null"],"description":"The prefix of the API key that made the call, as the keys page shows it. Never more of the key; null for a call a connected app made."},"oauth_client_id":{"type":["string","null"],"description":"The connected app that made the call, for a call made with its access token; null for a key."},"mode":{"type":["string","null"],"enum":["test","live",null],"description":"Whether a test or a live credential made the call."},"error_code":{"type":["string","null"],"description":"The error's `code`, for a call answered with a status of 400 or more."}}},"OrderTimelineDelivery":{"type":"object","required":["webhook_id","endpoint_host","event","event_id","attempt","outcome","status_code","failure_cause","latency_ms","next_retry_at"],"properties":{"webhook_id":{"type":"string","description":"The endpoint the attempt was sent to."},"endpoint_host":{"type":["string","null"],"description":"The host of the URL the attempt was sent to, as it was then (an endpoint's URL can change since); never its path or query. Null on attempts logged before the log kept it."},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied","test.ping"]},"event_id":{"type":["string","null"],"description":"The `X-PS-Event-Id` the attempt carried. Every attempt at one event shares it; null only on attempts logged before the log carried it."},"attempt":{"type":["integer","null"],"minimum":1,"maximum":7,"description":"The rung of the retry ladder (`X-PS-Delivery-Attempt`), 1 for the first send and at most 7; null for a redelivery you asked for, and on attempts logged before the log kept it."},"outcome":{"type":"string","enum":["delivered","failed"],"description":"`delivered` when your endpoint answered 2xx."},"status_code":{"type":["integer","null"],"description":"The status your endpoint answered; null when nothing came back."},"failure_cause":{"type":["string","null"],"enum":["dns","unsafe_url","refused","timeout","tls","reset","network","redirect","unauthorized","http_4xx","http_5xx","inbox_refused","egress","signing","internal",null],"description":"Why a failed attempt failed, from the delivery log's causes; null when delivered, and on a failure logged with neither a cause nor a status."},"latency_ms":{"type":["integer","null"],"minimum":0,"description":"Milliseconds from sending to the answer or the failure; null when nothing was sent, and on attempts logged before the log kept it."},"next_retry_at":{"type":["string","null"],"format":"date-time","description":"When the next attempt at this event is due, on the latest failed attempt of a retry ladder still running to an active endpoint; null otherwise, and while the endpoint is paused or disabled (its attempts are held until it is resumed)."}}},"QuoteLandedCost":{"type":"object","required":["destination_country","duty_basis","duty_rate","tax_rate","breakdown","total_landed_cents","pre_collected_cents","due_on_delivery_cents","presentment","estimated","note"],"properties":{"destination_country":{"type":"string"},"duty_basis":{"type":"string","enum":["DDP","DAP"],"description":"Who pays at the border. The VALUE is the incoterm; what customs assesses on is goods plus shipping plus duty, which is a different fact and was the whole of this description before."},"duty_rate":{"type":"number"},"tax_rate":{"type":"number"},"breakdown":{"type":"object","required":["goods_cents","shipping_cents","duties_cents","tax_cents"],"properties":{"goods_cents":{"type":"integer"},"shipping_cents":{"type":"integer"},"duties_cents":{"type":"integer"},"tax_cents":{"type":"integer"}}},"total_landed_cents":{"type":"integer"},"pre_collected_cents":{"type":"integer","description":"Charged with the order."},"due_on_delivery_cents":{"type":"integer","description":"What the courier bills the recipient. The number a buyer is surprised by if nobody told them."},"presentment":{"type":"object","required":["currency","amount_cents","minor_units","fx_rate"],"description":"The same total in the destination's currency.","properties":{"currency":{"type":"string"},"amount_cents":{"type":"integer"},"minor_units":{"type":"integer"},"fx_rate":{"type":"number"}}},"estimated":{"type":"boolean","description":"Always true off this path, and sent anyway: a consumer must be able to tell an estimate from a settled charge without knowing which endpoint produced it."},"note":{"type":"string"}}},"OrderQuote":{"type":"object","required":["quote_id","quote_token","expires_at","lines","breakdown","margin","shipping"],"properties":{"fulfillment_policy":{"$ref":"#/components/schemas/OwnerFulfillmentPolicy"},"quote_id":{"type":"string"},"address_check":{"$ref":"#/components/schemas/AddressCheck","description":"What POST /addresses/validate finds in `address_to`, when the quote was sent one. A live key is refused an address with an `error` finding (422 address_invalid); on a test key `valid: false` here says a live quote would have been."},"quote_token":{"type":"string","description":"Signed, and what `POST /v1/order` honours. It carries the quote's own numbers, so an order placed against it cannot be re-pointed at a cheaper one."},"expires_at":{"type":"string","format":"date-time","description":"After this the token is refused and the quote must be taken again."},"lines":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLine"},"description":"The priced lines, each { sku, quantity, unit_cents, line_total_cents }."},"breakdown":{"type":"object","required":["items_cents","shipping_cents","processing_fee_cents","total_cents"],"properties":{"items_cents":{"type":"integer"},"shipping_cents":{"type":"integer"},"processing_fee_cents":{"type":"integer","description":"Compatibility field for platform and card-processing surcharges in cents. Zero on orders priced under the current no-added-fees policy; retained on historical records."},"platform_fee_cents":{"type":"integer","description":"The platform fee as a line of its own. Always 0: there is no platform fee on any order, on any plan (since 2026-10-17). Kept so a client that reads it still parses."},"card_fee_cents":{"type":"integer","description":"Card processing on everything else the order is charged, passed through at cost."},"total_cents":{"type":"integer","description":"The total before sales tax."},"tax_cents":{"type":"integer","description":"The sales tax on the goods and shipping for `address_to`, calculated by Stripe Tax and signed into `quote_token`. Present only when `tax.status` is `quoted`."},"total_with_tax_cents":{"type":"integer","description":"What an order placed against this quote is charged: `total_cents` + `tax_cents`. Present only with `tax_cents`."}}},"tax":{"type":"object","required":["status","note"],"description":"Whether sales tax is in this quote. Present only while sales tax collection is on, for a live key, on a lane we charge for; absent otherwise, and then an order against the quote carries no tax. `quoted`: the tax is in `breakdown.tax_cents` and inside the signature, and the order must ship to the quoted `address_to` (another address is refused 400 quote_mismatch, `details.reason: tax_address_changed`). `at_order`: the quote had no `address_to`, so the tax is calculated when the order is placed and added to its charge. While tax collection is on, an order refuses a quote signed without this block (400 quote_mismatch, `details.reason: tax_not_quoted`): request a fresh one.","properties":{"status":{"type":"string","enum":["quoted","at_order"]},"tax_cents":{"type":"integer","description":"On `quoted` only: the same figure as `breakdown.tax_cents`."},"note":{"type":"string"}}},"margin":{"type":"object","required":["suggested_retail_cents","margin_pct"],"description":"What to sell it for and what that leaves. A suggestion, not a commitment.","properties":{"suggested_retail_cents":{"type":"integer"},"margin_pct":{"type":"number"}}},"presentment":{"type":"object","description":"The total in the buyer's currency. Present only on a cross-border quote."},"duties":{"type":"object","description":"Present only when duties apply."},"landed_cost":{"$ref":"#/components/schemas/QuoteLandedCost"},"guarantee":{"type":"object","description":"The delivery guarantee this quote would carry. Present only when one is offered on the lane."},"dispatch_promise":{"type":"object","required":["dispatch_by","business_days","remedy"],"description":"A dispatch-by date for an order made at our own plant, and what is paid if it leaves the plant later. Present only when the platform has switched the dispatch promise on AND every line's measured dispatch p95 is inside its window — so today, with the promise switched off, no quote carries it. Inside the quote signature; an order placed against this quote is held to exactly this date and remedy.","properties":{"dispatch_by":{"type":"string","format":"date-time","description":"The order is promised to leave the plant by this instant."},"business_days":{"type":"integer","description":"The window the date was computed with, in business days (Saturday and Sunday do not count)."},"remedy":{"type":"object","required":["type","cents"],"properties":{"type":{"type":"string","enum":["shipping","credit"],"description":"`shipping`: the order's shipping charge is refunded. `credit`: a fixed amount is refunded."},"cents":{"type":"integer","description":"What a miss refunds, capped by what the order's charges have left."}}}}},"delivery_risk":{"$ref":"#/components/schemas/DeliveryRisk"},"estimated_delivery":{"type":["object","null"],"required":["earliest","latest","ships_by","is_estimate"],"description":"When an order placed now should arrive, as an estimate: the facility's production estimate (at our plant, its slowest line's: a wall decal of our catalog takes the 3 days its owner's sheet states, everything else the plant's own estimate) plus the shipping method's transit window (our plant's standard parcel from the plant's own transit estimate), in business days counted in UTC, Monday to Friday, never on a day GET /calendar lists (the federal holidays, each on the weekday it is observed, and the plant's own closure days). Null for the exact-five approved fulfillment policy: production/transit dates are not established by a shipping benchmark. Not a promise and not inside `quote_token`: the binding date, where the account has one, is `dispatch_promise`. Present on a quote priced at one of our own facilities; absent on a degraded quote and on any other lane, which sets its own times.","properties":{"earliest":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC."},"latest":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC."},"ships_by":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC: the day an order placed now should leave the facility, when its production estimate ends, counted the same way."},"is_estimate":{"type":"boolean","enum":[true],"description":"Always true: say it as an estimate wherever it is shown."}}},"deliver_by_check":{"$ref":"#/components/schemas/DeliverByCheck"},"shipping":{"$ref":"#/components/schemas/ShippingCharge"},"pricing_degraded":{"type":"string","enum":["provider_unavailable","pricing_unavailable"],"description":"Sandbox-only. The $0 breakdown beside it is an ABSENCE of a price, not a price — the routing failed and a test key was given a shaped answer anyway."},"variable_data":{"type":"array","items":{"$ref":"#/components/schemas/VariableDataSummary"},"description":"One entry per line carrying `variable_data`: the variant proofs (first, last and longest unit) and each slot's headroom, before the order is placed. Absent when no line carries it. The breakdown beside it is the same with or without it."}}},"QuoteLine":{"type":"object","required":["sku","quantity","unit_cents","line_total_cents"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer"},"unit_cents":{"type":"integer","description":"What this API charges per unit, in USD cents. Where a price is a pack total that does not divide into whole cents a unit, this is the SKU's total over its quantity on the quote, rounded to the cent, for display, and the lines of one SKU share it: it is not necessarily this line's `line_total_cents` over `quantity`. Read each line's money from `line_total_cents`, never as this times `quantity`."},"line_total_cents":{"type":"integer","description":"What an order placed against this quote charges for this line's goods, exact, in USD cents; the lines sum to `breakdown.items_cents`. Equal to `unit_cents` × `quantity` on every line priced in whole cents a unit."}}},"ShippingOption":{"type":"object","required":["method","label","cost","cost_cents","estimated_delivery","surcharges"],"properties":{"method":{"$ref":"#/components/schemas/ShippingMethod"},"label":{"type":"string"},"cost":{"type":"string","description":"Formatted, e.g. \"$7.50\". `cost_cents` is the number to compute with."},"cost_cents":{"type":"integer","description":"Flat per PARCEL, not per item: these items ship once, and this is what the order path will charge for the same method."},"estimated_delivery":{"type":["object","null"],"required":["earliest","latest","ships_by","transit_days"],"description":"An estimate: the producing facility's own production estimate plus this method's transit window (our plant's standard parcel from the plant's own transit estimate), in business days counted in UTC, Monday to Friday, never on a day GET /calendar lists (the federal holidays, each on the weekday it is observed, and the plant's own closure days). Null for the exact-five approved benchmark policy, which establishes no production/transit dates.","properties":{"earliest":{"type":"string","format":"date","description":"A date, not a timestamp: YYYY-MM-DD in UTC, business days after today."},"latest":{"type":"string","format":"date"},"ships_by":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC: the day an order placed now should leave the facility, when its production estimate ends."},"transit_days":{"type":"string","description":"Prose, e.g. \"3-5 business days\": the transit window after production."},"is_estimate":{"type":"boolean","enum":[true],"description":"Always true: these are estimates, never a promise."}}},"surcharges":{"type":"array","items":{"$ref":"#/components/schemas/ShippingSurcharge"},"description":"Compatibility itemisation of charges already inside `cost_cents`, never added to it. Current quotes contain no seasonal peak-postage surcharge: PrintedSimply absorbs the carrier expense, so this array is empty."}}},"ShippingSurcharge":{"type":"object","required":["code","reason","amount_cents","window","source","included_in"],"description":"A historical surcharge already inside the shipping figure beside it, never added again. Current quotes contain no seasonal peak-postage surcharge: PrintedSimply absorbs the carrier expense. This shape remains available for historical records.","properties":{"code":{"type":"string","enum":["peak_season"],"description":"`peak_season`: the carrier's temporary holiday price change."},"reason":{"type":"string","description":"What it is, in words."},"amount_cents":{"type":"integer","minimum":1,"description":"How much of the shipping figure this is, in cents, folded the way that figure is (all-in where the figure is all-in)."},"window":{"type":"object","required":["from","through"],"description":"The days it applies on, both inclusive.","properties":{"from":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC."},"through":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC."}}},"source":{"type":"string","format":"uri","description":"Where the amount and the dates come from: the carrier's own announcement."},"included_in":{"type":"string","enum":["shipping"],"description":"Always `shipping`: the figure this is part of. Never an additional charge."}}},"OwnerFulfillmentPolicy":{"type":"object","additionalProperties":false,"description":"Optional exact-five approved fulfillment disclosure. Applies only to the four listed double-sided flags and one Santa hat, each a separate quantity-1 US standard order. Actual manufacturing expense and contribution remain unknown; the shipping benchmark includes packing. Supplies no live mapping by itself.","required":["fulfillment_cost_basis","goods_cost_cents","manufacturing_cost_cents","actual_contribution_cents","maximum_quantity","shipping_constraints","shipping_benchmark_cents","packaging","artwork_layout","artwork_instructions","artwork_guide_url"],"properties":{"fulfillment_cost_basis":{"type":"string","enum":["approved_size_shipping_benchmark"],"description":"Approved per-size US standard shipping benchmark; actual postage, manufacturing cost and profit remain unknown."},"goods_cost_cents":{"type":"null","description":"Actual manufacturing cost is unknown, never represented by zero."},"manufacturing_cost_cents":{"type":"null"},"actual_contribution_cents":{"type":"null"},"maximum_quantity":{"type":"integer","enum":[1],"description":"One product, one SKU, quantity 1 in its own order."},"shipping_constraints":{"type":"object","required":["destination_country","method","separate_order"],"properties":{"destination_country":{"type":"string","enum":["US"]},"method":{"type":"string","enum":["standard"]},"separate_order":{"type":"boolean","enum":[true]}}},"shipping_benchmark_cents":{"type":"integer","minimum":0,"description":"The approved quantity-1 US standard benchmark, packing included."},"packaging":{"type":"string","enum":["included_in_shipping_benchmark"]},"artwork_layout":{"type":"string","enum":["prepared_combined_all"]},"artwork_instructions":{"type":"string","description":"Prepare one combined ALL PNG: flag front and back, or Santa hat front and band, according to the exact guide; remove guide lines."},"artwork_guide_url":{"type":"string","description":"Local path to the layout guide SVG. This reference is not customer artwork and must not be sent as print_file."}}},"ShippingCharge":{"type":"object","required":["method","surcharges"],"description":"The shipping the figure in `breakdown` is for: its method, and the surcharges that figure already holds, itemised. Nothing here is added to the total.","properties":{"fulfillment_policy":{"$ref":"#/components/schemas/OwnerFulfillmentPolicy"},"method":{"type":"string","description":"The shipping method the figure was priced for."},"surcharges":{"type":"array","items":{"$ref":"#/components/schemas/ShippingSurcharge"},"description":"Compatibility itemisation, never added to the total. Current quotes and new orders contain no seasonal peak-postage surcharge: PrintedSimply absorbs the carrier expense, so this array is empty. Historical stored orders may retain their original itemisation; a fee-bearing old quote must be refreshed before a new charge."}}},"DeliverByCheck":{"type":"object","required":["requested","meets","earliest_method","latest_order_day","status","advisories"],"description":"The answer to `deliver_by`, read as an estimate: production and transit are the facility's and the carrier's estimates, so a day that `meets` can still be missed. Present only when `deliver_by` was sent and there is an estimate to judge it against.","properties":{"requested":{"type":"string","format":"date","description":"The day asked about, as sent."},"meets":{"type":"boolean","description":"On a quote: whether its own method's estimate ends on or before `requested` (`estimated_delivery.latest`, never the earliest day). On a shipping estimate: whether any parcel method listed does. False when a carrier advisory touches the destination (`status` is `at_risk`): true exactly when `status` is `meets`."},"earliest_method":{"oneOf":[{"$ref":"#/components/schemas/ShippingMethod"},{"type":"null"}],"description":"The parcel method on offer whose estimate ends soonest, when it ends on or before `requested`; null when no method on offer is estimated to arrive by then. Never `letter`."},"latest_order_day":{"type":["string","null"],"format":"date","description":"The last UTC day an order can be placed and still be estimated to arrive by `requested`, computed by the same estimate as `estimated_delivery` from the same facility: on a quote, for its own method; on a shipping estimate, for the parcel method listed that allows the latest day. Any time that day counts, and it may be a weekend or a holiday (an order placed then starts counting on the next operating day). Null exactly when no order placed today or later is estimated to arrive by `requested`, so it is null when `status` is `misses`. It reads the estimate alone, never a carrier advisory. An estimate, like the dates it reads."},"status":{"type":"string","enum":["meets","at_risk","misses"],"description":"The verdict. `misses`: the estimate ends after `requested`. `at_risk`: the estimate ends by then, but a carrier advisory (`advisories`) touches the destination while the parcel would be on its way. `meets`: the estimate ends by then and no advisory touches it."},"advisories":{"type":"array","items":{"$ref":"#/components/schemas/CarrierAdvisory"},"description":"The carrier advisories that touch the destination between today and the estimate's latest day; empty when none does."}}},"CarrierAdvisory":{"type":"object","required":["id","carrier","states","zip_prefixes","from","through","active","extra_days","message","source"],"description":"A delay a carrier posted, as the plant entered it from the carrier's own notice: every date and figure is the carrier's, never computed. Our parcel methods do not name a carrier, so an advisory applies to every parcel method into the destinations it names, and a destination a request does not place (an estimate by country alone) is touched by every one.","properties":{"id":{"type":"string","description":"`adv_…`."},"carrier":{"type":"string","description":"The carrier that posted it, as it names itself, e.g. \"USPS\"."},"states":{"type":"array","items":{"type":"string"},"description":"USPS state codes it touches. With `zip_prefixes` both empty, it touches every US destination."},"zip_prefixes":{"type":"array","items":{"type":"string"},"description":"ZIP prefixes (one to five digits) it touches, beside `states`."},"from":{"type":"string","format":"date","description":"The first day it applies, YYYY-MM-DD in UTC, inclusive."},"through":{"type":"string","format":"date","description":"The last day it applies, YYYY-MM-DD in UTC, inclusive."},"active":{"type":"boolean","description":"Whether today (UTC) is inside `from`–`through`."},"extra_days":{"type":"integer","description":"The carrier's estimate of the business days it adds, as posted. An estimate."},"message":{"type":"string","description":"One sentence about it."},"source":{"type":"string","description":"The carrier's own notice, as a URL."}}},"NonOperatingDay":{"type":"object","required":["date","kind","reason","source"],"properties":{"date":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC."},"kind":{"type":"string","enum":["federal_holiday","plant_closure"],"description":"`federal_holiday`: a legal public holiday of 5 U.S.C. 6103, on the weekday it is observed. `plant_closure`: a day the plant is closed."},"reason":{"type":"string","description":"Why the day is not counted, in its source's words, e.g. \"Thanksgiving Day\", or \"Independence Day (observed)\" for the weekday that stands in for a weekend holiday."},"source":{"type":"string","description":"Where the day comes from: the statute's URL for a federal holiday."}}},"OrderByDay":{"type":"object","required":["deliver_by","occasion","method","latest_order_day","is_estimate"],"description":"The last day to order our own plant's goods by one shipping method and still be estimated to arrive by one day, computed by the same estimate POST /orders/quote dates an order with, for the goods the plant takes longest to make (wall decals, 3 production days), so any of our own plant's goods ordered by then is estimated to arrive. An estimate: production and transit are estimates, so an order placed that day can still arrive later.","properties":{"deliver_by":{"type":"string","format":"date","description":"The day the order should arrive by, YYYY-MM-DD in UTC."},"occasion":{"type":"string","description":"What the day is: \"Christmas Eve\", \"Christmas Day\", \"New Year's Eve\", or the next federal holiday's name as `days` lists it."},"method":{"$ref":"#/components/schemas/ShippingMethod"},"latest_order_day":{"type":["string","null"],"format":"date","description":"The last UTC day an order of the goods the plant takes longest to make (wall decals, 3 production days) placed by `method` is estimated to arrive by `deliver_by` (its estimated window's latest day on or before it), so an order of any of our own plant's goods placed by then is too; any time that day counts, and it may be a weekend or a holiday. Null when no such order placed today or later is, which is the answer once the day has passed. Goods made sooner can often be ordered later, null or not: a quote with `deliver_by` gives their own `deliver_by_check.latest_order_day`."},"is_estimate":{"type":"boolean","enum":[true],"description":"Always true."}}},"PeakWindow":{"type":"object","required":["from","through","in_window","cents","methods","carrier_zones","carrier_max_weight_lb","source"],"description":"The carrier's peak postage window, as the cost book holds it: literal dates the carrier published, never a computed season. The window describes carrier expenses; PrintedSimply absorbs seasonal peak postage instead of adding a customer surcharge.","properties":{"from":{"type":"string","format":"date","description":"The first day of the window, YYYY-MM-DD in UTC, inclusive."},"through":{"type":"string","format":"date","description":"The last day of the window, YYYY-MM-DD in UTC, inclusive."},"in_window":{"type":"boolean","description":"Whether today (UTC) is inside the window."},"cents":{"type":"integer","description":"The surcharge per parcel, in cents, bare: the most the window adds to one parcel's rate before that rate is folded into an all-in figure. The amount on an order is the quote's `shipping.surcharges[].amount_cents`, folded the way its shipping is; a priority or express price set above the parcel's rate can hold some or all of it."},"methods":{"type":"array","items":{"$ref":"#/components/schemas/ShippingMethod"},"description":"The parcel methods whose carrier expense can include it. PrintedSimply absorbs this expense; it is not a customer surcharge. Never `letter`: a letter carries no peak postage."},"carrier_zones":{"type":"object","required":["from","through"],"description":"The USPS zones the carrier's surcharge applies in, both inclusive. Our own parcel rate is one figure for every zone, so inside the window it holds the surcharge whatever the zone.","properties":{"from":{"type":"integer"},"through":{"type":"integer"}}},"carrier_max_weight_lb":{"type":"integer","description":"The heaviest parcel, in pounds, the carrier's surcharge names. A heavier parcel carries the same amount here."},"source":{"type":"string","description":"The carrier's own announcement of the window."}}},"ProductOptions":{"type":"object","required":["products"],"description":"What a custom size of each of our plant's configurable products may be. One document: there is no next page.","properties":{"products":{"type":"array","items":{"$ref":"#/components/schemas/ProductOption"}}}},"ProductOption":{"type":"object","required":["product","name","family","sku_template","example_sku","size","quantity","artwork","finishes","sizes","materials","shapes","price_book","plant_confirms_sku"],"properties":{"product":{"type":"string","enum":["sticker-die-cut","sticker-kiss-cut","sticker-bumper","sticker-sheet","decal-vinyl","decal-wall","magnet","poster"],"description":"The product, as `product_type` on POST /artwork/validate takes it."},"name":{"type":"string","description":"Its name: \"Die-Cut Sticker\"."},"family":{"type":"string","enum":["stickers","decals","magnets","posters"],"description":"Its family, as a catalog row's `family` names it."},"sku_template":{"type":"string","description":"How to spell a SKU of it at a size: replace `{width}` and `{height}` with the size in decimal inches (`sticker-die-cut-{width}x{height}` → `sticker-die-cut-4x6`). Case does not matter. A sticker's material word names its material wherever it stands (`sticker-die-cut-clear-3x3`, the sizes and materials in `sizes`), and a shape or finish word after the size (`-circle`, `-matte`) names how it is cut and finished without changing its price; any other word after the size changes neither the product nor its price."},"example_sku":{"type":"string","description":"A SKU of it, spelled by `sku_template`."},"size":{"$ref":"#/components/schemas/ProductSizeOption"},"quantity":{"type":"object","required":["minimum","maximum"],"description":"How many units one order line may carry: the bounds `items[].quantity` on POST /order is held to.","properties":{"minimum":{"type":"integer"},"maximum":{"type":"integer"}}},"artwork":{"type":"object","required":["min_dpi","refuse_below_dpi","bleed_in"],"description":"What the order grades an artwork file of this product against, at the size its SKU names.","properties":{"min_dpi":{"type":"integer","description":"The recommended resolution. Below it a line is warned (`below_recommended_resolution`) and still prints."},"refuse_below_dpi":{"type":"number","description":"Below this effective DPI a live order refuses the line (`low_resolution`)."},"bleed_in":{"type":"number","description":"The margin the file should run past the trim, or keep clear around a die-cut outline, in inches."}}},"finishes":{"type":"array","items":{"type":"string","enum":["glossy","matte","holographic","clear","satin"]},"description":"The finishes offered as an option: a sticker's are its sheet's (`glossy`, the bare SKU's, which a SKU does not spell, and `matte`, spelled `-matte` after the size), neither changing its price; a wall decal's is its sheet's one, `matte`, and a poster's is its one finish, `matte`, neither of which its SKU spells; a magnet has none. The SKUs to send for each are `sizes[].materials[].sku` with the option words `sizes[]` names, as `GET /v1/products` lists them in `options.variant_skus`. Empty for every other product, where a finish named in a SKU is not priced or made differently."},"sizes":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ProductListedSize"},"description":"The sizes our catalog lists for this product (die-cut and kiss-cut stickers; wall decals, 36 sizes; posters, 46 sizes; magnets, 16 sizes), each with the SKU of every material it is made in. Each is priced at its listed price or price list, not `price_book`: each row's `quantity_breaks` on GET /products, and the quote. Another size within `size` is still the book's. Null for a product the catalog lists no size of, whose sizes are all the book's."},"materials":{"type":"array","items":{"type":"string","enum":["white","clear","glitter","holographic","film","fabric","paper","transparent-film","bopp","polyester"]},"description":"The materials those sizes are made in, as a SKU names them (a sticker's white, a wall decal's film: the bare stem's, with no word). Empty where `sizes` is null, and for magnets, which name none."},"shapes":{"type":"array","items":{"type":"string","enum":["contour","circle","square","rectangle","oval","kiss"]},"description":"The shapes a SKU of those sizes may name after its size (`contour` is the bare stem's, a cut to the artwork's outline), none changing the price. Empty where `sizes` is null, for a kiss-cut sticker, whose shape is the cut, and for wall decals, posters, bumpers and single sheets, which name none. Magnet cuts are custom contour, circle and square on square sizes, or custom contour, oval and rectangle on rectangular sizes."},"price_book":{"type":["object","null"],"required":["base_cents","cents_per_sq_in"],"description":"The price book's list price for one unit: `base_cents` + width × height × `cents_per_sq_in`, rounded to the cent. Quantity breaks (each catalog row's `quantity_breaks`), any plan discount and the unit floor apply after it; POST /orders/quote answers the price an order is charged. Null for a product our catalog lists sizes of (`sizes`: die-cut and kiss-cut stickers), which is sold at those sizes only, each at its sheet's price: no other size of it is priced (custom sizes retired since 2026-10-23), and `size` states only the spec's bounds.","properties":{"base_cents":{"type":"integer"},"cents_per_sq_in":{"type":"number"}}},"plant_confirms_sku":{"type":"boolean","description":"Always true: a live quote or order also asks our plant for the exact SKU. A SKU our catalog lists (`sizes`, and each row's `options.variant_skus` on GET /products) that our plant's catalog does not carry yet is refused 422 `sku_not_orderable_yet` before anything is charged, and a test key orders it now at the listed price; any other size our plant's catalog does not carry is refused 400 `pricing_error`, and a test key's takes any size the book prices within these bounds."}}},"ProductListedSize":{"type":"object","required":["sku","width_in","height_in","materials","shapes","finishes"],"description":"One size our catalog lists: a row of GET /products in each material.","properties":{"sku":{"type":"string","description":"The bare stem's SKU at this size: the white sticker's (`sticker-die-cut-3x3`), the film wall decal's (`wall-decal-24x36`), the magnet's (`magnet-3x3`)."},"width_in":{"type":"number","description":"The width, in inches, as the catalog spells the size; either orientation is taken."},"height_in":{"type":"number"},"materials":{"type":"array","description":"Each material this size is made in, with its row's SKU. Empty for a magnet, which names none: its one SKU is `sku`.","items":{"type":"object","required":["material","sku"],"properties":{"material":{"type":"string","enum":["white","clear","glitter","holographic","film","fabric","paper","transparent-film","bopp","polyester"]},"sku":{"type":"string","description":"`sticker-die-cut-holographic-3x3`, `wall-decal-fabric-24x36`."}}}},"shapes":{"type":"array","items":{"type":"string","enum":["contour","circle","square","rectangle","oval","kiss"]},"description":"The shapes a SKU may name after this size, the bare stem's first; empty for kiss cuts and products with no cut choice; magnets offer valid cuts for their geometry."},"finishes":{"type":"array","items":{"type":"string","enum":["gloss","matte"]},"description":"The finish words a SKU may name after this size, the bare stem's first, as a SKU spells them."}}},"ProductSizeOption":{"type":"object","required":["unit","max_width_in","max_height_in","either_orientation","min_area_sq_in","step_in"],"description":"The sizes the price book prices. Outside them the book prices nothing, and an order of the SKU is refused unless our plant's own catalog lists it.","properties":{"unit":{"type":"string","enum":["in"],"description":"Every size here, and in a SKU, is in inches."},"max_width_in":{"type":"number","description":"The widest side the book prices."},"max_height_in":{"type":"number","description":"The tallest side the book prices."},"either_orientation":{"type":"boolean","description":"True: `{height}x{width}` is priced wherever `{width}x{height}` is."},"min_area_sq_in":{"type":["number","null"],"description":"The least width × height, in square inches, the book prices for a custom size, to a hundredth: a smaller print does not pay what one unit costs the plant alone. Null when every size is priced. It bounds a size you build, not the catalog's: every size in `sizes` is orderable whatever its area (a wall decal's 2×10 is 20 sq in)."},"step_in":{"type":["number","null"],"description":"Null: there is no step, and a side is any decimal number of inches."}}},"PlantCalendar":{"type":"object","required":["from","through","timezone","weekly","days","order_by","peak","advisories"],"description":"The days no delivery estimate counts, over a fixed window, with the last day to order for a short table of days and the carrier's peak postage window. One document: there is no next page.","properties":{"from":{"type":"string","format":"date","description":"Today, YYYY-MM-DD in UTC."},"through":{"type":"string","format":"date","description":"The last day this answer covers: the day before the same date next year, or the table's last day when that is sooner."},"timezone":{"type":"string","enum":["UTC"],"description":"Every date here, and in every estimate, is a UTC day."},"weekly":{"type":"array","items":{"type":"string","enum":["saturday","sunday"]},"description":"The days of every week no estimate counts."},"days":{"type":"array","items":{"$ref":"#/components/schemas/NonOperatingDay"},"description":"Every other day from `from` through `through` that no estimate counts, in date order."},"order_by":{"type":"array","items":{"$ref":"#/components/schemas/OrderByDay"},"description":"The last day to order, by each parcel method on offer, for each day of a short table: Christmas Eve, Christmas Day, New Year's Eve (each in the next year it falls on or after today) and the next federal holiday after today, in date order. For our own plant's goods, dated for those it takes longest to make (wall decals, 3 production days), so any of them ordered by a day listed is estimated to arrive. Goods made sooner, another day, or another facility's goods are answered by `deliver_by_check.latest_order_day` on POST /orders/quote. Estimates."},"peak":{"oneOf":[{"$ref":"#/components/schemas/PeakWindow"},{"type":"null"}],"description":"The carrier's peak postage window, or null once it has ended (a new one is published when the carrier announces it)."},"advisories":{"type":"array","items":{"$ref":"#/components/schemas/CarrierAdvisory"},"description":"Every carrier advisory not withdrawn and not yet over, soonest first: in effect today (`active`) or starting later. Empty when the carriers have posted none. A quote or shipping estimate into a destination one touches answers `deliver_by_check.status` `at_risk`."}}},"ShippingEstimate":{"type":"object","required":["destination","total_items","options"],"properties":{"fulfillment_policy":{"$ref":"#/components/schemas/OwnerFulfillmentPolicy"},"destination":{"type":"object","required":["zip","country"],"description":"Echoed, not priced from.","properties":{"zip":{"type":["string","null"],"description":"Null when the caller estimated by country alone."},"country":{"type":"string"}}},"total_items":{"type":"integer","description":"Units across the request. The cost is per parcel regardless."},"shipping_class":{"type":["object","null"],"required":["name","weight_oz"],"description":"The parcel these items pack into, which prices every parcel option: a weight band (`parcel_1lb`, `parcel_2lb`, `parcel_5lb`, `parcel_over_5lb`), with `_tube` when a poster or wall decal ships rolled. Estimated from the items' print area, not weighed. Null for the exact-five approved benchmark policy; no weight is inferred.","properties":{"name":{"type":"string"},"weight_oz":{"type":"number","description":"Estimated shipped weight in ounces, packing tube included."}}},"options":{"type":"array","items":{"$ref":"#/components/schemas/ShippingOption"}},"deliver_by_check":{"$ref":"#/components/schemas/DeliverByCheck"},"delivery_risk":{"$ref":"#/components/schemas/DeliveryRisk"}}},"DeliveryRisk":{"type":"object","description":"Present only when the lane carries a known risk worth stating before the order. Non-binding. When a carrier advisory touches the destination between today and the estimate's latest day, `at_risk` is true and `advisories` lists them; a quote may also carry its lane's lead-time read beside them.","properties":{"at_risk":{"type":"boolean","enum":[true],"description":"Present, and true, when `advisories` is."},"advisories":{"type":"array","items":{"$ref":"#/components/schemas/CarrierAdvisory"},"description":"The carrier advisories that touch the destination while the parcel would be on its way."}}},"CreateOrderResponse":{"type":"object","required":["order_id","portal_order_id","provider","tracking_token","tracking_url","charged","breakdown","shipping"],"properties":{"order_id":{"type":"string","description":"WordPress order ID"},"provider":{"type":"string","description":"The fulfilment network the order was placed with."},"tracking_token":{"type":"string","description":"Opaque token for the public tracking page. Safe to share; it names no account."},"tracking_url":{"type":"string","description":"The buyer-facing tracking page for this order."},"test_mode":{"type":"boolean","description":"True when the order was placed with a sandbox key. Present on sandbox orders only."},"lifecycle":{"type":"string","description":"`simulated` on a sandbox order — the order was priced and recorded but never sent to a facility."},"pricing_degraded":{"type":"string","enum":["provider_unavailable","pricing_unavailable"],"description":"Present only when the price could not be computed: `provider_unavailable` or `pricing_unavailable`. The breakdown is then an absence of a price, not a price of zero."},"portal_order_id":{"type":"string","description":"This platform's own id for the order (`ord_…`)."},"reference":{"type":"string","description":"The reference you supplied, echoed back. Absent when you supplied none."},"vat":{"type":"string","description":"The tax-registration number you supplied, echoed back. Absent when you supplied none."},"charged":{"type":"string","description":"Amount charged (e.g. '$12.50')."},"quote_id":{"type":"string","description":"The id of the honored quote, echoed when the order was placed with a quote_token. Absent otherwise."},"reordered_from":{"type":"string","description":"On an order POST /order/{id}/reorder placed: the source order's `portal_order_id` (GET /order/{id} takes it). Absent on any other order."},"variable_data":{"type":"array","items":{"$ref":"#/components/schemas/VariableDataSummary"},"description":"One entry per line carrying `variable_data`: the variant proofs (first, last and longest unit) of what the order prints. Absent when no line carries it."},"artwork_check":{"$ref":"#/components/schemas/ArtworkCheck","description":"Each own-plant line's artwork, read and graded before anything was charged (resolution at its print size, a file that is not there or not an image, CMYK, a die-cut sticker's transparency). On a test key, `would_refuse` says whether a live order would have been refused, and `missing_artwork` lists the own-plant lines that carry none. Absent when no line is made at our own plant with artwork, and (on a test key) none is made there without it."},"address_check":{"$ref":"#/components/schemas/AddressCheck","description":"What POST /addresses/validate finds in `address_to`, warnings included. A live order whose address has an `error` finding is refused 422 address_invalid before anything is charged; on a test key `valid: false` here says a live order would have been."},"breakdown":{"type":"object","required":["items","shipping","processing_fee","total"],"properties":{"items":{"type":"string"},"shipping":{"type":"string"},"processing_fee":{"type":"string","description":"Compatibility field for platform and card-processing surcharges. Zero on orders priced under the current no-added-fees policy; retained on historical records."},"platform_fee":{"type":"string","description":"The platform fee as a line of its own. Always 0: there is no platform fee on any order, on any plan (since 2026-10-17). Kept so a client that reads it still parses."},"card_fee":{"type":"string","description":"Card processing on everything else the order is charged, passed through at cost."},"total":{"type":"string","description":"The order's total before sales tax."},"tax":{"type":"string","description":"The sales tax charged on top of `total`: the tax the honoured quote signed, or calculated by Stripe Tax when the order was placed, on the goods and shipping for `address_to`. Present only on a live order that was taxed (sales tax collection on, a lane we charge for)."},"total_with_tax":{"type":"string","description":"What the card was charged, sales tax included: the same figure as `charged`. Present only with `tax`."}}},"shipping":{"$ref":"#/components/schemas/ShippingCharge"},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"When the order should arrive, as an estimate: the quote's `estimated_delivery` for the order as placed, counted from now. Null on a lane that is not one of our own facilities. GET /order/{id}, GET /orders and the order's `order.in_production`, `order.quality_check` and `order.shipped` events carry the same date."}}},"OrderLandedCost":{"type":["object","null"],"description":"What the border cost on this order, as recorded for the parcel that ships. Null on a domestic shipment — there is no border, so there is nothing to state, and a row of zeros would read like a customs assessment that came to nothing. When a cross-border order has no stored figures, `recorded` is false and only `destination_country` and `not_recorded_reason` are present: the duty, tax and FX rates that applied on the day are not recoverable, and backfilling them with today's rates would be a fabricated record rather than a missing one. That is what an order placed before these figures were stored looks like, and also a free replacement order, which carries no charged goods value to assess. The figures are a snapshot, never recomputed on read, so a stale figure cannot be mistaken for a current one — but they ARE re-recorded when the parcel itself changes, because the border is costed against the destination, the goods and the shipping charge. Change the address, add or cancel a line, change a quantity or change the shipping method and the order is costed again at that moment; `recorded_at` moves with it, and it is the order's creation time until something does. An order re-routed to a domestic address goes back to null. `estimated` stays true throughout: on DAP the courier bills the recipient directly, so we never learn what customs actually assessed.","properties":{"recorded":{"type":"boolean","description":"False when no landed cost was stored for this order."},"destination_country":{"type":"string","description":"ISO 3166-1 alpha-2."},"not_recorded_reason":{"type":"string","description":"Present only when `recorded` is false."},"recorded_at":{"type":"string","format":"date-time","description":"When the figures were computed — never the moment of the read. The order's creation time, or the moment of the last change to its destination, lines or shipping charge."},"duty_basis":{"type":"string","enum":["DDP","DAP"],"description":"DAP today: goods and shipping are collected at checkout, and the courier bills the recipient for duties and import tax on delivery."},"duty_rate":{"type":"number"},"tax_rate":{"type":"number"},"breakdown":{"type":"object","properties":{"goods_cents":{"type":"integer"},"shipping_cents":{"type":"integer"},"duties_cents":{"type":"integer"},"tax_cents":{"type":"integer"}}},"total_landed_cents":{"type":"integer","description":"What the buyer ends up paying in total."},"pre_collected_cents":{"type":"integer","description":"Collected at checkout. Equals goods + shipping — never more than was charged."},"due_on_delivery_cents":{"type":"integer","description":"Billed by the courier on delivery. Sums with `pre_collected_cents` to the total."},"presentment":{"type":"object","properties":{"currency":{"type":"string"},"amount_cents":{"type":"integer"},"minor_units":{"type":"integer"},"fx_rate":{"type":"number"}}},"estimated":{"type":"boolean"},"note":{"type":"string"}},"required":["recorded"]},"ShippingMethod":{"type":"string","enum":["standard","priority","express","letter"],"description":"The shipping tiers this platform prices. Per parcel, not per item: `standard` by the parcel's weight class (POST /v1/shipping/estimate reports it), and for items made at the partner facility (apparel, mugs) at the larger of that and the partner's own estimate for the parcel; `priority` and `express` at their price and never below standard for the same parcel. An unrecognised value is refused with 400 validation_error rather than priced as `standard`. `standard` is always offered; `priority` and `express` are offered when priced (each at or above the USPS cost of the service), and until then a request for either is refused 400 `shipping_method_unavailable`, with `details.offered` naming the methods that are, and nothing is charged. `letter` is untracked letter mail for stickers and small vinyl decals from our own plant, offered only where the platform prices it, for an order whose every item qualifies, within a unit and an order-value ceiling: otherwise a request for it is refused 400 `letter_mail_unavailable` with `details.reason`, and nothing is charged. On the API it is never chosen for you — no default, estimate or storefront mapping picks it — and it is placed through POST /v1/order only (bulk and bundle orders ship by parcel). A hosted store is the one exception, and only where its merchant turned on small orders by letter in the dashboard's store editor (off by default, launched stores included): a qualifying basket bought on its store page or a buy link ships as a letter, and the shopper is told before paying. A letter has no carrier tracking: its order events and tracking page state the expected window, and POST /v1/order/{id}/missing-letter reships it once if it does not arrive."},"OrderEstimatedDelivery":{"type":["object","null"],"required":["earliest","latest","ships_by","is_estimate"],"description":"When the order should arrive, as an estimate: the producing facility's production estimate (at our plant, its slowest line's: a wall decal of our catalog takes the 3 days its owner's sheet states, everything else the plant's own estimate) plus the shipping method's transit window (our plant's standard parcel from the plant's own transit estimate), in business days counted in UTC, Monday to Friday, never on a day GET /calendar lists (the federal holidays, each on the weekday it is observed, and the plant's own closure days), counted from when the order was placed (when a person approved it, for an order placed with approval). The same function POST /orders/quote dates an order with, so it is the quote's `estimated_delivery` for the order as placed. Not a promise, and it does not move when an order runs late. Null on a lane that is not one of our own facilities (it sets its own times), on an order with no known shipping method, and whenever the order is not on its way: `pending_approval`, `pending_payment`, `delivered`, `cancelled`, `issue_flagged`, `rerouting` and every legacy status. Present (non-null) only on `paid`, `accepted`, `in_production`, `quality_check` and `shipped`.","properties":{"earliest":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC: the soonest the order should arrive."},"latest":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC: the latest it should, before anything goes wrong."},"ships_by":{"type":"string","format":"date","description":"YYYY-MM-DD, UTC: the day it should leave the facility, when its production estimate ends, counted the same way."},"is_estimate":{"type":"boolean","enum":[true],"description":"Always true: say it as an estimate wherever it is shown."}}},"OrderProvenance":{"type":["object","null"],"description":"The independent network shop that produced an order, or null when an in-house lane did. `GET /v1/orders` and `GET /v1/order/{id}` build it from the same helper and both `$ref` this, so they cannot describe one fact two ways — which they did, one of them calling it a string. Every order placed on this portal is made at our own plant, so it is null on them; a shop is named only on an order placed before that.","required":["name","slug","profile_url"],"properties":{"name":{"type":"string","description":"The shop's display name."},"slug":{"type":"string","description":"The shop's directory slug."},"profile_url":{"type":"string","format":"uri","description":"Its public profile page on this platform."}}},"OrderGuarantee":{"type":["object","null"],"required":["guarantee_id","deliver_by","quality_standard","confidence","remedy","status","issued_at","resolved_at","remedies"],"description":"The delivery guarantee in force on this order, or null when the order carries none (guarantees are issued only on lanes whose measured on-time record clears the floor, and only while the feature is enabled). The keys mirror the `guarantee` block returned by POST /orders/quote, so this is the object you were quoted, read back, plus what has since happened to it.","properties":{"guarantee_id":{"type":"string","description":"Same id the quote issued."},"deliver_by":{"type":"string","format":"date-time","description":"The date this order is guaranteed to arrive by."},"quality_standard":{"type":"string","description":"The quality standard guaranteed (\"standard\" in v1)."},"confidence":{"type":"number","description":"Lower bound on the lane's on-time probability at issue time (Wilson), between 0 and 1."},"remedy":{"type":"object","required":["type","cap_cents"],"description":"The promise. cap_cents is the ceiling on what a breach can pay — never an amount owed or paid; read `remedies` for what was actually recorded.","properties":{"type":{"type":"string","enum":["credit","remake","expedite"],"description":"credit is the only remedy issued today, and the one that settles as a real refund. remake and expedite are reserved for the ops-driven remedies; one would settle as not_applicable rather than pay."},"cap_cents":{"type":"integer"}}},"status":{"type":"string","enum":["issued","honored","remedied"],"description":"issued while the promise is open, honored once the order was delivered on time, remedied once it was not. There is deliberately no `breached` value: a breach and its capped remedy are recorded in one transaction (so two overlapping sweeps cannot both pay out), so the terminal state of a broken promise is `remedied` and whether the money moved is the remedy's own status."},"issued_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time","description":"When the guarantee was honored or remedied; null while still open."},"remedies":{"type":"array","description":"Remedies recorded against this guarantee. Empty unless it was breached.","items":{"type":"object","required":["type","amount_cents","status","applied_at","created_at"],"properties":{"type":{"type":"string","enum":["credit","remake","expedite"]},"amount_cents":{"type":"integer","description":"What was recorded, capped at the order value at issue."},"status":{"type":"string","enum":["pending","applied","failed","not_applicable"],"description":"applied is the only value that means money moved — a real refund on the order's charge. not_applicable is the ordinary sandbox outcome (a test order has no charge) and also covers a remake/expedite, which is an ops action rather than an automatic payment. failed is an owed remedy that has not been paid, kept distinct from not_applicable so it stays visible."},"applied_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}}}}},"ArtworkAccessGrant":{"type":"object","description":"One print file released to one independent shop for this order. An order produced by our own facilities has none. Grants are created before the order is submitted, expire 15 minutes after the last time they were issued or re-issued, and are withdrawn outright if the shop declines, the order is cancelled, or the order is re-routed elsewhere.","properties":{"asset_id":{"type":"string","description":"Stable handle for the file this grant covers — a digest of the artwork URL, so two order lines printing the same design share one grant and one count. The artwork URL itself is deliberately not returned: it is held only so the shop never receives it."},"location":{"type":["string","null"],"description":"Print area (\"front\", \"back\", …) or \"mockup\"; null when the order line named none."},"shop":{"description":"The shop the file was released to. Null only if that shop's record has since been deleted — a suspended or de-listed shop is still named here, because an audit entry that disappeared with the shop's directory listing would hide exactly the disclosure worth reading.","anyOf":[{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"}}},{"type":"null"}]},"status":{"type":"string","enum":["live","expired","revoked"],"description":"live while the signed window is open, expired once it closed without being re-issued (the ordinary end state — we simply stop re-issuing), revoked if we withdrew it first. live is necessary but not sufficient for a fetch to succeed: access also requires the shop to still be live on the network and the order not to be cancelled, both of which this response already reports via made_by and status."},"granted_at":{"type":"string","format":"date-time","description":"When the file was first released to this shop."},"expires_at":{"type":"string","format":"date-time","description":"End of the current signed window. In the past on an expired grant."},"access_count":{"type":"integer","description":"Fetches recorded against this grant. 0 means the file was granted and never downloaded."},"last_accessed_at":{"type":["string","null"],"format":"date-time","description":"Most recent fetch, or null if there has never been one."},"revoked_at":{"type":["string","null"],"format":"date-time"},"revoked_reason":{"type":["string","null"],"enum":["provider_declined","submission_failed","order_cancelled","order_rerouted","reroute_submit_failed","account_purged",null],"description":"Why the grant was withdrawn; null unless status is revoked. A closed set: provider_declined (the shop refused the order), submission_failed (submission failed after the URLs had gone out), order_cancelled, order_rerouted (the order moved to another maker), reroute_submit_failed (the replacement refused it too)."}},"required":["asset_id","location","shop","status","granted_at","expires_at","access_count","last_accessed_at","revoked_at","revoked_reason"]},"OrderApproval":{"type":"object","required":["status","url","expires_at","decided_at","decided_by","failure"],"description":"A person's approval of an order placed with `approval: \"required\"`. Present only on such an order. The decision is the person's, recorded by the approval page: apart from a line's `artwork.approved_by`, which the agent types.","properties":{"status":{"type":"string","enum":["pending","approving","approved","declined","expired","cancelled","failed"],"description":"`pending`: waiting for the person. `approving`: a decision is placing it now. `approved`: approved and placed. `declined`: the person declined; the order is cancelled. `expired`: nobody decided in time; cancelled. `cancelled`: you cancelled it before anyone decided. `failed`: an approval did not place it (see `failure`); nothing was charged and it may be approved again until it expires."},"url":{"type":["string","null"],"description":"The signed page the person approves or declines on, with no account. Null once it no longer decides (decided, expired, cancelled)."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the link stops deciding: 72 hours after the hold."},"decided_at":{"type":["string","null"],"format":"date-time","description":"When the person approved or declined it. Null until then."},"decided_by":{"type":["string","null"],"description":"The name the person typed with their decision, if any. No address or other identifier is recorded."},"failure":{"type":["object","null"],"description":"Why the last approval placed nothing: the code and message POST /order answered. Null unless an approval failed.","required":["code","message","at"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"at":{"type":"string","format":"date-time"}}}}},"CancelOrderResponse":{"type":"object","required":["order_id","refunded","refunded_cents"],"properties":{"order_id":{"type":"string"},"test_mode":{"type":"boolean","description":"True when the cancelled order was a sandbox order."},"refunded":{"type":"string","description":"Amount refunded (e.g. '$12.50')"},"refunded_cents":{"type":"integer"},"refund_status":{"type":"string","enum":["refunded","not_owed","pending"],"description":"What happened to the money. `refunded`: everything the order's charges still held went back (`refunded_cents` equals `owed_cents`). `not_owed`: nothing was charged or it was already refunded, as for every sandbox order. `pending`: the order is cancelled but some or all of the refund failed at the card processor; it is retried automatically, `refunded_cents` is what went back now and `owed_cents` what is due in total."},"owed_cents":{"type":"integer","description":"What the order's charges held when it was cancelled: the amount the cancellation refunds."}}},"AddItemRequest":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","minimum":1,"maximum":10000},"print_file":{"type":"string","format":"uri"}}},"ItemShippingChange":{"type":"object","required":["method","shipping_class","delta_cents","shipping_cents"],"description":"What an item change did to the order's shipping line. The order ships one parcel, so adding a line or changing a quantity re-prices it: by the order's recorded method on its own lane, the lines before and after priced on today's terms, the difference charged with the goods on an increase and refunded with them on a decrease.","properties":{"method":{"$ref":"#/components/schemas/ShippingMethod","description":"The method the parcel was priced by: the order's recorded one, or `standard` when it recorded none."},"shipping_class":{"type":["object","null"],"required":["name","weight_oz"],"description":"The parcel the order's lines now pack into, as POST /shipping/estimate reports it: a weight band (`parcel_1lb`, `parcel_2lb`, `parcel_5lb`, `parcel_over_5lb`), with `_tube` when it ships rolled. Null on a letter.","properties":{"name":{"type":"string"},"weight_oz":{"type":"number","description":"Estimated shipped weight in ounces, packing tube included."}}},"delta_cents":{"type":"integer","description":"What this change moved on the shipping line, in cents: positive when it was charged, negative when it was refunded, 0 when the parcel's price did not change (or a decrease's shipping could not be priced and stays as charged)."},"shipping_cents":{"type":"integer","description":"The order's shipping line after this change, in cents."}}},"AddItemResponse":{"type":"object","required":["item","shipping","total_delta_cents"],"properties":{"item":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"quantity":{"type":"integer"},"unit_price":{"type":"string"},"charged":{"type":"string","description":"The line's goods. What the card was charged is `total_delta_cents`."}}},"shipping":{"$ref":"#/components/schemas/ItemShippingChange"},"total_delta_cents":{"type":"integer","description":"Everything this call charged, in cents: the line's goods and `shipping.delta_cents`. The figure the key's spend caps see and order.updated's `total_delta_cents`. On a sandbox order nothing is charged and this is what a live order would be."},"test_mode":{"type":"boolean","description":"Present and true on a sandbox order; nothing was charged."},"pricing_degraded":{"type":"string","enum":["provider_unavailable"],"description":"Sandbox only. The SKU's facility catalog was unreachable, so the line was added at $0.00 — an absence of a price, not a price — exactly as POST /order, /orders/quote and /pricing/estimate degrade for a test key. A live order gets 400 pricing_error instead."},"artwork_check":{"$ref":"#/components/schemas/ArtworkCheck","description":"The verdict on the added line's artwork when the order is made at our own plant, as POST /order's artwork_check (its one line is item_index 0); on a test key, `missing_artwork` when the line has no print_file. Absent otherwise."}}},"UpdateItemRequest":{"type":"object","required":["quantity"],"properties":{"quantity":{"type":"integer","minimum":1,"maximum":10000}}},"UpdateItemResponse":{"type":"object","required":["item","shipping","total_delta_cents"],"properties":{"test_mode":{"type":"boolean","description":"True when the order is a sandbox order."},"item":{"type":"object","properties":{"id":{"type":"string"},"sku":{"type":"string"},"previous_quantity":{"type":"integer"},"new_quantity":{"type":"integer"},"unit_price":{"type":"string"},"delta":{"type":"string","description":"The line's goods delta. What the card moved is `total_delta_cents`."}}},"shipping":{"$ref":"#/components/schemas/ItemShippingChange"},"total_delta_cents":{"type":"integer","description":"Everything this call moved, in cents: the line's goods delta and `shipping.delta_cents`, charged when positive and refunded across the order's charges when negative. The figure the key's spend caps see on an increase and order.updated's `total_delta_cents`. On a sandbox order nothing moves and this is what a live order would move."}}},"CancelItemResponse":{"type":"object","required":["item_id","refunded","refunded_cents"],"properties":{"item_id":{"type":"string"},"test_mode":{"type":"boolean","description":"True when the order is a sandbox order."},"refunded":{"type":"string"},"refunded_cents":{"type":"integer"}}},"PricingEstimateRequest":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":100,"description":"At most 100 lines of at most 10,000 units each, the bounds POST /order takes.","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","minimum":1,"maximum":10000}}}},"shipping_method":{"type":"string","enum":["standard","priority","express","letter"],"default":"standard"},"destination_country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO-3166-1 alpha-2. Optional. This estimate prices our own facilities' lanes, which ship to US addresses only, so any other country is refused 422 destination_unavailable."}}},"PricingEstimateResponse":{"type":"object","required":["items","shipping","processing_fee","total","total_cents"],"properties":{"fulfillment_policy":{"$ref":"#/components/schemas/OwnerFulfillmentPolicy"},"items":{"type":"string","description":"Item subtotal (e.g. '$25.00')"},"pricing_degraded":{"type":"string","enum":["provider_unavailable","pricing_unavailable"],"description":"Present only when the estimate could not be priced. The figures above are then an absence of a price, not a price of zero. Never present on a live estimate — those answer 503."},"landed_cost":{"type":"object","description":"Duty and tax for a cross-border destination. Not returned today: this estimate prices our own lanes, which ship to US addresses only, so a non-US `destination_country` is refused 422 destination_unavailable and a US one has nothing at the border. Kept in the shape for when our own lanes ship abroad.","properties":{"destination_country":{"type":"string"},"duty_basis":{"type":"string","enum":["DDP","DAP"]},"duty_rate":{"type":"number"},"tax_rate":{"type":"number"},"breakdown":{"type":"object","properties":{"goods_cents":{"type":"integer"},"shipping_cents":{"type":"integer"},"duties_cents":{"type":"integer"},"tax_cents":{"type":"integer"}}},"total_landed_cents":{"type":"integer"},"pre_collected_cents":{"type":"integer"},"due_on_delivery_cents":{"type":"integer"},"presentment":{"type":"object","description":"The same total in the buyer's own currency.","properties":{"currency":{"type":"string"},"amount_cents":{"type":"integer"},"minor_units":{"type":"integer"},"fx_rate":{"type":"number"}}}}},"shipping":{"type":"string"},"processing_fee":{"type":"string","description":"Compatibility field for platform and card-processing surcharges. Zero on orders priced under the current no-added-fees policy; retained on historical records."},"platform_fee":{"type":"string","description":"The platform fee as a line of its own. Always 0: there is no platform fee on any order, on any plan (since 2026-10-17). Kept so a client that reads it still parses."},"card_fee":{"type":"string","description":"Card processing on everything else the order is charged, passed through at cost."},"total":{"type":"string"},"total_cents":{"type":"integer"}}},"BulkOrderResult":{"type":"object","required":["index","status"],"properties":{"index":{"type":"integer","description":"Position of this entry in the request's orders array"},"status":{"type":"string","enum":["success","error"]},"order_id":{"type":"string","description":"The public order id, on success"},"portal_order_id":{"type":"string"},"provider":{"type":"string","description":"The facility that took the order"},"charged":{"type":"string","description":"What was charged for this entry, e.g. '$23.79', sales tax included"},"tax":{"type":"string","description":"The sales tax inside `charged`, on a live entry that was taxed only (sales tax collection on): the entry's quoted tax, or calculated for its `address_to` when it was placed."},"tracking_url":{"type":"string","format":"uri"},"test_mode":{"type":"boolean"},"quote_id":{"type":"string","description":"Set when this entry was charged a signed quote's locked total"},"reference":{"type":"string","description":"Your own identifier for this entry, echoed back"},"artwork_check":{"$ref":"#/components/schemas/ArtworkCheck","description":"On success: the verdict on each of this entry's lines made at our own plant, numbered by the entry's own item_index, as POST /order's artwork_check. Absent when no line has one and, on a test key, no own-plant line lacks one."},"address_check":{"anyOf":[{"$ref":"#/components/schemas/AddressCheck"},{"type":"null"}],"description":"What POST /addresses/validate finds in this entry's `address_to`, warnings included: POST /order's `address_check`. A live entry with an `error` finding is refused (`error_code: address_invalid`) before anything is charged; on a test key `valid: false` here says a live entry would have been. Null only where the check did not run."},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"On success: when this entry's order should arrive, as POST /order's `estimated_delivery` dates it. Null on a refused entry and on a lane that is not one of our own facilities."},"error":{"type":"string","description":"Why this entry was refused, on error. Every refusal the route words itself starts with its code (`destination_unavailable:`, `duplicate_reference:`, `invalid_quote:`, `spend_cap_exceeded:`, `payment_failed:`, …), which `error_code` carries as a field of its own; `plant_unreachable:` means our plant gave no answer about a SKU — nothing was charged, and the entry may be placed again after the response's `Retry-After`. `production_reconciliation_required:` means production may exist and support must reconcile it; do not refund or resubmit this entry under another key. `fulfillment_error:` and `order_error:` entries were charged: the sentence says whether the payment was refunded or the order stands."},"error_code":{"type":["string","null"],"enum":["address_invalid","authentication_required","below_cost_floor","destination_unavailable","duplicate_reference","fraud_review","fulfillment_error","invalid_quote","order_error","payment_failed","plant_unreachable","pricing_error","production_reconciliation_required","provider_cannot_produce","provider_unavailable","quote_mismatch","reference_check_unavailable","sku_not_orderable_yet","spend_cap_exceeded","tax_unavailable","unknown_sku",null],"description":"The code `error` starts with, to branch on without splitting the string: the code POST /order answers the same refusal with (`plant_unreachable` is POST /order's `details.reason` on its 503 `provider_unavailable`). Null on success, and on an error the route did not word (an unexpected failure, whose `error` starts with no code)."}}},"BulkOrdersResponse":{"type":"object","required":["total","succeeded","failed","results"],"properties":{"total":{"type":"integer"},"succeeded":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/BulkOrderResult"}}}},"MultiShipShipmentResult":{"type":"object","required":["shipment_id","index","status","address","items"],"properties":{"shipment_id":{"type":"string"},"index":{"type":"integer","description":"Position of this shipment in the request's shipments array"},"status":{"type":"string","enum":["success","error"]},"order_id":{"type":["string","null"],"description":"The order placed for this shipment, on success"},"portal_order_id":{"type":["string","null"]},"tracking_url":{"type":["string","null"],"format":"uri"},"charged":{"type":["string","null"]},"error":{"type":["string","null"],"description":"Why this shipment was refused, on error: POST /v1/orders/bulk's row error, so `plant_unreachable:` means our plant gave no answer about a SKU and nothing was charged for it."},"error_code":{"type":["string","null"],"enum":["address_invalid","authentication_required","below_cost_floor","destination_unavailable","duplicate_reference","fraud_review","fulfillment_error","invalid_quote","order_error","payment_failed","plant_unreachable","pricing_error","production_reconciliation_required","provider_cannot_produce","provider_unavailable","quote_mismatch","reference_check_unavailable","sku_not_orderable_yet","spend_cap_exceeded","tax_unavailable","unknown_sku",null],"description":"POST /v1/orders/bulk's row `error_code`: the code `error` starts with, the code POST /order answers the same refusal with. Null on success, and on an error whose `error` starts with no code."},"address_check":{"anyOf":[{"$ref":"#/components/schemas/AddressCheck"},{"type":"null"}],"description":"POST /order's `address_check` for this shipment's address, warnings included. A live shipment with an `error` finding is refused (`error_code: address_invalid`) before anything is charged. Null only where the check did not run."},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"On success: when this shipment's order should arrive, as POST /order's `estimated_delivery` dates it. Null on a refused shipment and on a lane that is not one of our own facilities."},"address":{"$ref":"#/components/schemas/Address"},"items":{"type":"array","items":{"type":"object"},"description":"The items that ship to this address"}}},"MultiShipResponse":{"type":"object","required":["group_id","order_ids","shipments","total_items","total_shipments","succeeded","failed","test_mode"],"properties":{"group_id":{"type":"string","description":"The group the per-shipment orders were placed under (msh_…)"},"order_ids":{"type":"array","items":{"type":"string"},"description":"The order ids of the shipments that succeeded"},"shipments":{"type":"array","items":{"$ref":"#/components/schemas/MultiShipShipmentResult"}},"total_items":{"type":"integer"},"total_shipments":{"type":"integer"},"succeeded":{"type":"integer"},"failed":{"type":"integer"},"test_mode":{"type":"boolean"}}},"OrderCreatedWebhook":{"type":"object","description":"The body of a `order.created` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.created"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderCreatedWebhookData"}}},"OrderCreatedWebhookData":{"type":"object","description":"`order.created`'s data. A path may add keys of its own beside these, which are not part of the contract: handle the documented keys and ignore the rest.","required":["order_id","portal_order_id","provider","status","total_cents","items","created_at","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The public provider name every create path reports; the lifecycle events report the facility making the order."},"status":{"type":"string","description":"The order's status when placed.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"total_cents":{"type":"integer","description":"What the order totals, in cents."},"items":{"type":"array","description":"The lines the order was placed for.","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","description":"The product."},"quantity":{"type":"integer","description":"How many."},"artwork":{"$ref":"#/components/schemas/ArtworkDeclaration","description":"How the line's artwork was made and who approved it, as the order accepted it. Absent on a line that declared none."}}}},"created_at":{"type":"string","format":"date-time","description":"When the order was placed: the order's own time, not the event's."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}},"additionalProperties":true},"OrderPaidWebhook":{"type":"object","description":"The body of a `order.paid` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.paid"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderPaidWebhookData"}}},"OrderPaidWebhookData":{"type":"object","description":"`order.paid`'s data. A path may add keys of its own beside these, which are not part of the contract: handle the documented keys and ignore the rest.","required":["order_id","portal_order_id","provider","status","amount_cents","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"provider":{"type":"string","description":"The public provider name the order's `order.created` reported."},"status":{"type":"string","description":"Always `paid`.","enum":["paid"]},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"charge_id":{"type":"string","description":"The id of the charge that captured the money. Absent when nothing was charged (a test key's order) or the path that took the payment has no charge id to send."},"amount_cents":{"type":"integer","description":"The order's total, in cents. A test key's order is charged nothing; this is still its total."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}},"additionalProperties":true},"OrderInProductionWebhook":{"type":"object","description":"The body of a `order.in_production` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.in_production"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderInProductionWebhookData"}}},"OrderInProductionWebhookData":{"type":"object","description":"`order.in_production`'s data.","required":["order_id","portal_order_id","provider","status","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The facility making the order."},"status":{"type":"string","description":"The order's status after this event.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"When the order should arrive, as an estimate counted from placement: the same date GET /order/{id} reports. Absent where no honest date exists (a lane that is not one of our own facilities, an order with no known shipping method); never carried by `order.delivered`, `order.cancelled` or `order.issue`."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number, once a shipment exists. Absent before one does."},"tracking_carrier":{"type":["string","null"],"description":"The carrier, once a shipment exists. Absent before one does."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking page, once a shipment exists. Absent before one does.","format":"uri"},"tracking":{"type":"object","deprecated":true,"description":"The same three values as `tracking_number`, `tracking_carrier` and `tracking_url`, nested; kept for receivers written before those existed. Read the flat fields.","properties":{"number":{"type":["string","null"],"description":"As `tracking_number`."},"carrier":{"type":["string","null"],"description":"As `tracking_carrier`."},"url":{"type":["string","null"],"description":"As `tracking_url`."}}},"affected_items":{"type":"array","items":{"type":"string"},"description":"The lines this change is about, when the facility's status feed names them. Absent otherwise."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderShippedWebhook":{"type":"object","description":"The body of a `order.shipped` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.shipped"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderShippedWebhookData"}}},"OrderShippedWebhookData":{"type":"object","description":"`order.shipped`'s data.","required":["order_id","portal_order_id","provider","status","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The facility making the order."},"status":{"type":"string","description":"The order's status after this event.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number, once a shipment exists. Absent before one does."},"tracking_carrier":{"type":["string","null"],"description":"The carrier, once a shipment exists. Absent before one does."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking page, once a shipment exists. Absent before one does.","format":"uri"},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"When the order should arrive, as an estimate counted from placement: the same date GET /order/{id} reports. Absent where no honest date exists (a lane that is not one of our own facilities, an order with no known shipping method); never carried by `order.delivered`, `order.cancelled` or `order.issue`."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"tracking":{"type":"object","deprecated":true,"description":"The same three values as `tracking_number`, `tracking_carrier` and `tracking_url`, nested; kept for receivers written before those existed. Read the flat fields.","properties":{"number":{"type":["string","null"],"description":"As `tracking_number`."},"carrier":{"type":["string","null"],"description":"As `tracking_carrier`."},"url":{"type":["string","null"],"description":"As `tracking_url`."}}},"affected_items":{"type":"array","items":{"type":"string"},"description":"The lines this change is about, when the facility's status feed names them. Absent otherwise."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderDeliveredWebhook":{"type":"object","description":"The body of a `order.delivered` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.delivered"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderDeliveredWebhookData"}}},"OrderDeliveredWebhookData":{"type":"object","description":"`order.delivered`'s data.","required":["order_id","portal_order_id","provider","status","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The facility making the order."},"status":{"type":"string","description":"The order's status after this event.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"delivered_at":{"type":"string","format":"date-time","description":"When the carrier confirmed delivery. Absent when the facility gave no time."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number, once a shipment exists. Absent before one does."},"tracking_carrier":{"type":["string","null"],"description":"The carrier, once a shipment exists. Absent before one does."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking page, once a shipment exists. Absent before one does.","format":"uri"},"tracking":{"type":"object","deprecated":true,"description":"The same three values as `tracking_number`, `tracking_carrier` and `tracking_url`, nested; kept for receivers written before those existed. Read the flat fields.","properties":{"number":{"type":["string","null"],"description":"As `tracking_number`."},"carrier":{"type":["string","null"],"description":"As `tracking_carrier`."},"url":{"type":["string","null"],"description":"As `tracking_url`."}}},"affected_items":{"type":"array","items":{"type":"string"},"description":"The lines this change is about, when the facility's status feed names them. Absent otherwise."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderCancelledWebhook":{"type":"object","description":"The body of a `order.cancelled` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.cancelled"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderCancelledWebhookData"}}},"OrderCancelledWebhookData":{"type":"object","description":"`order.cancelled`'s data.","required":["order_id","portal_order_id","provider","status","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The facility making the order."},"status":{"type":"string","description":"The order's status after this event.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"reason":{"type":"string","description":"The facility's own words for the cancellation. Absent when it gave none; never made up."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number, once a shipment exists. Absent before one does."},"tracking_carrier":{"type":["string","null"],"description":"The carrier, once a shipment exists. Absent before one does."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking page, once a shipment exists. Absent before one does.","format":"uri"},"tracking":{"type":"object","deprecated":true,"description":"The same three values as `tracking_number`, `tracking_carrier` and `tracking_url`, nested; kept for receivers written before those existed. Read the flat fields.","properties":{"number":{"type":["string","null"],"description":"As `tracking_number`."},"carrier":{"type":["string","null"],"description":"As `tracking_carrier`."},"url":{"type":["string","null"],"description":"As `tracking_url`."}}},"affected_items":{"type":"array","items":{"type":"string"},"description":"The lines this change is about, when the facility's status feed names them. Absent otherwise."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderFailedWebhook":{"type":"object","description":"The body of a `order.failed` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.failed"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderFailedWebhookData"}}},"OrderFailedWebhookData":{"type":"object","description":"`order.failed`'s data.","required":["order_id","portal_order_id","error_code","error_message","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"error_code":{"type":"string","description":"Why the order will not be produced: `payment_failed`, a payment taken after the order was accepted was declined or never funded.","enum":["payment_failed"]},"error_message":{"type":"string","description":"One sentence about the order's state and what is owed. Never a facility's raw error."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderUpdatedWebhook":{"type":"object","description":"The body of a `order.updated` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.updated"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderUpdatedWebhookData"}}},"OrderUpdatedWebhookData":{"type":"object","description":"`order.updated`'s data.","required":["order_id","portal_order_id","changes","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"changes":{"type":"object","required":["action","total_delta_cents","total_cents"],"description":"What changed. `action` says which, and exactly one of `item`, `shipping`, `ship_to`, `reship` and `previous_order_id` carries the detail.","properties":{"action":{"type":"string","description":"Which change.","enum":["item_added","item_updated","item_cancelled","shipping_changed","address_changed","letter_reshipped","provider_order_id_assigned"]},"item":{"type":"object","required":["id","sku","quantity"],"description":"`item_added`, `item_updated`, `item_cancelled`: the line, with its quantity after the change (0 when cancelled).","properties":{"id":{"type":"string","description":"The line's id."},"sku":{"type":"string","description":"The line's product."},"quantity":{"type":"integer","description":"Its quantity after the change."}}},"shipping":{"type":"object","required":["method","shipping_cents"],"description":"`shipping_changed`: the method the order now ships by, and what it costs.","properties":{"method":{"type":"string","description":"The shipping method."},"shipping_cents":{"type":"integer","description":"What that shipping costs, in cents."}}},"ship_to":{"$ref":"#/components/schemas/Address","description":"`address_changed`: the destination the parcel now goes to, in full."},"reship":{"type":"object","required":["replacement_order_id","replacement_portal_order_id"],"description":"`letter_reshipped`: the free replacement a missing-letter report created.","properties":{"replacement_order_id":{"type":"string","description":"The replacement's public id."},"replacement_portal_order_id":{"type":"string","description":"The replacement's row id."}}},"previous_order_id":{"type":"string","description":"`provider_order_id_assigned`: the id the order answered to before its fulfilment provider assigned its own — the `pfy_pending_…` id an order accepted with 202 was handed out under. It keeps resolving on GET /order/{id}; `order_id` is the new id, and a receiver that stores ids remaps it."},"total_delta_cents":{"type":"integer","description":"How the order's total moved: positive when it grew, negative on a refund, 0 when no money moved."},"total_cents":{"type":"integer","description":"The order's total after the change, in cents."}}},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderQualityCheckWebhook":{"type":"object","description":"The body of a `order.quality_check` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.quality_check"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderQualityCheckWebhookData"}}},"OrderQualityCheckWebhookData":{"type":"object","description":"`order.quality_check`'s data.","required":["order_id","portal_order_id","provider","status","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The facility making the order."},"status":{"type":"string","description":"The order's status after this event.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"estimated_delivery":{"$ref":"#/components/schemas/OrderEstimatedDelivery","description":"When the order should arrive, as an estimate counted from placement: the same date GET /order/{id} reports. Absent where no honest date exists (a lane that is not one of our own facilities, an order with no known shipping method); never carried by `order.delivered`, `order.cancelled` or `order.issue`."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number, once a shipment exists. Absent before one does."},"tracking_carrier":{"type":["string","null"],"description":"The carrier, once a shipment exists. Absent before one does."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking page, once a shipment exists. Absent before one does.","format":"uri"},"tracking":{"type":"object","deprecated":true,"description":"The same three values as `tracking_number`, `tracking_carrier` and `tracking_url`, nested; kept for receivers written before those existed. Read the flat fields.","properties":{"number":{"type":["string","null"],"description":"As `tracking_number`."},"carrier":{"type":["string","null"],"description":"As `tracking_carrier`."},"url":{"type":["string","null"],"description":"As `tracking_url`."}}},"affected_items":{"type":"array","items":{"type":"string"},"description":"The lines this change is about, when the facility's status feed names them. Absent otherwise."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderIssueWebhook":{"type":"object","description":"The body of a `order.issue` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.issue"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderIssueWebhookData"}}},"OrderIssueWebhookData":{"type":"object","description":"`order.issue`'s data.","required":["order_id","portal_order_id","provider","status","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"provider":{"type":"string","description":"The facility making the order."},"status":{"type":"string","description":"The order's status after this event.","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting"]},"issue_reason":{"type":"string","description":"Why the order is flagged, when the facility said: one closed code. Absent when no reason fits. A test key raises it with POST /order/{id}/simulate.","enum":["artwork_unprintable","address_undeliverable","shipment_exception","on_hold","cancellation_pending"]},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"tracking_number":{"type":["string","null"],"description":"The carrier's tracking number, once a shipment exists. Absent before one does."},"tracking_carrier":{"type":["string","null"],"description":"The carrier, once a shipment exists. Absent before one does."},"tracking_url":{"type":["string","null"],"description":"The carrier's tracking page, once a shipment exists. Absent before one does.","format":"uri"},"tracking":{"type":"object","deprecated":true,"description":"The same three values as `tracking_number`, `tracking_carrier` and `tracking_url`, nested; kept for receivers written before those existed. Read the flat fields.","properties":{"number":{"type":["string","null"],"description":"As `tracking_number`."},"carrier":{"type":["string","null"],"description":"As `tracking_carrier`."},"url":{"type":["string","null"],"description":"As `tracking_url`."}}},"affected_items":{"type":"array","items":{"type":"string"},"description":"The lines this change is about, when the facility's status feed names them. Absent otherwise."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderApprovalRequestedWebhook":{"type":"object","description":"The body of a `order.approval_requested` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.approval_requested"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderApprovalRequestedWebhookData"}}},"OrderApprovalRequestedWebhookData":{"type":"object","description":"`order.approval_requested`'s data.","required":["order_id","portal_order_id","status","total_cents","approval_url","expires_at","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"status":{"type":"string","description":"Always `pending_approval`.","enum":["pending_approval"]},"total_cents":{"type":"integer","description":"What the person is asked to approve, in cents: what approval charges."},"approval_url":{"type":"string","description":"The signed page the person approves or declines the order on. No account is needed; it stops deciding at `expires_at`."},"expires_at":{"type":"string","format":"date-time","description":"When the link stops deciding, and the order is cancelled if nobody did."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderApprovalApprovedWebhook":{"type":"object","description":"The body of a `order.approval_approved` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.approval_approved"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderApprovalApprovedWebhookData"}}},"OrderApprovalApprovedWebhookData":{"type":"object","description":"`order.approval_approved`'s data.","required":["order_id","portal_order_id","status","decided_at","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"status":{"type":"string","description":"The placed order's status: `paid`.","enum":["paid"]},"decided_at":{"type":"string","format":"date-time","description":"When the person approved it."},"decided_by":{"type":"string","description":"The name the person typed with their decision. Absent when they typed none."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderApprovalDeclinedWebhook":{"type":"object","description":"The body of a `order.approval_declined` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.approval_declined"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderApprovalDeclinedWebhookData"}}},"OrderApprovalDeclinedWebhookData":{"type":"object","description":"`order.approval_declined`'s data.","required":["order_id","portal_order_id","status","decided_at","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"status":{"type":"string","description":"Always `cancelled`.","enum":["cancelled"]},"decided_at":{"type":"string","format":"date-time","description":"When the person declined it."},"decided_by":{"type":"string","description":"The name the person typed with their decision. Absent when they typed none."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"OrderApprovalExpiredWebhook":{"type":"object","description":"The body of a `order.approval_expired` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["order.approval_expired"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/OrderApprovalExpiredWebhookData"}}},"OrderApprovalExpiredWebhookData":{"type":"object","description":"`order.approval_expired`'s data.","required":["order_id","portal_order_id","status","expires_at","test_mode"],"properties":{"order_id":{"type":"string","description":"The order's public id: what GET /order/{id} takes and every order read answers with."},"portal_order_id":{"type":"string","description":"The order's own row id (`ord_…`), which never changes."},"reference":{"type":"string","description":"Your own identifier for the order, as you sent it on create. Absent when you sent none."},"status":{"type":"string","description":"Always `cancelled`.","enum":["cancelled"]},"expires_at":{"type":"string","format":"date-time","description":"When the link expired."},"test_mode":{"type":"boolean","description":"True when the event is about a test-key (sandbox) object, false for a live one. It is the side that routed the event: a subscription scoped to `test` or `live` receives only that side."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."}}},"ProductUpdatedWebhook":{"type":"object","description":"The body of a `product.updated` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["product.updated"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/ProductUpdatedWebhookData"}}},"ProductUpdatedWebhookData":{"type":"object","description":"`product.updated`'s data.","required":["provider","sku","changes"],"properties":{"provider":{"type":"string","enum":["printedsimply"],"description":"The catalog the product is in: always `printedsimply` here."},"sku":{"type":"string","description":"The product."},"changes":{"type":"object","description":"Each field that changed on the facility's feed, with its value before and after: `priceCents` (the feed's own list price in cents, which for our plant's goods our price book prices is a cross-check, never the charge), `active` (whether the feed offers it: its active, purchasable, stock_status or status, or false when the feed no longer carries the SKU) and `name`.","additionalProperties":{"type":"object","required":["from","to"],"properties":{"from":{"description":"Before."},"to":{"description":"After."}}}},"unit_cents":{"type":"object","required":["from","to"],"description":"What one unit is charged before and after, in cents, the same on every plan (there is no platform fee): the price book's price for our plant's goods it prices, else the feed's, through the order's own unit steps. A change to the plant's own figure can leave it unchanged. Null where there is no price to charge.","properties":{"from":{"type":["integer","null"]},"to":{"type":["integer","null"]}}},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."},"test_mode":{"type":"boolean","description":"Absent on a real delivery of this event, which is about the account rather than a test or live object. Present, and true, on a rehearsal sent by the dashboard's webhook tester."}}},"CatalogSyncedWebhook":{"type":"object","description":"The body of a `catalog.synced` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["catalog.synced"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/CatalogSyncedWebhookData"}}},"CatalogSyncedWebhookData":{"type":"object","description":"`catalog.synced`'s data.","required":["provider","product_count","changed_count"],"properties":{"provider":{"type":"string","enum":["printedsimply"],"description":"The catalog that was synced: always `printedsimply` here."},"product_count":{"type":"integer","description":"How many products the catalog holds."},"changed_count":{"type":"integer","description":"How many changed this cycle (each one also fired `product.updated`)."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."},"test_mode":{"type":"boolean","description":"Absent on a real delivery of this event, which is about the account rather than a test or live object. Present, and true, on a rehearsal sent by the dashboard's webhook tester."}}},"WebhookDisabledWebhook":{"type":"object","description":"The body of a `webhook.disabled` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["webhook.disabled"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/WebhookDisabledWebhookData"}}},"WebhookDisabledWebhookData":{"type":"object","description":"`webhook.disabled`'s data.","required":["webhook_id","url","reason","consecutive_failures"],"properties":{"webhook_id":{"type":"string","description":"The endpoint that was switched off."},"url":{"type":"string","description":"Its address.","format":"uri"},"reason":{"type":"string","description":"Why.","const":"consecutive_failures"},"consecutive_failures":{"type":"integer","description":"The events in a row it failed to accept."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."},"test_mode":{"type":"boolean","description":"Absent on a real delivery of this event, which is about the account rather than a test or live object. Present, and true, on a rehearsal sent by the dashboard's webhook tester."}}},"AlertOpenedWebhook":{"type":"object","description":"The body of a `alert.opened` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["alert.opened"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/AlertOpenedWebhookData"}}},"AlertOpenedWebhookData":{"type":"object","description":"`alert.opened`'s data.","required":["alert_id","kind","status","subject_type","subject_id","summary","opened_at","resolved_at","resolution","last_evaluated_at","evidence","latest"],"properties":{"alert_id":{"type":"string","description":"The alert's id: GET /v1/alerts lists it by this id."},"kind":{"type":"string","description":"Which rule opened it.","enum":["webhook_failing","error_rate_spike","key_expiring"]},"status":{"type":"string","description":"Open on `alert.opened`, resolved on `alert.resolved`.","enum":["open","resolved"]},"subject_type":{"type":"string","description":"What it is about: an endpoint (`webhook`), a key (`api_key`) or the account's live calls (`account`).","enum":["webhook","account","api_key"]},"subject_id":{"type":"string","description":"The endpoint's id, the key's id, or `live`."},"summary":{"type":"string","description":"One sentence: what tripped it, or why it resolved."},"opened_at":{"type":"string","format":"date-time","description":"When it opened."},"resolved_at":{"type":["string","null"],"format":"date-time","description":"When it resolved; null on `alert.opened`."},"resolution":{"type":["string","null"],"enum":["cleared","subject_removed","endpoint_switched_off","key_stopped","dormant",null],"description":"Why it resolved; null on `alert.opened`."},"last_evaluated_at":{"type":"string","format":"date-time","description":"When it was last read."},"evidence":{"$ref":"#/components/schemas/IntegrationAlertEvidence","description":"The reading that opened it."},"latest":{"$ref":"#/components/schemas/IntegrationAlertEvidence","description":"The newest reading: the one it resolved on, on `alert.resolved`."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."},"test_mode":{"type":"boolean","description":"Absent on a real delivery of this event, which is about the account rather than a test or live object. Present, and true, on a rehearsal sent by the dashboard's webhook tester."}}},"AlertResolvedWebhook":{"type":"object","description":"The body of a `alert.resolved` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["alert.resolved"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/AlertResolvedWebhookData"}}},"AlertResolvedWebhookData":{"type":"object","description":"`alert.resolved`'s data.","required":["alert_id","kind","status","subject_type","subject_id","summary","opened_at","resolved_at","resolution","last_evaluated_at","evidence","latest"],"properties":{"alert_id":{"type":"string","description":"The alert's id: GET /v1/alerts lists it by this id."},"kind":{"type":"string","description":"Which rule opened it.","enum":["webhook_failing","error_rate_spike","key_expiring"]},"status":{"type":"string","description":"Open on `alert.opened`, resolved on `alert.resolved`.","enum":["open","resolved"]},"subject_type":{"type":"string","description":"What it is about: an endpoint (`webhook`), a key (`api_key`) or the account's live calls (`account`).","enum":["webhook","account","api_key"]},"subject_id":{"type":"string","description":"The endpoint's id, the key's id, or `live`."},"summary":{"type":"string","description":"One sentence: what tripped it, or why it resolved."},"opened_at":{"type":"string","format":"date-time","description":"When it opened."},"resolved_at":{"type":["string","null"],"format":"date-time","description":"When it resolved; null on `alert.opened`."},"resolution":{"type":["string","null"],"enum":["cleared","subject_removed","endpoint_switched_off","key_stopped","dormant",null],"description":"Why it resolved; null on `alert.opened`."},"last_evaluated_at":{"type":"string","format":"date-time","description":"When it was last read."},"evidence":{"$ref":"#/components/schemas/IntegrationAlertEvidence","description":"The reading that opened it."},"latest":{"$ref":"#/components/schemas/IntegrationAlertEvidence","description":"The newest reading: the one it resolved on, on `alert.resolved`."},"event_id":{"type":"string","description":"An id some emitters also write into `data`. It is not the envelope's `id`, and the events about one sale can share it: deduplicate on the envelope's `id` (`X-PS-Event-Id`)."},"test_mode":{"type":"boolean","description":"Absent on a real delivery of this event, which is about the account rather than a test or live object. Present, and true, on a rehearsal sent by the dashboard's webhook tester."}}},"TestPingWebhook":{"type":"object","description":"The body of a `test.ping` delivery.","required":["id","event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of this event and as `event_id` on GET /events. Deduplicate on it."},"event":{"type":"string","enum":["test.ping"]},"timestamp":{"type":"string","format":"date-time","description":"When the event happened, not when this attempt was sent."},"data":{"$ref":"#/components/schemas/TestPingWebhookData"}}},"TestPingWebhookData":{"type":"object","description":"`test.ping`'s data.","required":["message"],"properties":{"message":{"type":"string","description":"A line naming the portal that sent the test."}}},"WebhookPayload":{"type":"object","deprecated":true,"description":"The envelope every delivery shares, with `data` read as an order event's. Each event's own body is its `webhooks` entry's schema (`OrderShippedWebhook`, `ProductUpdatedWebhook`, …), whose `data` is that event's; this generic form is kept for clients generated before those existed.","required":["event","timestamp","data"],"properties":{"id":{"type":"string","description":"The event's id: the `X-PS-Event-Id` header, the same on every retry and redelivery of the event. Deduplicate on it."},"event":{"type":"string","enum":["order.created","order.paid","order.in_production","order.shipped","order.delivered","order.cancelled","order.failed","order.updated","order.rerouted","order.quality_check","order.issue","order.approval_requested","order.approval_approved","order.approval_declined","order.approval_expired","campaign.opened","campaign.order_added","campaign.closed","campaign.batch_in_production","campaign.completed","design.proof_sent","design.changes_requested","design.approved","return.requested","return.approved","return.remake_created","return.resolved","return.rejected","drop.opened","drop.sale","drop.closed","drop.sold_out","checkout_link.sale","gift.claimed","gift.expired","product.updated","catalog.synced","webhook.disabled","alert.opened","alert.resolved","autopilot.run.created","autopilot.run.awaiting_approval","autopilot.run.completed","autopilot.run.failed","autopilot.run.cancelled","design_run.item_ready","design_run.completed","guarantee.issued","guarantee.honored","guarantee.breached","guarantee.remedied","test.ping"],"description":"Every event a delivery can carry: the subscribable registry plus `test.ping`, which the dashboard's Test button sends and nothing can subscribe to."},"timestamp":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"order_id":{"type":"string"},"status":{"type":"string","enum":["pending_approval","pending_payment","paid","accepted","in_production","quality_check","shipped","delivered","cancelled","issue_flagged","rerouting","pending","processing","on-hold","completed","refunded","failed"]},"issue_reason":{"type":"string","enum":["artwork_unprintable","address_undeliverable","shipment_exception","on_hold","cancellation_pending"],"description":"On `order.issue` only, and only when the facility said why the order is flagged."},"tracking":{"type":"object","properties":{"carrier":{"type":"string"},"tracking_number":{"type":"string"},"tracking_url":{"type":"string","format":"uri"}}},"affected_items":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"WebhookReceived":{"description":"Any 2xx: the delivery is done and is not sent again. The body is not read. Answer within 10 seconds (a slower answer is a timeout, and is retried), so acknowledge first and do the work after."},"WebhookRetried":{"description":"408, 429 or any 5xx, a timeout or a refused connection: the same body is sent again, up to 7 attempts over about 10 hours (waiting 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, then 6 hours), with the same `X-PS-Event-Id`. `Retry-After` is not read."},"WebhookRefused":{"description":"Any other status, a 3xx included (redirects are not followed): the delivery fails and is not retried. A delivery that fails for good counts toward switching the endpoint off: after 20 in a row it is disabled, and `webhook.disabled` is sent to your other endpoints. A missed event can be read from GET /events and replayed."},"QuoteRefused":{"description":"variable_data_invalid — `items[].variable_data` failed the preflight, before anything was priced or charged. `details.problems[]` names each problem (up to 50; `details.problem_count` is the total) by `item_index`, `unit` and `slot`, with a `code`. variable_data_unavailable — a LIVE order or quote carries variable data while production is switched off; nothing was charged, and a test key (ps_test_*) accepts the same request end to end. destination_unavailable — `address_to.country` (else `destination_country`) is outside the US: our own lanes ship to US addresses only. `details.reason` is `destination_not_us` and `details.country` names the destination. below_cost_floor — the order would be made at our own plant and would cost more to make and ship than it is charged, at worst-case postage, and its loss is not one our cost floor accepts: only today's named losses are, no larger than they are today — the flat standard parcel under its postage, for a mailer up to a pound or a poster tube up to two, and list-priced goods short only of the per-order handling cost (or a partner SKU would be charged below what the partner bills for it). `details.skus` names the items and `details.contribution_cents` how far below zero the order falls. Nothing was charged; add units, combine it with another order, or choose another shipping method on offer. sku_not_orderable_yet — live keys only: a line names a modeled product our catalog lists that our plant has not confirmed it makes yet. `details.skus` names them as sent, and `details.plant_skus` the spelling our plant adopts for one sent another way. Refused before anything is asked of the plant or charged; a test key orders a modeled listing at its listed price. catalog_only — a flag or Santa hat is a catalog reference whose artwork, production cost and shipping weight are unconfirmed. Automatic quotes and orders are refused in both test and live mode before pricing, routing or charging, including requests carrying an earlier quote; `details.skus`, `details.readiness` and `details.reason` explain the refusal. shipment_capacity_exceeded — a basket of sized own-plant products exceeds the modeled 70 lb single-parcel limit, including stickers, magnets, posters, decals, packs and rolls. Both test and live requests are refused before charging; `details.skus`, `details.weight_oz` and `details.max_weight_oz` state the affected SKUs, whole-basket estimated weight and limit. Reduce the quantity or split the order. address_invalid — a live quote's `address_to` cannot be right as written: `details.findings[]` names each `error` (`zip_format`, `state_unknown`, `zip_state_mismatch`, or `delivery_point_not_found`) with its field and `details.address_check` is the whole check, as POST /addresses/validate answers it. A test key is never refused for it; its 200 carries `address_check`. sku_not_available — a line names a product our own plant does not make: only PrintedSimply's own products (stickers, decals, labels, magnets and posters, as GET /products lists them) are sold here. `details.skus` names each one. Asked before anything is priced, so nothing was priced or charged. provider_not_available — `provider` names a lane other than our own plant. Orders here are made by PrintedSimply: omit `provider`, or send `printedsimply`. `details.provider` echoes what was sent and `details.available` lists what is accepted. Nothing was priced or charged.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["variable_data_invalid","variable_data_unavailable","destination_unavailable","below_cost_floor","address_invalid","sku_not_orderable_yet","sku_not_available","provider_not_available"]},"details":{"type":"object","properties":{"reason":{"type":"string","enum":["destination_not_us","Artwork, production cost and shipping weight are not confirmed."],"description":"destination_unavailable: why the destination was refused. catalog_only: the production inputs still awaiting confirmation."},"country":{"type":"string","description":"destination_unavailable only: the destination's ISO-3166 alpha-2 country, upper-cased."},"skus":{"type":"array","items":{"type":"string"},"description":"sku_not_available: the SKUs on the request this portal does not sell. below_cost_floor: the SKUs of the refused order, or the one partner SKU priced below its bill. sku_not_orderable_yet: the SKUs, as sent, our plant has not confirmed yet."},"contribution_cents":{"type":"integer","description":"below_cost_floor only: what the order would contribute at worst-case postage, in cents (below zero). Absent when one partner SKU is priced below what the partner bills for it."},"readiness":{"type":"string","enum":["catalog_only"],"description":"catalog_only only: automatic test and live quotes and orders are unavailable."},"weight_oz":{"type":"number","description":"shipment_capacity_exceeded only: estimated weight of the complete basket in ounces, counting every physical piece in each pack or roll."},"max_weight_oz":{"type":"number","description":"shipment_capacity_exceeded only: maximum modeled single-parcel weight, 1120 ounces (70 lb)."},"plant_skus":{"type":"object","additionalProperties":{"type":"string"},"description":"sku_not_orderable_yet only: for a SKU sent in another case than the spelling our plant is asked to adopt, that spelling, keyed by the SKU as sent. Absent when every SKU was sent as the plant spells it."},"problem_count":{"type":"integer","minimum":1},"problems":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["not_own_plant","count_mismatch","unknown_slot","slot_conflict","missing_value","overflow","too_many_units"]},"item_index":{"type":"integer","minimum":0,"description":"The line, 0-based. Absent on a problem with the whole order."},"unit":{"type":"integer","minimum":1,"description":"The unit on the line, 1-based."},"slot":{"type":"string","description":"The slot, or `qr` for the QR payload."},"message":{"type":"string"}}}},"did_you_mean":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"},"maxItems":3},"description":"sku_not_available only, and only when one is plausible: for each refused SKU, keyed as sent, up to three SKUs this portal sells that it most likely meant, best first. A SKU from a family the plant does not make gets no key."},"provider":{"type":"string","description":"provider_not_available only: the `provider` the request named."},"available":{"type":"array","items":{"type":"string","enum":["printedsimply"]},"description":"provider_not_available only: the `provider` values this portal accepts."}}}}}}}]},"example":{"status":"error","error":{"code":"variable_data_invalid","message":"Variable data on this order failed the preflight (1 problem). Nothing was priced or charged; details.problems names each one by item, unit and slot.","details":{"problems":[{"code":"overflow","item_index":0,"unit":17,"slot":"name","message":"items[0] unit 17: \"name\" is 27 characters and the slot holds 24 (\"Maximiliana Featherstonehau\")."}],"problem_count":1}},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"AddressChangeRefused":{"description":"destination_unavailable — the new address is outside the US, and the order is already with one of our own facilities, which ship to US addresses only. `details.reason` is `destination_not_us` and `details.country` names the destination; cancel the order and place a new one. address_invalid — a live order's new address cannot be right as written: `details.findings[]` names each `error` (`zip_format`, `state_unknown`, `zip_state_mismatch`, or `delivery_point_not_found` where street verification is configured) with its field, and `details.address_check` is the whole check, as POST /addresses/validate answers it. Nothing was sent to the facility and the order still ships to its previous address. A test order is never refused for it.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["destination_unavailable","address_invalid"]},"details":{"type":"object","properties":{"reason":{"type":"string","enum":["destination_not_us"],"description":"destination_unavailable only: why the destination was refused."},"country":{"type":"string","description":"destination_unavailable only: the destination's ISO-3166 alpha-2 country, upper-cased."},"findings":{"type":"array","items":{"$ref":"#/components/schemas/AddressFinding"},"description":"address_invalid only: each `error` finding, with its field."},"address_check":{"$ref":"#/components/schemas/AddressCheck"}}}}}}}]}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"EstimateRefused":{"description":"destination_unavailable — the destination is outside the US, and this estimate prices our own lanes only: our own facilities ship to US addresses only. `details.reason` is `destination_not_us` and `details.country` names the destination. below_cost_floor — the order would be made at our own plant and would cost more to make and ship than it is charged, at worst-case postage, and its loss is not one our cost floor accepts: only today's named losses are, no larger than they are today — the flat standard parcel under its postage, for a mailer up to a pound or a poster tube up to two, and list-priced goods short only of the per-order handling cost (or a partner SKU would be charged below what the partner bills for it). `details.skus` names the items and `details.contribution_cents` how far below zero the order falls. Nothing was charged; add units, combine it with another order, or choose another shipping method on offer. sku_not_orderable_yet — live keys only: a line names a modeled product our catalog lists that our plant has not confirmed it makes yet. `details.skus` names them as sent, and `details.plant_skus` the spelling our plant adopts for one sent another way. Refused before anything is asked of the plant or charged; a test key orders a modeled listing at its listed price. catalog_only — a flag or Santa hat is a catalog reference whose artwork, production cost and shipping weight are unconfirmed. Automatic quotes and orders are refused in both test and live mode before pricing, routing or charging, including requests carrying an earlier quote; `details.skus`, `details.readiness` and `details.reason` explain the refusal. shipment_capacity_exceeded — a basket of sized own-plant products exceeds the modeled 70 lb single-parcel limit, including stickers, magnets, posters, decals, packs and rolls. Both test and live requests are refused before charging; `details.skus`, `details.weight_oz` and `details.max_weight_oz` state the affected SKUs, whole-basket estimated weight and limit. Reduce the quantity or split the order. sku_not_available — a line names a product our own plant does not make: only PrintedSimply's own products (stickers, decals, labels, magnets and posters, as GET /products lists them) are sold here. `details.skus` names each one. Asked before anything is priced, so nothing was priced or charged.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["destination_unavailable","below_cost_floor","sku_not_orderable_yet","sku_not_available"]},"details":{"type":"object","properties":{"reason":{"type":"string","enum":["destination_not_us","Artwork, production cost and shipping weight are not confirmed."],"description":"destination_unavailable: why the destination was refused. catalog_only: the production inputs still awaiting confirmation."},"country":{"type":"string","description":"destination_unavailable only: the destination's ISO-3166 alpha-2 country, upper-cased."},"skus":{"type":"array","items":{"type":"string"},"description":"sku_not_available: the SKUs on the request this portal does not sell. below_cost_floor: the SKUs of the refused order, or the one partner SKU priced below its bill. sku_not_orderable_yet: the SKUs, as sent, our plant has not confirmed yet."},"contribution_cents":{"type":"integer","description":"below_cost_floor only: what the order would contribute at worst-case postage, in cents (below zero). Absent when one partner SKU is priced below what the partner bills for it."},"readiness":{"type":"string","enum":["catalog_only"],"description":"catalog_only only: automatic test and live quotes and orders are unavailable."},"weight_oz":{"type":"number","description":"shipment_capacity_exceeded only: estimated weight of the complete basket in ounces, counting every physical piece in each pack or roll."},"max_weight_oz":{"type":"number","description":"shipment_capacity_exceeded only: maximum modeled single-parcel weight, 1120 ounces (70 lb)."},"plant_skus":{"type":"object","additionalProperties":{"type":"string"},"description":"sku_not_orderable_yet only: for a SKU sent in another case than the spelling our plant is asked to adopt, that spelling, keyed by the SKU as sent. Absent when every SKU was sent as the plant spells it."},"did_you_mean":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"},"maxItems":3},"description":"sku_not_available only, and only when one is plausible: for each refused SKU, keyed as sent, up to three SKUs this portal sells that it most likely meant, best first. A SKU from a family the plant does not make gets no key."}}}}}}}]}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"OrderRefused":{"description":"variable_data_invalid or variable_data_unavailable — as POST /orders/quote: `items[].variable_data` failed its preflight, or a live order carries it while production is switched off. artwork_not_printable — a live order's artwork on a line made at our own plant cannot be printed as it is: `details.problems[]` names each line by `item_index` with a code (unreachable, not_an_image, low_resolution) and a message saying what it needs, and `details.artwork_check` carries every line's verdict. Nothing was charged and the Idempotency-Key is free for the corrected order. artwork_required — a live order has a line made at our own plant with no artwork at all (no `artwork_url`, no `print_file`), so there is nothing to print: `details.items[]` names every such line by `item_index` and `sku`, checked before any file is read, and nothing was charged. A test key is never refused for artwork; its 200 carries the same verdict with `artwork_check.would_refuse`, and a line with no artwork in `artwork_check.missing_artwork`. destination_unavailable — the address is outside the US and the order would be made at our own plant, which ships to US addresses only: `details.reason` is `destination_not_us` and `details.country` names the destination. A test key gets the same answer. below_cost_floor — the order would be made at our own plant and would cost more to make and ship than it is charged, at worst-case postage, and its loss is not one our cost floor accepts: only today's named losses are, no larger than they are today — the flat standard parcel under its postage, for a mailer up to a pound or a poster tube up to two, and list-priced goods short only of the per-order handling cost (or a partner SKU would be charged below what the partner bills for it). `details.skus` names the items and `details.contribution_cents` how far below zero the order falls. Nothing was charged; add units, combine it with another order, or choose another shipping method on offer. A test key gets the same answer. sku_not_orderable_yet — live keys only: a line names a modeled product our catalog lists that our plant has not confirmed it makes yet. `details.skus` names them as sent, and `details.plant_skus` the spelling our plant adopts for one sent another way. Refused before anything is asked of the plant or charged; a test key orders a modeled listing at its listed price. catalog_only — a flag or Santa hat is a catalog reference whose artwork, production cost and shipping weight are unconfirmed. Automatic quotes and orders are refused in both test and live mode before pricing, routing or charging, including requests carrying an earlier quote; `details.skus`, `details.readiness` and `details.reason` explain the refusal. shipment_capacity_exceeded — a basket of sized own-plant products exceeds the modeled 70 lb single-parcel limit, including stickers, magnets, posters, decals, packs and rolls. Both test and live requests are refused before charging; `details.skus`, `details.weight_oz` and `details.max_weight_oz` state the affected SKUs, whole-basket estimated weight and limit. Reduce the quantity or split the order. address_invalid — a live order's `address_to` cannot be right as written: `details.findings[]` names each `error` (`zip_format`, `state_unknown`, `zip_state_mismatch`, or `delivery_point_not_found`: the configured service could not validate the street address) with its field, and `details.address_check` is the whole check, as POST /addresses/validate answers it. Nothing was charged; a test key is never refused for it and its 200 carries `address_check`. sku_not_available — a line names a product our own plant does not make: only PrintedSimply's own products (stickers, decals, labels, magnets and posters, as GET /products lists them) are sold here. `details.skus` names each one. Asked before anything is priced, so nothing was priced or charged. provider_not_available — `provider` names a lane other than our own plant. Orders here are made by PrintedSimply: omit `provider`, or send `printedsimply`. `details.provider` echoes what was sent and `details.available` lists what is accepted. Nothing was priced or charged.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["variable_data_invalid","variable_data_unavailable","artwork_not_printable","artwork_required","destination_unavailable","below_cost_floor","address_invalid","max_provider_cost_required","sku_not_orderable_yet","sku_not_available","provider_not_available"]},"details":{"type":"object","properties":{"problem_count":{"type":"integer","minimum":1},"problems":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["not_own_plant","count_mismatch","unknown_slot","slot_conflict","missing_value","overflow","too_many_units","unreachable","not_an_image","low_resolution","format_not_accepted","not_fetchable","design_embeds_raster","geometry_unverified"]},"item_index":{"type":"integer","minimum":0,"description":"The line, 0-based. Absent on a problem with the whole order."},"unit":{"type":"integer","minimum":1,"description":"variable_data only: the unit on the line, 1-based."},"slot":{"type":"string","description":"variable_data only: the slot, or `qr` for the QR payload."},"message":{"type":"string"}}}},"artwork_check":{"$ref":"#/components/schemas/ArtworkCheck","description":"artwork_not_printable only: every own-plant line's verdict, the passing ones included."},"items":{"type":"array","description":"artwork_required only: every line made at our own plant that carries no artwork, in item order.","items":{"$ref":"#/components/schemas/MissingArtwork"}},"item_count":{"type":"integer","minimum":1,"description":"artwork_required only: how many lines `items` names."},"reason":{"type":"string","enum":["destination_not_us","Artwork, production cost and shipping weight are not confirmed."],"description":"destination_unavailable: why the destination was refused. catalog_only: the production inputs still awaiting confirmation."},"country":{"type":"string","description":"destination_unavailable only: the destination's ISO-3166 alpha-2 country, upper-cased."},"skus":{"type":"array","items":{"type":"string"},"description":"sku_not_available: the SKUs on the request this portal does not sell. below_cost_floor: the SKUs of the refused order, or the one partner SKU priced below its bill. sku_not_orderable_yet: the SKUs, as sent, our plant has not confirmed yet."},"contribution_cents":{"type":"integer","description":"below_cost_floor only: what the order would contribute at worst-case postage, in cents (below zero). Absent when one partner SKU is priced below what the partner bills for it."},"readiness":{"type":"string","enum":["catalog_only"],"description":"catalog_only only: automatic test and live quotes and orders are unavailable."},"weight_oz":{"type":"number","description":"shipment_capacity_exceeded only: estimated weight of the complete basket in ounces, counting every physical piece in each pack or roll."},"max_weight_oz":{"type":"number","description":"shipment_capacity_exceeded only: maximum modeled single-parcel weight, 1120 ounces (70 lb)."},"plant_skus":{"type":"object","additionalProperties":{"type":"string"},"description":"sku_not_orderable_yet only: for a SKU sent in another case than the spelling our plant is asked to adopt, that spelling, keyed by the SKU as sent. Absent when every SKU was sent as the plant spells it."},"did_you_mean":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"},"maxItems":3},"description":"sku_not_available only, and only when one is plausible: for each refused SKU, keyed as sent, up to three SKUs this portal sells that it most likely meant, best first. A SKU from a family the plant does not make gets no key."},"provider":{"type":"string","description":"provider_not_available only: the `provider` the request named."},"available":{"type":"array","items":{"type":"string","enum":["printedsimply"]},"description":"provider_not_available only: the `provider` values this portal accepts."}}}}}}}]},"example":{"status":"error","error":{"code":"artwork_not_printable","message":"The artwork on one line of this order cannot be printed as it is, so the order was not placed and nothing was charged. details.problems says what each one needs.","details":{"problems":[{"code":"low_resolution","item_index":0,"message":"items[0] (sticker-die-cut-3x3): 512×512 px is 171 DPI at 3×3 in, below the 225 DPI a die-cut sticker can be printed from (300 recommended). It needs at least 675 px on the long side; 900 prints at the recommended resolution."}],"problem_count":1,"artwork_check":{"lines":[{"item_index":0,"sku":"sticker-die-cut-3x3","status":"refused","width_px":512,"height_px":512,"print_size_in":[3,3],"effective_dpi":171,"min_dpi":300,"findings":[{"code":"low_resolution","severity":"error","message":"items[0] (sticker-die-cut-3x3): 512×512 px is 171 DPI at 3×3 in, below the 225 DPI a die-cut sticker can be printed from (300 recommended). It needs at least 675 px on the long side; 900 prints at the recommended resolution."}]}]}}},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"ValidationError":{"description":"Invalid request body or parameters. When the request failed the operation's schema, `details` carries `formErrors` (problems with the request as a whole), `fieldErrors` (messages keyed by top-level field) and `errors`: one `{ pointer, code, message }` per refused value, `pointer` an RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) — `/items/0/sku`, with `~` written `~0` and `/` written `~1`, and `\"\"` for the request as a whole. POST /v1/orders/bulk answers `errors` as one `{ index, details }` per refused order, each `details` with its own pointers from the request body (`/orders/3/items/0/sku`); POST /v1/orders/multi-ship keeps `errors` as the `{ formErrors, fieldErrors }` it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top of `details`, with `errors` beside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it in `message` and carries no `errors`.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"validation_error","message":"Request body failed validation.","details":{"formErrors":[],"fieldErrors":{"address_to":["Invalid input: expected string, received undefined"]},"errors":[{"pointer":"/address_to/zip","code":"invalid_type","message":"Invalid input: expected string, received undefined"}]}},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"AuthError":{"description":"Missing, invalid or expired API key: `missing_api_key` (none sent), `invalid_api_key` (unknown or revoked), `key_expired` (past the expiry set when it was created; create a new key).","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"invalid_api_key","message":"The API key provided is invalid or has been revoked."},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"PaymentError":{"description":"Payment failed or no payment method configured","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"RateLimitError":{"description":"Rate limit exceeded. Two windows can produce this status and `error.code` says which: `rate_limit_exceeded` for the per-minute sliding window, `daily_quota_exceeded` for the plan's daily quota. Both carry `Retry-After`; the daily one also carries `X-Daily-Limit` and `X-Daily-Remaining`. The per-minute limits are plan-based — see Rate Limits above, or call GET /v1/me for this credential's own figures.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"traceparent":{"$ref":"#/components/headers/traceparent"},"Retry-After":{"$ref":"#/components/headers/Retry-After"},"X-Daily-Limit":{"$ref":"#/components/headers/X-Daily-Limit"},"X-Daily-Remaining":{"$ref":"#/components/headers/X-Daily-Remaining"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"examples":{"per_minute":{"summary":"Per-minute sliding window","value":{"status":"error","error":{"code":"rate_limit_exceeded","message":"Rate limit exceeded. Try again later."},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"daily":{"summary":"Daily quota — retrying inside the same day will not clear it","value":{"status":"error","error":{"code":"daily_quota_exceeded","message":"Daily request quota exceeded for your plan."},"request_id":"550e8400-e29b-41d4-a716-446655440000"}}}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"NotFoundError":{"description":"Resource not found","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"PayloadTooLargeError":{"description":"payload_too_large — the request body exceeded this route's byte cap (256 KB unless the route raises it with a written reason). It is refused before it is parsed, so nothing was created. Send fewer rows, or use the endpoint's own asynchronous door where it has one.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"IdempotencyUnavailable":{"description":"idempotency_unavailable — the request carried an `Idempotency-Key` and the ledger that makes it mean at-most-once could not be reached, so nothing ran and nothing was charged; retry the same request with the same key after `Retry-After`.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"idempotency_unavailable","message":"This request's Idempotency-Key could not be checked right now, so the request was not run and nothing was charged. Retry it with the same key."},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"LineAmendmentUnavailable":{"description":"order_lock_unavailable — changes to one order's lines are taken one at a time, and whether another was in progress could not be checked, so nothing was changed, charged or refunded and the production system was not asked; retry the same request after `Retry-After`. Sent with or without an `Idempotency-Key`. idempotency_unavailable — the request carried an `Idempotency-Key` and the ledger that makes it mean at-most-once could not be reached, so nothing ran and nothing was charged; retry the same request with the same key after `Retry-After`.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"status":"error","error":{"code":"order_lock_unavailable","message":"Whether another change to this order is in progress could not be checked right now, so nothing was changed, charged or refunded. Retry in a moment."},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"MethodNotAllowed":{"description":"method_not_allowed — no operation at this path takes the request's method. `Allow` lists the methods it does (`HEAD` wherever it takes `GET`, and `OPTIONS` everywhere), and so does `details.allow`; nothing ran. A path no operation is at answers `404 not_found` the same way, with a `request_id`.","headers":{"Allow":{"$ref":"#/components/headers/Allow"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"traceparent":{"$ref":"#/components/headers/traceparent"},"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["method_not_allowed"]},"details":{"type":"object","required":["method","allow"],"properties":{"method":{"type":"string","description":"The method that was sent."},"allow":{"type":"array","items":{"type":"string"},"description":"The methods the path takes, as `Allow` lists them."}}}}}}}]},"example":{"status":"error","error":{"code":"method_not_allowed","message":"/v1/order does not take GET. It takes OPTIONS, POST.","details":{"method":"GET","allow":["OPTIONS","POST"]}},"request_id":"550e8400-e29b-41d4-a716-446655440000"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"UpstreamError":{"description":"Production system error","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"ShippingEstimateRefused":{"description":"destination_unavailable — the destination is outside the US, and this estimate prices our own plant's shipping, which goes to US addresses only. `details.reason` is `destination_not_us` and `details.country` names the destination. sku_not_available — a line names a product our own plant does not make: only PrintedSimply's own products (stickers, decals, labels, magnets and posters, as GET /products lists them) are sold here. `details.skus` names each one. Asked before anything is priced, so nothing was priced or charged.","headers":{"Vary":{"$ref":"#/components/headers/Vary"}},"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ErrorEnvelope"},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["destination_unavailable","sku_not_available"]},"details":{"type":"object","properties":{"reason":{"type":"string","enum":["destination_not_us"],"description":"destination_unavailable only: why the destination was refused."},"country":{"type":"string","description":"destination_unavailable only: the destination's ISO-3166 alpha-2 country, upper-cased."},"skus":{"type":"array","items":{"type":"string"},"description":"sku_not_available: the SKUs on the request this portal does not sell."},"did_you_mean":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"},"maxItems":3},"description":"sku_not_available only, and only when one is plausible: for each refused SKU, keyed as sent, up to three SKUs this portal sells that it most likely meant, best first. A SKU from a family the plant does not make gets no key."}}}}}}}]}},"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}},"webhooks":{"order.created":{"post":{"operationId":"onOrderCreated","summary":"order.created","description":"Fired when a new order is submitted. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCreatedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.paid":{"post":{"operationId":"onOrderPaid","summary":"order.paid","description":"Fired when payment is successfully captured. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderPaidWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.in_production":{"post":{"operationId":"onOrderInProduction","summary":"order.in_production","description":"Fired when the order enters production at the fulfillment provider. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderInProductionWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.shipped":{"post":{"operationId":"onOrderShipped","summary":"order.shipped","description":"Fired when the order ships with tracking information. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderShippedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.delivered":{"post":{"operationId":"onOrderDelivered","summary":"order.delivered","description":"Fired when the carrier confirms delivery. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDeliveredWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.cancelled":{"post":{"operationId":"onOrderCancelled","summary":"order.cancelled","description":"Fired when an order is cancelled. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCancelledWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.failed":{"post":{"operationId":"onOrderFailed","summary":"order.failed","description":"Fired when an order that was accepted will not be produced — `error_code` is `payment_failed` (a payment taken after the order was accepted was declined or never funded). It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderFailedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.updated":{"post":{"operationId":"onOrderUpdated","summary":"order.updated","description":"Fired when a placed order changes — a line added, a quantity changed, a line cancelled, the shipping service changed, the destination address changed, a letter reported missing was reshipped (`letter_reshipped`, carrying the replacement's ids), or the fulfilment provider assigned its own id to an order accepted under a pending id (`provider_order_id_assigned`: `order_id` is the new id and `changes.previous_order_id` the one it replaces, which keeps resolving). `changes.action` says which. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderUpdatedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.quality_check":{"post":{"operationId":"onOrderQualityCheck","summary":"order.quality_check","description":"Fired when the order enters quality inspection before shipping. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderQualityCheckWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.issue":{"post":{"operationId":"onOrderIssue","summary":"order.issue","description":"Fired when fulfillment flags an issue with the order (hold, defect, or error) — `issue_reason` says why when the facility did: `artwork_unprintable`, `address_undeliverable`, `shipment_exception`, `on_hold` or `cancellation_pending`; it is absent when no reason fits. A test key raises it with POST /v1/order/{id}/simulate. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderIssueWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.approval_requested":{"post":{"operationId":"onOrderApprovalRequested","summary":"order.approval_requested","description":"Fired when an order placed with `approval: \"required\"` is held for a person's approval — nothing is charged or sent to production. `approval_url` is the signed page the person approves or declines it on, with no account; it stops deciding at `expires_at` (72 hours), and the order is then cancelled. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderApprovalRequestedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.approval_approved":{"post":{"operationId":"onOrderApprovalApproved","summary":"order.approval_approved","description":"Fired when a person approved a held order on its approval page and the order was placed and charged — `order.created` and `order.paid` fire beside it. `order_id` is the placed order's own id; `portal_order_id` never changes. `decided_by` is the name they typed, when they typed one. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderApprovalApprovedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.approval_declined":{"post":{"operationId":"onOrderApprovalDeclined","summary":"order.approval_declined","description":"Fired when a person declined a held order on its approval page: the order is cancelled and nothing was charged. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderApprovalDeclinedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"order.approval_expired":{"post":{"operationId":"onOrderApprovalExpired","summary":"order.approval_expired","description":"Fired when a held order's approval link expired with no decision: the order is cancelled and nothing was charged. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderApprovalExpiredWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"product.updated":{"post":{"operationId":"onProductUpdated","summary":"product.updated","description":"Fired when the catalog sync, which reads each facility's catalog every 15 minutes, sees a product change: its price on the facility's feed, its availability (the feed's `active`, `purchasable`, `stock_status` or `status`), its name, or a SKU the feed no longer carries. `changes` holds each field's `from` and `to`; `unit_cents` holds what one unit is charged before and after, the same on every plan (our price book's price for our plant's goods it prices, so a change in the plant's own figure can leave it unchanged). A change reaches you within one cycle of the feed showing it; GET /v1/products reads the feed live (cached five minutes), so it can show a change first. It is about the account, so it reaches every subscription whatever its mode and carries no `test_mode`.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductUpdatedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"catalog.synced":{"post":{"operationId":"onCatalogSynced","summary":"catalog.synced","description":"Fired when a provider catalog sync cycle completes, every 15 minutes for each configured facility. It is about the account, so it reaches every subscription whatever its mode and carries no `test_mode`.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogSyncedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"webhook.disabled":{"post":{"operationId":"onWebhookDisabled","summary":"webhook.disabled","description":"Fired when a webhook subscription is automatically disabled after consecutive terminal delivery failures. Re-enabling the webhook resets the count; missed events can be replayed. It is about the account, so it reaches every subscription whatever its mode and carries no `test_mode`.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDisabledWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"alert.opened":{"post":{"operationId":"onAlertOpened","summary":"alert.opened","description":"Fired when an integration-health alert opens on the account: a live endpoint's deliveries failing on its side (webhook_failing), live API calls failing well above the account's own rate (error_rate_spike), or a live key that stops working within days still answering calls (key_expiring). evidence holds the numbers that tripped it, an error_rate_spike's request ids and top errors aside (GET /v1/alerts lists them to a key with no scope list). A best-effort signal read from the request and delivery logs every few minutes, sent to the account's own live endpoints, never to the endpoint it is about; a key_expiring alert is kept in the account feed and sent to no endpoint. Sent to the account's own endpoints that take live events: never to the failing endpoint an alert is about, a connected app's endpoint or a test-only one, and a `key_expiring` alert to none (GET /v1/events lists it to a key with no scope list). An `error_rate_spike`'s `top_errors` and `example_request_ids` are empty here, whichever credential registered the endpoint: GET /v1/alerts lists them to a key with no scope list. It is about the account, so it reaches every subscription whatever its mode and carries no `test_mode`.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertOpenedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"alert.resolved":{"post":{"operationId":"onAlertResolved","summary":"alert.resolved","description":"Fired when an open integration-health alert resolves: resolution says why (cleared, subject_removed, endpoint_switched_off, key_stopped, or dormant when there was nothing left to read for 7 days, which is no recovery), and latest holds the reading it resolved on. It is about the account, so it reaches every subscription whatever its mode and carries no `test_mode`.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertResolvedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"test.ping":{"post":{"operationId":"onTestPing","summary":"test.ping","description":"Sent when you press Test on an endpoint in the dashboard, and by nothing else: no subscription names it. Signed and logged like every delivery, so it rehearses your verification.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestPingWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}}},"x-webhooks":{"orderInProduction":{"post":{"operationId":"onOrderInProduction","summary":"order.in_production","description":"Fired when the order enters production at the fulfillment provider. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderInProductionWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"orderShipped":{"post":{"operationId":"onOrderShipped","summary":"order.shipped","description":"Fired when the order ships with tracking information. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderShippedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"orderDelivered":{"post":{"operationId":"onOrderDelivered","summary":"order.delivered","description":"Fired when the carrier confirms delivery. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderDeliveredWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"orderCancelled":{"post":{"operationId":"onOrderCancelled","summary":"order.cancelled","description":"Fired when an order is cancelled. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderCancelledWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"productUpdated":{"post":{"operationId":"onProductUpdated","summary":"product.updated","description":"Fired when the catalog sync, which reads each facility's catalog every 15 minutes, sees a product change: its price on the facility's feed, its availability (the feed's `active`, `purchasable`, `stock_status` or `status`), its name, or a SKU the feed no longer carries. `changes` holds each field's `from` and `to`; `unit_cents` holds what one unit is charged before and after, the same on every plan (our price book's price for our plant's goods it prices, so a change in the plant's own figure can leave it unchanged). A change reaches you within one cycle of the feed showing it; GET /v1/products reads the feed live (cached five minutes), so it can show a change first. It is about the account, so it reaches every subscription whatever its mode and carries no `test_mode`.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductUpdatedWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}},"orderIssue":{"post":{"operationId":"onOrderIssue","summary":"order.issue","description":"Fired when fulfillment flags an issue with the order (hold, defect, or error) — `issue_reason` says why when the facility did: `artwork_unprintable`, `address_undeliverable`, `shipment_exception`, `on_hold` or `cancellation_pending`; it is absent when no reason fits. A test key raises it with POST /v1/order/{id}/simulate. It is about one test or live object: a subscription scoped to `test` or `live` receives only its own side, and `data.test_mode` says which.","tags":["Webhooks"],"parameters":[{"$ref":"#/components/parameters/WebhookSignatureV2"},{"$ref":"#/components/parameters/WebhookSignature"},{"$ref":"#/components/parameters/WebhookSignaturePrevious"},{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookEventHeader"},{"$ref":"#/components/parameters/WebhookEventId"},{"$ref":"#/components/parameters/WebhookDeliveryAttempt"},{"$ref":"#/components/parameters/WebhookTraceparent"},{"$ref":"#/components/parameters/LegacyWebhookSignatureV2"},{"$ref":"#/components/parameters/LegacyWebhookSignature"},{"$ref":"#/components/parameters/LegacyWebhookSignaturePrevious"},{"$ref":"#/components/parameters/LegacyWebhookTimestamp"},{"$ref":"#/components/parameters/LegacyWebhookEventHeader"},{"$ref":"#/components/parameters/LegacyWebhookEventId"},{"$ref":"#/components/parameters/LegacyWebhookDeliveryAttempt"}],"requestBody":{"required":true,"description":"Verify `X-PS-Signature-V2` against the raw bytes before parsing them. Every `X-PS-*` header is also sent under its deprecated `X-MF-*` name with the same value, for receivers written before the PrintedSimply rename.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderIssueWebhook"}}}},"responses":{"408":{"$ref":"#/components/responses/WebhookRetried"},"429":{"$ref":"#/components/responses/WebhookRetried"},"2XX":{"$ref":"#/components/responses/WebhookReceived"},"5XX":{"$ref":"#/components/responses/WebhookRetried"},"default":{"$ref":"#/components/responses/WebhookRefused"}}}}}}