parsr.

Specialist parser

Bank statements to JSON, with balance-chain integrity

Parse PDFs and images of bank statements from 50+ EU banks. Confidence scores, bounding boxes, and balance-chain validation included. Multi-currency, multi-page, IBAN-aware. EU residency by default.

parse-statement.shbash
curl -X POST https://eu-api.tryparsr.dev/v1/parse/bank_statement \
  -H "Authorization: Bearer $PARSR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "document_url": "https://example.com/april-2026-kbc.pdf",
    "wait": 60
  }'

Format coverage

50+ EU banks

Languages

30+

Avg latency

~3.2s p50

Field accuracy

95%+ field-level

What we extract

Every field, with confidence and citations

Every field comes back with a confidence score in [0,1] and a normalized bounding box on the source page. Your agent decides on its own when to escalate to human review.

Input

Anonymized April 2026 KBC retail statement, page 1 of 3 (Dutch layout)

Anonymized bank statements preview
response.jsonjson
{
  "schema_version": "bank_statement.v2",
  "result": {
    "institution_name": "KBC Bank",
    "account_holder": "Pieter Janssens",
    "account_iban": "BE68539007547034",
    "currency": "EUR",
    "statement_period_start": "2026-04-01",
    "statement_period_end": "2026-04-30",
    "opening_balance":  { "amount": "2840.50", "currency": "EUR" },
    "closing_balance":  { "amount": "1956.30", "currency": "EUR" },
    "transactions": [
      {
        "posted_date": "2026-04-03",
        "value_date":  "2026-04-03",
        "description": "Maandelijkse loon Acme NV",
        "amount":      { "amount": "2950.00", "currency": "EUR" },
        "transaction_type": "credit",
        "balance_after":   { "amount": "5790.50", "currency": "EUR" },
        "counterparty_iban": "BE54000123456789",
        "confidence": 0.97,
        "bbox": { "page": 1, "x": 0.06, "y": 0.34, "w": 0.88, "h": 0.018 }
      }
    ],
    "validation": {
      "balance_chain": {
        "valid": true,
        "computed_closing":  "1956.30",
        "declared_closing":  "1956.30",
        "diff": "0.00",
        "tolerance": "0.01"
      }
    }
  },
  "field_metadata": {
    "closing_balance.amount": { "confidence": 0.98 },
    "account_iban":            { "confidence": 0.99 }
  }
}
FieldTypeDescriptionConf. typical
institution_namestringBank name as printed on the statement.99%
account_holderstringPrimary account holder; joint accounts return the first name and surface the second in field_metadata.97%
account_ibanstringNormalized IBAN — spaces stripped, uppercased, validated against ISO 13616 mod-97 checksum.99%
currencystring (ISO 4217)Statement currency. Multi-currency statements return per-transaction currency.99%
statement_period_start / _enddate (ISO 8601)Inclusive period covered by the statement.98%
opening_balance / closing_balancemoney { amount, currency }Balances at the start and end of the period. Both feed validation.balance_chain.97%
transactions[]array of TransactionPosted_date, value_date, description, amount, transaction_type (credit|debit), balance_after, counterparty_iban (when present), confidence, bbox.95%
validation.balance_chainobjectComputed vs declared closing balance. valid=false flags fraud / missing-transaction / sign error.100%

Domain-specific validation

What makes this a specialist

Balance-chain integrity

Opening balance + Σ transactions = closing balance, computed and returned per statement, within a 1-cent tolerance for rounding. valid=false is the strongest single fraud signal underwriters care about.

validation.balance_chain.valid

exampleStatement claims closing 1956.30 EUR; computed closing from opening + transactions is 1854.30 EUR. diff: 102.00. Underwriter flags for manual review.

IBAN format + checksum

Detected IBANs are validated against ISO 13616 mod-97. Failures are surfaced in field_metadata, not silently dropped.

field_metadata.account_iban.checksum_valid

exampleExtracted 'BE68539007547035' fails mod-97 (off by one digit, common OCR error). Lender pipeline downgrades confidence and queues for review.

Currency consistency

Multi-currency statements return per-transaction currency. Pure single-currency statements are checked against statement-level currency for drift.

validation.currency_consistency.valid

exampleStatement-level currency EUR but transaction reports USD with no FX conversion column. Returned as inconsistency, not silently coerced.

Format coverage

Tested across 50+ EU bank formats

50+ EU banks · 10 US banks · 6 countries with multi-bank coverage

Belgium

  • KBC
  • Belfius
  • BNP Paribas Fortis
  • ING Belgium
  • Argenta
  • Crelan
  • AXA Bank
  • Beobank
  • Triodos
  • Keytrade

Germany

  • Sparkasse
  • Deutsche Bank
  • Commerzbank
  • Volksbank
  • DKB
  • ING-DiBa
  • N26
  • comdirect
  • Targobank
  • Postbank

France

  • BNP Paribas
  • Crédit Agricole
  • Société Générale
  • BPCE
  • La Banque Postale
  • Crédit Mutuel
  • LCL
  • HSBC France
  • ING France
  • Boursorama

Netherlands

  • ING NL
  • Rabobank
  • ABN AMRO
  • Triodos NL
  • Knab
  • bunq
  • ASN Bank
  • SNS Bank
  • RegioBank
  • Van Lanschot

United Kingdom

  • HSBC
  • Barclays
  • Lloyds
  • Natwest
  • Santander UK
  • Nationwide
  • Monzo
  • Starling
  • Revolut
  • First Direct

United States

  • Chase
  • Bank of America
  • Wells Fargo
  • Citi
  • Capital One
  • US Bank
  • PNC
  • TD Bank
  • Truist
  • Charles Schwab

Code recipes

From document to JSON in five lines

parse.shbash
curl -X POST https://eu-api.tryparsr.dev/v1/parse/bank_statement \
  -H "Authorization: Bearer $PARSR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "document_url": "https://example.com/april.pdf",
    "wait": 60
  }'
parse_statement.pypython
import os, httpx

resp = httpx.post(
    "https://eu-api.tryparsr.dev/v1/parse/bank_statement",
    headers={"Authorization": f"Bearer {os.environ['PARSR_API_KEY']}"},
    json={
        "document_url": "https://example.com/april.pdf",
        "wait": 60,
    },
    timeout=70,
)
result = resp.json()["result"]
chain = result["validation"]["balance_chain"]
if not chain["valid"]:
    raise ValueError(f"Balance chain broken — diff {chain['diff']}")
for tx in result["transactions"]:
    print(tx["posted_date"], tx["amount"]["amount"], tx["description"])
parseStatement.tstypescript
const resp = await fetch("https://eu-api.tryparsr.dev/v1/parse/bank_statement", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PARSR_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    document_url: "https://example.com/april.pdf",
    wait: 60,
  }),
});
const { result } = await resp.json();
if (!result.validation.balance_chain.valid) {
  throw new Error(`balance chain broken — diff ${result.validation.balance_chain.diff}`);
}
agent.pypython
from langchain_parsr import ParsrToolkit
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

tools = ParsrToolkit.from_env().get_tools()
agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools)

result = await agent.ainvoke({
    "messages": [(
        "user",
        "Parse this April KBC statement and tell me total income vs spend: "
        "https://example.com/april.pdf"
    )]
})
print(result["messages"][-1].content)

Compared

How parsr's bank-statement parsing compares

VendorPricing per pageEU residencyConfidence + bboxBalance-chain validatorEU bank coverage
parsrfrom €0.022Default (eu-api region-bound key)Per field, per transactionYes — built into response50+ formats tested
Mindee~$0.10 (Pro tier)Pro tier+ only (€179/mo entry)YesNoGeneric OCR — no per-bank fixtures
ReductoCustomGrowth tier (custom)PartialNoGeneral-purpose parser
Veryfi$500/mo minimumNoYesNoUS-focused
DocuPipeQuote-basedNoYesNoDashboard-driven, custom configs

Need a new bank or format change? 48 hours.

We don't train models — we curate prompts, schemas, validators, and fixture tests. A new bank or a Q1 layout change goes live in two business days. Mindee's pre-trained models take months to add new formats. Email a sample (anonymized fine) and we'll confirm within 24 hours.

Request a format →

FAQ

Common questions

  • Which bank formats are supported?

    50+ EU banks across BE / DE / FR / NL / UK plus 10 US banks. Each format has a dedicated detail page under /banks/{country}/{bank} with format quirks documented. If your bank isn't listed, email a sample to support@ — we ship new formats in 48 hours.

  • How does balance-chain validation work?

    Every parsed statement returns validation.balance_chain with valid (bool), computed_closing, declared_closing, diff, and tolerance (1-cent default). Computed = opening + Σ(credits − debits). If diff > tolerance, valid=false — the strongest single signal that a statement has missing transactions, sign errors, or has been tampered with.

  • Multi-currency statements?

    Yes. Each transaction returns its own currency in transaction.amount.currency. Statement-level currency is the dominant currency. Mixed-currency statements return validation.currency_consistency reporting any drift between statement-level and per-transaction currency.

  • What about scanned / photo statements?

    Vision LLMs handle PDF, JPEG, PNG, and HEIC. Image quality matters — phone photos work down to ~150 DPI equivalent. Statements with deep skew or partial occlusion drop confidence; field_metadata.<field>.confidence < 0.85 is the recommended escalation threshold for review.

  • Where is the data processed?

    Region-bound API keys (sk_eu_… vs sk_us_…) route to either eu-api.tryparsr.dev (Exoscale Frankfurt, EU operator) or us-api.tryparsr.dev (Hetzner Ashburn). EU keys never leave the EU. Object storage uses Cloudflare R2 with jurisdiction='eu' for the EU bucket — true residency, dedicated EU endpoint.

  • Can I cache results to avoid re-billing on retries?

    Yes — file-hash caching is built in. Re-uploading the same bytes within 30 days hits the cache, returns the original parse, and is not billed (billable=FALSE in usage events). Idempotency-Key on the request makes retry-safety explicit at the request level.

200 free pages. No credit card. No sales call.

Drop bank statements parsing into your stack in an afternoon. If it doesn't earn its keep, walk away — no lock-in.

Get an API key