Is this the complete bank statement population, or just the rows that happened to parse?
Every bank statement parser takes files in and gives rows out. No parser answers whether a statement is missing. The first panel below answers it by hand. The second panel is the same three files in banktrail, which answers it with a proof.
| Date | Description | Reference | Amount | Balance | Account | |
|---|---|---|---|---|---|---|
| 1 | 2025-01-03 | INVOICE SETTLEMENT ACME LOGISTICS | INV-2201 | 188,400.00 | 670,590.55 | 0132-4477810-03 |
| 2 | 2025-01-07 | RENT UNIT 4 HARBOUR ESTATE rent | SO-1180 | -96,250.00 | 574,340.55 | 0132-4477810-03 |
| 3 | 2025-01-09 | SUPPLIER PAYMENT KHALID BROS | CHQ 100442 | -41,875.20 | 532,465.35 | 0132-4477810-03 |
| 4 | 2025-01-14 | SALARY BATCH JANUARY payroll | PR-2025-01 | -238,914.00 | 293,551.35 | 0132-4477810-03 |
| 5 | 2025-01-14 | SALARY BATCH JANUARY payroll | PR-2025-01 | -238,914.00 | 54,637.35 | 0132-4477810-03 |
| 6 | 2025-01-21 | INVOICE SETTLEMENT ACME LOGISTICS | INV-2214 | 215,600.00 | 270,237.35 | 0132-4477810-03 |
| 7 | 2025-01-28 | UTILITIES K-ELECTRIC | DD-0093 | -18,442.10 | 251,795.25 | 0132-4477810-03 |
| 8 | 2025-01-31 | BANK CHARGES | -1,250.00 | 250,545.25 | 0132-4477810-03 | |
| 9 | 2025-02-03 | INVOICE SETTLEMENT NORTHWIND RETAIL | INV-2219 | 142,300.00 | 392,845.25 | 0132-4477810-03 |
| 10 | 2025-02-06 | RENT UNIT 4 HARBOUR ESTATE rent | SO-1180 | -96,250.00 | 296,595.25 | 0132-4477810-03 |
| Chain break · account 0132447781003 · 2025-02-06 → 2025-02-14 · expected 55,295.25, found 119,795.25 · 64,500.00 unaccounted for · 1 unread row(s) in northwind-2025-02.csv may hold this | ||||||
| 11 | 2025-02-14 | SALARY BATCH FEBRUARY payroll | PR-2025-02 | -241,300.00 | 119,795.25 | 0132-4477810-03 |
| 12 | 2025-02-19 | INVOICE SETTLEMENT ACME LOGISTICS | INV-2223 | 198,750.00 | 318,545.25 | 0132-4477810-03 |
| 13 | 2025-02-25 | SUPPLIER PAYMENT KHALID BROS | CHQ 100467 | -52,300.00 | 266,245.25 | 0132-4477810-03 |
| 14 | 2025-02-28 | BANK CHARGES | -1,250.00 | 264,995.25 | 0132-4477810-03 | |
| Chain break · account 0132447781003 · 2025-02-28 → 2025-04-04 · expected 441,895.25, found 554,675.75 · 112,780.50 unaccounted for · no statement covers this window | ||||||
| 15 | 2025-04-04 | INVOICE SETTLEMENT NORTHWIND RETAIL | INV-2241 | 176,900.00 | 554,675.75 | 0132-4477810-03 |
| 16 | 2025-04-08 | RENT UNIT 4 HARBOUR ESTATE rent | SO-1180 | -96,250.00 | 458,425.75 | 0132-4477810-03 |
| 17 | 2025-04-14 | SALARY BATCH APRIL payroll | PR-2025-04 | -243,050.00 | 215,375.75 | 0132-4477810-03 |
| 18 | 2025-04-17 | SUPPLIER PAYMENT KHALID BROS | CHQ 100501 | -38,120.00 | 177,255.75 | 0132-4477810-03 |
| 19 | 2025-04-22 | INVOICE SETTLEMENT ACME LOGISTICS | INV-2247 | 205,300.00 | 382,555.75 | 0132-4477810-03 |
| 20 | 2025-04-29 | UTILITIES K-ELECTRIC | DD-0101 | -19,105.40 | 363,450.35 | 0132-4477810-03 |
| 21 | 2025-04-30 | BANK CHARGES | -1,250.00 | 362,200.35 | 0132-4477810-03 | |
| Account | Currency | Txns | From | To | Money in | Money out | Balance chain |
|---|---|---|---|---|---|---|---|
| 0132-4477810-03 | — | 21 | 2025-01-03 | 2025-04-30 | 1,127,250.00 | 1,424,520.70 | 2 break(s) |
An account with statements that print no running balance is reported as unverifiable. It is never reported as intact, because no continuity check is possible.
| Document | Read as | Rows | Unread | Arithmetic | SHA-256 |
|---|---|---|---|---|---|
| northwind-2025-01.csv | csv / generic.table | 8 | 0 | foots | fafb74f84594a7a9… |
| northwind-2025-02.csv | csv / generic.table | 6 | 1 | does-not-foot: 1 unread row(s) recorded | df56c5a50302573e… |
| northwind-2025-04.csv | csv / generic.table | 7 | 0 | foots | a83c01c2eeb7d355… |
Each transaction records its source document and position in it. The hash comes from the uploaded file. The service uses that hash to recognise the same statement on a second import.
| Result | Check | Detail |
|---|---|---|
| [ ok ] | uid integrity | 0 row(s) whose content no longer matches their uid |
| [ ok ] | provenance present | 0 transaction(s) with no source file recorded |
| [ ok ] | amount mirror | 0 row(s) where amount_num has drifted from amount |
| [ ok ] | search index | index holds 21 rows for 21 transactions |
| [ ok ] | annotations indexed | all 7 sampled tags are searchable |
| [ ok ] | tag references | 0 orphaned tag row(s) |
| [ ok ] | link references | 0 orphaned link row(s) |
| [ ok ] | attachment_target references | 0 orphaned attachment_target row(s) |
| [ note ] | attached images | 0 image(s) present in … |
| [ ok ] | currency consistency | 0 account(s) holding more than one currency |
| [ note ] | duplicate review | 1 group(s) share account… — kept deliberately, review with `duplicate_report()` |
| [ FAIL ] | balance continuity | 2 break(s) in the running balance, 177,280.50 unaccounted for |
| [ note ] | sub-tolerance continuity | 0 additional break(s) below 0.10 (0.00 net) — listed separately, not silently ignored |
| [ note ] | unread rows | 1 row(s) across 1 document(s) were printed by a statement but never read — the population is knowingly short; see `unparsed()` for page and line |
| [ ok ] | ingest history | 3 recorded import(s) |
The continuity check verifies that the statements form a continuous chain. It fails here and reports 2 chain breaks across 1 unread row(s). This product does not report a pass while the statement set is incomplete.
Every figure in the workbook is defensible on its own. The person who maintains it is not careless. The workbook cannot show whether a statement is missing. banktrail draws each chain break between the two rows it separates, with the date window and the unaccounted amount.
The same rows now show a chain break.
banktrail imports those bank statements into a ledger. Every transaction is traceable to a byte range in a source file. A machine-checkable proof shows that no statement is missing from the chain. The viewer at the top of the page shows the sample company. The rows in it are what the import and the continuity check return.
- Consecutive statements must chain: each expected closing balance must appear among the balances the bank printed. A chain break is a failure to connect by closing balance. The viewer shows the break between the two rows it separates, with the date window and the unaccounted amount. The note “diff 64,500??” is now 64,500.00 unaccounted for between 2025-02-06 and 2025-02-14.
- A break has one of two causes. An unread row is a bank-printed row that banktrail cannot parse. The first break comes from an unread row, the cell printed as #####. The row stays in the unread list with its file, line, and the reason it did not parse. The second break comes from a missing statement. The ledger reports the cause.
- The row struck out in the workbook is present. Two payments with the same amount and payee on the same day are two payments, not one. The statement printed both at different positions, and banktrail treats each position as a separate transaction. A parser that discards rows creates a ledger that looks complete but is short. That is the most expensive failure mode, because the output has the authority of every row that parsed.
4 views of the same statements
The panel above is the viewer, not a screenshot. The viewer has 4 tabs. Select a tab to see the same 3 statements as transactions, accounts, documents, or the integrity report. Each view shows different information. All 4 views come from the same source data as the grid.
- Transactions is a virtualised grid. It renders only the visible rows, so it scrolls, sorts and filters a 1,000,000-row ledger at the speed of a 1,000-row one. A chain break appears in the grid between the two rows it separates, not on a separate report page. Tags appear on their rows. Tag colour comes from a hash of the tag name, so a tag has the same colour across sessions. A selection copies as TSV into a spreadsheet.
- Accounts shows the state of each balance chain. A balance chain is the sequence of closing balances across statements. Each account is listed with its currency, transaction count, date span, money in and money out, and a state: intact, has breaks, or unverifiable. Unverifiable means the statements print no running balance, so no continuity check is possible. It is a separate state, never reported as passed.
- Documents lists every imported file. For each one: the format it was read as, rows produced, unread rows, whether statement arithmetic balances, and the SHA-256 of the uploaded file. An unread row is a row the reader cannot parse. Each transaction records its source document and byte range.
- Integrity is the report in full: uid integrity, provenance, the amount mirror, statement arithmetic and balance continuity, each with pass or fail and the count behind it. The sample company fails balance continuity, because two chain breaks and one unread row mean the ledger is incomplete.
The viewer cannot alter a transaction, tag or figure. Evidence attaches to many rows at once as one attachment identified by content hash. A selection, a view or the whole ledger can be exported as TSV, CSV or SQLite.
Speed on a ledger of 249,996 transactions
Every figure below is from a ledger of 249,996 transactions in 1,140 statements across 6 accounts. The machine is Apple M4 Pro. The operations are the same ones the viewer performs. Each timing is the median of repeated runs after a warm-up. The slow timings are included. A page for checking figures cannot quote only the fast ones.
- A page of rows anywhere in the ledger
- 6.2 ms
- At row 249,596 of 249,996
- The same viewer, sorted by amount
- 6.3 ms
- The data store performs the sort.
- An annotation after it has been written and read
- 0.20 ms
- The cost to an agent per tag
- Searching the text of every row
- 77.4 ms
- It matches one word against every description and reference.
- Summing the whole population
- 515 ms
- It sums 249,996 amounts in Decimal, split by currency.
- Checking the balance chain
- 834 ms
- The check compares each statement with the next statement.
- The full integrity report
- 4.89 s
- The report lists hashes, provenance, footing and continuity.
- Ledger file
- 1,046 bytes/txn
- Size 262 MB. Provenance is included.
- Scrolling speed does not depend on ledger size. A window at the end of 249,996 transactions costs 6.2 ms. The same window at the start costs 3.0 ms. The grid requests the on-screen rows, not the whole ledger.
- Writing a tag and reading it back takes 0.20 ms. An agent can tag a few thousand rows as one task instead of a batch job.
- The ledger occupies 262 MB, or 1,046 bytes per transaction. This size includes provenance for every row: the source document, the position in that document, and the file hash.
- The full integrity report takes 4.89 s. It recomputes the content hash of every row, confirms provenance, verifies the arithmetic of each statement, and re-checks the balance chain across all 249,996 rows.
Proof that the statement set is complete
Extracting rows from statement files is assumed. banktrail came from a real reconciliation across 284 bank statements and 6 banks. The initial answer was “eleven such transfers”. It was one account’s subset of a 3-account population. Every row had parsed correctly. The error was assuming a date range was contiguous. No statement parser we could find would have detected it. Accountants, auditors and controllers meet the same situation: every row parses, and the population is still short.
- money
Money is always handled as Decimal.
A ledger must foot to the minor unit. It cannot be built on a data type where 0.1 + 0.2 does not equal 0.3. Every amount is transferred as a string. The web page never performs arithmetic on money.
- dates
Adapters declare the date order.
An adapter is a banktrail component that reads a bank's statement format. It declares the date order for that bank's files. The date 03/01/2023 is 3 January in Karachi and 1 March in Chicago. When a date is ambiguous, the system reports a warning.
- files
File type is detected from the content.
Banks send .xls files that are HTML documents and .xls files that are genuine BIFF. The extension does not distinguish the two formats. The system detects the file type from the content. This is the only safe way to identify the format.
- footing
A statement must balance before it imports.
The import is refused unless opening plus net movement equals closing. This check detects a misread column and a defective adapter. Neither one is imported.
- scans
A scan is rejected until every token is accounted for.
banktrail rejects an image-only PDF instead of parsing it to nothing. OCR converts the image to text. A capture report accounts for every token that looks like money before the import is allowed. A partial parse is visible in the report.
- identity
Identical payments on the same day are two payments.
Occurrence-slot identity is the position of a transaction in the statement. Two payments with the same amount, same payee and same day are two transactions. Both salary payments on the 14th survive when the export format shuffles rows.
- annotations
Annotations record their provenance.
Provenance is the record of where an annotation came from. Tags carry their source. Cross-references carry their method and confidence. A cross-reference matched on amount and account differs from one matched on amount alone. An audit distinguishes the two claims.
- audit
The ledger downloads as a standalone file.
The snapshot is the downloaded ledger file. It is plain SQLite with views, plus a manifest that lists the content hash and source byte range of every row. An auditor can open it with existing tools. No conclusion depends on the service uptime.
Accounts whose statements have no balance column are reported as unverifiable. They are not reported as passed. Reporting an unchecked account as intact would claim a proof that was never run.
The service reads bank statement exports.
banktrail is a hosted service for accountants, auditors and finance teams. It turns bank statement exports into a transaction ledger (a list of transactions). It produces an integrity report (a report on whether the statements are complete). It creates a downloadable snapshot (a file of the results). There is no installation and no configuration. The first reconciliation takes about as long as opening the files would take.
- It reads and converts XLS/XLSX, CSV, HTML or text e-statements. File type detection is by content, not file extension. An .xls file that is really an HTML document is read as HTML.
- Adding a new bank does not require a software release. For most banks, the statement layout is described by naming the header fragments the bank prints. A layout not seen before is a configuration change. banktrail can add a new bank from one statement.
- Agents and integrations use a keyed HTTP API with reader, annotator, importer and editor roles. It can import, tag, cross-reference and record attributed edits. Annotation round-trips take under a millisecond at 50k rows.