Bank statements are the hardest common document in business workflows, and the one most often parsed by hand. Lenders need them for underwriting, accountants for reconciliation, property managers for income verification, fintechs for anything the aggregators don’t cover — and every bank formats its statements differently, changes those formats without notice, and spreads transaction tables across many pages.
This guide covers what a statement parser needs to extract, why bank statements defeat template-based tools, how to validate extracted transactions mathematically, and how the API integration works end to end.
What a bank statement contains
Despite wildly different layouts, virtually every consumer and business bank statement carries the same information:
- ▪Account identity — institution name, account holder, masked account number, statement period start and end dates
- ▪Balance summary — opening balance, closing balance, and usually totals for deposits/credits and withdrawals/debits
- ▪The transaction table — one row per transaction with date, description, amount, and (at most banks) a running balance column
- ▪Section structure — many banks split the table into separate sections for deposits, checks, electronic withdrawals, and fees, each with its own subtotal
- ▪Noise you must survive — marketing inserts, overdraft notices, page headers repeating mid-table, and multi-account statements that restart the whole structure per account
The extraction schema
A production statement schema captures the summary once and the transactions as an array. This is the shape used in our lending use case:
| Field | Type | Required | Notes |
|---|---|---|---|
| bank_name | string | Yes | |
| account_holder | string | Yes | |
| account_number_last4 | string | Yes | Statements mask all but the last digits |
| statement_period_start | date | Yes | |
| statement_period_end | date | Yes | |
| opening_balance | currency | Yes | Anchor for math validation |
| closing_balance | currency | Yes | Anchor for math validation |
| total_deposits | currency | No | Bank-stated credit total |
| total_withdrawals | currency | No | Bank-stated debit total |
| transactions | array | Yes | Array of { date, description, amount, balance } |
Why statements defeat templates — and what works instead
Invoice-style template parsers fail on bank statements for a structural reason: there is no standard. Chase, Bank of America, Wells Fargo, every credit union, and every neobank lay out the same information differently — different column orders, different section groupings, different date formats, negative amounts as minus signs or parentheses or a separate debit column. A template per bank means maintaining hundreds of templates that silently break on every redesign.
The failure modes are specific and repeatable: transaction tables that span pages with repeated headers mid-table, multi-line transaction descriptions that wrap into what looks like a new row, statement sections that restart numbering per account, and check images embedded between table sections.
Schema-first extraction flips the problem: instead of describing where data lives on each bank’s page, you describe what you want (the schema above), and the extraction maps any layout onto it. New bank, redesigned statement — the same schema slug on the shared endpoint, with no new template.
API integration
One API call per statement; credit use depends on page count, at one credit per 5-page block:
Request — POST /api/v1/extract
curl -X POST https://dokyumi.com/api/v1/extract \
-H "Authorization: Bearer dk_live_your_api_key" \
-F "file=@march-statement.pdf" \
-F "schema=bank-statement-parser"Response
{
"id": "8f5416be-972d-48ac-99b1-5c8edfe2a738",
"status": "completed",
"request_id": "a2f59b9e-b8bd-4158-93b3-0cd190207471",
"schema": "bank-statement-parser",
"data": {
"bank_name": "First Meridian Bank",
"account_holder": "Hazel & Pine Cafe LLC",
"account_number_last4": "4471",
"statement_period_start": "2026-03-01",
"statement_period_end": "2026-03-31",
"opening_balance": 18240.55,
"closing_balance": 21874.03,
"total_deposits": 46110.20,
"total_withdrawals": 42476.72,
"transactions": [
{
"date": "2026-03-02",
"description": "DEPOSIT - CARD SETTLEMENT 03/01",
"amount": 1834.90,
"balance": 20075.45
},
{
"date": "2026-03-03",
"description": "ACH DEBIT - SYSCO FOODS INV 88213",
"amount": -1211.38,
"balance": 18864.07
}
]
},
"confidence": {
"opening_balance": 0.99,
"closing_balance": 0.99,
"transactions": 0.94
},
"validation": {
"valid": true,
"errors": [],
"low_confidence_fields": []
},
"meta": {
"processing_time_ms": 6480,
"page_count": 8,
"credits_used": 2,
"ocr_cached": false,
"model": "anthropic/claude-sonnet-4"
}
}Note page_count: 8 — an eight-page statement is one submission costing 2 credits (one per 5 pages), not eight separately metered pages. That distinction is the whole cost model for statement-heavy workflows, because underwriting typically wants three months of statements per applicant.
Amounts are normalized to signed floats (credits positive, debits negative) regardless of how the bank printed them, and dates arrive as ISO 8601 no matter the source format.
Math validation: the statement parser’s superpower
Bank statements are the one document type where you can verify extraction correctness arithmetically. If opening_balance plus the sum of transaction amounts equals closing_balance, the transaction table is almost certainly complete and correct — a dropped row, a duplicated row, or a misread amount breaks the equation immediately. Run that check on every extraction; it costs one line of code.
Use the model-reported confidence map and validation details alongside arithmetic checks. When the model reports a score for the transactions array, apply the schema threshold; on any review result, inspect both validation errors and low-confidence fields before accepting totals.
What it costs
Open-ended per-page pricing punishes bank statements more than any other document. Dokyumi uses flat monthly credit tiers instead: each five-page block consumes one credit, so longer statements use a predictable number of credits.
At two credits per statement, an underwriting flow pulling 3 statements per applicant can process about 80 applicants/month within Starter's 500-credit allowance. The free tier's 25 credits are enough to benchmark representative bank formats before paying.
- ▪Free — $0/month: 25 extraction credits, 2 schemas. Enough to validate extraction quality on your real documents before paying anything.
- ▪Starter — $99/month: 500 extraction credits, 10 schemas, REST API and webhook delivery.
- ▪Growth — $499/month: 3,000 extraction credits, 50 schemas, 25 white-label upload portals.
- ▪Enterprise — quoted: custom volume, documents beyond the 50-page self-serve limit, unlimited schemas and portals, schemas built for you.
- ▪One credit covers a document of up to 5 pages. A 6–10 page document uses 2 credits, 11–15 uses 3, and so on up to the 50-page self-serve ceiling — no per-page metering and no overage billing.
Full details on the pricing page.