{"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://portal.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 /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://portal.printedsimply.com/docs/api-changelog before it ships.","contact":{"name":"PrintedSimply Developer Support","url":"https://portal.printedsimply.com"},"license":{"name":"PrintedSimply Terms of Service","url":"https://portal.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://portal.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://portal.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."},{"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://portal.printedsimply.com/api/openapi.json","documentation":"https://portal.printedsimply.com/docs/api-reference","api_changelog":"https://portal.printedsimply.com/docs/api-changelog","mcp_config":"https://portal.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://portal.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://portal.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://portal.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://portal.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://portal.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://portal.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":"$72.99","shipping":"$8.99","total_cents":7299,"shipping_cents":899,"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":64}],"line_items":[{"id":"itm_fowT5-1iZwSnQcGX0ByoA64l","sku":"sticker-die-cut-3x3","quantity":100,"unit_price_cents":64}],"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://portal.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://portal.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://portal.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":"$64.00","shipping":"$8.99","processing_fee":"$0.00","platform_fee":"$0.00","card_fee":"$0.00","total":"$72.99"},"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`. 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`."},"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":64}],"actions":{"cancel":true,"update_address":true,"update_shipping":true,"upgrade_shipping":true,"add_item":true,"update_item":true,"cancel_item":true},"total_cents":7299,"shipping_cents":899,"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://portal.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":7799,"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":"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":"c2g0xl6j1z0c35hx7fd355s1c","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,"items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","minimum":1},"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","description":"standard (default); priority or express once they are priced — until then a request for either is 400 shipping_method_unavailable."}}},"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":64}],"breakdown":{"items_cents":6400,"shipping_cents":899,"processing_fee_cents":0,"platform_fee_cents":0,"card_fee_cents":0,"total_cents":7299},"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.","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://portal.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://portal.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":"$128.00","shipping":"$13.99","processing_fee":"$0.00","platform_fee":"$0.00","card_fee":"$0.00","total":"$141.99"},"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.47","charged":"$61.75"},"shipping":{"method":"standard","shipping_class":{"name":"parcel_5lb","weight_oz":35},"delta_cents":1200,"shipping_cents":2099},"total_delta_cents":7375,"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":"order_not_modifiable — the order has gone into production (or later), past the point where items can be added. 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.","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_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.47","delta":"$-49.40"},"shipping":{"method":"standard","shipping_class":{"name":"parcel_2lb","weight_oz":17.1},"delta_cents":-700,"shipping_cents":1399},"total_delta_cents":-5640,"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":"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.","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. 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.","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/IdempotencyUnavailable"}},"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.","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 processing failed","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/IdempotencyUnavailable"}},"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; for anything else (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` (the all-in unit price an order charges at each quantity where it changes, from the function the quote and the order charge with), 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).","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`). 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":"2.67","regular_price":"2.67","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 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`, `orderable`, `not_orderable_code` and `not_orderable_reason`, as on the list.","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":"2.67","regular_price":"2.67","category":"stickers","purchasable":true,"stock_status":"instock","size_in":[3,3],"finish":null,"family":"stickers","minimum_quantity":1,"quantity_breaks":[{"min_qty":1,"unit_cents":267},{"min_qty":10,"unit_cents":160},{"min_qty":25,"unit_cents":104},{"min_qty":50,"unit_cents":72},{"min_qty":100,"unit_cents":64}],"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 (none yet) 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). 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. 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},"finishes":[],"price_book":{"base_cents":177,"cents_per_sq_in":10},"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},"finishes":[],"price_book":{"base_cents":177,"cents_per_sq_in":10},"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},"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},"finishes":[],"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":12,"max_height_in":12,"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},"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":48,"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},"finishes":[],"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, refunded_cents, net_cents — refunds attributed to the order they refund), 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\",\"4499\",\"0\",\"4499\",\"false\",\"\",\"\"\n\"test_ord_8zJ_6hfCTgPoJN4qw3R7zXKn\",\"ord_8zJ_6hfCTgPoJN4qw3R7zXKn\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:19.000Z\",\"sticker-die-cut-3x3 x50\",\"4499\",\"0\",\"4499\",\"false\",\"\",\"\"\n\"test_ord_qbpi4ALIbckcSICFfIPmmOKl\",\"ord_qbpi4ALIbckcSICFfIPmmOKl\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:18.000Z\",\"sticker-die-cut-3x3 x100\",\"7299\",\"0\",\"7299\",\"false\",\"\",\"\"\n\"test_ord_JyZLq7sUxTZxc7qjzI76ZQYU\",\"ord_JyZLq7sUxTZxc7qjzI76ZQYU\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:18.000Z\",\"sticker-die-cut-3x3 x100\",\"7299\",\"0\",\"7299\",\"false\",\"\",\"\"\n\"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk\",\"ord_wVK9ZiG-VYiPrlSeBUBmrUVk\",\"\",\"paid\",\"true\",\"printedsimply\",\"2026-10-15T15:00:10.000Z\",\"sticker-die-cut-3x3 x100\",\"7799\",\"0\",\"7799\",\"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://portal.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://portal.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://portal.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://portal.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`): $8.99 up to 1 lb, $13.99 to 2 lb, $20.99 to 5 lb, $4.69 a pound above, $5.20 more for a tube (posters and wall decals), and 55¢ more on parcels from 2026-10-04 to 2027-01-16, each bare. 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. A hundred 3×3 stickers and one ship for the same price. Like a product price, it 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":"$8.99","cost_cents":899,"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, so a quote that day answers `meets: true`. For any other day send `deliver_by` on POST /orders/quote and read `deliver_by_check.latest_order_day`. `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-27","is_estimate":true},{"deliver_by":"2026-12-24","occasion":"Christmas Eve","method":"standard","latest_order_day":"2026-12-10","is_estimate":true},{"deliver_by":"2026-12-25","occasion":"Christmas Day","method":"standard","latest_order_day":"2026-12-10","is_estimate":true},{"deliver_by":"2026-12-31","occasion":"New Year's Eve","method":"standard","latest_order_day":"2026-12-16","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. Does not create or store anything and asks no outside service: it checks 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 flags 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`. It does not verify that the street or the house exists; a live POST /order does that with USPS where the deployment has it configured (the `delivery_point_*` codes on `AddressFinding`).","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 our own plant prints (30 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://portal.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://portal.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 (stickers 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, except the 18×24, which is $10.71 each from 5 and $9.89 each from 10 and $9.66 each from 25, and the 24×36, which is $19.31 each from 5; 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. 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 the products our own plant makes (stickers, labels, decals, magnets and posters); 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 (stickers 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, except the 18×24, which is $10.71 each from 5 and $9.89 each from 10 and $9.66 each from 25, and the 24×36, which is $19.31 each from 5; 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. 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":"$64.00","shipping":"$8.99","processing_fee":"$0.00","platform_fee":"$0.00","card_fee":"$0.00","total":"$72.99","total_cents":7299},"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 (unit price x quantity). Our own plant's products priced by our price book carry quantity breaks (stickers 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, except the 18×24, which is $10.71 each from 5 and $9.89 each from 10 and $9.66 each from 25, and the 24×36, which is $19.31 each from 5; 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. 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":"magnet-3x3","name":"Magnet, 3×3 in","category":"magnets","price":"$1.32","price_cents":132,"priced_from":"catalog","estimated_items_total_cents":13200,"estimated_items_total":"$132.00","score":1,"match_reasons":["keyword: a"]},{"sku":"magnet-4x4","name":"Magnet, 4×4 in","category":"magnets","price":"$1.74","price_cents":174,"priced_from":"catalog","estimated_items_total_cents":17400,"estimated_items_total":"$174.00","score":1,"match_reasons":["keyword: a"]},{"sku":"sticker-die-cut-3x3","name":"Die-Cut Sticker, 3×3 in","category":"stickers","price":"$0.64","price_cents":64,"priced_from":"catalog","estimated_items_total_cents":6400,"estimated_items_total":"$64.00","score":1,"match_reasons":["keyword: stickers"]},{"sku":"wall-decal-24x36","name":"Wall Decal, 24×36 in","category":"decals","price":"$22.03","price_cents":2203,"priced_from":"catalog","estimated_items_total_cents":220300,"estimated_items_total":"$2203.00","score":1,"match_reasons":["keyword: a"]}],"quantity":100,"budget":null,"catalog":{"stale":false,"reason":null,"source":"catalog","considered":6,"matched":4}},"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).","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":{"$ref":"#/components/responses/UpstreamError"}},"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","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":"cpxetfsy9xdfbm20z1s61pn2j","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":7799,"total_delta_cents":0},"event_id":"evt_ORmnOh7KCgTFDahmHVzDhdg9","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","test_mode":true,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}},{"id":"cypad8vwky282y6c5jaqkkpgx","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":9034,"total_delta_cents":-5640},"event_id":"evt_gwqAUgOZe7itGr-n6zvxYDi0","order_id":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","test_mode":true,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}},{"id":"cxii3vd3gq0upj60blxhlx76a","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":7299,"portal_order_id":"ord_wVK9ZiG-VYiPrlSeBUBmrUVk"}},{"id":"c956sb2eftr8jj7tu4m0dw6h5","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":7299,"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"]}]}},"/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 and its record.","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`, `*`; 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.","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`, `*`; 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.","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.","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.","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"]}]}},"/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","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://portal.printedsimply.com/track/eyJ2IjoxLCJ0IjoidHJhY2tpbmciLCJ0b2siOiJ0cmtfQXF1SkZxRExEa2lzSzZKaC1IQUJIejlMIiwiZXhwIjoiMjAyNy0wMS0xM1QxNTowMDozMi4wMDBaIn0.50de0b297f5ed5a39f7b618a2e6f8138f8d50b31a2daabe2709af8f2d7265b25","number":"TRK-TEST-JCWX8ET5IGBT","carrier":"UPS"},"test_mode":true,"tracking_url":"https://portal.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://portal.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"],"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://portal.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"],"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://portal.printedsimply.com/oauth/authorize","tokenUrl":"https://portal.printedsimply.com/api/oauth/token","refreshUrl":"https://portal.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 delivery-point verification, which only a live POST /order, POST /orders/bulk entry or PUT /order/{id}/address runs, on a US address the other checks pass, and only where the deployment has USPS verification configured: `delivery_point_not_found` (`error`) — USPS answered that no delivery point exists at the street address; `delivery_point_incomplete` (`warning`) — the building exists but its unit is missing or not confirmed, or more than one address matched; `delivery_point_confirmed` (`info`). When USPS cannot be reached the order proceeds without them."},"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"},"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"],"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"},"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","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","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."}}},"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":30,"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 (30 today), stickers, decals (the wall decal), magnets and posters in that order, smallest first, each family's last the largest our own plant prints of it. 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 or artwork_empty."},"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":{"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"],"description":"Present only on a row this API 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`. Absent on every row a facility sent."},"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 a sticker sheet or 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."},"family":{"type":"string","enum":["stickers","decals","magnets","posters"],"description":"Our plant's rows only: which of its product families the SKU is, the family its quantity ladder and its cost are read under."},"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: the unit price an order of this SKU charges, from one unit up, one entry per quantity where it changes — computed by the function POST /v1/orders/quote and POST /v1/order charge each unit with (no platform fee is added on any plan). The first entry (`min_qty` 1) is the row's own price. A single entry when the SKU climbs no ladder (a SKU our price book does not price). Counted on an order's whole quantity of the SKU, across its lines. 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`)."},"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":["facility_not_configured","plant_unreachable","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. `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`). `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_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":"The unit price, in cents, an order of `min_qty` or more charges, until the next entry's `min_qty`."}}},"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 the products our own plant makes 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 the products our own plant makes (stickers, labels, decals, magnets, posters) this plan GRANTS, never below a unit's cost floor. 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 this account's next order."}}},"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","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."}}}]},"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","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":{"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 }."},"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"}}},"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","required":["earliest","latest","ships_by","is_estimate"],"description":"When an order placed now should arrive, as an estimate: the facility's production 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). 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"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer"},"unit_cents":{"type":"integer","description":"What this API charges per unit, in USD cents."}}},"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","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).","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."}}},"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":{"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. 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 placed by `method` is estimated to arrive by `deliver_by` (its estimated window's latest day on or before it); any time that day counts, and it may be a weekend or a holiday. Null when no order placed today or later is, which is the answer once the day has passed."},"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","price_book","plant_confirms_sku"],"properties":{"product":{"type":"string","enum":["sticker-die-cut","sticker-kiss-cut","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 word after the size (a finish) 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. Empty: none is offered yet, and a finish named in a SKU is not priced or made differently."},"price_book":{"type":"object","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.","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, and a size its catalog does not carry is refused 400 `pricing_error`. A test key's takes any size within these bounds."}}},"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, to a hundredth: a smaller print does not pay what one unit costs the plant alone. Null when every size is priced."},"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; another day, or another facility's goods, is 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":{"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","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.","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"}}},"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 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,"items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","minimum":1}}}},"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":{"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'"},"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","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","spend_cap_exceeded","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","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","spend_cap_exceeded","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."}}},"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","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. 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`) 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_available","provider_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. below_cost_floor: the SKUs of the refused order, or the one partner SKU priced below its bill."},"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."},"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 USPS 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_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_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. below_cost_floor: the SKUs of the refused order, or the one partner SKU priced below its bill."},"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."},"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. 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`: USPS has no delivery point at the street address, where the deployment has USPS verification configured) 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_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"],"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. below_cost_floor: the SKUs of the refused order, or the one partner SKU priced below its bill."},"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."},"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"}}}},"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"}}}},"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"}}}}}}