Read a bank statement
A PDF bank statement is a table pretending to be a document, and pulling transactions out of it by hand is the single most tedious part of reconciliation. This endpoint reads a statement — any bank's layout — and returns each transaction's date, description and amount as structured JSON, ready to reconcile or categorize.
Run it online
Run this on our servers with your account. Free tools run in your browser; this one bills your KIT balance per the price above.
Why statements are harder than they look
Every bank formats its statement PDF differently: some list a running balance column, some split debits and credits into separate columns, some wrap long merchant descriptions onto a second line that a naive text extractor treats as an unrelated row. Multiply that by a business with accounts at three or four banks, or an accountant handling dozens of clients, and manual transcription stops being a task and becomes a bottleneck. Even copy-pasting from a PDF viewer often scrambles the column order, which is why so many people still end up retyping numbers by hand.
Who feeds statements through this
Bookkeepers and accountants reconciling client accounts without native bank feeds, personal-finance apps letting users import history from any bank by uploading a PDF, auditors sampling transaction history from statements rather than live feeds, and underwriting tools that need a structured read of cash flow from bank statements submitted as part of a loan application.
What the output looks like
Send the statement, get a task_id immediately, and the completed job returns account details, statement period, and every transaction line — date, description, amount, and running balance where present — as an ordered JSON array, ready to load into a ledger or a reconciliation script without re-parsing a PDF table.
Where it sits in a reconciliation pipeline
Because it's asynchronous and priced per page, a one-page statement with a handful of transactions costs a fraction of a twelve-page annual statement, scaling fairly with what's actually being read: a reconciliation tool ingests uploaded statements, submits each on receipt, and the webhook delivers the transaction list at the moment it's ready to be matched against the general ledger.
Reading a format never designed to be read by code
Bank statements were designed for a person scanning a printed page, not for a program parsing structured data — that's the root of the difficulty. Column alignment that looks obvious to a human eye carries no explicit structure in the underlying PDF, so extracting a table of transactions accurately requires understanding statement layout as a category, not just running generic text extraction over the page.
What you can do with it
Bookkeeping reconciliation
An accountant uploads a client's monthly PDF statement and matches the extracted transactions against recorded entries in minutes.
Personal finance import
A budgeting app lets a user upload a statement from any bank and builds a transaction history without a live bank connection.
Loan underwriting
A lender extracts cash-flow patterns from applicant-submitted bank statements as part of an automated underwriting check.
Audit sampling
An auditor pulls structured transaction data from historical statements to sample and verify against internal records.
FAQ
How does the Bank Statement Parser API work?
Send the statement file to POST /ocr/bank-statement, receive a task_id right away, and get the transaction list by webhook or a signed link once parsing finishes.
Is it free to test?
There's no free tier — free tiers get abused and slow everyone down. Access runs on a prepaid ForHosting KIT balance: top up from $10.00 (it never expires) and each request is charged at its published price, so a call with no balance returns HTTP 402. No subscription, no tokens, no invented credits, and a failed task is never charged.
How much does it cost?
$0.010 per request plus $0.0575 per page, published and flat, so a longer statement costs proportionally more.
Does it work with any bank's statement format?
Yes, it's built to read statement layouts generally rather than a fixed template per bank, so it adapts across formats without per-bank configuration.
What transaction fields does it return?
Date, description, amount and running balance where the statement shows one, along with account details and the statement period.
Can it handle a scanned paper statement, not just a PDF export?
Yes, clear scans work as well as digital PDF exports; a legible source page gives the most complete extraction.
Is my financial data kept?
No. Data is deleted after the retention period and is never used for training; results are delivered by signed webhook or a link valid 24 hours.
Am I billed if parsing fails?
No. A failed task is retried automatically up to three times and never charged; you get a clear error instead.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/ocr/bank-statement \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"…"}'const res = await fetch("https://api.kit.forhosting.com/ocr/bank-statement", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"input": "…"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/ocr/bank-statement",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"input": "…"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/ocr/bank-statement", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"input":"…"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"input":"…"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/ocr/bank-statement", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"input": "…"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "ocr.bank_statement",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 25 |
max_pages | 10 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
413 | input_too_large | The file exceeds the size limit. |
422 | task_failed | The task failed after 3 retries. You are never charged for it. |