Bank Statements
Upload a bank statement and extract transactions as structured data. Get JSON, CSV, or Excel. Accepts PDF, PNG, JPG, and WebP.
Quickstart
Same five-step flow as documents: create, upload, start, poll, 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: PDF (up to 100 MB), PNG/JPG/WebP (up to 20 MB).
Output formats: ?format=json, ?format=csv, or ?format=xlsx (Excel).
Statuses: awaiting_upload → processing → completed or failed.
/v1/bank-statementsCreate 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. PDF max 100 MB, images max 20 MB. |
| 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"
}/v1/bank-statementsList 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
}/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": false,
"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 | Whether a human should review the flagged items. false means every row was math-checked against the statement's running balances. |
| outputs | object | No | Available output formats (json, csv, xlsx). |
| error | object | No | Present when status is failed. See Errors. |
/v1/bank-statements/{id}/startStart 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).
/v1/bank-statements/{id}/outputGet a presigned download URL for the extracted data.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| format | string | Yes | json, csv, or xlsx. |
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.
/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": false,
"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": false,
"verification": {
"needs_review": false,
"has_opening_anchor": true,
"endpoints_tie_out": true,
"chain_broken": false,
"chain_breaks": [],
"flagged_issues": [],
"page_status_map": { "1": "reconciled" }
},
"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 if the transaction passed all verification checks. false means human review is recommended. See Verification below. |
Verification
Every extraction runs through two independent verification 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 is arithmetically verified.
2. Vision grounding
Each extracted amount is cross-checked against the raw OCR word positions in the original document. If an amount cannot be matched to a word at the expected position on the page, the transaction is flagged as ungrounded. This catches hallucinated or misread values.
needs_review
Every account — and the document as a whole — carries a boolean needs_review field.
needs_review: false
Every row was math-checked against the statement's running balances, from the opening balance through to the closing balance. Safe for automated processing.
needs_review: true
Either the balance chain broke at specific rows, or the statement prints fewer than two running balances so no math check was possible. Check the verification object to tell which one happened.
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. |
| chain_breaks | array | Yes | The specific rows where the balance chain broke, if any. |
| flagged_issues | array | Yes | Objects shaped { row_index?, page?, type, reason } for every item that needs review, including an INSUFFICIENT_BALANCES entry when the statement does not print enough running balances to math-check. |
| page_status_map | object | Yes | Reconciliation status per page, keyed by page number. |
Per-transaction verified field
Each transaction carries a boolean verified field. It is true only when both the balance chain and the vision grounding passed for that row. When processing results programmatically:
verified: true— the transaction is arithmetically correct and grounded in the source. Safe to process without review.verified: false— the balance chain broke at this row, or the amount could not be confirmed against the source document. Flag this transaction for human review.
Tip: The document-level needs_review is true if any account needs review. If needs_review is true, check verification.flagged_issues for the exact rows — each entry carries a row_index or page so you can locate it in transactions.
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. |
CSV example
date,time,description,reference,type,debit,credit,balance,page,verified,account_number,account_type
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,savingsExcel 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).