# StatementBear API documentation, in full > Every documentation page for the StatementBear statement parsing API, concatenated. Generated from the same source the pages render from. Canonical index: https://statementbear.com/docs === # Bank statement parsing API documentation > One POST turns a bank or credit card statement PDF into structured JSON: every transaction, the account it belongs to, and a check that the figures reconcile. Section: Get started Source: https://statementbear.com/docs Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- StatementBear turns a bank or credit card statement PDF into JSON. One POST returns every transaction, the account it belongs to, and a check that the figures reconcile. No bank login, no end-user account, nothing stored. `POST /api/v1/documents` - [Quickstart](https://statementbear.com/docs/quickstart): From key to parsed JSON in about five minutes. - [API reference](https://statementbear.com/docs/api/documents): Every header, status code and field. - [Errors](https://statementbear.com/docs/errors): What can come back and what to do with it. - [Pricing](https://statementbear.com/docs/billing): $0.49 a document, 25 free, billed on success. ## The request Send the PDF as a raw body or as a multipart file part. There is one endpoint, one verb, no SDK and nothing to poll. Request and response: ```http POST /api/v1/documents HTTP/1.1 Host: statementbear.com Authorization: Bearer sb_live_... Content-Type: application/pdf Idempotency-Key: 6f1b1f22-9c62-4c0e-b6a4-1d0f5f0b6f5a HTTP/1.1 200 OK Content-Type: application/json X-Billable-Documents: 1 { "id": "doc_...", "issuer": { ... }, "accounts": [ ... ], "verification": { ... } } ``` ## What you get back - **Every transaction**, with the date and payee line as printed, a positive amount and an explicit `direction`. - **Every account on the PDF**, kept separate. Some issuers bill a current account and three savings accounts on one statement. - **Totals we summed and totals the issuer printed**, side by side. - **A verification object** holding our check of the extraction against the statement's own balances. - **The issuer and the period**, so a document can be filed without anyone opening it. ## What it does not do - **Store anything.** The PDF is parsed and returned. See [Security and data retention](https://statementbear.com/docs/security). - **Score or decide.** You get the rows. Affordability, risk and eligibility are yours. - **Create an account for your customer.** The person whose statement it is never hears from us. - **Clean up payee names.** `description` is the line as printed, branch and reference included. - **Read scans.** Send the PDF the bank issued. A photo or a flattened scan returns `422` and is not charged. ## When to use it Open Banking needs the account holder present, consenting, and banking with a covered institution. Mortgage files, SME lending, tenant screening, immigration and legal disclosure still run on PDFs the applicant hands over. Use this for those. Plenty of customers run both. ## What it costs $0.49 for every document that returns `200`. The first 25 are free and need no payment method. One file is one document, however many accounts or months are inside it. Errors are free. Every response carries `X-Billable-Documents`. See [Pricing and billing](https://statementbear.com/docs/billing). --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Quickstart > Parse your first statement in about five minutes, from creating a key to reading the response. Section: Get started Source: https://statementbear.com/docs/quickstart Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- From signing up to parsed JSON in about five minutes. ## 1. Create a key Sign in and open the [developer console](https://statementbear.com/dashboard/api). Creating an organisation issues your first key in the same step. No payment method is needed. The key is shown once, so copy it into your secret store now. > **There is no test mode** > > No `sb_test_` key and no sandbox host. Your first 25 documents are free on the live endpoint. See [Authentication](https://statementbear.com/docs/authentication). ## 2. Send a statement POST the PDF as a raw body with `Content-Type: application/pdf`, or as `multipart/form-data` with a part named `file`. Up to 20MB. A parse takes 10 to 40 seconds, so set a client timeout of at least 180 seconds. cURL: ```bash 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 ``` Node.js: ```javascript import { readFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; const pdf = await readFile("statement.pdf"); const res = await fetch("https://statementbear.com/api/v1/documents", { method: "POST", headers: { Authorization: `Bearer ${process.env.STATEMENTBEAR_KEY}`, "Content-Type": "application/pdf", // One per document. Retrying with it does not charge twice. "Idempotency-Key": randomUUID(), }, body: pdf, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${error.code}: ${error.message}`); } const doc = await res.json(); for (const account of doc.accounts) { console.log(account.type, account.last4, account.transactions.length); } ``` Python: ```python import os, uuid, requests with open("statement.pdf", "rb") as f: res = requests.post( "https://statementbear.com/api/v1/documents", headers={ "Authorization": f"Bearer {os.environ['STATEMENTBEAR_KEY']}", "Content-Type": "application/pdf", # One per document. Retrying with it does not charge twice. "Idempotency-Key": str(uuid.uuid4()), }, data=f.read(), timeout=180, ) if not res.ok: err = res.json()["error"] raise RuntimeError(f"{err['code']}: {err['message']}") doc = res.json() for account in doc["accounts"]: print(account["type"], account["last4"], len(account["transactions"])) ``` Java: ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.UUID; byte[] pdf = Files.readAllBytes(Path.of("statement.pdf")); HttpRequest request = HttpRequest.newBuilder(URI.create("https://statementbear.com/api/v1/documents")) .header("Authorization", "Bearer " + System.getenv("STATEMENTBEAR_KEY")) .header("Content-Type", "application/pdf") // One per document. Retrying with it does not charge twice. .header("Idempotency-Key", UUID.randomUUID().toString()) .timeout(Duration.ofSeconds(180)) .POST(HttpRequest.BodyPublishers.ofByteArray(pdf)) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IllegalStateException(response.body()); } ``` PHP: ```php true, CURLOPT_POSTFIELDS => $pdf, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 180, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('STATEMENTBEAR_KEY'), 'Content-Type: application/pdf', // One per document. Retrying with it does not charge twice. 'Idempotency-Key: ' . bin2hex(random_bytes(16)), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status !== 200) { throw new RuntimeException("{$data['error']['code']}: {$data['error']['message']}"); } foreach ($data['accounts'] as $account) { echo $account['type'], ' ', count($account['transactions']), PHP_EOL; } ``` Ruby: ```ruby require "net/http" require "json" require "securerandom" uri = URI("https://statementbear.com/api/v1/documents") req = Net::HTTP::Post.new(uri) req["Authorization"] = "Bearer #{ENV.fetch('STATEMENTBEAR_KEY')}" req["Content-Type"] = "application/pdf" # One per document. Retrying with it does not charge twice. req["Idempotency-Key"] = SecureRandom.uuid req.body = File.binread("statement.pdf") res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 180) do |http| http.request(req) end doc = JSON.parse(res.body) raise "#{doc['error']['code']}: #{doc['error']['message']}" unless res.code == "200" doc["accounts"].each do |account| puts [account["type"], account["last4"], account["transactions"].size].join(" ") end ``` Go: ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" "github.com/google/uuid" ) type Document struct { ID string `json:"id"` Accounts []struct { Type string `json:"type"` Last4 string `json:"last4"` Transactions []struct { Date string `json:"date"` Description string `json:"description"` Amount float64 `json:"amount"` Direction string `json:"direction"` } `json:"transactions"` } `json:"accounts"` } func main() { pdf, err := os.ReadFile("statement.pdf") if err != nil { panic(err) } req, _ := http.NewRequest("POST", "https://statementbear.com/api/v1/documents", bytes.NewReader(pdf)) req.Header.Set("Authorization", "Bearer "+os.Getenv("STATEMENTBEAR_KEY")) req.Header.Set("Content-Type", "application/pdf") // One per document. Retrying with it does not charge twice. req.Header.Set("Idempotency-Key", uuid.NewString()) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var doc Document json.NewDecoder(res.Body).Decode(&doc) for _, a := range doc.Accounts { fmt.Println(a.Type, a.Last4, len(a.Transactions)) } } ``` ## 3. Read the response A success is a `document` object. Every account on the PDF is in `accounts`, with its rows inside it. `amount` is always positive and `direction` carries the sign. 200 OK: ```json { "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 } } ``` ## 4. Check the response 1. **Only count credits as income on `type: "bank"` accounts.** A credit card's credits are the monthly repayment and merchant refunds. See [Amounts and directions](https://statementbear.com/docs/amounts-and-directions). 2. **Check `verification.reconciled`.** When it is `false`, send the document to manual review instead of using the totals. See [Verification](https://statementbear.com/docs/verification). Node.js: ```javascript // Credit card credits are repayments and refunds. Only count // credits on bank accounts. const income = doc.accounts .filter((a) => a.type === "bank") .reduce((sum, a) => sum + a.totals.credits, 0); // Send anything that did not reconcile to manual review. if (!doc.verification.reconciled) { await queueForManualReview(doc.id, doc.verification.issues); } ``` Python: ```python # Credit card credits are repayments and refunds. Only count # credits on bank accounts. income = sum( a["totals"]["credits"] for a in doc["accounts"] if a["type"] == "bank" ) # Send anything that did not reconcile to manual review. if not doc["verification"]["reconciled"]: queue_for_manual_review(doc["id"], doc["verification"]["issues"]) ``` Ruby: ```ruby # Credit card credits are repayments and refunds. Only count # credits on bank accounts. income = doc["accounts"] .select { |a| a["type"] == "bank" } .sum { |a| a["totals"]["credits"] } # Send anything that did not reconcile to manual review. unless doc["verification"]["reconciled"] queue_for_manual_review(doc["id"], doc["verification"]["issues"]) end ``` ## 5. Make retries safe Send an `Idempotency-Key` with every request: one unique string per document, reused by every attempt at that document. A repeat of a successful call returns the stored response instead of parsing again. See [Idempotency and retries](https://statementbear.com/docs/idempotency). ## Next - [The document object](https://statementbear.com/docs/api/document-object): Every field in the response. - [Errors](https://statementbear.com/docs/errors): Every code, and which to retry. - [Going live](https://statementbear.com/docs/going-live): What to check before real traffic. - [Limits](https://statementbear.com/docs/api/limits): Size, concurrency and the monthly cap. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Authentication > How to send your API key, how keys are stored, and how to rotate one. Section: Get started Source: https://statementbear.com/docs/authentication Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- Send your key as a bearer token. There is no OAuth flow, no signature to compute and no expiry to refresh. ## The header ```http POST /api/v1/documents HTTP/1.1 Host: statementbear.com Authorization: Bearer sb_live_9f2c41d0a7b34e58c6d1f0a2b8e37c94 Content-Type: application/pdf ``` A request with no `Authorization` header returns `401 missing_key`. An unknown or revoked key returns `401 invalid_key`. ## Key format `sb_live_` followed by a long random string. The prefix makes a key recognisable in a log, a paste or a secret scanner, and lets us answer a support question without seeing the rest of it. ## How keys are stored Hashed. The full key is shown once, when it is created. We cannot read it back to you, so a lost key means issuing a new one. > **Keep keys server side** > > A key has no scope and no origin restriction. Keep it in your backend and your secret store. Never ship it to a browser, a mobile app or a customer's device. ## Rotating and revoking - Hold more than one key at a time. Issue the new key, deploy it, then revoke the old one. - Revocation applies to the next request. Nothing is cached. - A revoked key is kept rather than deleted, so past usage stays attributable on your invoice. - Issue and revoke keys yourself in the [console](https://statementbear.com/dashboard/api). ## There is no test mode No `sb_test_` key, no sandbox host and no fixture responses. Your first 25 documents are free against the live endpoint, so the code you evaluate with is the code that ships. ## Authentication errors | Status | Code | Meaning | | --- | --- | --- | | 401 | `missing_key` | No `Authorization` header. | | 401 | `invalid_key` | Unknown, malformed or revoked key. | | 402 | `canceled` | The key is valid but billing was cancelled. Keys are kept, so reactivating needs no redeploy. | None of these are charged or count against your monthly cap. Full list on [Errors](https://statementbear.com/docs/errors). --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # How parsing works > Which statements work, what to send, and how long and multi-account files are handled. Section: Guides Source: https://statementbear.com/docs/how-parsing-works Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- There is no supported bank list, no per-issuer template and nothing to configure before you start. ## Supported banks All of them, as far as the API is concerned. Nothing is set up per institution, so there is no list to check your customers against. - A statement from an issuer we have never seen parses like one we test against daily. - A bank redesigning its statement does not break your integration. - There is no queue to join and no per-institution work to schedule. - UK, US and European statements are all in regular use. - Business accounts, joint accounts and statements from closed accounts work the same way. ## What to send - **The PDF the bank issued**, downloaded from online banking or emailed to your customer. - **Not password protected.** Remove the password first. - **A statement**, rather than a payslip, invoice, receipt or screenshot. - **Up to 20MB.** A year of rows in one file is fine. > **Scans are not supported** > > Amounts are read from the document's own text, so they are exact. A photograph or a flattened scan has no text to read and returns `422 not_a_bank_statement`. It is not charged. ## Long and multi-account statements A file holding six months of rows, or four accounts, is one document at one price. Nothing is truncated to fit. If part of a long statement could not be read, `verification` says so. See [Accounts and periods](https://statementbear.com/docs/accounts-and-periods). ## Accuracy Before a document is returned, the extraction is checked against the statement. Where the issuer prints an opening and a closing balance, the transactions have to account for the movement between them exactly. See [Verification](https://statementbear.com/docs/verification). --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Accounts and periods > How multi-account and multi-month statements come back, and how to match an account to your own records. Section: Guides Source: https://statementbear.com/docs/accounts-and-periods Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- One PDF can hold several accounts and several months. Iterate `accounts` rather than reading `accounts[0]`. ## Accounts `accounts` holds one entry per account printed on the document, each with its own rows, totals and identity. Usually there is one. Capital One prints a checking account and three savings accounts. A multi-account statement, abridged: ```json "accounts": [ { "type": "bank", "last4": "8821", "name": null, "primary": true, "openingBalance": 1204.11, "closingBalance": 1602.56, "totals": { "credits": 3240.00, "debits": 2841.55 }, "transactions": [ ... ] }, { "type": "bank", "last4": "4410", "name": "360 Performance Savings", "primary": false, "openingBalance": null, "closingBalance": null, "totals": { "credits": 500.00, "debits": 0 }, "transactions": [ ... ] } ] ``` > **Do not merge accounts** > > A transfer between two accounts on the same statement is printed twice: a debit on one and a credit on the other. Flatten them into one list and every internal movement counts as both spending and income. ## The primary account Exactly one entry has `primary: true`: the account the issuer printed first. It is the only account carrying `openingBalance` and `closingBalance`, which the statement prints once for the document rather than per account. ## Periods The document has a `period`, and so does each account. They match except on a combined PDF that repeats one account across several months, where each entry carries the month it covers. `month` is 1-12. A statement straddling a month boundary, as most card statements do, is reported under the month the issuer bills it as. That is the month printed on the document. ## Matching an account to your records | Field | Use it for | Null when | | --- | --- | --- | | `last4` | Matching a document to an application or an account on file. | The issuer printed no account or card number. | | `name` | Telling savings sub-accounts apart on a multi-account statement. | The issuer printed the product name at the top of the page rather than against the account. | | `type` | Deciding whether credits can be income. See [Amounts and directions](https://statementbear.com/docs/amounts-and-directions). | Never. It is always `bank` or `credit_card`. | `bank` covers every deposit account: current, checking and savings. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Amounts and directions > How amounts are signed, how direction is decided, and which credits count as income. Section: Guides Source: https://statementbear.com/docs/amounts-and-directions Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- `amount` is always positive. `direction` carries the sign. To get a signed figure, negate when `direction` is `debit`. ## How direction is decided From the document, never from the payee name. Issuers encode it four ways: 1. **A sign on the amount.** A leading or trailing minus, or brackets. 2. **A section heading** that owns every row beneath it, such as `CHECKS AND OTHER DEBITS`. The rows themselves carry no marker. 3. **Paired columns**, `Débit | Crédit`, where the column decides the direction. 4. **A marker against a default.** On a card statement every row is a charge unless it carries `CR`. > **A trailing minus means two different things** > > On a bank statement it is money leaving the account. On a card statement it is money coming back to the customer. The account type settles it. ## Income Only count credits on `type: "bank"` accounts. A credit card's credits are the monthly repayment and merchant refunds. Summing credits across every account in a file inflates income by whatever the customer spent on the card. Filter on account type: ```javascript const income = doc.accounts .filter((a) => a.type === "bank") .reduce((sum, a) => sum + a.totals.credits, 0); ``` The same applies in reverse. A payment from a current account to a credit card is not spending if you also hold that card's statement for the month, because the purchases it settles are itemised there. ## Totals | | `totals` | `statedTotals` | | --- | --- | --- | | Where it comes from | Summed from the rows in this response. | Copied from the figure the issuer printed. | | Use it to | Add up money. | Check our extraction against the document. | | Watch out for | Nothing. It agrees with `transactions` by construction. | Convention. Amex's total new spend is net of refunds; most banks' is not. | A gap between the two is usually a difference of convention. See [Verification](https://statementbear.com/docs/verification). --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Verification > The check on every response, what reconciled means, and what to do when it is false. Section: Guides Source: https://statementbear.com/docs/verification Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- Every response carries a check of the extraction against the document it came from. Read it before you use the figures. ## What is checked - **Every amount appears in the document.** An amount printed nowhere in the PDF is an error, and it is caught before the document is returned. - **Balances close.** Where the statement prints an opening and a closing balance, the transactions must account for the movement between them exactly. - **Printed totals are compared** against our own sum. - **Missing sections are reported**, so a statement missing part of itself does not come back looking complete. ## The field ```json "verification": { "reconciled": false, "issues": [ "Closing balance 1602.56 does not follow from opening balance 1204.11 and the rows returned (difference 42.10)." ] } ``` | `reconciled` | `issues` | Meaning | | --- | --- | --- | | `true` | empty | Checked, nothing wrong. Use the figures. | | `false` | non-empty | Something is wrong. Treat the figures as unverified and put the document in front of a person. | | `false` | empty | The document could not be checked, usually because the issuer printed no balances and no totals. | > **Branch on reconciled, display issues** > > `issues` is written for a person triaging a document. The wording changes. `reconciled` does not. ## Printed totals Issuers disagree about what their own totals mean. Amex's total new spend is net of refunds; most banks' equivalent is not. A gap between `totals` and `statedTotals` does not on its own make a document unreconciled. ## When reconciled is false 1. Keep the document. The rows are still there and are usually right. 2. Do not present the totals as verified. 3. Send it to manual review with `doc.id` and `verification.issues` attached. 4. If you think a document should have passed, email us the `doc.id`. We keep no transactions, so that is what we need to look into it. A document that fails the check is still billable. It returned `200` and the rows were extracted. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Idempotency and retries > How to retry a request safely without being charged twice. Section: Guides Source: https://statementbear.com/docs/idempotency Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- Send an `Idempotency-Key` header on every request and retries are safe. A repeat of a successful call returns the stored response instead of parsing again. ## One key per document Any unique string; a UUID is fine. Generate it once for a document and reuse it for every attempt, including attempts after a timeout where you never saw a response. > **Reuse the key across retries** > > A fresh key on each attempt is a new document, and you are charged for both. Store the key alongside whatever you are parsing for. ## What a repeat does | State of the first call | The repeat | Charged | | --- | --- | --- | | Succeeded, within 24 hours | Returns the stored response with `Idempotent-Replayed: true`. | No | | Failed with any status | Runs fresh. Failures are not stored against a key. | Only if this attempt succeeds | | Succeeded, more than 24 hours ago | `409 idempotency_key_not_replayable`, `reason: "expired"`. | No | | Succeeded, response too large to store | `409 idempotency_key_not_replayable`, `reason: "too_large"`. | No | | Still in flight | Counts towards the four concurrent documents and may return `429`. Retry shortly with the same key. | No | ## Retrying While a document keeps failing, every attempt runs fresh and none of them cost anything. Retry `429` and `5xx` with backoff. Node.js: ```javascript import { randomUUID } from "node:crypto"; // One key per document, reused by every attempt. A new key on a retry // is a new document, and is charged as one. async function parse(pdf, { key = randomUUID(), attempt = 1 } = {}) { const res = await fetch("https://statementbear.com/api/v1/documents", { method: "POST", headers: { Authorization: `Bearer ${process.env.STATEMENTBEAR_KEY}`, "Content-Type": "application/pdf", "Idempotency-Key": key, }, body: pdf, }); if (res.ok) return res.json(); // Retry 429 (concurrency) and 5xx. const retryable = res.status === 429 || res.status >= 500; if (!retryable || attempt >= 4) { const { error } = await res.json(); throw new Error(`${error.code}: ${error.message}`); } await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); return parse(pdf, { key, attempt: attempt + 1 }); } ``` Python: ```python import os, time, uuid, requests # One key per document, reused by every attempt. A new key on a retry # is a new document, and is charged as one. def parse(pdf: bytes, key: str | None = None): key = key or str(uuid.uuid4()) for attempt in range(1, 5): res = requests.post( "https://statementbear.com/api/v1/documents", headers={ "Authorization": f"Bearer {os.environ['STATEMENTBEAR_KEY']}", "Content-Type": "application/pdf", "Idempotency-Key": key, }, data=pdf, timeout=180, ) if res.ok: return res.json() # Retry 429 (concurrency) and 5xx. if res.status_code != 429 and res.status_code < 500: break time.sleep(2 ** attempt) err = res.json()["error"] raise RuntimeError(f"{err['code']}: {err['message']}") ``` ## The 409 Stored responses are kept for 24 hours and then deleted. After that the same key returns `409` rather than parsing again, which would charge a second time for one call. The `reason` field says whether the response expired or was too large to store. Retry with a new key if you do want the document parsed again. ## Response headers | Header | Meaning | | --- | --- | | `Idempotent-Replayed: true` | The body came from storage. Nothing was parsed or charged. | | `X-Billable-Documents` | `1` when the call is on your invoice, `0` when it is not. A replay is always `0`. | --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Errors > Every status and error code, what is charged, and what to retry. Section: Guides Source: https://statementbear.com/docs/errors Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- Errors come back in one envelope with a stable `code`. Branch on the code: messages get reworded, codes do not. ## The envelope ```json { "error": { "code": "not_a_bank_statement", "message": "This document does not look like a bank or card statement.", "documentId": "doc_5f2a9c4b1e77d0a3c8b6e412" } } ``` `documentId` is present whenever we got far enough to allocate one. Quote it in a support question. Some errors carry extra fields, such as `reason` on a `409`. ## Every code | Status | Code | Description | | --- | --- | --- | | 400 | `empty_body` | No PDF arrived. Returned as missing_file when a multipart request has no file part. | | 401 | `missing_key` | No Authorization header on the request. Check that your client is sending the header. It is a separate code from invalid_key so the two cases are easy to tell apart in a log. | | 401 | `invalid_key` | The key is unknown or has been revoked. Revoking a key in the console applies to the next request. Check the value before assuming the key was revoked: a typo returns the same code. | | 402 | `trial_exhausted` | The free allowance is spent and no card is on file. Add a payment method in the console and the same key keeps working. Nothing needs redeploying. | | 402 | `monthly_cap_reached` | Your monthly document ceiling was hit. There is nothing to buy. Tell us your volume and we raise it. | | 402 | `canceled` | Billing for this account was cancelled. Contact us to reactivate. Keys are kept, so nothing needs reissuing. | | 405 | `method_not_allowed` | Only POST. The response carries an Allow header. GET, HEAD, PUT, PATCH and DELETE all land here. | | 409 | `idempotency_key_not_replayable` | The key succeeded but its stored response is gone. It expired after 24 hours or was too large to store; the reason field says which. Retry with a new key if you want the document parsed again. | | 413 | `file_too_large` | Over 20MB. Refused as soon as the size is known. Split the statement or export a smaller range. | | 415 | `not_a_pdf` | The body is not a PDF. Decided from the file itself rather than the content type you declare, so a mislabelled file fails immediately. | | 415 | `unsupported_media_type` | Neither an application/pdf body nor a multipart file part. Send the PDF as a raw body with Content-Type: application/pdf, or as multipart/form-data with a part named file. | | 422 | `not_a_bank_statement` | Readable, but not a statement. A payslip, a receipt, an invoice or a scan with no text layer. Usually the right thing to show whoever uploaded the file. | | 429 | `too_many_concurrent_requests` | More than 4 documents in flight at once. A limit on documents in flight at once. There is no per-minute limit. Wait a couple of seconds and retry with the same idempotency key. | | 500 | `parse_failed` | Our error. Nothing is charged and no allowance is spent. Retry with the same key; if the same file fails three times, send us the documentId. | ## What is charged Only a `200` is billable. Every error is free and none of them count against your monthly cap. `X-Billable-Documents` is on every response. ## What to retry | Status | Retry | How | | --- | --- | --- | | `429` | Yes | Four documents are already in flight. Wait a couple of seconds and retry with the same idempotency key. | | `500` | Yes | Retry with the same key and exponential backoff. If the same file fails three times, send us the `documentId`. | | `402` | After a change | Add a payment method, or ask us to raise the cap. Until then the answer stays the same. | | `400`, `401`, `405`, `413`, `415`, `422` | No | The request or the file is the problem. Fix it and send again. | | `409` | No | The call already succeeded. Use a new idempotency key only if you want the document parsed and charged again. | > **422 is validation** > > `not_a_bank_statement` means the file was readable and is not a statement: a payslip, an invoice, a screenshot or a scan. It is free, and it is usually what to show whoever uploaded the file. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Going live > What to check before you put the API in front of real customers. Section: Guides Source: https://statementbear.com/docs/going-live Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- Check these before real traffic. 1. **Send an `Idempotency-Key` on every request**, generated once per document and reused by every attempt. Without it, a timeout you retry is a second charge. See [Idempotency and retries](https://statementbear.com/docs/idempotency). 2. **Set a client timeout of at least 180 seconds.** A long statement takes tens of seconds to parse, and a 30 second default turns a success into a retry. 3. **Filter on `account.type` before summing credits as income.** See [Amounts and directions](https://statementbear.com/docs/amounts-and-directions). 4. **Branch on `verification.reconciled`** and route anything false to a person. See [Verification](https://statementbear.com/docs/verification). 5. **Iterate `accounts`** rather than reading `accounts[0]`, and keep them separate. See [Accounts and periods](https://statementbear.com/docs/accounts-and-periods). 6. **Branch on `error.code`**, not on the message or a substring of it. 7. **Keep four documents in flight or fewer**, or handle `429` with a short backoff. See [Limits](https://statementbear.com/docs/api/limits). 8. **Store `doc.id`** against whatever you parsed for. It is the handle for any support question later. ## Volume Every organisation has a monthly document cap, which stops a runaway loop turning into an invoice. There is nothing to buy. Email [api@statementbear.com](mailto:api@statementbear.com) with your expected volume and we raise it before you hit it. ## Support [api@statementbear.com](mailto:api@statementbear.com), with the `documentId` if there is one. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Parse a document > POST /api/v1/documents: request headers, body, response headers, statuses and versioning. Section: API reference Source: https://statementbear.com/docs/api/documents Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- `POST /api/v1/documents` Parses one bank or credit card statement PDF and returns it as JSON. This is the whole API. ## Request headers | Header | Required | Value | | --- | --- | --- | | `Authorization` | Yes | `Bearer sb_live_...`. See [Authentication](https://statementbear.com/docs/authentication). | | `Content-Type` | Yes | `application/pdf` for a raw body, or `multipart/form-data` with a part named `file`. | | `Idempotency-Key` | Recommended | One unique string per document, reused by every retry. See [Idempotency and retries](https://statementbear.com/docs/idempotency). | | `Content-Length` | No | When present, an oversized file is refused before the upload finishes. | ## Request body The PDF itself, up to 20MB, in one of two shapes: - **Raw body.** `Content-Type: application/pdf` and the bytes. - **Multipart.** `Content-Type: multipart/form-data` with the PDF in a part named `file`. Other parts are ignored. The file is identified from its own contents rather than the content type you declare, so a mislabelled PNG fails immediately with `415 not_a_pdf`. ## Response headers | Header | On | Meaning | | --- | --- | --- | | `X-Billable-Documents` | Every response | `1` when the call is on your invoice, `0` when it is not. Free trial documents, replays and errors return `0`. | | `Idempotent-Replayed` | Replays | `true` when the body came from storage. | | `Allow` | `405` | `POST`. | | `Cache-Control` | Every response | `no-store`. | ## Responses | Status | Body | Billable | | --- | --- | --- | | `200` | A [document object](https://statementbear.com/docs/api/document-object). | Yes | | `400` | `empty_body` or `missing_file`. | No | | `401` | `missing_key` or `invalid_key`. | No | | `402` | `trial_exhausted`, `monthly_cap_reached` or `canceled`. | No | | `405` | `method_not_allowed`, with an `Allow` header. | No | | `409` | `idempotency_key_not_replayable`, with a `reason`. | No | | `413` | `file_too_large`. | No | | `415` | `not_a_pdf` or `unsupported_media_type`. | No | | `422` | `not_a_bank_statement`. | No | | `429` | `too_many_concurrent_requests`. | No | | `500` | `parse_failed`. No allowance is spent. | No | ## Example request cURL: ```bash 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 ``` Node.js: ```javascript import { readFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; const pdf = await readFile("statement.pdf"); const res = await fetch("https://statementbear.com/api/v1/documents", { method: "POST", headers: { Authorization: `Bearer ${process.env.STATEMENTBEAR_KEY}`, "Content-Type": "application/pdf", // One per document. Retrying with it does not charge twice. "Idempotency-Key": randomUUID(), }, body: pdf, }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${error.code}: ${error.message}`); } const doc = await res.json(); for (const account of doc.accounts) { console.log(account.type, account.last4, account.transactions.length); } ``` Python: ```python import os, uuid, requests with open("statement.pdf", "rb") as f: res = requests.post( "https://statementbear.com/api/v1/documents", headers={ "Authorization": f"Bearer {os.environ['STATEMENTBEAR_KEY']}", "Content-Type": "application/pdf", # One per document. Retrying with it does not charge twice. "Idempotency-Key": str(uuid.uuid4()), }, data=f.read(), timeout=180, ) if not res.ok: err = res.json()["error"] raise RuntimeError(f"{err['code']}: {err['message']}") doc = res.json() for account in doc["accounts"]: print(account["type"], account["last4"], len(account["transactions"])) ``` Java: ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.UUID; byte[] pdf = Files.readAllBytes(Path.of("statement.pdf")); HttpRequest request = HttpRequest.newBuilder(URI.create("https://statementbear.com/api/v1/documents")) .header("Authorization", "Bearer " + System.getenv("STATEMENTBEAR_KEY")) .header("Content-Type", "application/pdf") // One per document. Retrying with it does not charge twice. .header("Idempotency-Key", UUID.randomUUID().toString()) .timeout(Duration.ofSeconds(180)) .POST(HttpRequest.BodyPublishers.ofByteArray(pdf)) .build(); HttpResponse response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new IllegalStateException(response.body()); } ``` PHP: ```php true, CURLOPT_POSTFIELDS => $pdf, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 180, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('STATEMENTBEAR_KEY'), 'Content-Type: application/pdf', // One per document. Retrying with it does not charge twice. 'Idempotency-Key: ' . bin2hex(random_bytes(16)), ], ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $data = json_decode($body, true); if ($status !== 200) { throw new RuntimeException("{$data['error']['code']}: {$data['error']['message']}"); } foreach ($data['accounts'] as $account) { echo $account['type'], ' ', count($account['transactions']), PHP_EOL; } ``` Ruby: ```ruby require "net/http" require "json" require "securerandom" uri = URI("https://statementbear.com/api/v1/documents") req = Net::HTTP::Post.new(uri) req["Authorization"] = "Bearer #{ENV.fetch('STATEMENTBEAR_KEY')}" req["Content-Type"] = "application/pdf" # One per document. Retrying with it does not charge twice. req["Idempotency-Key"] = SecureRandom.uuid req.body = File.binread("statement.pdf") res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 180) do |http| http.request(req) end doc = JSON.parse(res.body) raise "#{doc['error']['code']}: #{doc['error']['message']}" unless res.code == "200" doc["accounts"].each do |account| puts [account["type"], account["last4"], account["transactions"].size].join(" ") end ``` Go: ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" "os" "github.com/google/uuid" ) type Document struct { ID string `json:"id"` Accounts []struct { Type string `json:"type"` Last4 string `json:"last4"` Transactions []struct { Date string `json:"date"` Description string `json:"description"` Amount float64 `json:"amount"` Direction string `json:"direction"` } `json:"transactions"` } `json:"accounts"` } func main() { pdf, err := os.ReadFile("statement.pdf") if err != nil { panic(err) } req, _ := http.NewRequest("POST", "https://statementbear.com/api/v1/documents", bytes.NewReader(pdf)) req.Header.Set("Authorization", "Bearer "+os.Getenv("STATEMENTBEAR_KEY")) req.Header.Set("Content-Type", "application/pdf") // One per document. Retrying with it does not charge twice. req.Header.Set("Idempotency-Key", uuid.NewString()) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() var doc Document json.NewDecoder(res.Body).Decode(&doc) for _, a := range doc.Accounts { fmt.Println(a.Type, a.Last4, len(a.Transactions)) } } ``` ## Example response 200 OK: ```json { "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 } } ``` ## Example error 422 Unprocessable: ```json { "error": { "code": "not_a_bank_statement", "message": "This document does not look like a bank or card statement.", "documentId": "doc_5f2a9c4b1e77d0a3c8b6e412" } } ``` Branch on `error.code`. The full list is on [Errors](https://statementbear.com/docs/errors). ## Versioning The version is in the path. Anything that would break code written against `v1` ships as `v2` at a new path, with `v1` still answering, and we email the address on your organisation first. | Ships as a new version | Can happen in v1 | | --- | --- | | Removing or renaming a field. | Adding an optional field. Parse leniently and ignore what you do not recognise. | | Changing a field's type or meaning, including the units of an amount. | Adding an error `code` under a status you already handle. | | Adding a required request header or parameter. | Rewording an error `message`. | | Changing which status code a failure returns. | Improving extraction accuracy, which changes the rows a given PDF returns. | --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # The document object > Every field in a parsed document, and what is not included. Section: API reference Source: https://statementbear.com/docs/api/document-object Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- What a `200` returns. Fields are never removed or changed in meaning inside `v1`. ```json { "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 } } ``` ## Fields Hover or tap a field name for more detail. | Field | Type | Description | | --- | --- | --- | | `id` | `string` | Our id for this parse. Quote it in any support question. Also on the error body of a failed parse, so a failure can be traced to the call that produced it. | | `object` | `"document"` | Always document. Present so responses are discriminable. A constant today. It is here so a future object type can be told apart without inspecting the rest of the body. | | `created` | `number` | When we parsed it. Unix seconds. Our clock. The month the document covers is in period. | | `issuer` | `object` | The bank as printed, plus its domain for logo lookups. domain is null when the institution's website cannot be identified with confidence. | | `currency` | `string` | ISO 4217. One per document. Issuers do not mix currencies on one statement. A multi-currency provider issues a separate PDF per currency. | | `period` | `object` | The month the document covers: { year, month } with month 1-12. Each account carries its own period too. They differ only on a combined PDF holding several months for one account. | | `accounts` | `array` | One entry per account printed on the PDF. Usually one. Some issuers bill several accounts on one statement. They are kept separate because transfers between them are printed on both sides, so merging them counts each transfer as spending and as income. | | `accounts[].type` | `"bank" \| "credit_card"` | bank covers any deposit account: current, checking, savings. Decides how to read credits. On a credit card they are the repayment and merchant refunds, so counting them as income inflates it. | | `accounts[].last4` | `string \| null` | Last four of the account or card number, when it is printed. Null when the issuer prints no account or card number, or prints only a masked one. | | `accounts[].name` | `string \| null` | The account's own name, e.g. "360 Performance Savings". Null when the issuer prints the product name at the top of the page rather than against the account, which is usual on a single-account statement. | | `accounts[].period` | `object` | The month this account's rows cover. Equal to the document's period except on a combined PDF that holds several months for one account. | | `accounts[].primary` | `boolean` | The account the issuer printed first. Exactly one per document. Only the primary account carries opening and closing balances, because the statement prints them once for the document. | | `accounts[].totals` | `object` | Our sum of the rows: { credits, debits }, both positive. Summed from the transactions in this response, so it always agrees with them. | | `accounts[].statedTotals` | `object \| null` | What the statement itself prints, exactly as printed. Use it to check our extraction against the document. It is not comparable across issuers: Amex's spend total is net of refunds and most banks' is not. | | `accounts[].openingBalance` | `number \| null` | Opening balance, when the issuer prints one. With closingBalance it is the strongest check available, because the transactions have to account for the movement between them exactly. | | `accounts[].closingBalance` | `number \| null` | Closing balance, when the issuer prints one. Null on secondary accounts and on issuers that print no balances. On a credit card it is the balance owed, printed positive. | | `transactions[].date` | `string` | YYYY-MM-DD. The date printed against the row. The posting date on most statements and the transaction date on some. The document does not say which, so we return what is printed. | | `transactions[].description` | `string` | The payee line as printed. Not normalised. Includes the branch, reference and city where the issuer prints them. Clean it to suit your own matching. | | `transactions[].amount` | `number` | Always positive. direction carries the sign. Never negative, in any currency, on any account type. Negate when direction is debit for a signed figure. | | `transactions[].direction` | `"credit" \| "debit"` | Read from the document, never from the payee name. Issuers encode it four ways: a sign on the amount, a section heading owning the rows beneath it, paired debit and credit columns, or a CR marker against a default. A trailing minus means money out on a bank statement and money in on a card statement. | | `transactions[].category` | `string` | One of a fixed set. A hint for a first cut at spending mix. Not a merchant category code and not derived from one. Do not reconcile it against MCC data or use it where a regulator expects a defined taxonomy. | | `verification` | `object` | Our check of the extraction against the document. reconciled: true means we checked and found nothing wrong. false with a non-empty issues array means we found something: treat the figures as unverified. | | `verification.issues` | `string[]` | What did not add up. Often empty. Written for a person triaging a document. Branch on reconciled; show issues to whoever opens the PDF. | | `usage` | `object` | What the call consumed: { documents }. Always 1 today. A file holding four accounts and six months is one document. X-Billable-Documents says whether it was charged. | ## Guarantees - `amount` is always positive. `direction` carries the sign. - Exactly one account has `primary: true`. - `totals` always agrees with the `transactions` in the same response. - `period.month` is 1-12. - `currency` is one ISO 4217 code for the whole document. - `date` is `YYYY-MM-DD`. ## Not included - Scores, insights and affordability verdicts. - Normalised merchant names or merchant ids. `description` is the payee line as printed. - Balances on secondary accounts. The statement prints them once, for the primary account. - Anything about the person whose statement it is, beyond what the document prints. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Limits > File size, concurrency, parse duration and the monthly document cap. Section: API reference Source: https://statementbear.com/docs/api/limits Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- Four limits. There is no per-minute rate limit and no burst quota. | Limit | Value | Response | | --- | --- | --- | | File size | 20MB | `413 file_too_large` | | Concurrent documents | 4 per organisation | `429 too_many_concurrent_requests` | | Documents per calendar month | Set per organisation | `402 monthly_cap_reached` | | Replay storage | 24 hours, about 1MB per response | `409 idempotency_key_not_replayable` | ## Concurrency Four documents may be in flight at once per organisation. There is no per-minute quota, so four in the air continuously runs all day and a queue of ten thousand statements is a matter of time. > **Ask if you need more** > > The limit keeps one customer's burst from slowing everybody else down. It will never be tightened below four, and we will raise it if your workload needs the headroom. ## The monthly cap Every organisation has a document ceiling for the calendar month. It stops a runaway loop on your side turning into an invoice. There is nothing to buy: tell us your volume and it moves. The window rolls on your next request, so there is nothing to wait for at the start of a month. Errors do not count against it. Only documents that returned `200`. ## Parse duration 10 to 40 seconds, depending on how many rows the statement holds. A long multi-month statement takes proportionally longer. Set a client timeout of at least 180 seconds. ## Errors are free None of these responses are billable, and none of them spend the monthly cap. | Status | Code | Description | | --- | --- | --- | | 402 | `trial_exhausted` | The free allowance is spent and no card is on file. Add a payment method in the console and the same key keeps working. Nothing needs redeploying. | | 402 | `monthly_cap_reached` | Your monthly document ceiling was hit. There is nothing to buy. Tell us your volume and we raise it. | | 402 | `canceled` | Billing for this account was cancelled. Contact us to reactivate. Keys are kept, so nothing needs reissuing. | | 413 | `file_too_large` | Over 20MB. Refused as soon as the size is known. Split the statement or export a smaller range. | | 429 | `too_many_concurrent_requests` | More than 4 documents in flight at once. A limit on documents in flight at once. There is no per-minute limit. Wait a couple of seconds and retry with the same idempotency key. | --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Pricing and billing > $0.49 per parsed document, 25 free, and which responses are billable. No seats, no minimum. Section: Billing Source: https://statementbear.com/docs/billing Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- $0.49 per document that parses, with the first 25 free. No seats, no minimum, no platform fee. ## What counts as one document One file you send us. A PDF holding four accounts is one document. Six months of statements in one PDF is one document. We charge per file, not per account, month, page or transaction. ## What is billable | Response | Billable | Counts against the monthly cap | | --- | --- | --- | | `200`, parsed | Yes, $0.49 | Yes | | `200`, replayed from an idempotency key | No | No | | `422` not a bank statement | No | No | | `500` our failure | No | No | | `402`, `401`, `400`, `405`, `409`, `413`, `415`, `429` | No | No | > **Failed parses cost nothing** > > A failure on our side is free and takes nothing off your allowance. A `422` is free because the file was not a statement. ## The free allowance The first 25 parsed documents on an organisation are free and need no payment method. The pool is counted rather than timed, so it does not expire while you are still evaluating, and adding a card later does not forfeit what is left. ## How you are billed - Usage is invoiced monthly to the card on file. Nothing to pre-purchase, no credit to top up. - A retry cannot charge you twice for one document as long as it carries the same `Idempotency-Key`. See [Idempotency and retries](https://statementbear.com/docs/idempotency). - The [console](https://statementbear.com/dashboard/api) shows the running count for the month and the usage log behind it. - `X-Billable-Documents` is on every response, so you can reconcile against your own count. ## If a payment fails Your integration keeps working. Recovery runs for weeks before anything changes. Only a cancelled account is refused, with `402 canceled`, and its keys are kept so reactivating needs no redeploy. ## Volume The list price holds at any volume you can reach self-serve. Above that, email [api@statementbear.com](mailto:api@statementbear.com) with the number. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # Security and data retention > What happens to a statement you send, what we keep, and for how long. Section: Platform Source: https://statementbear.com/docs/security Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- The API stores no transactions. A statement is parsed and returned, and your system is the record. ## What happens to a PDF you send It is read, parsed, and dropped when the response is written. It is never written to disk, put in storage or attached to an account. ## What we keep | Kept | Why | For how long | | --- | --- | --- | | A usage row per request: timestamp, status, error code, byte size, duration, issuer name, account count, row count, and the filename if you sent one. | Invoices are settled from it. | The life of the account. | | The response body behind an `Idempotency-Key`. | So a replay can be honoured instead of re-parsed and charged twice. | 24 hours, then deleted automatically. | | A hash of each API key. | To authenticate you. The key itself is never stored. | Until the key is revoked. | > **The row count is a count** > > The usage log records how many transactions a document held. It does not record what they were. No merchant name or amount from an API call is stored anywhere. ## Idempotency replays A stored replay body is the only place the API holds transactions at rest. It is kept apart from the billing record and deletes itself after 24 hours. ## Transport and access - TLS on every request. The endpoint is not reachable over plain HTTP. - Keys are long random strings, stored hashed, and revocable by you at any time. See [Authentication](https://statementbear.com/docs/authentication). - Issuing a key and raising a cap are both recorded on our side. - The person whose statement it is has no account here and is never contacted by us. ## AI processing Reading a statement uses an AI model, and the statement's text is processed by our model provider to do it. Nothing else leaves our infrastructure, and the PDF does not. We train nothing on your documents. Email [api@statementbear.com](mailto:api@statementbear.com) if your compliance review needs the provider named and its data processing terms. ## Deletion There is no transaction data to delete. Usage rows can be removed on request, though they are what an invoice is explained from. Email [api@statementbear.com](mailto:api@statementbear.com). --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com # MCP server > Connect this documentation to Claude, Cursor, VS Code or any MCP client. Read only, no API key. Section: Platform Source: https://statementbear.com/docs/mcp Product: StatementBear statement parsing API, $0.49 per document, 25 free. --- A remote MCP server that gives your coding assistant this documentation as it is published today. `MCP https://statementbear.com/api/mcp` ## Connect it Claude Code: ```bash claude mcp add --transport http statementbear https://statementbear.com/api/mcp ``` Claude Desktop: ```json { "mcpServers": { "statementbear": { "type": "http", "url": "https://statementbear.com/api/mcp" } } } ``` Cursor: ```json { "mcpServers": { "statementbear": { "url": "https://statementbear.com/api/mcp" } } } ``` VS Code: ```json { "servers": { "statementbear": { "type": "http", "url": "https://statementbear.com/api/mcp" } } } ``` Claude Desktop keeps that JSON in `claude_desktop_config.json`, Cursor in `~/.cursor/mcp.json`, VS Code in `.vscode/mcp.json` beside the project. Any client that supports a remote MCP server works: it is one HTTP URL with no authentication and no session to manage. ## Tools | Tool | What it does | | --- | --- | | `search_documentation` | Finds the pages and headings matching a question, with a summary of each. | | `get_documentation_page` | Returns a whole page as Markdown, by path or slug. | | `list_documentation_pages` | Every page, its section and its summary. | Every page is also exposed as an MCP resource, for clients that attach documents instead of calling tools. > **It cannot spend money** > > The server serves documentation. It cannot parse a statement, read your usage, issue a key or see anything about your account, so there is no credential to give it. Parsing stays an authenticated POST from your backend. ## Without MCP Every page is served as Markdown at its own URL with a `.md` suffix. There is an index at [/docs/llms.txt](https://statementbear.com/docs/llms.txt) and the whole documentation in one file at [/docs/llms-full.txt](https://statementbear.com/docs/llms-full.txt). The **Copy page** button copies the Markdown of the page you are on. --- All documentation: https://statementbear.com/docs/llms.txt Questions: api@statementbear.com