Finance & Lending

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

FieldWhy it needs care
Account holder, number and sort codeUsually on page one only, so it has to be carried across a multi-page statement.
Statement periodDefines what the totals mean, and the thing most often missing when someone re-keys manually.
Transaction dateFrequently abbreviated with no year — the year comes from the statement period, not the row.
Description or counterpartyTruncated and abbreviated by the bank. A category Cleaner infers merchant and type from it.
Debit, credit and running balanceExtract all three. The balance is what lets you verify no rows were dropped.
Opening and closing balanceThe 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