DocsAPI reference

Parse a document

POST/api/v1/documents

Parses one bank or credit card statement PDF and returns it as JSON. This is the whole API.

Request headers#

HeaderRequiredValue
AuthorizationYesBearer sb_live_.... See Authentication.
Content-TypeYesapplication/pdf for a raw body, or multipart/form-data with a part named file.
Idempotency-KeyRecommendedOne unique string per document, reused by every retry. See Idempotency and retries.
Content-LengthNoWhen 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#

HeaderOnMeaning
X-Billable-DocumentsEvery response1 when the call is on your invoice, 0 when it is not. Free trial documents, replays and errors return 0.
Idempotent-ReplayedReplaystrue when the body came from storage.
Allow405POST.
Cache-ControlEvery responseno-store.

Responses#

StatusBodyBillable
200A document object.Yes
400empty_body or missing_file.No
401missing_key or invalid_key.No
402trial_exhausted, monthly_cap_reached or canceled.No
405method_not_allowed, with an Allow header.No
409idempotency_key_not_replayable, with a reason.No
413file_too_large.No
415not_a_pdf or unsupported_media_type.No
422not_a_bank_statement.No
429too_many_concurrent_requests.No
500parse_failed. No allowance is spent.No

Example request#

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

Example response#

200 OK
{
  "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
{
  "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.

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 versionCan 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.
Something wrong or missing on this page? [email protected]