A bank statement can run to hundreds of transactions, and manual copy-paste is quietly dangerous: it moves values to the wrong row, mangles dates, and eats negative signs. It works right up until it doesn't — and it never tells you when.
This guide extracts transactions from a PDF bank statement the controlled way, then checks for duplicates and reconciles the closing balance. Extraction without reconciliation is just optimism.
One rule before anything else: use synthetic or authorized statements. If you don't have permission to process a statement, don't upload it.
What good output looks like
One row per transaction:
| transaction_date | description | debit | credit | balance | currency |
|---|---|---|---|---|---|
| 2026-06-02 | Payroll deposit | 2450.00 | 5320.18 | JMD | |
| 2026-06-03 | Office supply store | 297.00 | 5023.18 | JMD | |
| 2026-06-04 | Monthly account fee | 25.00 | 4998.18 | JMD |
Statement-level values live in their own record:
| bank_name | account_number_masked | period_start | period_end | opening_balance | closing_balance |
|---|---|---|---|---|---|
| Example Bank | ****1842 | 2026-06-01 | 2026-06-30 | 2870.18 | 4998.18 |
Don't repeat the full account number on every transaction row. Keep only what your workflow actually needs — your security policy will thank you.
1. Define the transaction fields
These fields cover most bank statements:
| Field | Type | Extraction rule |
|---|---|---|
transaction_date | date | Use YYYY-MM-DD |
description | text | Keep all text for the transaction |
debit | number | Use a positive number for money out |
credit | number | Use a positive number for money in |
balance | number | Use the running balance after the transaction |
currency | text | Use the statement currency code |
Some banks prefer a single amount column and mark direction with a minus sign, or with DR and CR markers.
Pick one convention before extraction and hold the line. Separate debit and credit columns are usually the clearest for accounting imports — and whatever you choose, never mix negative debits with positive debit values in the same file.
2. Define the statement fields
Extract these values once per statement:
- bank name
- masked account number
- account holder name, when necessary
- statement start date
- statement end date
- opening balance
- closing balance
- currency
This split — statement once, transactions many — isn't just tidiness. It's how you spot a missing page, or the same statement uploaded twice.
3. Check the PDF type
Try to select one transaction description in your PDF viewer.
If the text highlights, the statement is usually digital. If it doesn't, you're looking at a scan, and OCR goes first.
On scanned statements, interrogate the usual suspects:
0andO1,I, andl- decimal points
- commas in large values
- minus signs
DRandCRmarkers
A missing decimal point turns 25.00 into 2500, which makes for a very different month. Always re-check high-value transactions after OCR.
4. Extract a small sample
Open the bank statement parser. Then:
- Upload one representative statement.
- Confirm the statement fields.
- Confirm the transaction fields.
- Start the extraction.
- Compare the rows with the source statement.
- Correct the schema rules when necessary.
- Process the remaining statements.
Pick a difficult sample on purpose — one with a page break, a wrapped description, or more than one amount column.
Resist the urge to run everything at once. A field error doesn't stay in one row; it quietly multiplies across the entire batch.
5. Normalize the dates
Statements love omitting the year — 03 JUN, 06/03, take your pick.
Fill in the year from the statement period, and stay alert when the period crosses a year boundary. A statement running from 2025-12-20 to 2026-01-19 contains both years, so stamping 2026 on everything gets two weeks of transactions wrong.
Store dates in YYYY-MM-DD format. It sorts correctly and sidesteps the regional day-month guessing game.
6. Keep wrapped descriptions in one row
Banks often split a transaction across two lines — the merchant on one, a reference on the next:
That's one transaction, and the output keeps it that way:
The continuation line is not a second transaction. Don't let it become one.
7. Detect duplicate transactions
Upload the same statement twice and you'll get every row twice. But two genuinely identical purchases also happen — two coffees, same day, same amount. So deduplication needs more than one field.
Build a comparison key from several:
Flag matching keys for review, and don't delete them automatically unless your business rules allow it. When two rows look identical, the running balance or a transaction reference is usually what separates "duplicate upload" from "yes, I really did buy two."
8. Reconcile the balance
This is the check that catches everything else. With debits and credits as positive values in separate columns:
A quick Python check:
If the numbers don't reconcile, the usual causes are:
- a missing page
- a missing transaction
- a duplicate transaction
- a debit in the credit column
- an OCR error
- a fee outside the main transaction table
- a different balance convention
One firm rule: never edit a value just to make the balance agree. Find the real discrepancy in the source statement — adjusting numbers to fit is how small errors become audit findings.
9. Protect sensitive data
Bank statements are about as sensitive as documents get. Treat them that way:
- Limit access to authorized users.
- Keep API keys on the server.
- Do not put statement data in application logs.
- Mask account numbers in previews and media.
- Delete temporary files according to your retention policy.
- Keep a record of manual corrections.
For accounting or lending decisions that carry consequences, put a human review between extraction and final use.
Select the output format
CSV for spreadsheets and accounting imports. JSON for an API or automation workflow. SQLite when you want SQL queries across many statements.
Start with the bank statement parser. You can also use the PDF to CSV converter, review the API documentation, or check pricing.