Changelog¶
Reverse-chronological. Entries are grouped Added / Changed /
Deprecated / Removed / Fixed. A Deprecated entry always
carries a sunset: date and names the deprecated route or field — see
Versioning.
Subscribe via Atom feed — one entry per dated section below.
2026-09-26¶
Fixed¶
- A Migration API CONVERSION journal line's
fx_amount— the foreign-currency face of a foreign bank account's opening balance, sent alongsidefx_currency— is now stored exactly. It was previously read as aMoneyobject instead of the documented bare decimal string, so a foreign-currency opening balance either failed to load or landed with the wrong minor-unit amount. A malformed, currency-mismatched or missingfx_amounton that journal now fails the whole batch withconversion_fx_invalidinstead of an opaqueinternal_error. - Every OTHER journal line's
fx_amount(a manual or bank-transaction journal) is now stored too — it was previously accepted but silently discarded, so it had no effect. Stricter validation comes with it: a Money-object, over-scale, bank-currency-mismatched, or (outside a CONVERSION bank line) negativefx_amountnow fails that ONE journal with its own code (invalid_fx_amount,fx_amount_missing,fx_currency_missing,fx_currency_bank_mismatch,amount_over_scale,invalid_amount) — every other journal in the same batch still posts. A journal line's fx face is kept only on a foreign bank leg or a non-monetary leg (an expense or income account). A non-bank monetary leg (a director's loan, VAT control, a receivable/payable, ...) never carries one from a journal line — those accounts' foreign balances are still correctly held and revalued when they come from a document or payment (an invoice, bill, or their payment), exactly as before; this only closes a gap where an arbitrary journal LINE could otherwise stamp a second, spurious foreign balance onto one. Dropping a face that landed on a receivable/payable account specifically now also emits afx_face_dropped_on_controlwarning, in case it was meant literally. - EUR/foreign-currency prepayment and overpayment bank legs settled
through a foreign bank account now carry the same exact
fx_amounttoo. - The Conversion Balances screen no longer replaces an exact foreign
amount with an ECB-derived estimate when the user re-saves the grid for
an unrelated reason. A foreign-currency row whose typed base value AND
typed FX rate are both unchanged now carries forward its live posting's
fx_currency,fx_amountandfx_rateverbatim — including a migrated row whose foreign and base balances have opposite signs, or whose foreign face is genuinely zero, neither of which has anfx_rateto type in the first place (a blank rate box always counts as unchanged there). A row whose base value the user actually edited, whose typed rate no longer matches the stored one, or a brand-new row, is still derived from the typed FX rate as before. The page itself never auto-fills an ECB rate onto a row this applies to, so a re-save can't silently turn "no rate needed" back into a re-derivation. - A migration re-attempt after a successful rollback is no longer
refused, including across repeated rollback/retry cycles. Rolling
back a commit batch voids (never deletes) its non-carry invoices and
bills, so a same-organisation retry (
POST .../migration/batches, accepted with202and queued as usual) used to fail asynchronously withorganisation_not_emptyin itserrors[]forever, even with a cleanrolled_backbatch behind it — the operator had to passallow_non_empty: trueto get past LuciBook's own leftovers. The organisation emptiness check (andGET .../migration/profile'sis_empty) now ignores a record only when the batch that reversed, voided or deleted it reachedrolled_backand nothing has edited that record since through a normal model save; any other document — one from a batch that is stilldone/running/failed/rollback_failed, or a genuine user-entered document — still refuses the load exactly as before. Two narrow write paths that update a voided invoice/bill through a queryset.update()(a dunning-pause toggle, a supplier merge) are not yet detectable this way and are not covered by this guarantee. This now holds across more than one rollback in a row (roll back, retry, roll that back too, retry again): the check no longer relies solely on each record's current id-map entry, which only ever remembers the most recent attempt. Two re-attempt behaviours to be aware of: - A re-attempt after a rollback re-creates every contact the rolled-back attempt itself created (rather than reusing it; the original stays archived) — this applies whether or not that contact has a VAT number, because an archived contact is never matched again by anything, VAT number included. Only a contact the organisation already had before any migration touched it, matched by VAT number, is reused.
- A fixed-asset register row rollback cannot undo (§B8) is reused,
never duplicated, on a later attempt — if that later attempt's own
package describes the asset differently (cost, accumulated
depreciation, useful life, depreciation method, dates, or the
asset/accumulated-depreciation/depreciation-expense accounts), the
batch now carries a
fixed_asset_register_divergedwarning instead of silently keeping the old figures or silently overwriting them; the register row itself is left as it was.
2026-09-25¶
Added¶
- Migration attachments (phase 3). The Migration API
gained
POST /api/v1/orgs/{org_id}/migration/batches/{batch_id}/attachmentsand itsGETcounterpart: amultipart/form-dataupload that attaches a source file to a migrated invoice, bill, credit note, supplier credit, journal entry (including migrated bank transactions) or contact, once the batch has finished (status: done).migration:write/migration:readscope,Idempotency-Keymust equal{batch_id}:attachments:{sha256}, and the endpoint is idempotent by construction — no stored reply cache. See Migration API: Attachments for the full contract, and rate limits for the per-file, per-batch and per-upload limits. New error codes:attachment_parent_ required,attachment_parent_unknown,attachment_parent_unsupported,attachment_size_mismatch,attachment_limit_exceeded,invalid_attachment_metadata,batch_not_attachable— see the error catalogue. Rolling back a batch now also unlinks every attachment it created and schedules its now-unreferenced files for deletion. - The
attachmentsentry ofGET .../migration/capabilitiesmoved fromNONEtoPARTIAL— see the guide above for exactly what is (and is not) covered.
2026-09-24¶
Added¶
- The Migration API gained
POST /api/v1/orgs/{org_id}/migration/batches/{batch_id}/finalise: an idempotent read confirming that a commit batch's lock date and VAT lock plan were applied at load time.migration:writescope, noIdempotency-Key. New error codebatch_not_finalisable(409) — see the error catalogue. The batch detail body (GET .../batches/{batch_id}and the new finalise response) now also carriesvat_lock_plan. - The migration batch lifecycle gained three new
errors[]codes aGET .../batches/{batch_id}or the finalise response can now carry:invalid_vat_return(a malformedvat_returnsentry, caught before any posting starts),finalise_failed(the finalise step itself failed after a successful load — never a half-applied lock date or VAT lock), andstale_batch(a batch a background sweep closed out because its worker job was no longer live). A batch can also carry avat_lock_plan_nothing_lockablewarning when the package declared filed VAT returns but none of them ended up lockable. See the Migration API guide and the error catalogue. - Rate limits now opens with Limits at a
glance, generated from code: max request body size and its actual
failure mode, per-resource list page sizes (bills/invoices/credit
notes/contacts, webhook deliveries, migration reference data and
id-map), OAuth token lifetimes (access, refresh rotation and inactivity
window, and authorization code),
Idempotency-Keyretention and max length per endpoint family, attachment limits, and migration batch/webhook ceilings. - Chunked migration batches (phase 2). A large migration package can
now be split across several
POST /api/v1/orgs/{org_id}/migration/batchesrequests (chunk_no/chunk_count > 1) instead of one — replacing the previous501 chunking_not_supportedresponse. See Migration API: Chunked batches for the full contract, and rate limits for the max-chunks and max-total-package-bytes ceilings. New error codes:chunk_out_of_order,chunk_mismatch,batch_not_receiving,chunk_header_mismatch,chunk_conflict,duplicate_record_across_chunks,unknown_package_list_keyandpackage_too_large— see the error catalogue.GET .../batches/{batch_id}gainedchunk_countandchunks[];GET .../batches/{batch_id}/id-mapgainedsince_chunkandentityquery parameters.
Changed¶
- Authenticated partner API requests against a demo company now count as activity for the 90-day inactivity purge, the same as actions recorded in the company's own activity log. See Testing with a demo company.
- The payroll pay-run receiver (
POST /api/v1/orgs/{org_id}/pay-runs) now compares the request body on anIdempotency-Keyreplay, matching the bills/invoices/credit-notes contract: the same key with the same body returns the original200replay, and the same key with a different body now returns409 idempotency_conflictinstead of silently echoing the original pay run. See Idempotency. - The consent screen's organisation picker now marks any organisation your app already has a live connection to for the signed-in user as already connected — still selectable (picking it again updates its granted scopes) — and defaults the selection to the first not-yet-connected organisation. See Connecting more organisations.
- Security:
client_credentials-style tokens (no user on the token) can no longer reach an ordinary authorization-code connection to any organisation. LuciMigration has moved to the normal authorization-code - PKCE flow (like every other partner app), and custom connections —
the only shape a
client_credentialstoken is meant to reach — are not yet available (see Custom connections), so this closes an unused path rather than changing behaviour for any shipped integration. - The migration API's
POST /api/v1/orgs/{org_id}/migration/batchesnow rejects any TOP-LEVEL request-body field outside the documented set (batch_id,package_sha256,mode,package,attempt,allow_non_empty,set_lock_date,bypass_lock_actor,chunk_no,chunk_count) with400 unknown_field, naming the offending key(s) (sorted, capped at 10, each truncated to 64 characters, with a(+N more)tail past that) instead of silently ignoring them — enforcing theadditionalProperties: falsethe published schema already declared. Thepackagepayload's own shape is unaffected. See Migration API. POST /api/v1/orgs/{org_id}/migration/batchesfor an unchunked batch_id replay (chunk_countunset or1) no longer takes the per-organisation advisory lock or re-checks the concurrency cap before returning the existing batch's summary — both are now skipped wheneverbatch_idalready names a stored batch, chunked or not, since neither applies to a request that creates no new row. The response body and status code for a replay are unchanged; only the extra lock/cap work a replay never needed is gone.
Fixed¶
- An oversized
/api/v1/request body now returns413 request_too_largein the standard partner error envelope. It previously returned Django's generic HTML 400 page:apps.core.idempotency.IdempotencyMiddlewarereads the request body ahead of routing, before any view's ownrequest_too_largehandling could run.
2026-09-23¶
Changed¶
- Token revocation (
/oauth/revoke_token/) is now scoped to the owning client — a client can no longer revoke another app's tokens — and revoking a refresh token now cascades to revoke every active connection for that(application, user)across the user's orgs. See Revoke. - PKCE
S256is now enforced end to end: a request that omitscode_challenge_methodor suppliesplainis refused at both the authorization and token legs, closing a gap where an omitted method defaulted toplain. - The per-scope role tier (
payroll:*/migration:*needOWNER/ADMIN; document writes needOWNER/ADMIN/EDITOR; reads need any member) is now re-checked live on every request, not only at consent — a grantor's demotion now takes effect on their connection's very next request. Seeinsufficient_role. - HTTP security headers on developer.lucibook.co.uk were hardened: HSTS,
a restrictive
Permissions-Policy, same-originCOOP/CORP, and a fix so that visiting a page without a trailing slash no longer redirects to an internal port number.
Fixed¶
- The webhook signature verification examples on
Signatures incorrectly documented the
v1value as hex-encoded; it has always been base64-encoded. The reference snippets and prose are corrected, and now also document that the secret-rotation grace window can send multiplev1=entries in oneX-LuciBook-Signatureheader — a verifier must accept if any entry matches, not just the last one.
2026-09-22¶
Added¶
- Connect with LuciBook: OAuth 2.0 Authorization Code + PKCE, consent per organisation, accumulating connections.
- The flat error envelope
{code, message, details, request_id}across every/api/v1/endpoint — see Errors. - Rate limiting with
X-RateLimit-*/X-Rate-Limit-Problemheaders. - The developer portal — register apps, manage secrets, view connections and activity.
- Webhooks: event subscriptions, HMAC-signed
delivery (
X-LuciBook-Signature), retry with backoff, and a delivery log with replay. - This documentation site.
Custom connections (client-credentials) are not yet available — that page describes the intended design, not a shipped feature, and will get its own Added entry once it's live.