# Changelog

Source: https://portal.printedsimply.com/docs/api-changelog

PrintedSimply uses date-based API versioning. Pin your integration to a specific version by sending the `X-API-Version` header with your requests. Below the versions is the [platform's release history](https://portal.printedsimply.com/docs/api-changelog#platform-releases) — the dashboard, the docs and the tools, release by release.

## What shipped this week

The newest 50 as a feed: [RSS](https://portal.printedsimply.com/docs/api-changelog.rss) · [JSON Feed](https://portal.printedsimply.com/docs/api-changelog.json)

**22 changes in the last 7 days, the newest on Thu, Oct 22 · show**

### Thu, Oct 22

- ~ [The webhook delivery log lists by when each event happened (DX-31)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-22-the-webhook-delivery-log-lists-by-when-each#2026-10-22-the-webhook-delivery-log-lists-by-when-each)

### Wed, Oct 21

- ~ [Every validation error points at what it refused (DX-40)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-21-every-validation-error-points-at-what-it-refused#2026-10-21-every-validation-error-points-at-what-it-refused)
- ~ [A body over the cap is 413 payload_too_large on every v1 operation that reads one (DX-40)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-21-a-body-over-the-cap-is-413-payload-too-large-on#2026-10-21-a-body-over-the-cap-is-413-payload-too-large-on)
- ~ [A die-cut mockup can show its cut line (AGT-36)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-21-a-die-cut-mockup-can-show-its-cut-line-agt-36#2026-10-21-a-die-cut-mockup-can-show-its-cut-line-agt-36)
- ~ [A GET /v1/products cursor is bound to the source the catalog was served from (DX-43)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-21-a-get-v1-products-cursor-is-bound-to-the-source#2026-10-21-a-get-v1-products-cursor-is-bound-to-the-source)

### Mon, Oct 19

- + [What a custom size may be, in one read (DX-34)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-what-a-custom-size-may-be-in-one-read-dx-34-get#2026-10-19-what-a-custom-size-may-be-in-one-read-dx-34-get)
- + [Bulk and multi-ship rows carry the fields a single order has (OPS-203, DX-34)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-bulk-and-multi-ship-rows-carry-the-fields-a#2026-10-19-bulk-and-multi-ship-rows-carry-the-fields-a)
- + [Which order a reorder placed again, on every read (DX-46)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-which-order-a-reorder-placed-again-on-every-read#2026-10-19-which-order-a-reorder-placed-again-on-every-read)
- + [When a placed order should arrive: `estimated_delivery` on the order (DX-34)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-when-a-placed-order-should-arrive-estimated#2026-10-19-when-a-placed-order-should-arrive-estimated)
- + [Bleed, margin and outline measured from the artwork, and an opt-in fix (AGT-36)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-bleed-margin-and-outline-measured-from-the#2026-10-19-bleed-margin-and-outline-measured-from-the)
- + [Follow an order without a webhook address (AGT-39)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-follow-an-order-without-a-webhook-address-agt-39#2026-10-19-follow-an-order-without-a-webhook-address-agt-39)
- + [Delivery-point verification for live orders (OPS-203)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-delivery-point-verification-for-live-orders-ops#2026-10-19-delivery-point-verification-for-live-orders-ops)
- + [An order can be changed or cancelled only before it goes into production](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-an-order-can-be-changed-or-cancelled-only-before#2026-10-19-an-order-can-be-changed-or-cancelled-only-before)
- ~ [A plant outage is not a pricing error on the bulk, multi-ship and add-item paths either (DX-34)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-a-plant-outage-is-not-a-pricing-error-on-the#2026-10-19-a-plant-outage-is-not-a-pricing-error-on-the)
- ~ [GET /v1/setup answers for the credential asking, and two order refusals say what is still possible](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-19-get-v1-setup-answers-for-the-credential-asking#2026-10-19-get-v1-setup-answers-for-the-credential-asking)

### Sun, Oct 18

- + [A webhook delivery says why it failed (OPS-201)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-a-webhook-delivery-says-why-it-failed-ops-201#2026-10-18-a-webhook-delivery-says-why-it-failed-ops-201)
- + [Recommendations survive a catalog outage, as the list does (PLT-32)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-recommendations-survive-a-catalog-outage-as-the#2026-10-18-recommendations-survive-a-catalog-outage-as-the)
- + [An order line can say how its artwork was made and who approved it (AGT-31)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-an-order-line-can-say-how-its-artwork-was-made#2026-10-18-an-order-line-can-say-how-its-artwork-was-made)
- + [Carrier advisories as data (OPS-208)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-carrier-advisories-as-data-ops-208-when-a#2026-10-18-carrier-advisories-as-data-ops-208-when-a)
- + [The holiday calendar as data](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-the-holiday-calendar-as-data-order-by-and-peak#2026-10-18-the-holiday-calendar-as-data-order-by-and-peak)
- ~ [New keys start with ps_live_ and ps_test_, and webhooks carry X-PS-* headers (PrintedSimply only ·…](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-new-keys-start-with-ps-live-and-ps-test-and#2026-10-18-new-keys-start-with-ps-live-and-ps-test-and)
- ~ [PUT /v1/order/{id}/address checks the new address as POST /v1/order does (OPS-203)](https://portal.printedsimply.com/docs/api-changelog?item=2026-10-18-put-v1-order-id-address-checks-the-new-address#2026-10-18-put-v1-order-id-address-checks-the-new-address)

Machine-readable spec

The full OpenAPI 3.1 document is published at [/api/openapi.json](https://portal.printedsimply.com/api/openapi.json). It carries the current version in `info.version` (`2026-07-26`) and the full supported-version list — statuses and sunset dates included — in `x-api-versions`, so a client can check its own pin without reading this page.

How versioning works

- Each API version is identified by its release date (e.g. `2026-03-14`)
- If you do not send an `X-API-Version` header, the latest version is used
- Non-breaking changes (new fields, new endpoints) are added without a version bump
- Breaking changes are released under a new version date with at least 30 days notice
- Deprecated versions are supported for a minimum of 12 months

Check the version you were served

Every response — successes, errors, and idempotent replays alike — carries the version it was _actually_ served under, in `MF-Version` and, under the same name as the request header, `X-API-Version`. The two always carry the same value.

Compare it against the version you pinned. A pin is not always honoured, and neither case is an error response:

- A version id we don't recognise resolves **down** to the nearest older supported version.
- A version id older than `2026-02-15` (the published minimum) resolves **up** to the current version.

So a typo in a pin gets you a contract you did not ask for, quietly, with a 200. The echo is the only place that shows up. Both SDKs record it after every call — `client.lastApiVersion` in TypeScript, `client.last_api_version` in Python — including on calls that raised.

Deprecation headers

When you use a deprecated API version, responses include these headers:

- `Deprecation: @<unix seconds>` — this version is deprecated, and since when (RFC 9745)
- `Sunset: <date>` — when this version will stop working
- `Link: <url>; rel="deprecation"` — migration guide

2026-07-26 Current

**Added (12 of 45) · show**

- + 2026-10-19What a custom size may be, in one read (DX-34): GET /v1/products/options answers, for each product our own plant makes to any size (die-cut and kiss-cut stickers, vinyl and wall decals, magnets, posters), `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, or null when it prices every size), the step (none), the quantity one line may carry, the artwork DPI the order grades at (`min_dpi` recommended, `refuse_below_dpi` refused on a live order) and its bleed, the finishes on offer (none yet) and the book's list price for one unit (`base_cents` + width × height × `cents_per_sq_in`, before quantity breaks, any plan discount and the unit floor). Every figure is read from the rule the order applies, never typed beside it. `plant_confirms_sku` says what it cannot settle: a live quote or order still asks our plant for the exact SKU, and a size its catalog does not carry is refused 400 pricing_error, while a test key's takes any size within the bounds. `products:read`; both SDKs (`products.options()`) and the MCP tool get_product_options, served on the PrintedSimply server. Additive: nothing else changed.
- + 2026-10-19Bulk and multi-ship rows carry the fields a single order has (OPS-203, DX-34). Each POST /v1/orders/bulk row and each POST /v1/orders/multi-ship shipment gains `error_code`: the code its `error` starts with (address_invalid, destination_unavailable, duplicate_reference, plant_unreachable, pricing_error, reference_check_unavailable or unknown_sku), so a client can branch without splitting the string; null on success and on a refusal whose error starts with no code. Each gains `address_check`, POST /v1/order's own check of the entry's address with its warnings, on a test key as on a live one (a test key is still never refused for it), null only where the check did not run; and `estimated_delivery`, POST /v1/order's date for the placed order, null on a refused row. Additive: `error` and every other field is unchanged, and both SDKs' types declare the three.
- + 2026-10-19Which order a reorder placed again, on every read (DX-46). GET /v1/order/{id} and every GET /v1/orders row carry `reordered_from`: on an order POST /v1/order/{id}/reorder placed, the source order's `portal_order_id`, the value the reorder's own answer stated, from the order's own record; null on any other order. Ours on a live order: a key of that name in the facility's body is replaced. Both SDKs' types declare it. Additive.
- + 2026-10-19When a placed order should arrive: `estimated_delivery` on the order (DX-34). POST /v1/order's 200, GET /v1/order/{id} and every GET /v1/orders row carry `estimated_delivery` { earliest, latest, ships_by, is_estimate: true }: the same estimate POST /v1/orders/quote gives, asked about the order as placed, counted from when it was placed (when a person approved it, for an order placed with approval) by its own shipping method at its own facility. It is null on a lane that is not one of our own facilities, on an order with no known shipping method, and whenever the order is not on its way (pending_approval, pending_payment, delivered, cancelled, issue_flagged, rerouting and every legacy status); it is never moved because an order runs late, and it is not a promise. `order.in_production`, `order.quality_check` and `order.shipped` carry the same object where it is not null, and omit it otherwise. Both SDKs' types declare it. Additive: no field was renamed or removed.
- + 2026-10-19Bleed, margin and outline measured from the artwork, and an opt-in fix (AGT-36). The artwork check (POST /v1/order's artwork_check, POST /v1/orders/quote and POST /v1/artwork/validate) now reads a whole file's alpha channel, bounded, and measures it at the line's print size against the product's bleed, adding three warning codes to findings: no_bleed (a poster, magnet or decal whose file stops at the trim, by the DPI the file names or by its proportions), content_in_bleed (a die-cut design closer to the canvas edge than the bleed) and no_cutline (a die-cut file that is entirely transparent, or separate shapes too far apart for one outline). Warnings only: no order is refused for them. POST /v1/artwork/validate's checks add bleed_area, and cut_line states the margin it measured. New on POST /v1/artwork/validate: auto_fix: true extends a rectangle's edges past the trim, widens a die-cut's transparent margin or joins its shapes with a white border, stores the result as a new asset (Asset.source fixup) and answers auto_fix with the new asset_id, url, the changes made and the fixed file's own status, findings and would_refuse; it needs sku and the assets:write scope as well as artwork:write. The validate_artwork MCP tool takes auto_fix (not on the keyless connection) and is now annotated as a write. Additive: every field and code already sent is unchanged.
- + 2026-10-19Follow an order without a webhook address (AGT-39). GET /v1/order/{id}/events takes `since` and `wait_seconds`. `meta.since` is the position of the last event an answer gave (or, on an empty answer, the position it read from) and, unlike `meta.next_cursor`, is not null at the end of the history: send it back as `since` and only later events are returned, at a cursor page's size. With `wait_seconds` (0 to 25, clamped, 0 by default) a request that has no event to return waits for one, re-reading every 2 seconds, and answers as soon as one arrives or with an empty `data` and the same `meta.since` when the wait ends; one request, counted once by the rate limit, scope `orders:read` as before. A `since` that is altered, from another order, account or key mode, or sent with `cursor` is 400 `validation_error` with `details.parameter: "since"`. The get_order_events MCP tool takes both, and the MCP server offers `order://{order_id}` as a resource (`resources/templates/list`, `resources/read`: the order and its event history in one JSON document); `resources/subscribe` is not offered, because the endpoint holds no stream to push an update down. Both SDKs' order-events parameters and `ListMeta` carry them. Additive.
- + 2026-10-19Delivery-point verification for live orders (OPS-203). Where the deployment has USPS verification configured, a live POST /v1/order, each live POST /v1/orders/bulk entry and PUT /v1/order/{id}/address on a live order ask the USPS Addresses API whether a US street address the other checks pass is a delivery point, before anything is charged or sent. Three finding codes join `AddressFinding.code`: `delivery_point_not_found` (an `error`: USPS answered that no delivery point exists at the address, refused 422 `address_invalid` like the other errors), `delivery_point_incomplete` (a `warning`, never refused: the building exists but its apartment or suite is missing or not confirmed, or more than one address matched) and `delivery_point_confirmed` (`info`). A USPS outage, timeout or error never blocks an order: it proceeds on the other checks alone. A test key, POST /v1/orders/quote and POST /v1/addresses/validate are not verified, and an address outside the US never is.
- + 2026-10-19An order can be changed or cancelled only before it goes into production. DELETE /v1/order/{id}/cancel used to refuse only a shipped or delivered order, and PUT /v1/order/{id}/address, PUT /v1/order/{id}/shipping, POST /v1/{id}/item, PUT /v1/{id}/item/{itemId} and DELETE /v1/{id}/item/{itemId}/cancel also took an order in production or at its quality check, putting it to the plant. Every one now answers 409 (not_cancellable for the cancel, order_not_modifiable for the rest), without asking the plant, for an order past pending_payment, paid or accepted: in_production, quality_check, shipped, delivered, issue_flagged and rerouting, on a test key as on a live one. `actions` on GET /v1/order/{id} and GET /v1/orders says the same, and the MCP tools are refused the same way. Within the window nothing changes: a live change or cancel is still asked of the plant first, and money moves only if it accepts.
- + 2026-10-18A webhook delivery says why it failed (OPS-201). GET /v1/webhooks/deliveries and GET /v1/webhooks/deliveries/{id} gain `failure_cause`: on a failed attempt, the cause the delivery log records (dns, refused, timeout, tls, reset, redirect, unauthorized, http_4xx, http_5xx and the others an order's timeline already publishes); null when delivered. The endpoint-side causes are your URL's to fix; egress, signing and internal are ours, and the attempt is retried on schedule. A status_code of null no longer reads as a timeout. Additive; both SDKs' generated types carry it.
- + 2026-10-18Recommendations survive a catalog outage, as the list does (PLT-32). POST /v1/products/recommend used to answer 502 upstream_error whenever our own plant's catalog had nothing of its own to serve (no live answer, no last-good snapshot, no local copy, as where it is not configured) while GET /v1/products answered 200 from the price book. It now ranks over the same price-book rows, at the price an order is charged, with X-Catalog-Stale: true, X-Catalog-Degraded naming why and the new `catalog.source` field saying `price_book` (`catalog` otherwise). Additive: a client that read the 502 as "try again later" now gets recommendations it can order.
- + 2026-10-18An order line can say how its artwork was made and who approved it (AGT-31). A line of POST /v1/order, POST /v1/orders/bulk and POST /v1/orders/multi-ship takes an optional `artwork`: provenance (human, ai_assisted or ai_generated), and approved_by (a name or an email address, at most 200 characters, sanitized) with approved_at (ISO 8601 with a time zone, not in the future). Strict: an unknown key, another value or half an approval is 400 validation_error. The order stores it and echoes it on each line of GET /v1/order/{id} and GET /v1/orders and on order.created; a line without one is unchanged, and it never changes the price, the routing or the artwork check. Every row our own plant makes on GET /v1/products gains `minimum_quantity: 1`. Additive; both SDKs and the create_order MCP tool describe them.
- + 2026-10-18Carrier advisories as data (OPS-208). When a carrier posts a delay, the plant enters it and it is published from the next request: GET /v1/calendar lists every one not yet over in `advisories` (carrier, states, ZIP prefixes, dates, extra days, message and source, each as the carrier posted it). A quote or shipping estimate into a destination one touches reports it in `delivery_risk`, and `deliver_by_check` gains `status` (meets, at_risk or misses) and `advisories`: under an advisory it answers at_risk, and `meets` is false. Additive; both SDKs and the get_calendar and quote_order MCP tools describe them.

**Deprecated (2) · show**

- ! 2026-10-15 `page` (and `offset`) on GET /v1/orders, GET /v1/order/{id}/events, GET /v1/assets and GET /v1/products (DX-43). Still accepted and answered as before, each with its own earlier page size, now with `meta.has_more` and `meta.next_cursor`; `cursor` wins when both are sent. Walk with `cursor` instead; the MCP tools no longer offer `page`.
- ! 2026-09-26pricing_model.processing_fee_pct on GET /v1/pricing/tiers. The single fee it described is now two lines; it holds the platform fee rate so a parser that requires it still parses. Read platform_fee_pct, card_fee_pct and card_fee_fixed_cents instead

Changes 1 of 10 [Older changes →](https://portal.printedsimply.com/docs/api-changelog?changes=2#v2026-07-26)

2026-03-14 Stable

**Added (7) · show**

- + 2026-07-18Signed, expiring order quotes — POST /v1/orders/quote
- + 2026-07-18$0 sandbox — time-compressed order lifecycle with real signed webhooks
- + 2026-07-18Webhook delivery replay
- + 2026-03-22Artwork validation, design lint, and mockup generation
- + 2026-03-22Template rendering API
- + 2026-03-22Product recommendations
- + 2026-03-22Multi-address shipping and bulk orders

**Changed (1) · show**

- ~ 2026-03-22Order response now includes estimated_delivery field

2026-03-01 Stable

**Added (3) · show**

- + 2026-03-22Shipping estimation endpoint
- + 2026-03-22Volume pricing tiers
- + 2026-03-22Product search and filtering

**Changed (1) · show**

- ~ 2026-03-22Rate limit increased from 30 to 60 req/min

**Deprecated (1) · show**

- ! 2026-03-22Legacy product IDs (use product_type instead)

2026-02-15 Deprecated

**Added (3) · show**

- + 2026-03-22Webhook signature verification
- + 2026-03-22Idempotency key support
- + 2026-03-22Order events endpoint

**Changed (1) · show**

- ~ 2026-03-22Error response format standardized to include request_id

## Platform releases

A dated log of what shipped on the dashboard, the docs and the tools, newest first. An entry records what was true when it shipped; a later entry corrects it rather than an edit. The newest 50 releases are a feed: [RSS](https://portal.printedsimply.com/docs/platform-releases.rss) or [JSON Feed](https://portal.printedsimply.com/docs/platform-releases.json); the API's own changes, above, are another: [RSS](https://portal.printedsimply.com/docs/api-changelog.rss) or [JSON Feed](https://portal.printedsimply.com/docs/api-changelog.json).

v43.1.0 New Feature August 3, 2026

### Replay-resistant webhook signatures and event ids

- • New X-MF-Signature-V2 header (t=<unix>,v1=<hex>) signs the timestamp together with the raw body, so you can reject a captured request replayed later — the body-only signature it replaces stayed valid forever
- • New X-MF-Event-Id header, mirrored as `id` in the payload — stable across our retry attempts and any manual redelivery, so you can dedupe instead of processing a retry twice
- • New X-MF-Timestamp header exposing the signed timestamp directly
- • X-MF-Signature (sha256=<hex> over the raw body) is unchanged and still sent on every request — existing verifiers keep working, no migration required
- • TypeScript SDK: client.webhooks.verify() now accepts either header and enforces a 5-minute replay window by default (override with { toleranceSeconds })

v42.0.0 Release March 22, 2026

### Phase 42 — Stripe Billing Portal & Plan Management

- • Stripe Customer Portal — self-service billing management (invoices, payment methods, billing history) via hosted Stripe portal
- • Usage summary — billing page now shows current plan, monthly order count, and month-to-date spend
- • Billing portal API — POST /api/portal/billing/portal creates Stripe Customer Portal session
- • Pricing page update — plan tiers now show monthly pricing alongside rate limits and discounts

v41.0.0 Release March 22, 2026

### Phase 41 — Audit & Polish

- • Error boundaries — added error.tsx to 9 dashboard pages (api-keys, assets, audit-log, billing, notifications, orders, settings, team, webhooks)
- • Loading states — added skeleton loading.tsx to notifications page
- • Page metadata — added title/description to webhook logs, webhook testing, and order detail pages
- • Brand cleanup — replaced all legacy PS_ variable names with MF_ in WooCommerce integration code examples and blog posts
- • Code example cleanup — renamed ps → mf variable names across all blog post code snippets

v40.0.0 Release March 22, 2026

### Phase 40 — API Versioning & Deprecation

- • Date-based API versioning — send X-API-Version header to pin your integration to a specific API version (Stripe-style)
- • Version negotiation — middleware resolves requested version, falls back to nearest older version for unknown dates
- • Deprecation headers — deprecated versions return Deprecation, Sunset, and Link headers per RFC 8594
- • API version config — centralized version registry with status (current/stable/deprecated) and sunset dates
- • Version context — apiVersion now available in withApiAuth handler context for version-specific behavior
- • Docs update — API changelog page now documents deprecation header format

v39.0.0 Release March 22, 2026

### Phase 39 — Admin Dashboard Enhancement

- • System health page — /admin/health shows database, Redis, and fulfillment provider status with response times and uptime
- • API usage analytics — /admin/usage with 24h/7d request counts, error rate, avg latency, top endpoints, top API keys, and errors by status code
- • Orders pagination — 30 per page with status filter preserved across pages
- • Admin sidebar — added API Usage and System Health nav items, fixed its branding

Page 1 of 8 · back to February 20, 2026 [Older releases →](https://portal.printedsimply.com/docs/api-changelog?releases=2#platform-releases)

Breaking changes are always announced at least 30 days in advance.
Questions? [Read the API docs](https://portal.printedsimply.com/docs) or contact us at developers@printedsimply.com.
