# 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
