# Errores, idempotencia y paginación

Tres cosas que hacen robusta cualquier integración: entender los errores, reintentar sin duplicar, y recorrer listas grandes.

## La forma de una respuesta

Toda respuesta correcta de la API viaja dentro de un **sobre**. No llega el
recurso pelado: llega envuelto, y el recurso está en `data`.

```json
{
  "success": true,
  "data": { "object": "run", "id": "run_01HXYZ" }
}
```

Un error usa el mismo sobre con el signo cambiado, y por eso `success` es el
primer campo que conviene mirar:

```json
{
  "success": false,
  "error": { "type": "invalid_request_error", "code": "missing_scope", "message": "…" },
  "request_id": "req_01J8XYZ"
}
```

**Con los SDK oficiales no tienes que pensar en esto**: te entregan el contenido
de `data` y convierten el sobre de error en una excepción. El sobre te importa si
llamas con `curl`, si escribes tu propio cliente, o si generas uno del contrato
—en cuyo caso ya viene declarado.

Una excepción que conviene conocer: **las listas paginadas ponen `pagination`
afuera de `data`**, como hermana y no como hija.

```json
{
  "success": true,
  "data": [ { "id": 9812 }, { "id": 9813 } ],
  "pagination": { "page": 1, "limit": 50, "total": 128, "pages": 3 }
}
```

## Errores tipados

Todos los errores de la API traen un cuerpo con `type`, `code`, `message` y un `request_id` (envíalo al soporte si algo falla). El SDK los mapea a **clases**:

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

try {
  await met.runs.create({ input: 'x' });
} catch (err) {
  if (err instanceof MetRateLimitError) {
    console.log(`Reintentar en ${err.retryAfter}s`);
  } else if (err instanceof MetAuthError) {
    console.log('Key inválida o revocada');
  } 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"Reintentar en {e.retry_after}s")
except MetAuthError:
    print("Key inválida o revocada")
except MetInvalidRequestError as e:
    print(e.code, e.message)
```

La taxonomía es un conjunto cerrado:

| `type` | Clase | HTTP |
|---|---|---|
| `authentication_error` | `MetAuthError` | 401 |
| `invalid_request_error` | `MetInvalidRequestError` | 400 |
| `rate_limit_error` | `MetRateLimitError` | 429 |
| `agent_error` | `MetAgentError` | 5xx del Met |
| `api_error` | `MetApiError` | otros |

## Cuando algo falla: mira tus propias requests

Cada error trae un **`request_id`**, y ese id es **buscable**. No tienes que
adivinar qué pasó ni abrir un ticket para saberlo.

```ts
try {
  await met.contacts.create({ name: 'Ada' });
} catch (err) {
  console.error(err.requestId);   // req_01J8… ← guarda esto
}
```
```python
try:
    met.contacts.create(name="Ada")
except Exception as e:
    print(e.request_id)   # req_01J8… ← guarda esto
```
```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 '{}'
# El id viene en el cuerpo y en el header X-Request-Id
```

Con ese id, entra a tu **Workbench** en **Ajustes → Desarrolladores → Actividad** y
tienes:

| pestaña | qué te dice |
|---|---|
| **Registros** | cada request de tu workspace, filtrable por key, por clase de status (2xx/4xx/5xx) y **por `request_id`**. Cada fila se expande y muestra los bodies — los de error incluyen el detalle que causó el 400 |
| **Resumen** | volumen, distribución de errores y latencia p95, por 24 h / 7 d / 30 d |
| **Salud** | errores recientes agrupados por código, más las alertas de cuota por key (80% / 100%) |
| **Webhooks** | entregas salientes y el log de intentos por endpoint |

Es el camino corto: pegas el `request_id` en Registros y ves exactamente qué mandaste
y qué contestamos. Si igual necesitas escribirnos, mándanos ese id — es lo primero
que vamos a pedir.

> El Workbench se sirve con tu sesión del panel, nunca por API key, y solo muestra el
> tráfico de tu propio workspace.

## Reintentos e idempotencia

El SDK **reintenta solo** los `429` y `5xx` con backoff exponencial (respeta `Retry-After`). No tienes que reimplementarlo.

Para que reintentar un `POST` no duplique (por ejemplo, crear dos runs por un corte de red), el SDK manda una **Idempotency-Key** automática en cada POST con efectos. Si el request se reenvía con la misma key, la API devuelve el mismo resultado sin volver a ejecutar.

```ts
// Ambas llamadas comparten idempotencia automática; un retry no crea dos runs.
await met.runs.create({ input: 'x' });
```

Con `curl`, manda tú el header:

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

## Paginación

**Hay dos formas de paginar en la API, y conviene saber cuál te toca** — no porque cambie mucho el código, sino porque usar la equivocada no da error.

### Por cursor

Runs, contactos e ítems de una colección devuelven `{ data, has_more }` con un cursor opaco. El SDK lo recorre por ti:

```ts
for await (const run of met.runs.iterate()) {
  // ...cada run, todas las páginas
}
```
```python
for run in met.runs.iterate():
    ...
```

Si prefieres controlar el cursor a mano, `list()` acepta `limit` y `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 });
}
```

Es el modo estable: aunque entren registros nuevos mientras recorres, no se te duplica ni se te salta nada.

### Por desplazamiento

Facturación, Mediateca, eventos, entregas de webhook y la búsqueda de ítems paginan con `limit` + `offset` (la búsqueda, con `limit` + `page`). Cada uno tiene su iterador:

```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('factura')) { /* ... */ }
```
```python
for e in met.billing.iterate_executions():
    ...
for a in met.assets.iterate(mime_prefix="image/"):
    ...
```

**Lo que hay que saber:** mandarle `starting_after` a uno de estos endpoints **no falla**. El parámetro se ignora, responde `200` y devuelves la primera página una y otra vez — un bucle infinito que parece funcionar. Si escribes la paginación a mano, revisa en la [referencia](../reference.html) qué parámetros acepta el endpoint.

Y el desplazamiento **no es estable**: si algo entra o sale del conjunto mientras recorres, una fila puede aparecer dos veces o ninguna. Es una limitación del endpoint. Para un barrido exacto sobre datos que se mueven, el que sirve es el de cursor.
