The bank statement parsing API

One POST turns a statement PDF into every transaction, its account, and a check that the figures reconcile.

$0.49

per document

25

free

to start

Nothing

stored by us

No minimum, no seat fees

Failed parses cost nothing

Regularly tested against real statements

From these issuers among many others. There is no supported bank list and no per-issuer template: we read the PDF's own glyph coordinates, so an issuer we have never seen behaves like one we have.

Chase
Bank of America
Wells Fargo
Citi
Capital One
American Express
Barclays
HSBC
Lloyds
Halifax
Nationwide
Monzo
Starling
N26

Parse a document

POST the PDF as a raw body or as multipart form data. Up to 20MB.

curl -X POST https://statementbear.com/api/v1/documents \
  -H "Authorization: Bearer $STATEMENTBEAR_KEY" \
  -H "Content-Type: application/pdf" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @statement.pdf

Retrying is always safe

Send an Idempotency-Key with every request: any unique string, one per document. If a request times out or the connection drops, send the same key again.

Retry as often as you need. While a document keeps failing, every attempt runs fresh and none of them cost anything. Once one succeeds we store that answer, and any later request with the same key gets it back instead of a second parse, marked Idempotent-Replayed: true. So you are never charged twice for one document, however many times you send it. Stored answers are kept for 24 hours, after which the same key returns a 409 rather than quietly parsing and charging again.

Response

{
  "id": "doc_5f2a9c4b1e77d0a3c8b6e412",
  "object": "document",
  "created": 1785372094,
  "issuer": { "name": "Monzo", "domain": "monzo.com" },
  "currency": "GBP",
  "period": { "year": 2026, "month": 3 },
  "accounts": [
    {
      "type": "bank",
      "last4": "4412",
      "name": null,
      "period": { "year": 2026, "month": 3 },
      "primary": true,
      "totals": { "credits": 3240.00, "debits": 2841.55 },
      "statedTotals": { "credits": 3240.00, "debits": 2841.55 },
      "openingBalance": 1204.11,
      "closingBalance": 1602.56,
      "transactions": [
        {
          "date": "2026-03-04",
          "description": "TESCO STORES 3412",
          "amount": 42.10,
          "direction": "debit",
          "category": "groceries"
        }
      ]
    }
  ],
  "verification": { "reconciled": true, "issues": [] },
  "usage": { "documents": 1 }
}
idstring
Our id for this parse. Quote it in any support question.
issuerobject
The bank as printed, plus its domain for logo lookups.
currencystring
ISO 4217. One per document.
periodobject
The month the document covers: { year, month } with month 1-12.
accountsarray
One entry per account printed on the PDF. Usually one.
accounts[].type"bank" | "credit_card"
bank covers any deposit account: current, checking, savings.
accounts[].primaryboolean
The account the issuer printed first. Exactly one per document.
accounts[].totalsobject
Our sum of the rows: { credits, debits }, both positive.
accounts[].statedTotalsobject | null
What the statement itself prints, exactly as printed.
openingBalancenumber | null
Opening balance, when the issuer prints one.
transactions[].amountnumber
Always positive. direction carries the sign.
transactions[].direction"credit" | "debit"
Resolved from the document, never guessed from the payee.
transactions[].descriptionstring
The payee line as printed. Not normalised or cleaned.
verificationobject
Our own check of the extraction against the document.

Errors

Branch on code, not on the message. We reword messages; we do not renumber codes. Hover any code for the detail.

400empty_body
No PDF arrived. Not charged.
401invalid_key
The key is unknown or has been revoked.
402trial_exhausted
The free allowance is spent and no card is on file.
402monthly_cap_reached
Your monthly document ceiling was hit.
402canceled
Billing for this account was cancelled.
405method_not_allowed
Only POST. The response carries an Allow header.
409idempotency_key_not_replayable
The key succeeded but its stored response is gone.
413file_too_large
Over 20MB.
415not_a_pdf
The body is not a PDF.
422not_a_bank_statement
Readable, but not a statement. Not charged.
429too_many_concurrent_requests
More than 4 documents in flight at once.
500parse_failed
Our error. Not charged, and your cap is not spent.

Before you use the numbers

Two checks worth writing on day one. Skip either and you get a plausible number instead of an error, so nothing tells you it went wrong.

// A card statement has no income on it: its credits are the repayment
// and merchant refunds. Only count credits on a deposit account.
const income = doc.accounts
  .filter((a) => a.type === "bank")
  .reduce((sum, a) => sum + a.totals.credits, 0);

// Do not present figures we could not reconcile as if they were checked.
if (!doc.verification.reconciled) {
  await queueForManualReview(doc.id, doc.verification.issues);
}

Frequently asked questions

Parse your first statement today

25 documents free, no payment method. Finish the integration before you decide anything.

Get an API key