Bank statement data extraction
Tavnit reads bank statement PDFs from any institution and returns every transaction as a row: date, description, debit or credit, and running balance. Because the balance is extracted too, the arithmetic can be checked rather than assumed — which is what makes the output safe to reconcile against.
Why this is painful
Every bank formats statements differently, and none of them format for machines. Columns shift, transactions wrap onto two lines, and the running balance is the only thing tying the sequence together. Anyone doing lending assessment, bookkeeping or reconciliation ends up re-keying or paying for a per-bank connector.
Open banking solves this where it is available and the customer consents. For everything else — historical periods, business accounts, banks without an API, statements supplied as evidence — the PDF is the only source there is.
What to extract
| Field | Why it needs care |
|---|---|
| Account holder, number and sort code | Usually on page one only, so it has to be carried across a multi-page statement. |
| Statement period | Defines what the totals mean, and the thing most often missing when someone re-keys manually. |
| Transaction date | Frequently abbreviated with no year — the year comes from the statement period, not the row. |
| Description or counterparty | Truncated and abbreviated by the bank. A category Cleaner infers merchant and type from it. |
| Debit, credit and running balance | Extract all three. The balance is what lets you verify no rows were dropped. |
| Opening and closing balance | The checksum for the whole statement. |
What makes bank statements hard
A dropped row is invisible without the balance
If extraction misses one transaction, the remaining rows still look perfectly plausible. Extracting the running balance turns that into a detectable error: a formula Cleaner can check each row's balance equals the previous balance plus the movement, and flag the statement rather than let it through.
Transactions wrap, and wrapped lines are not new transactions
Long counterparty names spill onto a second line, and naive table reading turns one payment into two rows with a blank amount. Table fields treat a row as a row, so a wrapped description stays attached to its amount.
Dates rarely carry a year
Most statements print '14 Mar' and rely on the period at the top for the year. That is fine for a human and wrong for a database — particularly across a year boundary in a January statement. Extract the statement period and derive the full date from it with a formula Cleaner.
How the pipeline handles it
- Flows — Transactions are table fields, so each one returns as its own typed row rather than a block of text.
- Cleaners — Derives full dates from the statement period, verifies running balances, and categorises counterparties.
- Buckets — Transactions land as a queryable table — filter by counterparty, group by month, chart the balance.
- Human in the Loop — Routes only the statements that fail a balance check to a person, rather than reviewing all of them.
Common questions
- Does it work with any bank?
- Yes. There is no per-bank template to configure — the same flow reads statements from any institution, including business accounts and historical periods that an open banking API would not cover.
- How do I know no transactions were missed?
- Extract the running balance alongside each transaction, then use a formula Cleaner to verify each row reconciles to the previous one. A statement that fails the check gets flagged rather than silently passing.
- Can transactions be categorised automatically?
- Yes. An AI category Cleaner assigns a category from the description, so bookkeeping starts from classified rows rather than raw bank text.
Other document types
Try it on one document
Build a flow, send a real bank statement through it, and see what comes back. Free credits to start.
Start free