v5.40.0 4 June 2026 Improvement Breaking

Pre-launch decision pass: honest shapes, wider ids, closed questions

Pre-launch is the one moment breaking changes cost nothing, so this release spends that moment deliberately. Six questions that would have become expensive after launch were decided and shipped in one pass.

Breaking: null means unset

Response fields for optional text (reference, notes, terms, contact address parts) and an unissued invoice’s sent-timestamp now return null when unset, instead of being coerced to empty strings. An empty string now means the field was deliberately set to empty; null means it was never set. Consumers can finally tell the difference, and the documentation got simpler by deleting an apology.

Breaking: the payment response tells the truth twice

Recording a payment now returns two figures: the amount recorded by that request, and the cumulative total paid against the invoice — both in pence, under names that say which is which. Previously a single field carried this request’s amount under the cumulative figure’s name. The cumulative value was already computed atomically on every payment; it is now surfaced rather than discarded.

Every VAT rate is an answer

VAT rate is now required at the database layer on every invoice line (Class M, migration 094). A missing rate is an unanswered question and MTD does not accept unanswered questions; zero is an answer (zero-rated). The API has always required the rate on the wire; this closes the historical possibility for good.

Room for nine quintillion

Internal record identifiers widened from 32-bit to 64-bit across the entire schema (migration 095) — every table, every reference, one migration, while the change is a boring rewrite of test data rather than future downtime under load. API identifiers are unaffected; nothing changes on the wire.

Money never floats

The wire-encoding rule for money is now mechanically enforced in both directions (registry v2.13): no request or response schema may declare a money-valued field as a floating-point number. Money crosses this API as integer pence or exact decimal strings, never as floats — now the build fails if anyone forgets, including us.

Fixed: manual journal amounts

The manual transaction endpoint was converting wire pence before storage, the write-side mirror of the report-scale defect fixed in 5.39.0 — each had historically masked the other. Non-round-pound amounts were rejected outright; the conversion is removed and integer pence now passes through unchanged. No stored data was affected (the defect rejected rather than corrupted).

Pre-launch, test data only — no customer figures were ever affected.