# Async reads

> Send a file with async=true, get 202 at once, and collect the result by webhook or by polling. Required past 60 pages.

Section: Guides
Source: https://statementbear.com/docs/async
Product: StatementBear statement parsing API, $0.08 per page, 200 pages free.

---

Add `async=true` and the POST answers `202` as soon as the file is accepted. The result arrives by webhook, or by fetching the document until it is done.

## When to use it

- **A file over 60 pages.** Up to 500 pages are read with async. Without it, a longer file returns `413 too_many_pages`.
- **A batch.** A synchronous request over 4 in flight returns `429`. Async requests wait their turn instead.
- **A short request timeout**, such as a serverless function or a request from a browser through your backend.

## Send it

Pass `async=true` as a query parameter, or as a multipart part named `async`. Everything else about the request is the same.

cURL:

```bash
# Answers 202 at once with the document's id.
curl -X POST "https://statementbear.com/api/v1/documents?async=true" \
  -H "Authorization: Bearer $STATEMENTBEAR_KEY" \
  -H "Content-Type: application/pdf" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @statement.pdf

# Then fetch it until status is succeeded or failed.
curl https://statementbear.com/api/v1/documents/doc_5f2a9c4b1e77d0a3c8b6e412 \
  -H "Authorization: Bearer $STATEMENTBEAR_KEY"
```

Node.js:

```javascript
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const headers = { Authorization: `Bearer ${process.env.STATEMENTBEAR_KEY}` };

const accepted = await fetch("https://statementbear.com/api/v1/documents?async=true", {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/pdf",
    "Idempotency-Key": randomUUID(),
  },
  body: await readFile("statement.pdf"),
});
if (!accepted.ok) {
  const { error } = await accepted.json();
  throw new Error(`${error.code}: ${error.message}`);
}

let doc = await accepted.json();
while (doc.status === "processing") {
  await new Promise((r) => setTimeout(r, 5000));
  doc = await (await fetch(`https://statementbear.com/api/v1/documents/${doc.id}`, { headers })).json();
}

if (doc.status === "failed") throw new Error(`${doc.error.code}: ${doc.error.message}`);
console.log(doc.accounts.length, "accounts");
```

Python:

```python
import os, time, uuid, requests

headers = {"Authorization": f"Bearer {os.environ['STATEMENTBEAR_KEY']}"}

with open("statement.pdf", "rb") as f:
    res = requests.post(
        "https://statementbear.com/api/v1/documents",
        params={"async": "true"},
        headers={**headers, "Content-Type": "application/pdf", "Idempotency-Key": str(uuid.uuid4())},
        data=f.read(),
        timeout=60,
    )
if res.status_code not in (200, 202):
    err = res.json()["error"]
    raise RuntimeError(f"{err['code']}: {err['message']}")

doc = res.json()
while doc["status"] == "processing":
    time.sleep(5)
    doc = requests.get(f"https://statementbear.com/api/v1/documents/{doc['id']}", headers=headers, timeout=30).json()

if doc["status"] == "failed":
    raise RuntimeError(f"{doc['error']['code']}: {doc['error']['message']}")
```

## The 202

The body is the document with `status: "processing"` and every read field empty. `Location` holds its URL. `X-Billable-Pages` and `usage.billablePages` say what will be billed if the read succeeds.

202 Accepted:

```json
{
  "id": "doc_8c1d40e97a2b5f3e6d0c9a71",
  "object": "document",
  "livemode": true,
  "created": 1785372094,
  "status": "processing",
  "error": null,
  "filename": "hsbc-2025.pdf",
  "pages": 84,
  "metadata": {},
  "collection": null,
  "issuer": null,
  "currency": null,
  "period": null,
  "accounts": [],
  "recurring": [],
  "verification": null,
  "authenticity": null,
  "usage": { "pages": 84, "billablePages": 84 }
}
```

Everything that can be decided from the file is decided before the `202`: authentication, size and type, the password, the page count, and your free pages and monthly cap. Those errors come back on the POST exactly as they do without async.

## Get the result

| Way | How |
| --- | --- |
| Webhook | Subscribe an endpoint to `document.succeeded` and `document.failed`. The event carries the whole document. See [Webhooks](https://statementbear.com/docs/webhooks). |
| Polling | `GET /api/v1/documents/{id}` every five seconds until `status` is `succeeded` or `failed`. Fetching a document is free. |

A read takes as long as it would synchronously, 10 to 40 seconds for a typical statement and longer for a long one. It starts as soon as one of your account's read slots is free.

## When the read fails

`status` becomes `failed` and `error` holds the same `code` and `message` a synchronous request would have returned, usually `not_a_bank_statement` or `parse_failed`. A failed read is not charged and `usage.billablePages` drops to `0`. A read interrupted on our side is restarted; if it still cannot finish it fails with `parse_failed`.

## Rules

- `format` other than `json` is refused with `400 format_requires_sync`. Fetch the finished document as JSON.
- Send an `Idempotency-Key`. A repeat returns the same document as it stands: `202` while processing, `200` once finished. See [Idempotency and retries](https://statementbear.com/docs/idempotency).
- A test key answers `202`, then `succeeded` a couple of seconds later, with the scenario you asked for.

---

All documentation: https://statementbear.com/docs/llms.txt
Questions: support@statementbear.com
