{"info":{"name":"PrintedSimply API","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.","schema":"https://schema.getpostman.com/json/collection/v2.1.0/collection.json"},"auth":{"type":"apikey","apikey":[{"key":"key","value":"x-api-key","type":"string"},{"key":"value","value":"{{apiKey}}","type":"string"},{"key":"in","value":"header","type":"string"}]},"item":[{"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.","item":[{"name":"API index (public discovery)","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/","host":["{{baseUrl}}"],"path":[]},"auth":{"type":"noauth"}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/","host":["{{baseUrl}}"],"path":[]},"auth":{"type":"noauth"}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Describe the calling credential","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/me","host":["{{baseUrl}}"],"path":["me"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/me","host":["{{baseUrl}}"],"path":["me"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Describe what this account still needs","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/setup","host":["{{baseUrl}}"],"path":["setup"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/setup","host":["{{baseUrl}}"],"path":["setup"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"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.","item":[{"name":"List orders","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/orders","host":["{{baseUrl}}"],"path":["orders"],"query":[{"key":"status","value":"","disabled":true,"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."},{"key":"reference","value":"","disabled":true,"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."},{"key":"from","value":"","disabled":true,"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."},{"key":"to","value":"","disabled":true,"description":"Filter orders created on or before this date. A value that is not a date returns 400 validation_error."},{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"20","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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`."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/orders","host":["{{baseUrl}}"],"path":["orders"],"query":[{"key":"status","value":"","disabled":true,"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."},{"key":"reference","value":"","disabled":true,"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."},{"key":"from","value":"","disabled":true,"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."},{"key":"to","value":"","disabled":true,"description":"Filter orders created on or before this date. A value that is not a date returns 400 validation_error."},{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"20","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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`."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Create an order","request":{"method":"POST","description":"Submit a print-on-demand order. Charges the default payment method, then submits to the production system. Returns order ID and charge breakdown.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order","host":["{{baseUrl}}"],"path":["order"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100,\n      \"artwork_url\": \"https://portal.printedsimply.com/samples/sticker-3x3.png\"\n    }\n  ],\n  \"address_to\": {\n    \"name\": \"Jane Doe\",\n    \"address1\": \"123 Main St\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\",\n    \"zip\": \"78701\",\n    \"country\": \"US\"\n  },\n  \"shipping\": {\n    \"method\": \"standard\"\n  }\n}","options":{"raw":{"language":"json"}}}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Keeps the id this request returned for the requests that use it.","if (pm.response.code < 300) {","  const body = pm.response.json();","  const pick = (path) => path.split(\".\").reduce((v, k) => (v == null ? undefined : v[k]), body);","  const orderId = pick(\"data.order_id\");","  if (orderId != null) pm.collectionVariables.set(\"orderId\", String(orderId));","}"]}}],"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order","host":["{{baseUrl}}"],"path":["order"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100,\n      \"artwork_url\": \"https://portal.printedsimply.com/samples/sticker-3x3.png\"\n    }\n  ],\n  \"address_to\": {\n    \"name\": \"Jane Doe\",\n    \"address1\": \"123 Main St\",\n    \"city\": \"Austin\",\n    \"state\": \"TX\",\n    \"zip\": \"78701\",\n    \"country\": \"US\"\n  },\n  \"shipping\": {\n    \"method\": \"standard\"\n  }\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Get order details","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/order/{{orderId}}","host":["{{baseUrl}}"],"path":["order","{{orderId}}"]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Keeps the id this request returned for the requests that use it.","if (pm.response.code < 300) {","  const body = pm.response.json();","  const pick = (path) => path.split(\".\").reduce((v, k) => (v == null ? undefined : v[k]), body);","  const itemId = pick(\"data.line_items.0.id\");","  if (itemId != null) pm.collectionVariables.set(\"itemId\", String(itemId));","}"]}}],"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/order/{{orderId}}","host":["{{baseUrl}}"],"path":["order","{{orderId}}"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Cancel an order","request":{"method":"DELETE","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.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/cancel","host":["{{baseUrl}}"],"path":["order","{{orderId}}","cancel"]}},"response":[{"name":"200 OK","originalRequest":{"method":"DELETE","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/cancel","host":["{{baseUrl}}"],"path":["order","{{orderId}}","cancel"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Update shipping address","request":{"method":"PUT","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/address","host":["{{baseUrl}}"],"path":["order","{{orderId}}","address"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"Jane Doe\",\n  \"address1\": \"456 Oak Ave\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zip\": \"78702\",\n  \"country\": \"US\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/address","host":["{{baseUrl}}"],"path":["order","{{orderId}}","address"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"Jane Doe\",\n  \"address1\": \"456 Oak Ave\",\n  \"city\": \"Austin\",\n  \"state\": \"TX\",\n  \"zip\": \"78702\",\n  \"country\": \"US\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Update shipping method","request":{"method":"PUT","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/shipping","host":["{{baseUrl}}"],"path":["order","{{orderId}}","shipping"]},"body":{"mode":"raw","raw":"{\n  \"method\": \"standard\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/shipping","host":["{{baseUrl}}"],"path":["order","{{orderId}}","shipping"]},"body":{"mode":"raw","raw":"{\n  \"method\": \"standard\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Get order events","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/events","host":["{{baseUrl}}"],"path":["order","{{orderId}}","events"],"query":[{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"500","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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."},{"key":"since","value":"","disabled":true,"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."},{"key":"wait_seconds","value":"0","disabled":true,"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."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/events","host":["{{baseUrl}}"],"path":["order","{{orderId}}","events"],"query":[{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"500","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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."},{"key":"since","value":"","disabled":true,"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."},{"key":"wait_seconds","value":"0","disabled":true,"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."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}}"}]},{"name":"Get an order's timeline","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/timeline","host":["{{baseUrl}}"],"path":["order","{{orderId}}","timeline"],"query":[{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."},{"key":"type","value":"","disabled":true,"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`."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/timeline","host":["{{baseUrl}}"],"path":["order","{{orderId}}","timeline"],"query":[{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."},{"key":"type","value":"","disabled":true,"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`."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Report a missing letter and reship it once","request":{"method":"POST","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`.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/missing-letter","host":["{{baseUrl}}"],"path":["order","{{orderId}}","missing-letter"]}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/missing-letter","host":["{{baseUrl}}"],"path":["order","{{orderId}}","missing-letter"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Place an order again","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/reorder","host":["{{baseUrl}}"],"path":["order","{{orderId}}","reorder"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 200\n    }\n  ]\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/reorder","host":["{{baseUrl}}"],"path":["order","{{orderId}}","reorder"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 200\n    }\n  ]\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Force a sandbox order to a lifecycle state","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/simulate","host":["{{baseUrl}}"],"path":["order","{{orderId}}","simulate"]},"body":{"mode":"raw","raw":"{\n  \"target_state\": \"shipped\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/order/{{orderId}}/simulate","host":["{{baseUrl}}"],"path":["order","{{orderId}}","simulate"]},"body":{"mode":"raw","raw":"{\n  \"target_state\": \"shipped\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Export orders as CSV","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/orders/export","host":["{{baseUrl}}"],"path":["orders","export"],"query":[{"key":"from","value":"","disabled":true,"description":"Inclusive lower bound on created_at."},{"key":"to","value":"","disabled":true,"description":"Exclusive upper bound."},{"key":"status","value":"","disabled":true,"description":"One order status; unknown values are refused."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/orders/export","host":["{{baseUrl}}"],"path":["orders","export"],"query":[{"key":"from","value":"","disabled":true,"description":"Inclusive lower bound on created_at."},{"key":"to","value":"","disabled":true,"description":"Exclusive upper bound."},{"key":"status","value":"","disabled":true,"description":"One order status; unknown values are refused."}]}},"status":"OK","code":200,"_postman_previewlanguage":"text","header":[{"key":"Content-Type","value":"text/csv"},{"key":"MF-Version","value":"2026-07-26"}],"body":"\"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"}]},{"name":"Create bulk orders","request":{"method":"POST","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:`.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/orders/bulk","host":["{{baseUrl}}"],"path":["orders","bulk"]},"body":{"mode":"raw","raw":"{\n  \"orders\": [\n    {\n      \"items\": [\n        {\n          \"sku\": \"sticker-die-cut-3x3\",\n          \"quantity\": 100,\n          \"artwork_url\": \"https://portal.printedsimply.com/samples/sticker-3x3.png\"\n        }\n      ],\n      \"address_to\": {\n        \"name\": \"Jane Doe\",\n        \"address1\": \"123 Main St\",\n        \"city\": \"Austin\",\n        \"state\": \"TX\",\n        \"zip\": \"78701\",\n        \"country\": \"US\"\n      },\n      \"shipping\": {\n        \"method\": \"standard\"\n      }\n    },\n    {\n      \"items\": [\n        {\n          \"sku\": \"sticker-die-cut-3x3\",\n          \"quantity\": 100,\n          \"artwork_url\": \"https://portal.printedsimply.com/samples/sticker-3x3.png\"\n        }\n      ],\n      \"address_to\": {\n        \"name\": \"Sam Lee\",\n        \"address1\": \"420 Pine St\",\n        \"city\": \"Seattle\",\n        \"state\": \"WA\",\n        \"zip\": \"98101\",\n        \"country\": \"US\"\n      },\n      \"shipping\": {\n        \"method\": \"standard\"\n      }\n    }\n  ]\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/orders/bulk","host":["{{baseUrl}}"],"path":["orders","bulk"]},"body":{"mode":"raw","raw":"{\n  \"orders\": [\n    {\n      \"items\": [\n        {\n          \"sku\": \"sticker-die-cut-3x3\",\n          \"quantity\": 100,\n          \"artwork_url\": \"https://portal.printedsimply.com/samples/sticker-3x3.png\"\n        }\n      ],\n      \"address_to\": {\n        \"name\": \"Jane Doe\",\n        \"address1\": \"123 Main St\",\n        \"city\": \"Austin\",\n        \"state\": \"TX\",\n        \"zip\": \"78701\",\n        \"country\": \"US\"\n      },\n      \"shipping\": {\n        \"method\": \"standard\"\n      }\n    },\n    {\n      \"items\": [\n        {\n          \"sku\": \"sticker-die-cut-3x3\",\n          \"quantity\": 100,\n          \"artwork_url\": \"https://portal.printedsimply.com/samples/sticker-3x3.png\"\n        }\n      ],\n      \"address_to\": {\n        \"name\": \"Sam Lee\",\n        \"address1\": \"420 Pine St\",\n        \"city\": \"Seattle\",\n        \"state\": \"WA\",\n        \"zip\": \"98101\",\n        \"country\": \"US\"\n      },\n      \"shipping\": {\n        \"method\": \"standard\"\n      }\n    }\n  ]\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Create multi-address order","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/orders/multi-ship","host":["{{baseUrl}}"],"path":["orders","multi-ship"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 50,\n      \"artwork_url\": \"https://example.com/logo.png\"\n    },\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 50,\n      \"artwork_url\": \"https://example.com/logo.png\"\n    }\n  ],\n  \"shipments\": [\n    {\n      \"address\": {\n        \"name\": \"Jane Doe\",\n        \"address1\": \"123 Main St\",\n        \"city\": \"Austin\",\n        \"state\": \"TX\",\n        \"zip\": \"78701\",\n        \"country\": \"US\"\n      },\n      \"item_indices\": [\n        0\n      ]\n    },\n    {\n      \"address\": {\n        \"name\": \"Sam Lee\",\n        \"address1\": \"420 Pine St\",\n        \"city\": \"Seattle\",\n        \"state\": \"WA\",\n        \"zip\": \"98101\",\n        \"country\": \"US\"\n      },\n      \"item_indices\": [\n        1\n      ]\n    }\n  ]\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/orders/multi-ship","host":["{{baseUrl}}"],"path":["orders","multi-ship"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 50,\n      \"artwork_url\": \"https://example.com/logo.png\"\n    },\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 50,\n      \"artwork_url\": \"https://example.com/logo.png\"\n    }\n  ],\n  \"shipments\": [\n    {\n      \"address\": {\n        \"name\": \"Jane Doe\",\n        \"address1\": \"123 Main St\",\n        \"city\": \"Austin\",\n        \"state\": \"TX\",\n        \"zip\": \"78701\",\n        \"country\": \"US\"\n      },\n      \"item_indices\": [\n        0\n      ]\n    },\n    {\n      \"address\": {\n        \"name\": \"Sam Lee\",\n        \"address1\": \"420 Pine St\",\n        \"city\": \"Seattle\",\n        \"state\": \"WA\",\n        \"zip\": \"98101\",\n        \"country\": \"US\"\n      },\n      \"item_indices\": [\n        1\n      ]\n    }\n  ]\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"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.","item":[{"name":"Add item to order","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/{{orderId}}/item","host":["{{baseUrl}}"],"path":["{{orderId}}","item"]},"body":{"mode":"raw","raw":"{\n  \"sku\": \"MAGNET-4X4\",\n  \"quantity\": 25,\n  \"print_file\": \"https://example.com/magnet-design.png\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/{{orderId}}/item","host":["{{baseUrl}}"],"path":["{{orderId}}","item"]},"body":{"mode":"raw","raw":"{\n  \"sku\": \"MAGNET-4X4\",\n  \"quantity\": 25,\n  \"print_file\": \"https://example.com/magnet-design.png\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Update item quantity","request":{"method":"PUT","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/{{orderId}}/item/{{itemId}}","host":["{{baseUrl}}"],"path":["{{orderId}}","item","{{itemId}}"]},"body":{"mode":"raw","raw":"{\n  \"quantity\": 5\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/{{orderId}}/item/{{itemId}}","host":["{{baseUrl}}"],"path":["{{orderId}}","item","{{itemId}}"]},"body":{"mode":"raw","raw":"{\n  \"quantity\": 5\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Remove item from order","request":{"method":"DELETE","description":"Remove an item from an order and refund its full cost.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/{{orderId}}/item/{{itemId}}/cancel","host":["{{baseUrl}}"],"path":["{{orderId}}","item","{{itemId}}","cancel"]}},"response":[{"name":"200 OK","originalRequest":{"method":"DELETE","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/{{orderId}}/item/{{itemId}}/cancel","host":["{{baseUrl}}"],"path":["{{orderId}}","item","{{itemId}}","cancel"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"status\":\"success\",\"data\":{\"item_id\":\"itm_vNx36IWd8kx2dCQqkFbsTVg3\",\"refunded\":\"$0.00\",\"refunded_cents\":0,\"test_mode\":true},\"request_id\":\"4e3753e1-48dd-4313-bfd8-48e4af4e116a\"}"}]}]},{"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.","item":[{"name":"Get a signed, expiring price quote","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/orders/quote","host":["{{baseUrl}}"],"path":["orders","quote"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100\n    }\n  ],\n  \"shipping\": {\n    \"method\": \"standard\"\n  }\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/orders/quote","host":["{{baseUrl}}"],"path":["orders","quote"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100\n    }\n  ],\n  \"shipping\": {\n    \"method\": \"standard\"\n  }\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Get plan tiers","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/pricing/tiers","host":["{{baseUrl}}"],"path":["pricing","tiers"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/pricing/tiers","host":["{{baseUrl}}"],"path":["pricing","tiers"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Get pricing estimate","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/pricing/estimate","host":["{{baseUrl}}"],"path":["pricing","estimate"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100\n    }\n  ],\n  \"shipping_method\": \"standard\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/pricing/estimate","host":["{{baseUrl}}"],"path":["pricing","estimate"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100\n    }\n  ],\n  \"shipping_method\": \"standard\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"name":"Products","description":"The catalog: every product, its print specs and its price, and recommendations for what you are making.","item":[{"name":"List products","request":{"method":"GET","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).","header":[],"url":{"raw":"{{baseUrl}}/products","host":["{{baseUrl}}"],"path":["products"],"query":[{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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."},{"key":"category","value":"","disabled":true,"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."},{"key":"search","value":"","disabled":true,"description":"Free-text search across product name, SKU, and description"},{"key":"size","value":"","disabled":true,"description":"Filter by size (e.g. '3x3', '4x4', '6x6')"},{"key":"finish","value":"","disabled":true,"description":"Filter by finish (e.g. 'glossy', 'matte', 'holographic')"},{"key":"provider","value":"printedsimply","disabled":true,"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."},{"key":"dedupe","value":"false","disabled":true,"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."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/products","host":["{{baseUrl}}"],"path":["products"],"query":[{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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."},{"key":"category","value":"","disabled":true,"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."},{"key":"search","value":"","disabled":true,"description":"Free-text search across product name, SKU, and description"},{"key":"size","value":"","disabled":true,"description":"Filter by size (e.g. '3x3', '4x4', '6x6')"},{"key":"finish","value":"","disabled":true,"description":"Filter by finish (e.g. 'glossy', 'matte', 'holographic')"},{"key":"provider","value":"printedsimply","disabled":true,"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."},{"key":"dedupe","value":"false","disabled":true,"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."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Get product by SKU","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/products/{{sku}}","host":["{{baseUrl}}"],"path":["products","{{sku}}"],"query":[{"key":"provider","value":"printedsimply","disabled":true,"description":"Which catalog to ask: `printedsimply`, our own plant's. It is the default and the only value; anything else is a 400."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/products/{{sku}}","host":["{{baseUrl}}"],"path":["products","{{sku}}"],"query":[{"key":"provider","value":"printedsimply","disabled":true,"description":"Which catalog to ask: `printedsimply`, our own plant's. It is the default and the only value; anything else is a 400."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"List what a custom size may be","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/products/options","host":["{{baseUrl}}"],"path":["products","options"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/products/options","host":["{{baseUrl}}"],"path":["products","options"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Get product recommendations","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/products/recommend","host":["{{baseUrl}}"],"path":["products","recommend"]},"body":{"mode":"raw","raw":"{\n  \"query\": \"logo stickers for a trade show\",\n  \"use_case\": \"events\",\n  \"quantity\": 100\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/products/recommend","host":["{{baseUrl}}"],"path":["products","recommend"]},"body":{"mode":"raw","raw":"{\n  \"query\": \"logo stickers for a trade show\",\n  \"use_case\": \"events\",\n  \"quantity\": 100\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"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.","item":[{"name":"Estimate shipping costs","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/shipping/estimate","host":["{{baseUrl}}"],"path":["shipping","estimate"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100\n    }\n  ],\n  \"shipping_address\": {\n    \"zip\": \"78701\",\n    \"country\": \"US\"\n  }\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/shipping/estimate","host":["{{baseUrl}}"],"path":["shipping","estimate"]},"body":{"mode":"raw","raw":"{\n  \"items\": [\n    {\n      \"sku\": \"sticker-die-cut-3x3\",\n      \"quantity\": 100\n    }\n  ],\n  \"shipping_address\": {\n    \"zip\": \"78701\",\n    \"country\": \"US\"\n  }\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"List the days no delivery estimate counts","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/calendar","host":["{{baseUrl}}"],"path":["calendar"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/calendar","host":["{{baseUrl}}"],"path":["calendar"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Check a shipping address","request":{"method":"POST","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`).","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/addresses/validate","host":["{{baseUrl}}"],"path":["addresses","validate"]},"body":{"mode":"raw","raw":"{\n  \"address1\": \"123 Main St. Apt 4\",\n  \"city\": \"Austin\",\n  \"state\": \"Texas\",\n  \"zip\": \"787011234\",\n  \"country\": \"US\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/addresses/validate","host":["{{baseUrl}}"],"path":["addresses","validate"]},"body":{"mode":"raw","raw":"{\n  \"address1\": \"123 Main St. Apt 4\",\n  \"city\": \"Austin\",\n  \"state\": \"Texas\",\n  \"zip\": \"787011234\",\n  \"country\": \"US\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"name":"Artwork","description":"Check a print file before you order: format, resolution at the print size, dimensions and bleed.","item":[{"name":"Validate artwork file","request":{"method":"POST","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`.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/artwork/validate","host":["{{baseUrl}}"],"path":["artwork","validate"]},"body":{"mode":"raw","raw":"{\n  \"file_url\": \"https://example.com/logo.png\",\n  \"sku\": \"sticker-die-cut-3x3\"\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/artwork/validate","host":["{{baseUrl}}"],"path":["artwork","validate"]},"body":{"mode":"raw","raw":"{\n  \"file_url\": \"https://example.com/logo.png\",\n  \"sku\": \"sticker-die-cut-3x3\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"name":"Mockups","description":"Render a product mockup from your artwork, as an SVG or a PNG or JPEG for a store listing.","item":[{"name":"Generate product mockup","request":{"method":"POST","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).","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/mockups/generate","host":["{{baseUrl}}"],"path":["mockups","generate"]},"body":{"mode":"raw","raw":"{\n  \"artwork_url\": \"https://example.com/logo.png\",\n  \"product_type\": \"sticker\",\n  \"background\": \"white\",\n  \"format\": \"png\",\n  \"size\": 1200\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/mockups/generate","host":["{{baseUrl}}"],"path":["mockups","generate"]},"body":{"mode":"raw","raw":"{\n  \"artwork_url\": \"https://example.com/logo.png\",\n  \"product_type\": \"sticker\",\n  \"background\": \"white\",\n  \"format\": \"png\",\n  \"size\": 1200\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]}]},{"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`.","item":[{"name":"List your uploaded assets","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/assets","host":["{{baseUrl}}"],"path":["assets"],"query":[{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"20","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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`."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/assets","host":["{{baseUrl}}"],"path":["assets"],"query":[{"key":"cursor","value":"","disabled":true,"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."},{"key":"limit","value":"20","disabled":true,"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."},{"key":"page","value":"1","disabled":true,"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`."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Upload an asset (base64)","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/assets","host":["{{baseUrl}}"],"path":["assets"]},"body":{"mode":"raw","raw":"{\n  \"filename\": \"logo.png\",\n  \"content_type\": \"image/png\",\n  \"data\": \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==\"\n}","options":{"raw":{"language":"json"}}}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Keeps the id this request returned for the requests that use it.","if (pm.response.code < 300) {","  const body = pm.response.json();","  const pick = (path) => path.split(\".\").reduce((v, k) => (v == null ? undefined : v[k]), body);","  const assetId = pick(\"data.id\");","  if (assetId != null) pm.collectionVariables.set(\"assetId\", String(assetId));","}"]}}],"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/assets","host":["{{baseUrl}}"],"path":["assets"]},"body":{"mode":"raw","raw":"{\n  \"filename\": \"logo.png\",\n  \"content_type\": \"image/png\",\n  \"data\": \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Get an asset","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/assets/{{assetId}}","host":["{{baseUrl}}"],"path":["assets","{{assetId}}"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/assets/{{assetId}}","host":["{{baseUrl}}"],"path":["assets","{{assetId}}"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Delete an asset","request":{"method":"DELETE","description":"Removes the stored file and its record.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/assets/{{assetId}}","host":["{{baseUrl}}"],"path":["assets","{{assetId}}"]}},"response":[{"name":"200 OK","originalRequest":{"method":"DELETE","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/assets/{{assetId}}","host":["{{baseUrl}}"],"path":["assets","{{assetId}}"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"status\":\"success\",\"data\":{\"deleted\":true},\"request_id\":\"4e3753e1-48dd-4313-bfd8-48e4af4e116a\"}"}]}]},{"name":"Events","description":"The account's event log: every order event recorded, filterable by name and time, whether or not a webhook received it.","item":[{"name":"List account events","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/events","host":["{{baseUrl}}"],"path":["events"],"query":[{"key":"event","value":"","disabled":true,"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."},{"key":"from","value":"","disabled":true},{"key":"to","value":"","disabled":true},{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."}]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Keeps the id this request returned for the requests that use it.","if (pm.response.code < 300) {","  const body = pm.response.json();","  const pick = (path) => path.split(\".\").reduce((v, k) => (v == null ? undefined : v[k]), body);","  const eventId = pick(\"data.events.0.event_id\");","  if (eventId != null) pm.collectionVariables.set(\"eventId\", String(eventId));","}"]}}],"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/events","host":["{{baseUrl}}"],"path":["events"],"query":[{"key":"event","value":"","disabled":true,"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."},{"key":"from","value":"","disabled":true},{"key":"to","value":"","disabled":true},{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]}]},{"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.","item":[{"name":"List webhook subscriptions","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/webhooks","host":["{{baseUrl}}"],"path":["webhooks"],"query":[{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."},{"key":"url","value":"","disabled":true,"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."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/webhooks","host":["{{baseUrl}}"],"path":["webhooks"],"query":[{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."},{"key":"url","value":"","disabled":true,"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."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Create a webhook subscription","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks","host":["{{baseUrl}}"],"path":["webhooks"]},"body":{"mode":"raw","raw":"{\n  \"url\": \"https://example.com/hooks/orders\",\n  \"events\": [\n    \"order.shipped\",\n    \"order.delivered\"\n  ],\n  \"mode\": \"test\"\n}","options":{"raw":{"language":"json"}}}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Keeps the id this request returned for the requests that use it.","if (pm.response.code < 300) {","  const body = pm.response.json();","  const pick = (path) => path.split(\".\").reduce((v, k) => (v == null ? undefined : v[k]), body);","  const webhookId = pick(\"data.id\");","  if (webhookId != null) pm.collectionVariables.set(\"webhookId\", String(webhookId));","}"]}}],"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks","host":["{{baseUrl}}"],"path":["webhooks"]},"body":{"mode":"raw","raw":"{\n  \"url\": \"https://example.com/hooks/orders\",\n  \"events\": [\n    \"order.shipped\",\n    \"order.delivered\"\n  ],\n  \"mode\": \"test\"\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Retrieve a webhook subscription","request":{"method":"GET","description":"One subscription: url, events, active, mode, and rotation state. Never the signing secret.","header":[],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Update a webhook subscription","request":{"method":"PATCH","description":"Change the endpoint URL, the subscribed events, the test/live scope, or pause it with active: false. Send at least one field.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}"]},"body":{"mode":"raw","raw":"{\n  \"events\": [\n    \"order.shipped\",\n    \"order.delivered\",\n    \"order.cancelled\"\n  ]\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"PATCH","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}"]},"body":{"mode":"raw","raw":"{\n  \"events\": [\n    \"order.shipped\",\n    \"order.delivered\",\n    \"order.cancelled\"\n  ]\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Delete a webhook subscription","request":{"method":"DELETE","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.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}"]}},"response":[{"name":"200 OK","originalRequest":{"method":"DELETE","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"status\":\"success\",\"data\":{\"id\":\"whk_hCyrY24j_gUrQWxJ7ji6s9st\",\"deleted\":true},\"request_id\":\"4e3753e1-48dd-4313-bfd8-48e4af4e116a\"}"}]},{"name":"Rotate a webhook signing secret","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}/rotate-secret","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}","rotate-secret"]},"body":{"mode":"raw","raw":"{\n  \"grace_hours\": 24\n}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}/rotate-secret","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}","rotate-secret"]},"body":{"mode":"raw","raw":"{\n  \"grace_hours\": 24\n}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"End an open rotation window","request":{"method":"DELETE","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.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}/rotate-secret","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}","rotate-secret"]}},"response":[{"name":"200 OK","originalRequest":{"method":"DELETE","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/{{webhookId}}/rotate-secret","host":["{{baseUrl}}"],"path":["webhooks","{{webhookId}}","rotate-secret"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Replay a stored event to your webhooks","request":{"method":"POST","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.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/events/{{eventId}}/replay","host":["{{baseUrl}}"],"path":["events","{{eventId}}","replay"]}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/events/{{eventId}}/replay","host":["{{baseUrl}}"],"path":["events","{{eventId}}","replay"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"status\":\"success\",\"data\":{\"replayed\":true,\"event\":\"order.cancelled\",\"event_id\":\"evt_F5S_u7olBRx4IBTVZNXoH-8d\",\"enqueued\":1},\"request_id\":\"4e3753e1-48dd-4313-bfd8-48e4af4e116a\"}"}]},{"name":"List webhook delivery attempts","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/webhooks/deliveries","host":["{{baseUrl}}"],"path":["webhooks","deliveries"],"query":[{"key":"webhook_id","value":"","disabled":true},{"key":"event_id","value":"","disabled":true,"description":"Every attempt at one logical event — the X-PS-Event-Id value."},{"key":"success","value":"","disabled":true,"description":"Filter to successful or failed attempts. Exactly `true` or `false` — any other spelling returns 400 validation_error rather than quietly applying no filter."},{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."}]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["// Keeps the id this request returned for the requests that use it.","if (pm.response.code < 300) {","  const body = pm.response.json();","  const pick = (path) => path.split(\".\").reduce((v, k) => (v == null ? undefined : v[k]), body);","  const deliveryId = pick(\"data.deliveries.0.id\");","  if (deliveryId != null) pm.collectionVariables.set(\"deliveryId\", String(deliveryId));","}"]}}],"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/webhooks/deliveries","host":["{{baseUrl}}"],"path":["webhooks","deliveries"],"query":[{"key":"webhook_id","value":"","disabled":true},{"key":"event_id","value":"","disabled":true,"description":"Every attempt at one logical event — the X-PS-Event-Id value."},{"key":"success","value":"","disabled":true,"description":"Filter to successful or failed attempts. Exactly `true` or `false` — any other spelling returns 400 validation_error rather than quietly applying no filter."},{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Retrieve a webhook delivery","request":{"method":"GET","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`.","header":[],"url":{"raw":"{{baseUrl}}/webhooks/deliveries/{{deliveryId}}","host":["{{baseUrl}}"],"path":["webhooks","deliveries","{{deliveryId}}"]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/webhooks/deliveries/{{deliveryId}}","host":["{{baseUrl}}"],"path":["webhooks","deliveries","{{deliveryId}}"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Replay a webhook delivery","request":{"method":"POST","description":"Re-sends the stored payload to your endpoint with a fresh signature. Safe to retry (Idempotency-Key supported).","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/deliveries/{{deliveryId}}/redeliver","host":["{{baseUrl}}"],"path":["webhooks","deliveries","{{deliveryId}}","redeliver"]}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/deliveries/{{deliveryId}}/redeliver","host":["{{baseUrl}}"],"path":["webhooks","deliveries","{{deliveryId}}","redeliver"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"status\":\"success\",\"data\":{\"delivery_id\":\"ccbkfrz5r3gcby08k4iqiuxwb\",\"redelivered\":true,\"status_code\":200,\"error\":null},\"request_id\":\"4e3753e1-48dd-4313-bfd8-48e4af4e116a\"}"}]},{"name":"Read the hosted test inbox","request":{"method":"GET","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.","header":[],"url":{"raw":"{{baseUrl}}/webhooks/inbox","host":["{{baseUrl}}"],"path":["webhooks","inbox"],"query":[{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."}]}},"response":[{"name":"200 OK","originalRequest":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/webhooks/inbox","host":["{{baseUrl}}"],"path":["webhooks","inbox"],"query":[{"key":"limit","value":"20","disabled":true,"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."},{"key":"cursor","value":"","disabled":true,"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."}]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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}}"}]},{"name":"Create the hosted test inbox","request":{"method":"POST","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.","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/inbox","host":["{{baseUrl}}"],"path":["webhooks","inbox"]},"body":{"mode":"raw","raw":"{}","options":{"raw":{"language":"json"}}}},"response":[{"name":"200 OK","originalRequest":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/inbox","host":["{{baseUrl}}"],"path":["webhooks","inbox"]},"body":{"mode":"raw","raw":"{}","options":{"raw":{"language":"json"}}}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"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\"}"}]},{"name":"Empty the hosted test inbox","request":{"method":"DELETE","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}.","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/inbox/entries","host":["{{baseUrl}}"],"path":["webhooks","inbox","entries"]}},"response":[{"name":"200 OK","originalRequest":{"method":"DELETE","header":[{"key":"Idempotency-Key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/webhooks/inbox/entries","host":["{{baseUrl}}"],"path":["webhooks","inbox","entries"]}},"status":"OK","code":200,"_postman_previewlanguage":"json","header":[{"key":"Content-Type","value":"application/json"},{"key":"X-Request-Id","value":"4e3753e1-48dd-4313-bfd8-48e4af4e116a"},{"key":"MF-Version","value":"2026-07-26"}],"body":"{\"status\":\"success\",\"data\":{\"webhook_id\":\"whk_Pxpo7Z0TfZI3H2ZY7yS2tukP\",\"removed\":1},\"request_id\":\"4e3753e1-48dd-4313-bfd8-48e4af4e116a\"}"}]}]}],"variable":[{"key":"baseUrl","value":"https://portal.printedsimply.com/api/v1","type":"string"},{"key":"apiKey","value":"ps_test_YOUR_KEY","type":"string","description":"Your API key. A test key places $0 orders that run the whole lifecycle."},{"key":"orderId","value":"test_ord_wVK9ZiG-VYiPrlSeBUBmrUVk","type":"string","description":"Set by \"Create an order\" from its response."},{"key":"sku","value":"sticker-die-cut-3x3","type":"string","description":"A SKU from GET /products."},{"key":"itemId","value":"itm_fowT5-1iZwSnQcGX0ByoA64l","type":"string","description":"Set by \"Get order details\" from its response."},{"key":"eventId","value":"evt_F5S_u7olBRx4IBTVZNXoH-8d","type":"string","description":"Set by \"List account events\" from its response."},{"key":"assetId","value":"ast_bptSUfSLn2FInuMnC5S4fVD1","type":"string","description":"Set by \"Upload an asset (base64)\" from its response."},{"key":"webhookId","value":"whk_SV8d5wGm2kUnSwx_RhZfY9OE","type":"string","description":"Set by \"Create a webhook subscription\" from its response."},{"key":"deliveryId","value":"ccbkfrz5r3gcby08k4iqiuxwb","type":"string","description":"Set by \"List webhook delivery attempts\" from its response."}]}