LettrLabs API

API Changelog

What changed in the LettrLabs public API — newest first, each entry flagged breaking or non-breaking, with migration notes where action is needed.

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 — 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

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

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

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

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

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

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

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, pagination, rate limits and batch sizes, versioning and addresses and deliverability.

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.

On this page