Bank Statements
Upload a bank statement and extract transactions as structured data. Get JSON, CSV, or best-effort XLSX. Accepts PDF, PNG, JPG, JPEG, and WebP images up to 20 MiB; PDFs up to 50 MiB.
This API is in private beta and access is allowlisted per account. Request access before creating a key.
What does the bank statement API return?
The API returns statement and transaction data as JSON or CSV, with XLSX available on a best-effort basis. Its review fields report whether balance-consistency and source-grounding checks found an issue. A no-issue result can support controlled automation; a flagged result should be compared with the source by a person. The signal is not a certification or a guarantee that every extracted value is correct.
Quickstart
Same five-step flow as documents: authenticate, create, upload to the presigned URL, start, poll, and download.
FASTOCR_KEY="your-api-key"
# 1. Create a bank statement job
curl -s -X POST https://api.fastocr.org/v1/bank-statements \
-H "Authorization: Bearer $FASTOCR_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filename":"statement.pdf","size_bytes":'$(wc -c < statement.pdf | tr -d ' ')'}'
# 2. Upload the file to the presigned URL (from step 1 response)
curl -s -X PUT "<upload_url>" \
-H "Content-Type: application/pdf" \
-T statement.pdf
# 3. Start extraction
curl -s -X POST https://api.fastocr.org/v1/bank-statements/<id>/start \
-H "Authorization: Bearer $FASTOCR_KEY"
# 4. Poll until ready (status: completed or failed)
curl -s https://api.fastocr.org/v1/bank-statements/<id> \
-H "Authorization: Bearer $FASTOCR_KEY"
# 5. Download as Excel
curl -s https://api.fastocr.org/v1/bank-statements/<id>/output?format=xlsx \
-H "Authorization: Bearer $FASTOCR_KEY"Accepted files: PDFs up to 50 MiB; PNG, JPG, JPEG, and WebP images up to 20 MiB.
Output formats: ?format=json, ?format=csv, or ?format=xlsx (best-effort Excel export).
Statuses: awaiting_upload → processing → completed or failed.
POST/v1/bank-statements
Create a new bank statement job and get a presigned upload URL.
Required headers
Idempotency-Key — a unique value per job. See Idempotency.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| filename | string | Yes | Must end with .pdf, .png, .jpg, .jpeg, or .webp. |
| size_bytes | integer | Yes | File size. PDFs up to 50 MiB (52,428,800 bytes); images up to 20 MiB (20,971,520 bytes). |
| sha256 | string | No | Hex-encoded SHA-256 checksum. |
| external_id | string | No | Your own correlation ID. |
Response 201
{
"id": "stmt_a1b2c3d4e5f6",
"status": "awaiting_upload",
"upload_url": "https://s3.amazonaws.com/...",
"expires_at": "2025-01-15T13:00:00Z"
}GET/v1/bank-statements
List your bank statement jobs with optional filtering. See Pagination for query parameters.
Response 200
{
"bank_statements": [
{
"id": "stmt_a1b2c3d4e5f6",
"status": "completed",
"external_id": null,
"pages_billed": 3,
"pages_total": 5,
"has_transactions": true,
"outputs": {
"json": { "available": true },
"csv": { "available": true },
"xlsx": { "available": true }
}
}
],
"next_cursor": null
}GET/v1/bank-statements/{id}
Get the current status and metadata for a single bank statement job.
Response 200
{
"id": "stmt_a1b2c3d4e5f6",
"status": "completed",
"external_id": null,
"pages_billed": 3,
"pages_total": 5,
"has_transactions": true,
"needs_review": true,
"outputs": {
"json": { "available": true },
"csv": { "available": true },
"xlsx": { "available": true }
}
}Response fields
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Statement job ID (starts with stmt_). |
| status | string | Yes | awaiting_upload, processing, completed, or failed. |
| external_id | string? | No | Your correlation ID, if set on create. |
| pages_billed | integer | No | Pages charged against your quota. |
| pages_total | integer | No | Total pages in the statement. |
| has_transactions | boolean | Yes | Whether transactions were extracted. |
| needs_review | boolean | No | Workflow signal from balance consistency and source-grounding checks. false means no issue was found by those checks; it is not a guarantee of correctness. |
| outputs | object | No | Available output formats. JSON and CSV are structured outputs; XLSX is best effort. |
| error | object | No | Present when status is failed. See Errors. |
POST/v1/bank-statements/{id}/start
Start extraction after the file has been uploaded. No request body required.
Response 202
{
"status": "processing"
}409 statement_not_startable— the job is not in a startable state.400 upload_not_found— the file has not been uploaded yet.400 size_mismatch— uploaded file size does not matchsize_bytes.429 concurrency_limit— too many jobs in flight. Retry after theRetry-Afterheader (30s).
GET/v1/bank-statements/{id}/output
Get a presigned download URL for the extracted data.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| format | string | Yes | json, csv, or xlsx (best effort). |
Response 200
{
"format": "xlsx",
"url": "https://s3.amazonaws.com/...",
"expires_at": "2025-01-15T13:00:00Z",
"pages_billed": 3,
"pages_total": 5
}The presigned URL expires in 1 hour. If the job is not yet complete, a 409 statement_not_ready error is returned. If the requested format is not available, a 404 output_not_found error is returned.
DELETE/v1/bank-statements/{id}
Delete a bank statement job and its files. Deletion is asynchronous — the job transitions to deleting and is cleaned up in the background.
Response 202
{
"status": "deleting"
}Returns 409 statement_processing if the statement is currently being processed.
Output Schema
When you download the JSON output (?format=json), you get a document object containing one or more accounts, each with its own transactions array. Multi-account statements (e.g. current + savings on the same PDF) are split automatically.
{
"status": "completed",
"needs_review": true,
"page_count": 3,
"accounts": [
{
"account_number": "12345678",
"account_type": "savings",
"bank_name": "Example Bank",
"account_holder": "Jane Smith",
"currency": "USD",
"period": {
"from": "01/06/2025",
"to": "30/06/2025"
},
"opening_balance": 5000000,
"closing_balance": 6234500,
"needs_review": true,
"verification": {
"needs_review": true,
"has_opening_anchor": true,
"endpoints_tie_out": true,
"chain_broken": false,
"flagged_issues": [{ "row_index": 2, "type": "SOURCE_GROUNDING", "reason": "Amount needs comparison with the source." }]
},
"totals": {
"credits": 3500000,
"debits": 2265500,
"net": 1234500,
"transaction_count": 28
},
"transactions": [
{
"date": "02/06/2025",
"time": null,
"description": "Direct Deposit - Payroll",
"reference": "PAY-2025-06",
"type": null,
"debit": null,
"credit": 250000,
"balance": 5250000,
"page": 1,
"verified": true
},
{
"date": "05/06/2025",
"time": "14:32",
"description": "POS Purchase - Grocery Store",
"reference": null,
"type": null,
"debit": 8750,
"credit": null,
"balance": 5162500,
"page": 1,
"verified": false
}
]
}
]
}Amounts are integer minor units
All money fields (debit, credit, balance, opening_balance, closing_balance, and totals.*) are integers in the currency's minor unit (e.g. cents for USD, paise for INR). Divide by 100 to get the display value: 250000 = 2,500.00. This avoids floating-point rounding errors.
Document-level fields
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | completed or no_transactions. |
| needs_review | boolean | Yes | Whether a human should review the flagged items. true if any account needs review. See Verification below. |
| page_count | integer | Yes | Total pages in the uploaded file. |
| accounts | array | Yes | One object per detected bank account. |
Account fields
| Field | Type | Required | Description |
|---|---|---|---|
| account_number | string? | No | Account number extracted from the statement, or null if not detected. |
| account_type | string? | No | E.g. savings, current, checking. |
| bank_name | string? | No | Name of the bank. |
| account_holder | string? | No | Account holder name. |
| currency | string? | No | ISO currency code (e.g. USD, GBP, INR). |
| period | object | No | { from, to } — statement date range as strings. |
| opening_balance | integer? | No | Opening balance in minor units. |
| closing_balance | integer? | No | Closing balance in minor units. |
| needs_review | boolean | Yes | Whether a human should review this account's flagged items. |
| verification | object | Yes | Evidence behind needs_review — see Verification below. |
| totals.credits | integer | Yes | Sum of all credit amounts in minor units. |
| totals.debits | integer | Yes | Sum of all debit amounts in minor units. |
| totals.net | integer | Yes | credits minus debits in minor units. |
| totals.transaction_count | integer | Yes | Number of transactions extracted. |
| transactions | array | Yes | Transaction objects for this account. |
Transaction fields
| Field | Type | Required | Description |
|---|---|---|---|
| date | string | Yes | Transaction date as printed on the statement. |
| time | string? | No | Transaction time, if present. null otherwise. |
| description | string | Yes | Transaction description / narration. |
| reference | string? | No | Reference or cheque number. null if not present. |
| type | string? | No | Transaction type (e.g. ATM, POS, NEFT). null if not detected. |
| debit | integer? | No | Debit amount in minor units. null if this is a credit transaction. |
| credit | integer? | No | Credit amount in minor units. null if this is a debit transaction. |
| balance | integer? | No | Running balance after this transaction in minor units. null if the statement does not print running balances. |
| page | integer | Yes | Page number where this transaction appears (1-indexed). |
| verified | boolean | Yes | true only when the row passes both the arithmetic-chain and source-grounding checks. false is a review signal. See Verification below. |
Verification
Every extraction runs through arithmetic-chain and source-grounding checks. Both must pass for a transaction to be marked verified: true.
1. Balance chain reconciliation
Starting from the opening balance, the engine walks each transaction in order and verifies that previous_balance + credit - debit = current_balance using integer arithmetic (no floating-point). If the chain holds from the opening balance through to the closing balance, the account passes the arithmetic-chain check.
2. Source grounding
Each extracted amount is cross-checked against the raw OCR word positions in the original document. If an amount cannot be matched to source text at the expected position on the page, the transaction is flagged for review. This can identify values that need comparison with the original document.
needs_review
Every account — and the document as a whole — carries a boolean needs_review field.
needs_review: false
The configured balance and source-grounding checks found no issue that requires review. This is a workflow signal, not a guarantee; apply your own controls before automation.
needs_review: true
A balance chain or source-grounding check found an issue, or there was not enough balance data to check it. Check the verification object and route the affected items to human review.
The verification object
Each account carries a verification object with the evidence behind its needs_review value.
| Field | Type | Required | Description |
|---|---|---|---|
| needs_review | boolean | Yes | Same value as the account-level needs_review field. |
| has_opening_anchor | boolean | Yes | Whether an explicit opening balance row was found on the statement. |
| endpoints_tie_out | boolean | Yes | Whether the computed closing balance matches the printed closing balance. |
| chain_broken | boolean | Yes | Whether the running-balance chain broke at one or more rows. |
| flagged_issues | array | Yes | Objects shaped { row_index?, page?, type, reason } for items that need review. Rows or pages are identified when available; broader document or account issues may also appear. |
Per-transaction verified field
Each transaction carries a boolean verified field. It is true only when both the arithmetic-chain and source-grounding checks passed for that row. When processing results programmatically:
verified: true— the transaction passed the arithmetic-chain and source-grounding checks. It is a candidate for a no-review workflow under your own controls.verified: false— a check failed or could not be completed. Flag this transaction for human review against the source document.
Tip: The document-level needs_review is true if any account needs review. If needs_review is true, check verification.flagged_issues for the row or page details when available. Some entries can describe broader document or account issues rather than one exact transaction row.
CSV & Excel Format
The CSV and Excel outputs flatten all accounts into a single table with a fixed column order. Multi-account statements include account_number and account_type columns so you can filter by account.
Columns
| Column | Type | Notes |
|---|---|---|
| date | string | Transaction date as printed. |
| time | string | Empty if not present on statement. |
| description | string | Transaction narration. |
| reference | string | Reference/cheque number. Empty if not present. |
| type | string | Transaction type (ATM, POS, NEFT, etc.). Empty if not detected. |
| debit | number | Debit amount. In JSON: integer minor units. In Excel: decimal (e.g. 87.50). Empty for credit rows. |
| credit | number | Credit amount. Same format rules as debit. Empty for debit rows. |
| balance | number | Running balance after the transaction. Same format rules as debit/credit. |
| page | integer | 1-indexed page number. |
| verified | boolean | true or false. See Verification above. |
| account_number | string | Identifies which account this row belongs to in multi-account statements. |
| account_type | string | E.g. savings, current, checking. |
| flag | string | Always the last column. Empty when nothing is wrong with the row; otherwise the problem types joined with +. best-effort means the row was read from the page but could not be math-checked, so it is returned with verified: false. In JSON the same information arrives as flag: [{ type, detail }], where detail says which check was unavailable: no-balance-column (the layout prints no running balance), chain, missing-balance, missing-amount, y-band, or dropped-sibling-row (another row on the same page could not be used, so nothing on that page is treated as checked). |
CSV example
date,time,description,reference,type,debit,credit,balance,page,verified,account_number,account_type,flag
02/06/2025,,Direct Deposit - Payroll,PAY-2025-06,,,,5250000,1,true,12345678,savings,
05/06/2025,14:32,POS Purchase - Grocery Store,,,8750,,5162500,1,false,12345678,savings,best-effort
06/06/2025,,ATM Withdrawal,,,20000,,,1,false,12345678,savings,best-effortExcel workbook
The .xlsx file contains two sheets:
- Transactions — same columns as the CSV. Money values are formatted as decimal numbers (not minor units). Rows where
verifiedisfalseare highlighted with an amber background so they stand out for review. The sheet includes frozen headers and auto-filters. - Summary — document status,
needs_review, and page count at the top, followed by a section per account with: account number, account type, bank name, account holder, currency, statement period, opening/closing balance, accountneeds_review, and computed totals (credits, debits, net, transaction count).
Note on amount formats: JSON uses integer minor units (e.g. 250000 = 2,500.00). CSV also uses integer minor units. Excel converts to decimal numbers with #,##0.00 formatting, so amounts display naturally (e.g. 2,500.00).