Bank statement API: PDFs to JSON, Excel and CSV
Send a bank or credit card statement, get every transaction back as clean JSON, with each row checked against the running balance and the printed totals. The same converter as the website, from code. A free key gives 100 pages a month.
Get a free API key
Enter an email address. A 6-digit code arrives in a minute; enter it and the key is shown once. No password and no card.
The address is used for the key only, never shared. Already have keys? The same form opens the page that lists and revokes them. See Privacy.
Quick start
Every request sends the key as Authorization: Bearer <key> (or X-API-Key: <key>). Upload a file, poll until status is done, then read the JSON or download the spreadsheet.
export DOCFIG_API_KEY=dfk_...
# 1. Upload. Returns an id at once (HTTP 202).
curl -H "Authorization: Bearer $DOCFIG_API_KEY" \
-F file=@statement.pdf https://docfig.com/v1/statements
# 2. Poll every 2 seconds until "status": "done".
curl -H "Authorization: Bearer $DOCFIG_API_KEY" \
https://docfig.com/v1/statements/st_XXXX
# 3. The same Excel or CSV file the website makes.
curl -H "Authorization: Bearer $DOCFIG_API_KEY" -o statement.xlsx \
https://docfig.com/v1/statements/st_XXXX.xlsx
Password-protected PDF: add -F password=XXXX. The password opens the file in memory once and is never stored or logged.
Python
pip install docfig
import docfig
client = docfig.Client() # reads DOCFIG_API_KEY
st = client.convert("statement.pdf") # uploads, waits, returns the result
print(st.bank, st.checks["status"], len(st.transactions))
st.download("statement.xlsx") # or "statement.csv"
combined = client.combine(["jan.pdf", "feb.pdf", "mar.pdf"])
for p in combined.problems:
print(p["kind"], p["text"]) # gaps, carry-over, breaks
Errors are typed: docfig.AuthenticationError, PasswordRequiredError, WrongPasswordError, LimitReachedError (with upgrade_url), RateLimitError, InvalidFileError, ConversionFailedError. Rate limits and busy moments are retried for you.
Node.js
npm install docfig-api
import { DocFig } from "docfig-api";
const client = new DocFig(); // reads DOCFIG_API_KEY
const st = await client.convert("statement.pdf"); // path, Buffer or Blob
console.log(st.bank, st.checks.status, st.transactions.length);
await st.download("statement.xlsx");
const combined = await client.combine(["jan.pdf", "feb.pdf"]);
console.log(combined.counts);
Node 18 or later, no dependencies (native fetch). ESM and CommonJS, with TypeScript types.
MCP server for AI assistants
DocFig also runs as an MCP server, so Claude, ChatGPT, Cursor, VS Code and other AI apps can read statements for you. Four tools:
- check_transactions: checks a transactions table (CSV or rows) for balance errors. Free, no key, nothing uploaded.
- convert_statement: a statement PDF or photo to checked transactions, with Excel and CSV links that work for 60 minutes.
- combine_statements: several statements to one set of accounts, with missing months and duplicate rows found.
- usage: pages left this month.
The hosted server is https://mcp.docfig.com/mcp (Streamable HTTP). Send the key as Authorization: Bearer dfk_.... Files go by https link or as base64; the local server below can also read files on the computer.
Claude Desktop
Settings, Developer, Edit Config, then add to claude_desktop_config.json and restart Claude:
{
"mcpServers": {
"docfig": {
"command": "npx",
"args": ["-y", "docfig-api", "mcp"],
"env": { "DOCFIG_API_KEY": "dfk_..." }
}
}
}
With Python instead of Node: "command": "uvx", "args": ["docfig", "mcp"]. On Claude.ai, add a custom connector with the URL https://mcp.docfig.com/mcp/dfk_....
Cursor
Add to ~/.cursor/mcp.json (or .cursor/mcp.json in a project):
{
"mcpServers": {
"docfig": {
"url": "https://mcp.docfig.com/mcp",
"headers": {
"Authorization": "Bearer dfk_..."
}
}
}
}
VS Code
Add to .vscode/mcp.json. VS Code asks for the key once and stores it securely:
{
"servers": {
"docfig": {
"type": "http",
"url": "https://mcp.docfig.com/mcp",
"headers": {
"Authorization": "Bearer ${input:docfig-key}"
}
}
},
"inputs": [
{
"id": "docfig-key",
"type": "promptString",
"description": "DocFig API key",
"password": true
}
]
}
ChatGPT
In ChatGPT settings, turn on developer mode for connectors, then create a connector with the URL https://mcp.docfig.com/mcp/dfk_... and no authentication. The key is part of that URL, so keep it private; if it is ever shared, revoke the key on the keys page and make a new one.
Local server
# Node 18 or later
npx -y docfig-api mcp
# Python
uvx docfig mcp
Without uv: pip install docfig, then docfig mcp. Both read DOCFIG_API_KEY from the environment. check_transactions works without it. The hosted and local servers keep nothing: results are deleted after 60 minutes, and keys and file contents are never logged.
Endpoints
| Method and path | What it does |
|---|---|
POST /v1/statements | Upload one file (multipart/form-data, field file, optional password). Returns 202 with the statement id. |
GET /v1/statements/{id} | Status (queued, running, done, failed) and, when done, the result. |
GET /v1/statements/{id}.xlsx | Excel workbook: transactions, checks and flagged rows, as on the website. |
GET /v1/statements/{id}.csv | CSV: date, description, money out, money in, amount, balance, check. |
DELETE /v1/statements/{id} | Delete the result now instead of after 60 minutes. |
POST /v1/combine | JSON {"statements": ["st_...", ...]}: one set of accounts with missing months, overlaps, removed duplicates and balance carry-over checked. Add .xlsx or .csv to the path for the combined workbook. |
GET /v1/files/{token} | The signed Excel or CSV links in a result's downloads. They work without the key until expires_at, so share them only with people who may see the statement. |
GET /v1/usage | Pages used and left this month. |
GET /v1/openapi.json | OpenAPI 3.1 spec (no key needed). |
Response
Money is in the statement's currency as decimal numbers; amount is signed (money in positive). Account numbers are masked before the AI step, so only the last 4 digits come back.
{
"id": "st_4kQx...", "object": "statement", "status": "done", "pages": 2,
"result": {
"bank": "Fernhollow Bank", "statement_type": "bank", "currency": "USD",
"account": {"last4": "4417", "masked": "****4417"},
"period": {"start": "2026-03-01", "end": "2026-03-31"},
"opening_balance": 2450.18, "closing_balance": 3127.40,
"transactions": [
{"row": 1, "date": "2026-03-02", "description": "CARD PURCHASE GROCER",
"money_in": null, "money_out": 64.20, "amount": -64.2, "balance": 2385.98,
"page": 1, "flags": [], "notes": []}
],
"checks": {"status": "reconciled", "rows": 38, "rows_passed": 38,
"rows_flagged": 0, "balance_checks": 38, "balance_failures": 0,
"totals": {"opening_closing": {"ok": true, "expected": 3127.4,
"printed": 3127.4}}}
}
}
checks.status is reconciled (every check passed), check (some rows are flagged; each has flags and plain-English notes) or unverified (the statement prints no balances or totals to check against).
Errors
Errors return JSON {"error": {"code": "...", "message": "..."}} with a stable code.
| HTTP | code | Meaning |
|---|---|---|
| 401 | missing_key, invalid_key, revoked_key | Check the key header. |
| 400 | no_file, empty_file, bad_request | Nothing usable arrived. |
| 413 | file_too_large, too_many_pages | Over 20 MB or 100 pages. Split the file. |
| 415 | unsupported_type | Send a PDF, JPG, PNG or HEIC. |
| 422 | password_required, password_wrong | Locked PDF: send or fix password. |
| 422 | infected, unreadable | The virus scan refused it, or no statement text could be read. |
| 429 | rate_limited, too_many_jobs | Slow down; wait for Retry-After seconds. |
| 429 | monthly_limit | The month's pages are used; the error carries upgrade_url. |
| 404 | not_found | Unknown id, or the result was already deleted. |
| 409 | not_ready, failed | Download asked for before the conversion finished, or it failed. |
| 503 | capacity, busy | Daily capacity reached, or the converter is busy. Retry later. |
Limits
- Free: 100 pages a month per account (one email, up to 5 keys). A photo counts as one page. Failed files do not count.
- Files: PDF (text, scanned or password-protected), JPG, PNG or HEIC, up to 20 MB and 100 pages each.
- Rate: 10 uploads a minute, 120 reads a minute and 2 files converting at once, per key. Responses carry
X-RateLimit-Remaining. - More pages: the Starter, Pro and Business plans on pricing add 150, 500 or 2,000 pages a month, used after the free pages. Plans are in free early access.
What happens to the files
The same rules as the website: every upload is virus scanned, read in memory and dropped once its text is read. Account and card numbers are masked before the AI model sees the text. Results stay in memory for 60 minutes so they can be fetched and downloaded, then are deleted; DELETE removes them sooner. Nothing from a statement is written to disk or logged. Stored: the key's email, a hash of the key, and page counts. Details on the Security page.
Free reconciliation check for CSV files
Both SDKs include a command that checks a transactions table you already have, offline: no key, no network, no cost. It walks the running balance row by row and reports rows where the balance does not follow, debit and credit columns that look swapped, sign errors, duplicated rows, missing rows, dates out of order and totals that miss the opening or closing balance, each with a likely fix.
pip install docfig # or: npm install -g docfig-api
docfig check statement.csv --opening 2450.18 --closing 3127.40
docfig check statement.csv --json # for scripts; exit code 0 clean, 1 problems, 2 bad input
More
- OpenAPI 3.1 JSON for code generators and API clients (see the OpenAPI 3.1 specification).
- Questions, or a statement layout that did not convert well: contact.