Export Bank Statement — bank statement PDF to Excel
Private beta

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_xxxxxxxxxxxxxxxxxxxxxxxx

Convert 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&currency=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 call

Errors

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_…" }
HTTPErrorMeaning
400bad_request / no_file / emptymalformed upload
401unauthorizedmissing or invalid API key
413too_largefile over the 15 MB cap
415not_pdfinput is not a PDF
422convert_failedthe engine couldn't convert this PDF
429rate_limited / quota_exceededburst or monthly limit (see Retry-After)
503extractor_unavailableengine temporarily unavailable

Limits

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.

Bank Statement API — Convert PDF to JSON, CSV & Excel