# Errors, idempotency and pagination

Three things that make any integration robust: understanding the errors, retrying without duplicating, and walking large lists.

## Typed errors

Every API error carries a body with `type`, `code`, `message` and a `request_id` (send it to support if something goes wrong). The SDK maps them to **classes**:

```ts
import { MetRateLimitError, MetAuthError, MetInvalidRequestError } from '@meteor.ia/sdk';

try {
  await met.runs.create({ input: 'x' });
} catch (err) {
  if (err instanceof MetRateLimitError) {
    console.log(`Retry in ${err.retryAfter}s`);
  } else if (err instanceof MetAuthError) {
    console.log('Invalid or revoked key');
  } else if (err instanceof MetInvalidRequestError) {
    console.log(err.code, err.message);
  } else throw err;
}
```
```python
from meteor_ia import MetRateLimitError, MetAuthError, MetInvalidRequestError

try:
    met.runs.create("x")
except MetRateLimitError as e:
    print(f"Retry in {e.retry_after}s")
except MetAuthError:
    print("Invalid or revoked key")
except MetInvalidRequestError as e:
    print(e.code, e.message)
```

The taxonomy is a closed set:

| `type` | Class | HTTP |
|---|---|---|
| `authentication_error` | `MetAuthError` | 401 |
| `invalid_request_error` | `MetInvalidRequestError` | 400 |
| `rate_limit_error` | `MetRateLimitError` | 429 |
| `agent_error` | `MetAgentError` | 5xx from the Met |
| `api_error` | `MetApiError` | others |

## When something fails: look at your own requests

Every error carries a **`request_id`**, and that id is **searchable**. You don't
have to guess what happened or open a ticket to find out.

```ts
try {
  await met.contacts.create({ name: 'Ada' });
} catch (err) {
  console.error(err.requestId);   // req_01J8… ← save this
}
```
```python
try:
    met.contacts.create(name="Ada")
except Exception as e:
    print(e.request_id)   # req_01J8… ← save this
```
```bash
curl -i -X POST https://api.met.meteor.com.co/api/v1/contacts \
  -H "Authorization: Bearer $MET_API_KEY" \
  -H "Content-Type: application/json" -d '{}'
# The id comes in the body and in the X-Request-Id header
```

With that id, go into your **Workbench** at **Settings → Developers → Activity** and
you get:

| tab | what it tells you |
|---|---|
| **Logs** | every request from your workspace, filterable by key, by status class (2xx/4xx/5xx) and **by `request_id`**. Each row expands and shows the bodies — the error ones include the detail that caused the 400 |
| **Summary** | volume, error distribution and p95 latency, over 24 h / 7 d / 30 d |
| **Health** | recent errors grouped by code, plus the per-key quota alerts (80% / 100%) |
| **Webhooks** | outbound deliveries and the attempt log per endpoint |

It is the short path: you paste the `request_id` into Logs and see exactly what you
sent and what we answered. If you still need to write to us, send us that id — it is
the first thing we are going to ask for.

> The Workbench is served with your panel session, never by API key, and only shows
> your own workspace's traffic.

## Retries and idempotency

The SDK **retries on its own** for `429` and `5xx` with exponential backoff (honoring `Retry-After`). You don't have to reimplement it.

So that retrying a `POST` doesn't duplicate — for example, creating two runs because of a network blip — the SDK sends an automatic **Idempotency-Key** on every POST with side effects. If the request is resent with the same key, the API returns the same result without running it again.

```ts
// Both calls share automatic idempotency; a retry does not create two runs.
await met.runs.create({ input: 'x' });
```

With `curl`, send the header yourself:

```bash
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000"
```

## Pagination

**There are two ways to paginate in the API, and it is worth knowing which one applies to you** — not because the code changes much, but because using the wrong one doesn't raise an error.

### By cursor

Runs, contacts and the items of a collection return `{ data, has_more }` with an opaque cursor. The SDK walks it for you:

```ts
for await (const run of met.runs.iterate()) {
  // ...every run, every page
}
```
```python
for run in met.runs.iterate():
    ...
```

If you'd rather drive the cursor by hand, `list()` accepts `limit` and `starting_after`:

```ts
const page = await met.runs.list({ limit: 20 });
if (page.has_more) {
  const next = await met.runs.list({ limit: 20, starting_after: page.data.at(-1).id });
}
```

This is the stable mode: even if new records come in while you walk, nothing gets duplicated or skipped.

### By offset

Billing, Media library, events, webhook deliveries and item search paginate with `limit` + `offset` (search, with `limit` + `page`). Each has its own iterator:

```ts
for await (const e of met.billing.iterateExecutions()) { /* ... */ }
for await (const a of met.assets.iterate({ mime_prefix: 'image/' })) { /* ... */ }
for await (const it of met.items.iterateSearch('invoice')) { /* ... */ }
```
```python
for e in met.billing.iterate_executions():
    ...
for a in met.assets.iterate(mime_prefix="image/"):
    ...
```

**What you need to know:** sending `starting_after` to one of these endpoints **does not fail**. The parameter is ignored, it answers `200` and you get the first page over and over — an infinite loop that looks like it works. If you write the pagination by hand, check in the [reference](../../reference.html) which parameters the endpoint accepts.

And offset **is not stable**: if something enters or leaves the set while you walk it, a row can show up twice or not at all. That is a limitation of the endpoint. For an exact sweep over data that moves, the cursor one is what you want.
