Parse a document
POST
/api/v1/documentsParses 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. |
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. |
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/pdfand the bytes. - Multipart.
Content-Type: multipart/form-datawith the PDF in a part namedfile. 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. | 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#
Example 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 } }
Example error#
{ "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 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. |
Something wrong or missing on this page? [email protected]