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, normallyPayment Needed(accepted, settling). This is distinct from the existingstatus, which reports the outcome of the call and still readsSuccess.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 toDraftif 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.