If you sync Xero — a bookkeeping integration, a payments reconciliation tool, an e-commerce-to-ledger bridge, or an in-house script that's been matching customer prepayments to invoices since forever — there's a one-line changelog entry with your name on it, and it doesn't throw an error.
For as long as most integrators can remember, the Reference field on Xero's Bank Transaction, Prepayment, and Payment endpoints has returned the invoice number for prepayment transactions. Not the reference you typed on the transaction — the invoice number Xero generated. That was never quite what the field name promised, but it was stable, and a lot of code came to depend on it: "to find the invoice this prepayment belongs to, read Reference." On July 13, 2026, that stops being true.
What actually changes
Xero is making the prepayment endpoints internally consistent with the rest of the API and with the web app. The rollout has two dates:
| Date | Change |
|---|---|
| April 13, 2026 | A new explicit InvoiceNumber field becomes available on these responses. It is the new source of truth for the RECEIVE-PREPAYMENT invoice number. Reference is unchanged — still returns the invoice number. |
| July 13, 2026 |
Reference stops returning the invoice number for RECEIVE-PREPAYMENTs and switches to returning the transaction's actual reference data — the free-text reference, which is usually different and often empty. |
So there's a three-month window where both behaviours coexist: the new field is present, the old field hasn't flipped yet, and everything keeps working. That window is exactly what makes this dangerous. Nothing breaks in April. Nothing breaks in May or June. Code reviews pass. Then on July 13 the meaning of a field changes underneath running integrations that were "tested and working" a quarter earlier.
Why this is a silent failure, not a loud one
Every property of the response that a client checks stays the same:
-
HTTP status: still
200 OK. -
JSON shape:
Referenceis still there, still a string. No field added or removed from the perspective of old code. -
Type: still a string. No deserialization error, no
nullwhere an object was expected. -
The value: silently changes from
"INV-0042"to whatever someone typed in the reference box —"Deposit","Stripe payout", or"".
A schema validator that checks "is Reference a present string?" passes on both sides of July 13. The only thing that changed is the semantics of the field — and semantics don't show up in a status code.
The three ways this bites
1. Invoice matching silently misses
The canonical prepayment-reconciliation loop reads Reference to find the invoice:
// Match a RECEIVE-PREPAYMENT bank transaction back to its invoice
const invoiceNo = txn.Reference; // "INV-0042" … until July 13
const invoice = await findInvoiceByNumber(invoiceNo);
if (invoice) applyPrepayment(invoice, txn.Total);
Before July 13, txn.Reference is "INV-0042" and the lookup hits. After July 13, txn.Reference is the free-text reference — often empty, sometimes a memo like "June deposit". findInvoiceByNumber("") returns nothing, applyPrepayment never runs, and the prepayment sits unreconciled. No exception, no log line, no failed request. Your books just quietly stop matching prepayments to invoices, and someone notices weeks later when the aged-receivables report doesn't foot.
2. The worse case: it matches the wrong invoice
Missing matches are the lucky outcome. The unlucky one is a collision. If a user happened to type something into the reference box that looks like another invoice number — or your findInvoiceByNumber does fuzzy/partial matching — the post-July-13 Reference value can resolve to a different, real invoice. Now a prepayment is applied against the wrong customer's invoice, two ledgers are wrong, and the only evidence is a number that matched something it shouldn't have. Silent-wrong is always more expensive than silent-missing, because you trust the result.
3. Filtering by reference changes what comes back
The same change makes filtering "optimised" to operate on real reference data. Code that filters or searches bank transactions and prepayments by Reference expecting to query on invoice numbers will, after July 13, be filtering on a different field's worth of data. Queries that returned the prepayment for INV-0042 yesterday return nothing today — or return a different set — and a paginated sync that keys off those results silently drops rows it used to pick up.
The fix is small — if you make it before July 13
Xero gave you the escape hatch in April: the explicit InvoiceNumber field. The migration is a one-line read change, and the safe window to do it is now, while Reference still agrees with InvoiceNumber so you can verify the swap against live data before the flip:
// Source of truth for the invoice number is InvoiceNumber, not Reference
const invoiceNo = txn.InvoiceNumber ?? txn.Reference; // belt-and-suspenders during the window
const invoice = await findInvoiceByNumber(invoiceNo);
What to audit in every Xero integration you own:
-
Grep for
Referenceon prepayment/payment paths. Anything that reads.Referenceoff a Bank Transaction, Prepayment, or Payment response and treats it as an invoice number is on the clock. Searchgrep -rn "\.Reference" .and triage every hit on these endpoints. -
Switch invoice-number reads to
InvoiceNumber. It's available now (since April 13). Make the swap, deploy, and confirm it matches the currentReferencevalue before July 13 — that's your free regression test. -
Re-check anything that filters by reference. If you query BankTransactions or Prepayments using
Referenceto pull by invoice number, move that filter to the new field and re-validate the result set. -
Don't assume
Referenceis now useless — assume it's now correct. After July 13 it carries the real user reference. If you have a separate need for that (display, search by user memo), this is an upgrade. The danger is only where the old meaning was load-bearing.
The cutoff date here is loud in the changelog and silent in production. April's new field is the gift; July 13 is the cliff. The whole point of the three-month overlap is to let you migrate while you can still see both values side by side — which only helps if you know the flip is coming.
FlareCanary monitors API responses for schema drift, silent field-semantics changes, and quiet removals across upstream providers. If you run integrations against Xero, Stripe, Shopify, PayPal, or anywhere else that ships breaking changes through a changelog post and a 200 OK, flarecanary.com catches the drift before your reconciliation does.
Top comments (0)