# API Changelog (/changelog) Release notes for the LettrLabs public API (`/api/v1/*`). Entries are listed newest first. Each change is flagged **breaking** or **non-breaking**; a breaking entry always carries a migration note describing what you must change. For the current shape of every endpoint, see the [API reference](/api-reference) — this page tells you *what changed*, the reference tells you *what the contract is now*. ## 2026-07-29 — Checkout tells you how long settlement takes [#2026-07-29--checkout-tells-you-how-long-settlement-takes] **Non-breaking** (three added response fields; nothing existing changed). `POST /api/v1/order/{id}/checkout` settles payment asynchronously, and its `200` has always meant *accepted*, not *charged* — but the response said nothing about what happened next, so sizing a polling window was guesswork. It now carries three fields: * **`orderStatus`** — the order's own state as the call returns, normally `Payment Needed` (accepted, settling). This is distinct from the existing `status`, which reports the outcome of the *call* and still reads `Success`. * **`pollAfterSeconds`** — how long to wait before your first status read. Reading sooner cannot tell you anything new. * **`settlementExpectedWithinSeconds`** — when settlement is expected to have reached a terminal state (`Paid`, or back to `Draft` if the payment failed). The operation's reference description also no longer says the call charges payment inline, which was never true of this endpoint. *What to check in your client:* if you hard-coded a polling timeout for checkout, replace it with `settlementExpectedWithinSeconds` from the response. Treat that value as an expectation, not a guarantee — if it elapses while the order is still `Payment Needed`, the order is not lost and no second charge is pending, so ask us to confirm settlement rather than treating it as a failure. **Never retry a checkout that returned 200**: the charge is already in flight and a second checkout charges again. Existing fields (`orderId`, `status`, and the "payment is being processed" warning) are unchanged, so a client that ignores the new fields keeps working exactly as before. ## 2026-07-28 — Checkout locks the order immediately: no more edits while payment settles [#2026-07-28--checkout-locks-the-order-immediately-no-more-edits-while-payment-settles] **Non-breaking** (it closes an edit window that was never part of the contract). `POST /api/v1/order/{id}/checkout` settles payment asynchronously — the `200` means the checkout was accepted, not that the order is paid. Previously the order kept reporting status `Draft` (with `paidDate: null`) until settlement completed, roughly a minute or two later, and during that window recipient deletes and re-checkouts still succeeded — so a client could strip recipients from an order that had already been charged, or trigger a second charge. Now, the moment checkout is accepted the order's status becomes **`Payment Needed`**, and it stops being editable immediately: `DELETE …/recipients`, `DELETE /order/{id}`, `GET …/checkout` (preview), and a second `POST …/checkout` all return the standard `400` invalid-status error until the order settles. On success the order moves to **`Paid`** as before; if payment fails it returns to **`Draft`**, so you can edit and check out again. *What to check in your client:* if you poll `GET /v1/order/{id}` after checkout, you will now see `orderStatus: "Payment Needed"` while payment settles (previously `"Draft"`). Treat it as "in flight — don't edit"; keep polling for `Paid` (or a return to `Draft`, which means the payment failed and you can retry). If your integration hard-codes the set of possible status strings, add `"Payment Needed"`. ## 2026-07-28 — Appending recipients now requires an editable order [#2026-07-28--appending-recipients-now-requires-an-editable-order] **Non-breaking** (it corrects behavior that was never part of the contract). `PUT /api/v1/order/{id}/recipients` previously accepted appends at any stage of an order's life: appending to an order that was already paid or in production returned `200` and silently increased the order's piece count **after** payment — pieces that were never charged. The operation now enforces the same rule the recipient-delete, order-delete, and checkout operations already enforce: the order must be editable (status **Draft** or **Edits Needed**). Appending to an order in any other status returns the standard `400` invalid-status error and changes nothing. *Migration note (only if you appended after checkout):* add all recipients before executing checkout. To send to additional recipients after an order is paid, create a new order — a post-payment append was never a supported way to grow a paid order, and it now fails loudly instead of shipping unpaid pieces. Error responses for nonexistent or inaccessible order ids are unchanged. ## 2026-07-27 — Order deletion is now immediate and reliable [#2026-07-27--order-deletion-is-now-immediate-and-reliable] **Non-breaking.** `DELETE /api/v1/order/{id}` no longer physically removes the order's data during the request — it marks the order deleted instantly. What you'll notice: * **Deletes are fast regardless of order size.** Previously, deleting an order with many recipients could take minutes and sometimes time out or fail; the request now completes quickly even for very large orders. * **Deleted orders disappear immediately, everywhere.** After a successful delete, the order behaves exactly like an order id that never existed on every other operation (listing, recipients, proof, checkout, append). Repeating the delete returns the same response as deleting a nonexistent id. * **Eligibility rules are unchanged** — and one is now explicit: only Draft / Needs Edits orders can be deleted, orders with paid recipient searches are still rejected, and an order that is in use as an automation's template is now rejected with a clear message (previously this failed with a generic error). Deleted orders' underlying records are retained internally for a period after deletion (they are never served to any API consumer) and are then permanently purged on a scheduled cleanup. No client change is required. ## 2026-07-26 — Four response-contract corrections: the API now does what its contract says [#2026-07-26--four-response-contract-corrections-the-api-now-does-what-its-contract-says] **Non-breaking.** Each of these corrects live behavior to the contract this documentation already declares. No request shape changes and no new API version; if your client compensated for any of the old behaviors below, remove the workaround. **The `paid` filter on `GET /api/v1/order` now means what it says.** Previously `paid=true` returned exactly the *unpaid* orders, and `paid=false` applied no filter at all. Now the parameter is a true tri-state: omit it to get orders regardless of payment state, `paid=true` for paid orders only, `paid=false` for unpaid orders only. *Migration note:* if you passed `paid=true` to get unpaid orders, switch to `paid=false`. **Auth failure on the address-book operations returns `401`.** `POST /api/v1/address-book` and `POST /api/v1/zapierActions/address-book` returned `404` when the `X-API-KEY` header was missing or invalid, while their contract declared `401` ("Authorization key not valid"). They now return the declared `401`, and `404` is no longer a declared response on these operations. *Migration note:* if your integration matched on `404` to detect an auth failure on these paths, match on `401` instead. **Recipient rejections on `POST /api/v1/integration-orders/{id}/recipients` carry a machine-readable reason code.** The `422` response previously carried only a human-readable string. It now also returns a `payload` with a stable `reasonCode` — one of `RETURN_ADDRESS_PARTIAL`, `RETURN_ADDRESS_MISSING_FIELDS`, `RETURN_NAME_PARTIAL`, `ORDER_NOT_AVAILABLE`, `ADDRESS_NOT_VALID`, `DUPLICATE_RECIPIENT`, `DO_NOT_MAIL`, `MISSING_MAIL_MERGE_FIELDS`, `ORDER_TEMPLATE_MISSING` — plus the processed recipient details (name, the standardized address, and whether it validated). The `errors` array and its existing messages are unchanged, so current parsers keep working. Two edge behaviors are also corrected: an order with no configured template now rejects with `ORDER_TEMPLATE_MISSING` instead of a generic message, and a genuinely unexpected server failure now returns `500` instead of masquerading as a `422`. *Migration note:* treat a `500` from this operation as retryable server error, not as input rejection. **Entitlement failures return the declared bodiless `403` — everywhere.** When a valid API key's subscription lacks the feature an operation requires, every `/api/v1/*` operation previously surfaced an empty `500` (documented here as a known issue). All of them now return the declared `403` with no response body, and the known-issue notes are gone from the reference. *Migration note:* a bodiless `500` no longer signals an entitlement failure — check for `403`. ## 2026-07-26 — Appending recipients is faster and no longer re-processes earlier batches [#2026-07-26--appending-recipients-is-faster-and-no-longer-re-processes-earlier-batches] **Non-breaking.** `PUT /api/v1/order/{id}/recipients:append` (and its published alias `PUT /api/v1/order/{id}/recipients`) now scopes its duplicate and do-not-mail processing to the recipients submitted in each call, so append time tracks the size of your batch instead of the size of the whole order — large orders built batch-by-batch no longer slow down with every call. One observable behaviour change rides along: recipients appended by **earlier** calls keep the duplicate/do-not-mail flags computed when they were appended — a later append no longer refreshes them against your current suppression lists (each new batch is always evaluated against the lists as they stand at that call). Request shape, response shape, and response-field semantics are unchanged — including the cumulative whole-order counts (`deliverable.addressValidated`, `undeliverable.doNotMail`, `undeliverable.duplicated`). No action is required. ## 2026-07-26 — Documentation and declared contract now match the running API [#2026-07-26--documentation-and-declared-contract-now-match-the-running-api] **Non-breaking.** A full audit of the published contract found places where the specification disagreed with the service. Nothing about the API's behaviour changed — what changed is that the documentation now tells the truth. No action is required, but **if you generate a client from our OpenAPI document, regenerate it** to pick up the corrections below. **Two published paths are now callable.** `PUT /api/v1/order/{id}/recipients` and `POST /api/v1/integration-orders/{id}-by-proximity` appeared in the reference but matched no route on the server, so calling them exactly as documented failed. Both now work. The older paths they were published alongside continue to work unchanged, so nothing you have built needs to move. **A vendor-neutral address-book path.** `POST /api/v1/address-book` is the canonical path for adding an address-book entry. The previous `POST /api/v1/zapierActions/address-book` behaves identically and is retained — use either. **Four response bodies were declared incorrectly.** Order analytics (`GET /api/v1/order/analytics`), order transactions (`GET /api/v1/order/transaction`) and both checkout operations declared a response envelope different from the one they actually return. A client generated from the old document deserialized the wrong shape on all four. The declarations now match what has always been sent — **regenerate your client** if you call any of these. **The proof endpoints return a PDF, not JSON.** `GET /api/v1/order/{id}/proof` and `GET /api/v1/order/{id}/proof/{recipient}` declared `application/json` while returning raw PDF bytes. They now declare `application/pdf` on success. Their *error* responses are still JSON, so branch on the status code before choosing how to read the body. **Optional fields are no longer marked required.** Every request field used to be published as required, including ones whose own description says to omit them — `useQr`, `qrUrl` and `handwritingFont` on order creation, and `holdUntilDate` and `autoBill` on checkout. Sending `null` for these to satisfy the old schema is no longer necessary; omit them to get the template's defaults. **Fields with a fixed set of values now publish that set.** Order status, delivery status, postage class, production speed, transaction type and sub-type, automation status, conversion type, conversion status and source, address DPV code, and font source all list their accepted or returned values in the reference, in the exact spelling the API uses. Two things worth noting: order status uses spaced display text, so send `"Ready For Production"`, not `"ReadyForProduction"`; and `product` is deliberately **not** a fixed list — it changes as products are added and retired, so treat it as an opaque string. **Some response fields are now declared nullable.** Order status and postage class on the order-list and statistics responses, and status and postage on the automations response, are declared nullable because the service has always been able to return `null` there — the schema previously denied it. Nothing changes on the wire. A regenerated client will widen those fields (in TypeScript, to `string | null`), which may require a null check you did not previously have. **Entitlement failures return no body.** A `403` on any endpoint gated by your subscription tier has no response body — do not attempt to parse one. Note a known issue: this condition currently surfaces as a `500` with an empty body rather than a `403`. **Treat a bodiless `500` on these endpoints as a permissions failure and do not retry it**; a retry will fail identically. A fix is in progress. **Field-level documentation and examples.** Every published field now carries a description, and the core integration flow carries worked examples. Several previously undocumented rules are now stated, including the 5,000-record limit on conversion uploads (a larger batch is rejected in full and nothing is stored), the 7-day expiry on uploaded-font URLs, and that handwriting accepts LettrLabs global fonts only. Four new pages cover what per-endpoint reference cannot: [response shapes and errors](/api-contract), [pagination](/pagination), [rate limits and batch sizes](/rate-limits), [versioning](/versioning) and [addresses and deliverability](/addresses). ## 2026-07-24 — Checkout preview is now side-effect-free, and checkout is faster [#2026-07-24--checkout-preview-is-now-side-effect-free-and-checkout-is-faster] **Non-breaking.** `GET /api/v1/order/{id}/checkout` (checkout preview) no longer has side effects: previewing an order does **not** modify the order and does **not** create a payment intent. Previously, each preview call persisted the requested postage type, production speed, and hold-until date onto the order and created or updated a payment-intent record. The request parameters, response shape, and pricing values are unchanged — no change is required in your integration. Preview as often as you like; only `POST /api/v1/order/{id}/checkout` commits anything. **Non-breaking.** Checkout latency is reduced on both the preview (`GET /api/v1/order/{id}/checkout`) and the execute (`POST /api/v1/order/{id}/checkout`) endpoints — the same requests now return faster. No request or response changes.