Skip to main content
API Docs

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.

quickstart.sh
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.

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

FieldTypeRequiredDescription
filenamestringYesMust end with .pdf, .png, .jpg, .jpeg, or .webp.
size_bytesintegerYesFile size. PDF max 100 MB, images max 20 MB.
sha256stringNoHex-encoded SHA-256 checksum.
external_idstringNoYour own correlation ID.

Response 201

response
{
  "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

response
{
  "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

response
{
  "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

FieldTypeRequiredDescription
idstringYesStatement job ID (starts with stmt_).
statusstringYesawaiting_upload, processing, completed, or failed.
external_idstring?NoYour correlation ID, if set on create.
pages_billedintegerNoPages charged against your quota.
pages_totalintegerNoTotal pages in the statement.
has_transactionsbooleanYesWhether transactions were extracted.
needs_reviewbooleanNoWhether a human should review the flagged items. false means every row was math-checked against the statement's running balances.
outputsobjectNoAvailable output formats (json, csv, xlsx).
errorobjectNoPresent 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

response
{
  "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 match size_bytes.
  • 429 concurrency_limit — too many jobs in flight. Retry after the Retry-After header (30s).
GET/v1/bank-statements/{id}/output

Get a presigned download URL for the extracted data.

Query parameters

FieldTypeRequiredDescription
formatstringYesjson, csv, or xlsx.

Response 200

response
{
  "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

response
{
  "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.

output.json
{
  "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

FieldTypeRequiredDescription
statusstringYescompleted or no_transactions.
needs_reviewbooleanYesWhether a human should review the flagged items. true if any account needs review. See Verification below.
page_countintegerYesTotal pages in the uploaded file.
accountsarrayYesOne object per detected bank account.

Account fields

FieldTypeRequiredDescription
account_numberstring?NoAccount number extracted from the statement, or null if not detected.
account_typestring?NoE.g. savings, current, checking.
bank_namestring?NoName of the bank.
account_holderstring?NoAccount holder name.
currencystring?NoISO currency code (e.g. USD, GBP, INR).
periodobjectNo{ from, to } — statement date range as strings.
opening_balanceinteger?NoOpening balance in minor units.
closing_balanceinteger?NoClosing balance in minor units.
needs_reviewbooleanYesWhether a human should review this account's flagged items.
verificationobjectYesEvidence behind needs_review — see Verification below.
totals.creditsintegerYesSum of all credit amounts in minor units.
totals.debitsintegerYesSum of all debit amounts in minor units.
totals.netintegerYescredits minus debits in minor units.
totals.transaction_countintegerYesNumber of transactions extracted.
transactionsarrayYesTransaction objects for this account.

Transaction fields

FieldTypeRequiredDescription
datestringYesTransaction date as printed on the statement.
timestring?NoTransaction time, if present. null otherwise.
descriptionstringYesTransaction description / narration.
referencestring?NoReference or cheque number. null if not present.
typestring?NoTransaction type (e.g. ATM, POS, NEFT). null if not detected.
debitinteger?NoDebit amount in minor units. null if this is a credit transaction.
creditinteger?NoCredit amount in minor units. null if this is a debit transaction.
balanceinteger?NoRunning balance after this transaction in minor units. null if the statement does not print running balances.
pageintegerYesPage number where this transaction appears (1-indexed).
verifiedbooleanYestrue 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.

FieldTypeRequiredDescription
needs_reviewbooleanYesSame value as the account-level needs_review field.
has_opening_anchorbooleanYesWhether an explicit opening balance row was found on the statement.
endpoints_tie_outbooleanYesWhether the computed closing balance matches the printed closing balance.
chain_brokenbooleanYesWhether the running-balance chain broke at one or more rows.
chain_breaksarrayYesThe specific rows where the balance chain broke, if any.
flagged_issuesarrayYesObjects 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_mapobjectYesReconciliation 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

ColumnTypeNotes
datestringTransaction date as printed.
timestringEmpty if not present on statement.
descriptionstringTransaction narration.
referencestringReference/cheque number. Empty if not present.
typestringTransaction type (ATM, POS, NEFT, etc.). Empty if not detected.
debitnumberDebit amount. In JSON: integer minor units. In Excel: decimal (e.g. 87.50). Empty for credit rows.
creditnumberCredit amount. Same format rules as debit. Empty for debit rows.
balancenumberRunning balance after the transaction. Same format rules as debit/credit.
pageinteger1-indexed page number.
verifiedbooleantrue or false. See Verification above.
account_numberstringIdentifies which account this row belongs to in multi-account statements.
account_typestringE.g. savings, current, checking.

CSV example

transactions.csv
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,savings

Excel 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 verified is false are 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, account needs_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).