Bank Statement PDF Extraction API
A bank statement API that extracts every transaction from a customer-supplied PDF — not an Open Banking feed. Get JSON inline, or XLSX and CSV as files.
curl -X POST https://api.mainbook.ai/api/v1/developer/jobs \
-H "Authorization: Bearer mb_live_…" \
-H "Content-Type: application/json" \
-d '{"filename":"march.pdf","file_format":"pdf","size_bytes":184320,"page_count":4}'{ "job_id": "8f14e45f-ceea-467a-9b2e-3f7c1a5d20b1", "credits_reserved": 4, "upload": { "url": "https://mainbook.nyc3.digitaloceanspaces.com/…", "method": "PUT", "headers": { "Content-Type": "application/pdf" }, "expires_at": "2026-08-14T09:15:00Z" }}This is a document API, not Open Banking
The source is a customer-supplied statement, not a connected bank account.
The same endpoint works as a bank statement to JSON API; scanned PDFs run through OCR — a bank statement OCR API in practice. For live account access, use an Open Banking provider.
How a job moves from PDF to result
Five ordered calls carry the file through eight public states; polling is the supported mechanism.
The five calls
- 01request
POST /api/v1/developer/jobsRequired: filename · file_format · size_bytes · page_count. Reserves page credits and returns upload instructions.
- 02request
PUT to the presigned upload.urlPUT within about 15 minutes. Return every upload.headers value verbatim; the headers are signed.
- 03request
POST /api/v1/developer/jobs/{job_id}/startStart after the PUT. Safe to retry after a timeout; it will not enqueue the same job twice.
- 04request
GET /api/v1/developer/jobs/{job_id}Poll every 3–5 seconds. Stop when the job enters any of five terminal states.
- 05result
GET /api/v1/developer/jobs/{job_id}/result?type=jsonRequired query: type, not format. Accepts json · xlsx · csv.
Keep polling
3 in-flight states · every 3–5sawaiting_uploadThe job is waiting for the presigned PDF upload.
queuedThe uploaded job is waiting for processing.
processingThe statement is being processed.
Stop polling
5 terminal statessucceededProcessing finished without warnings.
succeeded_with_warningsProcessing finished and the result carries warnings to review.
insufficient_creditsThe job stopped because the page-credit balance was insufficient.
failedThe job stopped with an error.
expiredThe upload window or reserved-credit hold closed before the job started.
What the result actually contains
Statement facts stay separate from transaction rows, whether the consumer reads JSON or opens Excel.
{ "document": { "bank_name": "Chase", "account_number_masked": "••••4821", "account_type": "checking", "currency": "USD", "period_start": "2026-03-01", "period_end": "2026-03-31", "starting_balance_cents": 412750, "ending_balance_cents": 388105, "net_credits_cents": 540000, "net_debits_cents": 564645, "transactions_count": 63 }, "transactions": [ { "row": 1, "date": "2026-03-03", "description": "ACH CREDIT · PAYROLL", "amount_cents": 240000, "transaction_type": "credit", "balance_after_cents": 652750, "currency": "USD", "validation_status": "ok", "warning_flags": [] } ], "has_warnings": false}Chase
bank
USD
currency
63
transactions
| A | B | C | D | E |
|---|---|---|---|---|
| Date | Description | Amount | Balance | Status |
| 2026-03-03 | ACH CREDIT · PAYROLL | +$2,400.00 | $6,527.50 | ok |
| 2026-03-04 | ACH DEBIT · RENT | −$1,525.00 | $5,002.50 | ok |
| 2026-03-05 | CARD · WHOLE FOODS | −$86.32 | $4,916.18 | ok |
| 2026-03-05 | CARD · STARBUCKS | −$18.75 | $4,897.43 | ok |
| 2026-03-06 | CARD · AWS | −$780.40 | $4,117.03 | ok |
| 2026-03-07 | CARD · OFFICE DEPOT | −$214.67 | $3,902.36 | ok |
| 2026-03-09 | ZELLE PAYMENT · J. MILLER | −$250.00 | $3,652.36 | ok |
| 2026-03-10 | CARD · T-MOBILE | −$65.49 | $3,586.87 | ok |
| 2026-03-11 | ACH CREDIT · CLIENT PAYMENT | +$450.00 | $4,036.87 | ok |
| 2026-03-12 | CARD · DROPBOX | −$39.95 | $3,996.92 | ok |
| 2026-03-13 | CARD · UBER | −$27.40 | $3,969.52 | ok |
| 2026-03-16 | ACH DEBIT · VENDOR PAYMENT | −$1,820.00 | $2,149.52 | ok |
| 2026-03-17 | ACH CREDIT · PAYROLL | +$2,400.00 | $4,549.52 | ok |
| 2026-03-18 | CARD · SHELL OIL | −$94.27 | $4,455.25 | ok |
| 2026-03-19 | CARD · ADOBE | −$59.99 | $4,395.26 | ok |
- Money in integer cents
- ISO 8601 dates
- Sorted keys
- UTF-8
The complete field list, nullable card fields, running-balance rules, and live types are in the OpenAPI schema.
Every job says whether the maths held
Read the three validation fields together; each answers a different question about the result.
Totals match
Opening + credits − debits = closing
reconcilable- true
- Both sides plus opening and closing figures exist. False means nothing to check; null means no verdict.
passed- true
- No warnings. If reconcilable is false, this does not confirm the arithmetic.
mismatched_rows- 0
- Rows whose own arithmetic needs human review.
"validation": { "reconcilable": true, "passed": true, "mismatched_rows": 0 }What to know before production
Keep the secret server-side, build to the published limits, and branch on stable reason codes.
Authorization: Bearer mb_live_…server only · full secret shown onceKeep it on your server
Never expose the account key in browser JavaScript.
Browser requests are refused
Third-party origins receive an opaque network error.
Idempotency-Key · optional
Same key plus a new body conflicts; v1 publishes no TTL.
Production limits
File boundary
- 500 pages
- maximum per file
- 50 MB
- maximum per file
Request pace
- 6 jobs
- in flight per API key
- 5 per second
- job creates or starts
- 2 per second
- status polls
- 2 per 10 seconds
- result downloads
- 30 per minute
- failed auth attempts per IP
Account and storage
- 20
- active keys per account
- 90 days
- document and result retention
| reason | HTTP | What to do |
|---|---|---|
invalid_key | 401 | Send a valid Bearer key; 401 uses {"detail":"invalid_key"} instead of reason. |
insufficient_credits | 402 | Top up page credits; the body carries available and requested. |
job_not_terminal | 409 | Keep polling the job and request the result only after a terminal state. |
rate_limited | 429 | Wait for the Retry-After delay before retrying. |
engine_paused | 503 | Processing is behind the global backstop; do not treat the message as a stable code. |
Except for the 401 body, branch on reason, not on the human-readable message. Rate limits return 429 with Retry-After. The live OpenAPI schema is the complete, current error contract.
API calls require accepted API Terms. Read how files are handled in Security; the remaining data terms are in the Privacy Policy and Data Processing Agreement.
Frequently asked questions
- Is this an Open Banking API?
- No. It turns a PDF the account holder already has into structured data. It does not log in to a bank, show a consent screen, open a live transaction feed or poll a bank balance.
- Can it read a scanned statement or a photo?
- The public API accepts a PDF, and a scanned PDF goes through OCR in the same pipeline as a digital one. If the source is a photo, put the image in a PDF before sending it; file_format accepts pdf.
- What formats can I get back?
- JSON inline, or XLSX and CSV as file attachments. The required result query parameter is named type and accepts json, xlsx or csv.
- How do I know the numbers are right?
- A finished job includes validation with reconcilable, passed and mismatched_rows. Read reconcilable and passed together: passed while reconcilable is false means nothing was off, not that the arithmetic was confirmed.
- How long does a conversion take?
- Processing is asynchronous. Poll every 3–5 seconds until the job reaches a terminal state; large statements take longer, and there is no published SLA.
- Are there webhooks?
- No. Polling the job status is the supported mechanism today.
- How much does it cost?
- Credits are counted in PDF pages and use the same wallet as the web app. A new account receives 20 page credits on signup. Packages start at $25 for 138 pages.
Build against the live contract
Create the key in your account, keep it on the server, and use the published schema as the source of truth.