Open the app
● API · Business plan

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

FieldWhat 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.checkThe balance check: status PASS or FAIL, and the arithmetic in detail (opening + credits − debits = closing, and each running balance).
statement.summaryWhat the statement says about itself: holder, account, IBAN, period, currency, opening and closing balance, as statement_fields.
statement.source_columnsThe 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.
integrityThe 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).
affordabilityThe affordability report: balances, salary regularity, monthly obligations and the debt burden ratio, per currency.
pagesPages 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.

StatusCodeWhen
400bad_requestNot multipart/form-data, or no field named file.
401no_key · bad_keyNo Bearer key, or a key that is wrong or revoked.
402upgrade · over_limitThe plan has no API, or the pages left are not enough (with when they reset).
404not_foundDeleted, someone else’s, or past its hour.
413too_largeOver 25 MB.
422password_required · password_wrongA locked PDF: send or correct its password.
429rate_limitedOver 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.