Bank Statement API
A simple REST API to convert bank statement PDFs into clean, reconciled data — JSON, CSV, Excel, OFX or QBO — with a single call. It reads scanned PDFs too (OCR), checks the totals reconcile, and returns structured transactions, the same engine as the web app. Manage your keys in your account.
Start free with a sandbox key, integrate with the Python or TypeScript SDK, and go live when you're ready.
Authentication
Create a key in your account (it's shown once — store it securely), then send it as a Bearer token. Keys are secrets: never expose them in client-side code. All requests must use HTTPS.
Authorization: Bearer ebs_live_xxxxxxxxxxxxxxxxxxxxxxxxConvert a statement
POST https://www.exportbankstatement.com/api/v1/convert with a multipart file (PDF, max 15 MB). Send an optional Idempotency-Keyso retries don't convert or meter twice.
Choose the output with a format query param — one of json (default), csv, xlsx, ofx, qbo. For ofx/qbo add a currency (e.g. ?format=ofx¤cy=GBP). Non-JSON formats download as a file.
curl
curl -X POST https://www.exportbankstatement.com/api/v1/convert \
-H "Authorization: Bearer $EBS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@statement.pdf"Node (fetch)
import { readFile } from "node:fs/promises";
const form = new FormData();
form.set("file", new Blob([await readFile("statement.pdf")], { type: "application/pdf" }), "statement.pdf");
const res = await fetch("https://www.exportbankstatement.com/api/v1/convert", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.EBS_API_KEY}` },
body: form,
});
const data = await res.json();
console.log(data.reconciled, data.totals);Python (requests)
import os, requests
with open("statement.pdf", "rb") as f:
res = requests.post(
"https://www.exportbankstatement.com/api/v1/convert",
headers={"Authorization": f"Bearer {os.environ['EBS_API_KEY']}"},
files={"file": ("statement.pdf", f, "application/pdf")},
)
res.raise_for_status()
data = res.json()
print(data["reconciled"], data["totals"])Response
The same structured shape as the web JSON export: figures are JSON numbers (no currency symbol or code), dates are ISO YYYY-MM-DD where normalisable, andreconciled is true only when the engine verified opening + credits − debits = closing.
{
"source": "Export Bank Statement",
"generatedAt": "2026-06-25T12:00:00.000Z",
"statementCount": 1,
"reconciled": true,
"totals": { "credits": 1500.00, "debits": 420.00, "transactions": 12 },
"statements": [
{
"account": "****3456",
"openingBalance": 1000.00,
"closingBalance": 2080.00,
"reconciled": true,
"transactions": [
{ "date": "2026-05-01", "description": "Salary", "moneyIn": 1500.00, "moneyOut": 0, "balance": 2500.00 }
]
}
]
}Account & usage
GET https://www.exportbankstatement.com/api/v1/account returns your plan, period, used, limit, remaining, and reset.
curl https://www.exportbankstatement.com/api/v1/account -H "Authorization: Bearer $EBS_API_KEY"
# { "plan": "beta", "period": "2026-06", "used": 3, "limit": 200, "remaining": 197, "reset": "2026-07-01T00:00:00.000Z" }Sandbox
Create a Test key (ebs_test_…) in your account and integrate before you go live. Test keys return a fixed, deterministic sample statement — no real conversion, no quota usage, no PDF needed — in any format. Responses carry an X-Sandbox-Mode: true header.
curl -X POST "https://www.exportbankstatement.com/api/v1/convert?format=json" \
-H "Authorization: Bearer ebs_test_xxxxxxxxxxxxxxxxxxxxxxxx"
# → the sample statement, identical every callErrors
Every response (success or error) carries a requestId (also in the x-request-id header). Errors use a consistent envelope:
{ "error": "too_large", "detail": "File too large (max 15 MB).", "requestId": "req_…" }| HTTP | Error | Meaning |
|---|---|---|
| 400 | bad_request / no_file / empty | malformed upload |
| 401 | unauthorized | missing or invalid API key |
| 413 | too_large | file over the 15 MB cap |
| 415 | not_pdf | input is not a PDF |
| 422 | convert_failed | the engine couldn't convert this PDF |
| 429 | rate_limited / quota_exceeded | burst or monthly limit (see Retry-After) |
| 503 | extractor_unavailable | engine temporarily unavailable |
Limits
- File size: 15 MB per request, PDF only.
- Burst: a short-term per-key rate limit; over it returns
429 rate_limitedwithRetry-After. - Monthly quota: a per-key conversion allowance; over it returns
429 quota_exceededuntil the period resets. - Every response includes
X-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset.
Async jobs & batch
For large files or many at once, submit an async job (repeat the file field to batch). You get a job id to poll; results are per-file. Requires the jobs scope.
# submit (one or more files)
curl -X POST "https://www.exportbankstatement.com/api/v1/jobs?format=json" \
-H "Authorization: Bearer $EBS_API_KEY" \
-F "file=@jan.pdf" -F "file=@feb.pdf"
# → 202 { "id": "123", "status": "queued", "fileCount": 2 }
# poll
curl https://www.exportbankstatement.com/api/v1/jobs/123 -H "Authorization: Bearer $EBS_API_KEY"
# → { "status": "completed", "results": [ { "filename": "jan.pdf", "status": "completed", "data": { … } }, … ] }Inputs are deleted the moment a job finishes (zero-retention). Non-JSON formats come back base64-encoded per file.
Scopes
Each key is limited to the scopes you pick when creating it — convert,account, jobs. A call outside a key's scope returns 403 forbidden.
Webhooks
Add an endpoint in your account to receive a signed job.completed POST when an async job finishes. Verify it with the per-endpoint secret:
# headers on the delivery
X-EBS-Event: job.completed
X-EBS-Timestamp: <unix seconds>
X-EBS-Signature: hex( HMAC_SHA256( secret, `${timestamp}.${rawBody}` ) )OpenAPI
The machine-readable contract for this API is the OpenAPI 3 spec at /api/v1/openapi.json — load it into Postman, Insomnia, or a generator to scaffold a client.
Privacy
Files are processed and then deleted — there is no retention beyond the request — and your data is never used to train AI. Exactly the same promise as the web product.
