Your converted bank statement adds up. That does not make it correct.
Thomas Gak-Deluen8 min read
reconciliationaccuracybank statements
Reconciliation is the strongest check anyone can run on a converted bank statement, and it is narrower than it sounds. It proves the amounts. It proves them completely, because it is arithmetic the bank itself printed, and it proves nothing else at all: not the dates, not the order, not the text beside the figures, not the currency they are labelled with.
We have shipped three bugs in one week that every one of those checks passed.
What reconciliation actually checks
A statement carries its own proof, in one of two forms.
The first is the running balance. Every row states what the account held after it, so every row can be checked against the row above:
amount balance
30 Sep Card Payment to Tesco -25.63 455.13
30 Sep Card Payment to Eazy-Print -120.00 335.13
1 Oct Card Payment to Rootes -6.89 328.24
455.13 - 120.00 = 335.13, and 335.13 - 6.89 = 328.24. If that chain holds
from the opening balance to the closing balance, a single misread digit anywhere
in the statement would have broken it. One wrong amount breaks one link. One
wrong balance breaks two, in equal and opposite directions, because the row
after it is measured from a wrong starting point.
The second form is the totals a statement declares about itself: an opening balance, a sum of debits, a sum of credits, a closing balance. Those four numbers have to agree, and if a row was dropped on the way out of the PDF they will not.
Either check is a real proof. A converter that says "970 transactions extracted" is reporting how much text it found. A converter that says "970 transactions, and the balance chain closes on the statement's own figure" is reporting something it cannot be wrong about.
That is exactly why the failures are interesting.
Three bugs that reconciled perfectly
A statement period that ran backwards
Most banks print transactions oldest first. Monzo prints newest first.
We were taking the statement period from the row order: first row's date to last
row's date. On a seventeen-month Monzo export that produced a period running
from July 2026 back to February 2025. Backwards. It was stored backwards, shown
that way in the app, and served that way by the API as period_start and
period_end, which is the field an integration would use to decide which month
a statement belongs to.
Every figure on that statement reconciled. Of course it did: a balance chain is identical in either direction. The arithmetic proves the amounts and says nothing whatsoever about the order they were printed in.
Nothing automated caught it. It took a person opening the list in a browser and reading a date range that ended before it began.
The period is now the minimum and maximum of the booked dates, which is what a period is, and is independent of how the bank chose to sort the page. Re-extracting the real statement gives 2025-02-12 to 2026-07-03, still reconciling, still 46 rows.
A transaction that swallowed the page footer
This one was reported from the running app. The final row of a Fortuneo statement came back as its real description followed by the deposit guarantee notice, the deferred card total, the overdraft terms, the TAEG explanation, the company registration and the postal address of the banking ombudsman.
2,149 characters, on a statement whose median description is 36.
The cause is mundane. Description text that cannot yet be attributed to a row is buffered, because some banks print a description above its amount and some below it. At the end of the document no further transaction ever arrives to claim the buffer, and the old behaviour handed whatever was left to the last row. What is left, at the end of a bank statement, is the entire legal footer.
Measuring the other fixtures showed it was never a Fortuneo problem. The last row's description was the longest on five of the six statements we test against. Barclays was the worst: 3,509 characters against a median of 44. Every one of those statements reconciled, because none of this touches an amount.
| Statement | Longest description before | After |
|---|---|---|
| Barclays | 3,509 | 53 |
| Monzo | 2,200 | 121 |
| Fortuneo | 2,149 | 140 |
| Revolut | 654 | 90 |
| Qonto | 315 | 97 |
Row counts unchanged, all six still reconciling. The reported row now reads
CARTE 19/12 BRASSERIE CHARLI NEUILLY SUR S, which is what the bank printed.
891 rows labelled with the wrong currency
A Revolut Business statement can hold three currency accounts in one PDF, and the reconciler already knows that. It splits the rows by currency, proves each group against its own running balance, and deliberately refuses to report a single combined closing balance, because adding euros to dollars produces a number that is not money.
Underneath that, every transaction was being persisted with the statement's currency rather than its own. On a 970-row statement, 891 rows were stored, displayed and exported as EUR: 475 of them were dollars and 416 were pounds. Only 79 were actually euros.
Reconciliation cannot catch this and never could. It groups on the in-memory currency of each row, which was right all along, and never sees what is written to the database. A customer reconciling that CSV would have been adding dollars to euros and getting a total that means nothing, on a file that arrived marked as verified.
The same shape, three times
Reconciliation is a proof about one claim: that the amounts are the amounts the bank printed. Everything else a statement carries sits outside it.
The order the rows were printed in is outside it. The prose beside them is outside it. The currency label on them is outside it. So is the account number, the account holder's name, and which of two accounts a row belongs to. A check that is silent on all of that is not a weak check. It is a precise one, and the mistake is hearing it as a verdict on the file rather than on the figures.
The practical consequence for anyone converting statements is that a green tick is worth exactly what it claims and no more.
What to check yourself, on any converter
None of this needs our tool, or any tool. Six things, on the file you just downloaded, in about two minutes:
- The closing balance. Take the opening balance printed on the statement, add every credit, subtract every debit, and compare with the closing balance printed on the statement. If a converter did not do this for you, do it in the spreadsheet. It is one formula and it is the whole ballgame.
- The row count. Many statements print one. If yours does not, count the transaction lines on the last page and check the tail of the file.
- The date range. Does it start before it ends? Does it match the period printed on page one? A reversed period is invisible in a spreadsheet until you sort by date and the file fights you.
- The longest description. Add a column with
=LEN(C2), sort descending, look at the top three. A description ten times longer than the median is not a description, it is a page footer that got attached to a real transaction. - The currencies. On a multi-currency statement, check that a dollar row says USD. If every row carries the statement's headline currency, the totals below them are meaningless.
- Duplicates. Same date, same amount, same description, twice. Sometimes that is a repeated header or a page boundary read twice. Often it is somebody buying the same coffee twice on the same day, which is why this one needs a human and not a rule.
Numbers one and two are the checks a converter can do for you. Three through six are the ones it can only make visible.
Where we landed
Each of the three bugs now has a regression test pinned to the real statement that produced it, because each was a general rule that was wrong rather than a bank that needed special treatment.
The period is the minimum and maximum of the booked dates. Description text is bounded per line against the upper quartile of that statement's own description lengths, so a bank of terse card references and a bank of full SEPA mandates each get their own threshold and neither needs a rule written for it. Every transaction carries its own currency, and a statement holding several accounts reports a closing balance for each rather than one number that means nothing.
And where a statement carries neither a running balance nor declared totals, extraction comes back marked unverified, never correct. That is the same principle from the other direction: the absence of a check is not a pass.
Reading a figure has never been evidence that it is right. Proving it is evidence about the figure, and about nothing standing next to it.