Bank statements in, structured data out.
Send any bank’s statement: a PDF or a spreadsheet in English or Arabic, or a scan in English. Get back every transaction with its category, the balance check, insights, and on request the tamper check and the affordability report: the same results as the app, as JSON. Nothing is kept.
- Base URL
- https://docsanalyzer.com
- Auth
- Bearer key
- Files
- PDF, scan, photo, XLS, XLSX, CSV · 25 MB
- Rate
- 60 requests a minute per key
- Kept
- In memory, one hour at most
What teams build with it
- Lenders and brokers. Income, obligations and the debt burden ratio from an applicant’s statements, with the tamper check before a decision.
- Accounting firms. Client statements straight into the books: categorized rows that reconcile, in your own system’s format.
- Landlords and onboarding. Proof of income and balance, read the same way every time, with nothing stored afterwards.
Quickstart
Make a key on your account page and keep it in DOCSANALYZER_KEY. It is shown once; we store only a hash of it. Then: send, poll, read, delete.
import os, time, requests
BASE = "https://docsanalyzer.com"
H = {"Authorization": f"Bearer {os.environ['DOCSANALYZER_KEY']}"}
# 1. Send the statement
with open("statement.pdf", "rb") as f:
r = requests.post(f"{BASE}/v1/statements", headers=H, files={"file": f})
r.raise_for_status()
sid = r.json()["id"]
# 2. Poll until it is read
while True:
res = requests.get(f"{BASE}/v1/statements/{sid}", headers=H).json()
if res["status"] != "reading":
break
time.sleep(2)
if res["status"] == "failed":
raise SystemExit(res["error"]["message"])
# 3. Use it, then delete it
print(res["statement"]["check"]["status"]) # PASS when every balance adds up
for t in res["statement"]["transactions"]:
print(t["PostingDate"], t["Description"], t["Debit"], t["Credit"], t["Balance"], t["Category"])
requests.delete(f"{BASE}/v1/statements/{sid}", headers=H)Endpoints
POST /v1/statements
Multipart form with the document in file, and password for a locked PDF (used once to open it, never kept). Answers 202 with {"id", "status": "reading"}. The pages are checked against your plan before reading starts.
GET /v1/statements/{id}
status is reading, done or failed. Once done, the result below; once failed, error.message, a sentence you can show. A text PDF takes about a second; a scan up to a minute a page. Poll every couple of seconds.
DELETE /v1/statements/{id}
Removes the result at once. Without it, a result goes an hour after it was sent.
The result
| Field | What it holds |
|---|---|
statement.transactions[] | Every row: PostingDate, Description, Debit, Credit, Balance, Currency and Category; Payee, Kind and Reference where they are found; Cells, the row in the statement’s own columns. |
statement.check | The balance check: status PASS or FAIL, and the arithmetic in detail (opening + credits − debits = closing, and each running balance). |
statement.summary | What the statement says about itself: holder, account, IBAN, period, currency, opening and closing balance, as statement_fields. |
statement.source_columns | The column names as the statement prints them, in order. |
insights.currencies.{code} | Per currency: overview (totals, the daily balance, spending by category, top payees), months, category_by_month, recurring payments and the insights, each with the rows behind it. |
integrity | The tamper check: level, a plain label, and the signals found (editor in the PDF’s history, later revisions, mixed fonts, boxes over figures, balances that do not add up). |
affordability | The affordability report: balances, salary regularity, monthly obligations and the debt burden ratio, per currency. |
pages | Pages counted against your plan for this statement. |
{
"id": "3f2a…", "status": "done", "pages": 2,
"statement": {
"transactions": [
{"PostingDate": "03-08-2026", "Description": "CARREFOUR CITY CENTRE", "Debit": 212.4, "Credit": 0,
"Balance": 7000.41, "Currency": "AED", "Category": "Groceries", "Cells": {"Date": "03-08-2026", …}},
…
],
"check": {"status": "PASS", "detail": "Opening 8,420.55 + credits … = 28,864.00; …"},
"summary": {"statement_fields": [{"label": "currency", "value": "AED"}, …]},
"source_columns": ["Date", "Description", "Debit", "Credit", "Balance"]
},
"insights": {"currencies": {"AED": {"overview": {…}, "months": […], "recurring": […], "insights": […]}}},
"integrity": {"level": "none", "label": "No signs of editing found", "signals": []},
"affordability": {"currencies": [{"currency": "AED", "balances": {…}, "dbr": {…}}]}
}Errors and limits
Every error is {"error": {"code", "message"}}; the message is a sentence you can show your users.
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Not multipart/form-data, or no field named file. |
| 401 | no_key · bad_key | No Bearer key, or a key that is wrong or revoked. |
| 402 | upgrade · over_limit | The plan has no API, or the pages left are not enough (with when they reset). |
| 404 | not_found | Deleted, someone else’s, or past its hour. |
| 413 | too_large | Over 25 MB. |
| 422 | password_required · password_wrong | A locked PDF: send or correct its password. |
| 429 | rate_limited | Over 60 requests a minute for this key; wait for Retry-After. |
Pages count against your plan exactly as in the app: a PDF or photo page is one page, a spreadsheet one page for every 50 rows. There are no webhooks: poll the result.
What happens to the data
- The file is read in memory on our own server, by our own models. No cloud AI and no outside service sees it.
- Nothing is written to disk. The result is held in memory for an hour at most, or until you delete it.
- A locked PDF’s password opens it once and is forgotten.
- We keep your account, page counts and a hash of each key; never a statement. See privacy.