> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors, retries, and pagination

> Catch typed errors, retry writes safely with idempotency keys, and read every page of a list.

## Catch an error

Every failed request throws a subclass of `PolymorfaError`:

```typescript theme={null}
import {
  PolymorfaConflictError,
  PolymorfaError,
  PolymorfaPaymentRequiredError,
  PolymorfaRateLimitError,
} from "@polymorfa/sdk";

try {
  await platform.sessions.start("<number-id>", { projectId: "<project-id>" });
} catch (error) {
  if (error instanceof PolymorfaPaymentRequiredError) {
    console.error("Add credit before starting this Number", error.requestId);
  } else if (error instanceof PolymorfaConflictError) {
    console.error("The Number changed state", error.code);
  } else if (error instanceof PolymorfaRateLimitError) {
    console.error("Rate limited", error.rateLimitReason, error.metadata?.headers["retry-after"]);
  } else if (error instanceof PolymorfaError) {
    console.error(error.status, error.code, error.message, error.requestId);
  } else {
    throw error;
  }
}
```

Each error has these fields:

| Field | Contains |
| - | - |
| `status` | HTTP status, when the API answered. |
| `code` | Stable error code, such as `missing_scope`. See [Errors](/api/errors). |
| `message` | Readable description. |
| `requestId` | Request ID. Include it when you contact support. |
| `requestLogUrl` | Console page for this request, when the API returns one. |
| `docUrl` | Documentation link for `code`. |
| `rateLimitReason` | Why a `429` happened. |
| `details` | Extra error data from the API. |
| `metadata` | Response status, headers, and attempt count. |

Error codes never change meaning. The API can add new ones, so handle unknown
codes. `isKnownPolymorfaErrorCode(code)` tells you whether the installed SDK
knows a code.

## Error classes

| Class | When |
| - | - |
| `PolymorfaValidationError` | `400`, `413`, or `422`: the request is invalid. |
| `PolymorfaAuthenticationError` | `401`: the key is missing, invalid, expired, or revoked. |
| `PolymorfaPaymentRequiredError` | `402`: the team cannot pay for the request. |
| `PolymorfaAuthorizationError` | `403`: the key lacks a permission, or the feature is not enabled for the team. |
| `PolymorfaNotFoundError` | `404`: the resource does not exist or the key cannot see it. |
| `PolymorfaConflictError` | `409`: the resource changed, or an idempotency key was reused. |
| `PolymorfaRateLimitError` | `429`: too many requests. |
| `PolymorfaServerError` | `5xx`: the API failed. |
| `PolymorfaTimeoutError` | No response within `timeoutMs`. |
| `PolymorfaConnectionError` | The SDK could not reach the API. |
| `PolymorfaCancelledError` | Your `signal` aborted the request. |
| `PolymorfaConfigurationError` | Invalid client options or arguments. The SDK throws it before sending. |
| `WebhookSignatureError` | A webhook signature did not match. |
| `PolymorfaMediaIntegrityError` | Downloaded WhatsApp media failed its integrity checks. |

### 402: the team cannot pay

Paid actions, such as starting a paid Number, reserve credit first. A `402`
means the team cannot pay for the request or lacks the entitlement. Retrying
does not help. Check the balance and plan in the Console, then try again. The
SDK never retries a `402`.

### 409: the resource changed

Settings with a revision, such as call settings and Number configuration, fail
with `state_conflict` when someone else changed them after you read them. Read
the current value, apply your change again, and send the new revision.

A `409` with an `idempotency_*` code concerns your idempotency key. See
[Retry a write safely](#retry-a-write-safely).

### 429: rate limited

`rateLimitReason` says which limit applied, such as `request_rate` or
`whatsapp`. Wait for the `retry-after` header before you send again. The SDK
already does this for requests it retries. See [Rate limits](/api/rate-limits).

## Automatic retries

The SDK retries a request when both are true:

* The request is a `GET`, `HEAD`, or `OPTIONS`, or it has an `idempotencyKey`.
* It failed with a network error, a timeout, or status `408`, `409`, `429`, or
  `5xx`.

It waits for `Retry-After` when the API sends it, up to 60 seconds. Otherwise it
backs off exponentially, up to 5 seconds between attempts. It retries twice by
default. Set `maxNetworkRetries` on the client or the request to change that,
or to `0` to turn retries off.

A write without an idempotency key is sent once. The SDK does not retry it,
because it cannot tell whether the first attempt ran.

## Retry a write safely

An idempotency key makes a write safe to repeat. The API runs it once and
returns the stored result for every repeat with the same key:

```typescript theme={null}
await messaging.messages.send(
  "<number-id>",
  { conversation: { phoneNumber: "+15550001111" }, content: { text: "Your order shipped" } },
  { idempotencyKey: "order-1042-shipped" },
);
```

Use an ID from your own system, such as an order event ID, so a restarted
process sends the same key. The API keeps each key for 24 hours per credential.

These methods create a random key when you do not pass one, so their automatic
retries are always safe: `messages.send`, `messages.react`,
`chats.editMessage`, `chats.deleteMessage`, `channels.reactToMessage`,
`campaigns.create`, `campaigns.launch`, `campaigns.pause`, `campaigns.resume`,
`campaigns.stop`, `webhooks.test`, and `operations.cancel`.

| Code | Meaning | What to do |
| - | - | - |
| `idempotency_conflict` | The key was used for a different request. | Use a new key for a new request. |
| `idempotency_in_progress` | The first request is still running. | The SDK waits and retries. |
| `idempotency_completed` | The first request already succeeded, but its response was lost. | Read the resource instead of writing again. |
| `idempotency_outcome_unknown`, `result_unknown` | The first attempt's outcome is unknown. | Check events or the resource before you send again with a new key. |

## Read every page

Lists of events, webhooks, webhook deliveries, operations, call records, and
call opt-outs return a `CursorPage`. Loop over it with `for await` and the SDK
fetches the next pages for you:

```typescript theme={null}
for await (const event of await project.events.list({ type: "message.received" })) {
  console.log(event.id, event.createdAt);
}
```

To fetch one page at a time, use `items`, `hasMore`, and `nextPage()`:

```typescript theme={null}
let page: Awaited<ReturnType<typeof project.events.list>> | null =
  await project.events.list({ limit: 100 });

while (page) {
  for (const event of page.items) console.log(event.id);
  page = await page.nextPage();
}
```

`nextPage()` returns `null` after the last page. `page.response.metadata` holds
the response metadata.

Some methods return the API's page envelope directly as `{ data, page }`. Pass
`page.nextCursor` as `cursor` while `page.hasMore` is `true`:

```typescript theme={null}
let cursor: string | undefined;
do {
  const response = await platform.customers.list({ projectId: "<project-id>", cursor });
  for (const customer of response.data.data) console.log(customer.id);
  cursor = response.data.page.hasMore ? response.data.page.nextCursor ?? undefined : undefined;
} while (cursor);
```

See [Pagination](/api/pagination) for page limits.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.