FlowParse
Developer API June 20, 2026 12 min read

Bank Statement API — Parse Statement PDFs to JSON

The FlowParse bank statement API turns a PDF, scanned image or CSV statement into clean, structured JSON in a single REST call — every transaction as a typed row with date, description, signed amount and running balance, plus the account header and a `raw_table` that preserves every original column 1:1. One key, one endpoint, any bank. Then validate, reconcile, Smart-merge or export to QuickBooks, QFX, Xero and more — all over the same authenticated API.

FlowParse
flowparse.io

A REST API for bank statement data

A bank statement API is an HTTP endpoint you send a statement file to and get structured data back — no templates, no manual mapping, no screen-scraping. FlowParse exposes exactly that: `POST /api/v1/extract` accepts a statement as base64 (PDF, scanned image, XLSX or CSV) and responds with typed JSON — the same multi-model pipeline the FlowParse workspace uses, now callable from your own backend.

It is built for teams that process statements at volume: lenders pulling income and balance signals for underwriting, fintechs onboarding business accounts, accounting platforms importing client books, and bookkeeping tools that need a reliable bank statement converter behind an API rather than a web upload. Authentication is a single bearer key, billing is per page, and the response schema is stable and documented.

Every call returns the account context (bank name, account holder, masked number, currency, opening and closing balance) and a `transactions` array where each row is normalised: ISO dates, a single signed `amount` (credits positive, debits negative) and the running `balance`. The original layout is never lost — a `raw_table` carries every source column exactly as printed, so reference codes, transaction types and card last-fours survive into your database.

FlowParse
flowparse.io

Bank statement API vs open-banking API: two different things

The phrase bank statement API is used for two products that work in opposite directions, and it is worth being precise about which one you need. An open-banking or account-aggregation API — the kind Plaid offers — connects to the customer's live bank account. The customer consents through a connection flow and the provider pulls transactions and balances from the institution. Plaid's own documentation says the initial pull defaults to 90 days of history and can be set to a maximum of 730 days (about 24 months), with more history accumulating while the connection stays alive. UK Open Banking likewise works only with the customer's explicit consent to a regulated firm.

FlowParse is the other kind. We parse documents; we do not connect to bank accounts. There is no login, no consent screen and no live feed. Your system sends the statement file the customer already has — a downloaded PDF, a scan, a CSV or XLSX export — and the API returns what is printed on it as structured JSON. The data is exactly as fresh as the document: a March statement tells you about March.

A live feed is the right tool for today's balance and an ongoing stream of new transactions; a document API is the right tool when the customer won't connect, the bank or account isn't reachable, or your process already works from documents, as most lending files and bookkeeping catch-ups do. Many teams run both.

Document API (FlowParse)Open-banking / aggregation APIManual keying
InputA statement file: PDF, scan (PNG/JPG), CSV or XLSXA live connection the customer authorisesA person reading the PDF
Period coveredWhatever the statement covers, any yearWhat the provider can pull from the institution (Plaid: 90 days by default, up to 730)Whatever the statement covers
Live / current balanceNo — the balances printed on the document onlyYes, while the connection is activeNo
Closed accounts, uncovered banksYes, if a statement existsDepends on provider coverage and an open accountYes
Built-in arithmetic checkOpening + Σ transactions vs printed closing, row-by-row running balanceNot applicable (data comes from the bank's systems)Only if someone adds it up
FlowParse
flowparse.io

Your first request in 60 seconds

Create a key in the API dashboard, base64-encode a statement, and POST it. The endpoint is `https://flowparse.io/api/v1/extract`; authenticate with `Authorization: Bearer pf_live_…` (an `X-API-Key` header also works). Base64 keeps the request a plain JSON body, so it works from any language and any HTTP client with zero multipart handling.

POST /api/v1/extract
curl -X POST https://flowparse.io/api/v1/extract \
  -H "Authorization: Bearer pf_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "file": "JVBERi0xLjcKJ...",        # base64 of october.pdf
    "filename": "october.pdf"
  }'

Bank statement PDF to JSON: one request, everything back

Converting a bank statement to JSON over the API is a single `POST /api/v1/extract`. The body is plain JSON: `file` (the document as base64 — a `data:` URI prefix is stripped for you), an optional `filename` so the type is detected from the extension, or an explicit `mimeType`. Accepted types are PDF, PNG, JPG, XLSX and CSV, up to 20 MB per file. If the PDF is password-protected, send the password in `password`; without it the API answers `422` with `code: "PASSWORD_REQUIRED"`, and with the wrong one `PASSWORD_INCORRECT`.

Two optional switches make the one call do everything an upload does in the web app. `validate: true` adds the deterministic quality report to the response, and `export: { format: "xlsx" }` (or `csv`, `xml`, a Google Sheets target or an accounting format) adds the finished file. Both ride on the extraction charge: you pay for the document's pages once, not again for the check or the file.

The response opens with `type` (`bank_statement`, `credit_card`, or `mixed` when a statement and an invoice share one file), `pages`, `billedPages` and a `price` object; then `data`, the extraction, with its own `type`, a `confidence` value and the statement object.

POST /api/v1/extract — request and 200 response (illustrative values)
curl -X POST https://flowparse.io/api/v1/extract \
  -H "Authorization: Bearer pf_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "file": "JVBERi0xLjcK...", "filename": "acme-october.pdf", "validate": true }'

{
  "type": "bank_statement",
  "pages": 3,
  "billedPages": 3,
  "price": { "eur": 0.105, "perPageEur": 0.035, "complexity": "standard" },
  "data": {
    "type": "bank_statement",
    "confidence": 0.94,
    "data": {
      "bank_name": "Example Bank",
      "account_holder": "ACME TRADING LTD",
      "account_number_masked": "****9471",
      "sort_code": "000000",
      "statement_period": "01/10/2026 - 31/10/2026",
      "currency": "GBP",
      "opening_balance": 4120.55,
      "closing_balance": 6134.80,
      "transactions": [
        { "date": "2026-10-03", "description": "CARD PROCESSOR PAYOUT", "category": null, "amount": 2480.00, "balance": 6600.55 },
        { "date": "2026-10-05", "description": "CLOUD HOSTING",          "category": null, "amount": -312.40, "balance": 6288.15 },
        { "date": "2026-10-19", "description": "OFFICE SUPPLIES",        "category": null, "amount": -153.35, "balance": 6134.80 }
      ],
      "raw_table": {
        "columns": ["Date", "Description", "Money Out", "Money In", "Balance"],
        "rows": [ ["03 Oct", "CARD PROCESSOR PAYOUT", "", "2,480.00", "6,600.55"], ... ]
      },
      "balance_check": { "status": "reconciled", "rowsChecked": 3, "rowsOk": 3, "failedRows": [], "accounts": 1,
                         "message": "All 3 transactions reconcile against the statement's own running balance." },
      "totals_check":  { "status": "reconciled", "openingBalance": 4120.55, "printedClosing": 6134.80,
                         "computedClosing": 6134.80, "gap": 0, "turnover": 2945.75, "accounts": 1 },
      "language": "en"
    }
  },
  "validations": { "score": { "value": 100, "grade": "green" }, "issues": [ ... ], ... }
}

Every field the API returns

The schema is snake_case and identical to the payload the validation, export and reconcile endpoints accept — so the output of one call is the input of the next with no reshaping.

FieldTypeDescription
bank_namestringIssuing bank or neobank as printed on the statement
account_holderstringName on the account
account_number_maskedstringThe account number as printed (never the IBAN)
iban / bic / sort_codestring, optionalPresent only when the statement prints them
statement_periodstringThe period as printed on the statement
currencystringCurrency code (GBP, USD, EUR…)
opening_balance / closing_balancenumber | nullPeriod balances used for the totals check
transactions[]arraydate, description, category (may be null), signed amount, balance; extra_columns when the statement has more
raw_tableobjectEvery original column and row, preserved 1:1 for audit
balance_check / totals_checkobjectArithmetic verdicts: reconciled, failed or not_verifiable

The balance-check fields: how the statement proves the extraction

A bank statement carries its own proof, and the API returns that proof with every result instead of asking you to trust the parse. Two verdicts travel inside `data.data`. `balance_check` works row by row on statements that print a running balance: each printed balance must equal the previous one plus the amount beside it. It reports `rowsChecked`, `rowsOk`, `failedRows` (the exact failing row indices) and an `accounts` count, since one PDF can hold several accounts.

`totals_check` asks the question an accountant asks first: does the opening balance plus the sum of all amounts land on the closing balance the statement prints? It needs no running-balance column, which matters for card statements and many European layouts that print only date, description and amount. When it fails it reports `computedClosing` and the `gap`, so a phantom or missing row shows up as money, not a percentage. Both allow two cents for rounding, nothing more.

Each verdict is `reconciled`, `failed`, or `not_verifiable` when the statement does not print what the check needs — use them as routing signals: auto-accept when both reconcile, send `failed` to a reviewer with the rows highlighted.

Response fieldWhat it tells youTypical use
data.data.opening_balance / closing_balanceThe balances printed on the statement (null when not printed)Period start/end balance in your ledger or credit file
transactions[].balanceRunning balance printed beside each row, when the statement has oneBalance history, low-balance days, overdraft spotting
balance_check.status / failedRowsWhether every printed running balance chains to the one before itHighlight exact rows for a reviewer
totals_check.status / gapOpening + Σ amounts vs the printed closing balance, and the money unaccounted forHard gate before posting or scoring
validations.score.gradegreen 90–100 · yellow 70–89 · red below 70 (with validate: true)Auto-accept vs human review

Error handling: every status the extract endpoint returns

Errors are JSON with an `error` message (and a `code` where a client should act on it). Two behaviours matter. First, the page balance is checked before any work starts: if the document needs more pages than the balance holds you get `429` immediately, with the needed and available counts in the message. Second, a document that turns out to have nothing extractable — a cover letter, a blank scan — returns `422` and is billed zero pages.

StatusWhen it happensWhat to do
400No `file`, invalid base64, empty file, file over 20 MB, unsupported type, or an unknown `export.format`Fix the request; the error lists supported formats
401Missing, invalid or revoked API keyCheck the key; rotate in the API dashboard
422 PASSWORD_REQUIRED / PASSWORD_INCORRECTEncrypted PDF without, or with the wrong, `password`Ask for the password or an unlocked copy
422No readable text, extraction failed or timed out, or nothing extractable (billed 0)Don't retry blindly — ask for a cleaner scan or a shorter file
429Page balance lower than the document's page countTop up (also over the API: POST /api/v1/topup), then retry
503Extraction temporarily unavailableRetry with exponential backoff

Why the data is trustworthy

Statement parsing fails silently when a converter drops rows or mis-signs a debit, so accuracy is the whole game. FlowParse runs a coordinate pass first (reading the PDF's own text geometry), falls back to AI OCR only for scanned pages, then applies multi-model field extraction and a deterministic check: opening balance plus the sum of transactions must equal the closing balance. When it doesn't, the discrepancy is flagged rather than hidden.

You can gate every result programmatically. Add `validate: true` to the extract call (or send the returned statement to `POST /api/v1/validate`) to get a 0–100 quality score, a green/yellow/red grade and a list of specific checks (balance reconciliation, duplicate rows, running-balance consistency, missing dates or amounts). That lets you auto-accept clean statements and route only the genuinely ambiguous ones to a human — see the validation engine for the rule set.

FlowParse
flowparse.io

Credit card statements to structured data

The same endpoint reads credit card statement PDFs. A cardholder statement is classified as `credit_card` and comes back in its own shape, because a card bill has a due date, a minimum payment and a credit limit, and usually no running balance. The `data.data` object carries `card_issuer`, `card_last4`, the statement period, `payment_due_date`, the summary box (`previous_balance`, `payments_and_credits`, `purchases`, `fees_charged`, `interest_charged`, `new_balance`, `minimum_payment_due`, `credit_limit`) and a `transactions` array of date, description, category and signed amount — charges positive, payments and credits negative.

Card statements get their own arithmetic gate, `credit_card_check`: previous balance plus purchases, cash advances, fees and interest, minus payments and credits, should equal the new balance the issuer prints. Its `checks` array names each comparison and whether it held. For merchant settlement statements from card processors — batches, fees and net deposits rather than a cardholder bill — see the credit card processor statement converter; for cardholder statements in a spreadsheet instead of JSON, the credit card statement converter uses the same engine.

Duplicate transaction detection — what it does and what it doesn't

Duplicate rows are the quiet way a statement parse goes wrong, so here is exactly what the pipeline does on bank statements. Statements repeat payments on purpose — a direct debit in the table and again in a notice block, or a detail section that the day-by-day ledger repeats. During extraction, a repeat that has the same date and amount as a real row but no running balance of its own is removed, because it is not part of the account's balance chain. Exact repeats on the same page are removed only when the statement's own printed lines show the row was read twice — so two genuine identical €8.00 purchases on the same day, each with its own balance, both stay.

Whatever survives is still checked. With `validate: true`, the bank-statement rule `bank_no_dupes` flags any rows that share date, description and amount, and returns their indices in `lines`. If two identical rows carry different balances, the money moved twice, so they are not flagged. A flagged pair is a warning, not a deletion — the API never decides for you that a genuine repeated charge is an error.

For credit card results the honest answer is narrower. The card pipeline checks the summary arithmetic (`credit_card_check`), but there is no dedicated duplicate-flag rule on `credit_card` results today. If duplicate charges matter to you, group the returned `transactions` yourself — a few lines, shown below. For matching payments across many documents rather than within one, see duplicate payment detection.

Flag possible duplicate card charges client-side (TypeScript)
type Tx = { date: string; description: string; amount: number | null }

export function possibleDuplicates(txs: Tx[]): number[][] {
  const groups = new Map<string, number[]>()
  txs.forEach((t, i) => {
    if (t.amount == null) return
    const key = `${t.date}|${t.amount.toFixed(2)}|${t.description.trim().toLowerCase()}`
    groups.set(key, [...(groups.get(key) ?? []), i])
  })
  return [...groups.values()].filter((g) => g.length > 1) // review, don't auto-delete
}
FlowParse
flowparse.io

Turn the JSON into accounting files

Most teams don't want JSON as the final artefact — they want it inside QuickBooks, Quicken or Xero. The same key calls `POST /api/v1/export` to turn any extracted statement into a real bank-feed file: `.QBO`/`.QFX`/`.OFX` (OFX 1.0.2, with FITID de-duplication so re-imports never double-post), or a clean CSV/XLSX. File exports are billed per page; accounting previews are free so you can inspect the mapping first.

POST /api/v1/export — bank feed
curl -X POST https://flowparse.io/api/v1/export \
  -H "Authorization: Bearer pf_live_xxx" \
  -d '{ "format": "quickbooks", "type": "bank_statement", "data": { ... } }'
# → { "format":"qbo", "filename":"acme-oct.qbo", "encoding":"base64", "content":"T0ZYSER..." }
FlowParse
flowparse.io

Many statements, one call

For a year of statements or a whole portfolio, `POST /api/v1/merge` consolidates up to 100 already-extracted documents into a single reconciled Excel — unified columns across banks, duplicate rows removed, per-row source tracking — exactly like Smart Merge in the app. Pass `preview: true` to get the summary and sheet previews for free before you spend pages on the file.

FlowParse
flowparse.io

Pricing, keys and limits

Billing is simple and per page. Extraction and file exports draw from their own balance — top-up pages, separate from your account's monthly site-plan pages, which the API never spends; validation and previews are free. When a request would exceed your balance the API returns `429` with a clear message, so you never get a surprise bill — top up on the pricing page. Each key tracks its own request and page totals, visible in the dashboard.

Keys come in under a minute from the API dashboard: create, reveal once, and revoke at any time. Use a separate key per environment or per customer so you can rotate without downtime. Full request/response reference, every format and live examples live in the API docs; you can also try calls in the browser with the API playground.

OperationEndpointBilling
Extract PDF → JSON (+ optional validate and export in the same call)POST /api/v1/extractPer page, once
Validate / quality scorePOST /api/v1/validateFree
Export data you already hold (QBO/QFX/OFX/Xero/CSV/XLSX)POST /api/v1/exportPer page (preview free)
Reconcile invoices ↔ paymentsPOST /api/v1/reconcileFree
Merge many → one ExcelPOST /api/v1/mergePer page (preview free)
Balance and spendGET /api/v1/usageFree

Security & data handling

Statements are sensitive, so the API is built to hold as little as possible. Calls run over HTTPS, authenticate with a hashed key, and the uploaded file is processed in memory to produce the JSON response — it is not retained as a downloadable document on your behalf. Each key is scoped to your account, request-counted, and instantly revocable, and every call is logged with the document label and page cost for a clean audit trail.

Because you control the request, you also control retention: store only the fields you need, drop the `raw_table` if you don't, and keep PII out of logs. FlowParse never uses your documents to train models. For the platform's wider posture see the security page.

FlowParse
flowparse.io

Using a bank statement API in lending and underwriting — without replacing your systems

The most common reason teams look for an API for mortgage or commercial loan underwriting is not that they want a new platform. They already have a loan origination system and a credit model; what slows them down is typing months of applicant statements into a spreadsheet. A document API removes that step and nothing else. Your system keeps receiving the applicant's PDFs exactly as it does today, a small worker sends each one to `/api/v1/extract`, and the JSON lands in the fields and tables your system already has.

What the API gives an underwriter is data, not a decision. It returns every transaction with a signed amount and date, the printed opening and closing balances, the running balance per row, the account holder and masked account number, and the two arithmetic verdicts that show whether the statement adds up. From that your own logic computes whatever your credit policy needs — monthly inflows, average and lowest balances, overdraft days, large deposits. FlowParse does not score applicants, verify income or make lending decisions, and it does not check whether a document has been altered; it reports what is printed and whether the printed numbers are internally consistent.

A statement whose rows do not chain to its own balances is exactly the file an underwriter should look at twice. For a lender-specific walkthrough see the bank statement API for lenders and bank statement analysis for loans.

1

Keep your intake

Applicants upload statements to your portal or LOS exactly as before.

2

Send each file to /extract

A background worker posts the file with `validate: true`. Password-protected PDFs return `PASSWORD_REQUIRED`, so the worker can ask for the password.

3

Gate on the checks

Both `balance_check` and `totals_check` reconciled → straight through. Failed or not verifiable → the analyst queue, with the failing rows.

4

Compute your own metrics

Your code turns `transactions` into the figures your credit policy uses and writes them into the existing file.

FlowParse
flowparse.io

Bank statement analysis and balance data from the JSON

Searches for a bank statement analysis API or a bank balance API usually want one of two things. A live account's current balance is an open-banking product — a document only shows the balances on the dates it prints. For balances and cash flow over the statement period, the JSON already holds everything: opening and closing balances, a running balance per row where printed, and every signed movement. Group `amount` by month for inflows and outflows; take the minimum `balance` for the low point.

Because every bank is normalised to the same shape, that code is written once. For the no-code route see cash flow from bank statements and the bank statement to JSON converter for the no-code route. To try the engine on a real file before writing any code, the free plan in the web app includes 10 pages a month; API calls themselves draw on a separate pre-paid page balance at €0.035 per page.

How it handles every bank format

The reason a template-based parser keeps breaking is that there is no single 'bank statement format'. One bank prints separate Money In and Money Out columns; another uses a single amount column with a Dr/Cr suffix; a third puts the running balance on the left, uses `DD/MM/YYYY` dates and a comma as the decimal separator. A US statement might show `MM/DD/YYYY` and `$`, a European one `1.234,56 €`. A naive parser needs a new rule for each of these; the FlowParse engine recognises the *meaning* of each column and normalises them all to one shape — ISO dates, a single signed amount, a numeric balance — so your code never branches on the bank.

FlowParse
flowparse.io

Designing a robust integration

The extract call is synchronous: you POST a statement and the structured JSON comes back on the same request. Large multi-page scans take longer than short digital PDFs, so set a generous client timeout, and at volume put extraction behind a queue and a pool of workers so a slow document never blocks a user-facing request.

FlowParse
flowparse.io

Start building today

The fastest path is: grab a key, send one statement to `/api/v1/extract`, and read the JSON. From there add `/validate` for a quality gate and `/export` or `/merge` for the output your users actually need. The guide to parsing bank statements with an API walks through a complete, production-ready integration with error handling and batching, including the queue-and-worker pattern above.

Looking for a specific format or document type? See PDF to JSON API for the generic schema view, the bank statement OCR API for scanned documents, and the document extraction API for invoices and receipts alongside statements. When you're ready to wire output into accounting software, PDF to QBO and bank statement to Xero cover the import side end to end.

Ship statement parsing this week

Create a key, POST one statement to /api/v1/extract, and get clean transaction JSON back. Validate, export to QBO/Xero and reconcile over the same API.

Frequently asked questions

Related