# Bank Statement PDF to Excel & CSV - Balance Reconciled (`northbound_works/bank-statement-to-csv`) Actor

Converts bank and credit-card statement PDFs into a clean transaction CSV, then checks that opening balance + transactions = closing balance. A statement that does not reconcile is rejected and not charged for, instead of returning a plausible CSV with a row missing.

- **URL**: https://apify.com/northbound\_works/bank-statement-to-csv.md
- **Developed by:** [Austin Cooley](https://apify.com/northbound_works) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bank Statement to CSV — reconciled, not just parsed

Turns bank and credit-card statement PDFs into a clean transaction CSV, and
checks the arithmetic before handing it over.

### The problem with every other statement converter

Producing a CSV from a statement is easy. Producing a **complete** one is not.

The failure that costs somebody a day is not a garbled row — you can see a
garbled row. It is the converter that returns 42 of the 43 transactions.
Correctly formatted. Sensible dates. Sensible amounts. Nothing about the file
looks wrong. The row that went missing was the one that straddled a page break,
or wrapped onto a second line, or sat in a column the parser guessed at.

Nobody notices until the account does not balance, and by then the CSV has been
imported, categorised and half-reconciled by hand.

### What this does instead

Parse first, then verify:

```
opening balance  +  sum of transactions  =  closing balance
```

and, wherever the statement prints a running balance, that **every row's
balance follows from the row above it**. When the chain breaks, you get the row
number, the description, and the exact size of the gap:

> row 5 ("ATM WITHDRAWAL 500 CONGRESS AVE"): previous balance 21,110.24
> −200.00 should give 20,910.24 but the statement shows 19,910.24 — a gap of
> −1,000.00, which usually means a transaction on this line was not captured.

**A document that does not reconcile is not charged for**, and its rows are
kept out of the dataset unless you explicitly ask for them. Unverified data
should not be sitting in the same table as verified data, where it will
eventually be used as though it were.

The opening and closing balances are only ever taken from figures the bank
printed. Filling a missing closing balance in from the last row we happened to
read would turn the equation into an identity that passes no matter how much of
the table was missed.

### It works out each statement's layout rather than assuming one

Every bank prints a different table, so nothing here is hardcoded to one of
them. The Actor builds several candidate readings of the page — from the column
header if there is one, otherwise from where the numbers line up — and keeps
the one whose figures reconcile. Reconciliation is not a report at the end; it
is what selects the answer.

Handled:

| | |
|---|---|
| Multi-page statements | Tables that continue across pages, with the header repeated or not |
| Wrapped descriptions | A description spilling onto a second line is joined back together |
| Debit / credit columns | Separate "Paid out / Paid in", "Debits / Credits", "Withdrawals / Deposits" |
| Single amount columns | Sign taken from parentheses, minus signs, DR/CR markers, section banners, or the balance column |
| `(1,234.56)` | Parenthesised negatives |
| Thousands separators | `12,345,678.90`, and European `1.234,56` |
| `MM/DD/YYYY` and `DD MMM YYYY` | Plus `DD/MM/YYYY`, `MMM DD, YYYY`, ISO, and dates with no year at all |
| Credit cards | Where a purchase *increases* the balance owed — the direction is worked out from the statement, not assumed |
| Other tables | Check listings, daily balance summaries and second accounts are kept out of the transactions |
| Password-protected PDFs | The kind a bank emails you |

When the columns cannot be identified with confidence, the document is
**rejected with a reason** rather than guessed at.

### What you get

- **Dataset** — one row per transaction: date, description, signed amount,
  balance, debit/credit, page, and the per-row `runningCheck`. Exportable as
  CSV, Excel, JSON straight from Apify.
- **CSV files** — one per statement, plus a combined `all-transactions.csv`,
  in the run's storage.
- **`summary.json`** — per document: reconciliation method, opening, closing,
  sum, discrepancy, the row where the chain broke, and which column layout was
  detected.

Amounts are signed, with **negative meaning money leaving the account**. Dates
are normalised to `YYYY-MM-DD`.

### Pricing

| Event | Price |
|---|---|
| Actor start | $0.01 |
| Statement parsed | $0.02 — only for a document that parses **and** reconciles |
| Transaction extracted | $0.002 per row — reconciled documents only |

A statement that fails to reconcile costs you nothing beyond the run start. So
does one that cannot be parsed, or that turns out to be a scan with no text
layer. The verification happens before anything becomes billable, which means
the work of proving the output is complete is done at our expense, not yours.

### What it is not

It does not do OCR. A scanned statement has no text to position, and the Actor
will say so rather than return an empty CSV — run it through
[Searchable PDF OCR](https://apify.com/northbound_works/searchable-pdf-ocr)
first, then through this.

It does not categorise transactions, guess merchants, or enrich anything. It
gets the numbers out intact, and tells you when it could not.

It cannot reconcile a statement that does not print any balances at all. If
there is no opening balance, no closing balance and no balance column, there is
nothing to check against, and the document is reported as unverifiable and not
charged for.

### Redacting before you share

If these statements are going anywhere else afterwards, run them through
[PDF PII Redactor](https://apify.com/northbound_works/pdf-pii-redactor) first —
account numbers and card numbers are removed from the file rather than covered
over.

# Actor input Schema

## `pdfUrls` (type: `array`):

Direct links to the bank or credit-card statement PDFs you want converted. Each document is parsed, reconciled and reported separately.

## `dateFormat` (type: `string`):

Only affects all-numeric dates like 05/03/2024. Auto reads the whole document first and picks day-first if any date could not be month-first — set it explicitly if your statement is ambiguous (every day of the month is 12 or lower).

## `pdfPassword` (type: `string`):

Statements emailed by a bank are often password-protected, usually with a date of birth or the last digits of the account number. Leave blank if the file opens without one.

## `includeUnreconciled` (type: `boolean`):

Off by default and worth leaving off. When a statement does not reconcile, its rows are held out of the dataset so unverified data cannot be consumed as though it were verified — you get a single loud failure row explaining what broke, plus a separate UNRECONCILED CSV in the run's storage if you want to investigate. Turn this on to put those rows in the dataset too, flagged. They are never charged for either way.

## `writeCsv` (type: `boolean`):

Produces one CSV per statement plus a combined all-transactions.csv. The dataset can also be exported as CSV directly from Apify, so turn this off if you only want the dataset.

## Actor input object example

```json
{
  "pdfUrls": [
    "https://api.apify.com/v2/key-value-stores/9oR7q55t1g1fqFWg1/records/us_bank.pdf"
  ],
  "dateFormat": "auto",
  "includeUnreconciled": false,
  "writeCsv": true
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "pdfUrls": [
        "https://api.apify.com/v2/key-value-stores/9oR7q55t1g1fqFWg1/records/us_bank.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbound_works/bank-statement-to-csv").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "pdfUrls": ["https://api.apify.com/v2/key-value-stores/9oR7q55t1g1fqFWg1/records/us_bank.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("northbound_works/bank-statement-to-csv").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "pdfUrls": [
    "https://api.apify.com/v2/key-value-stores/9oR7q55t1g1fqFWg1/records/us_bank.pdf"
  ]
}' |
apify call northbound_works/bank-statement-to-csv --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,northbound_works/bank-statement-to-csv"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/dz2pgYNVlwpv9jenB/builds/FN3Jd4HziyPvNZ1PU/openapi.json
