# Errores y reintentos

> Catálogo estable de códigos de error, forma del envelope de error, qué se puede reintentar y cómo lo hace el SDK.

## Forma de un error

Toda respuesta de error (salvo el token endpoint, que usa el formato RFC 6749) lleva `data: null` y un elemento en `errors`:

```json
{
  "data": null,
  "meta": { "api_version": "1", "request_id": "req_…" },
  "errors": [
    {
      "code": "VALIDATION_ERROR",
      "message": "El campo owner_uid no forma parte del contrato v1.",
      "field": "owner_uid",
      "retryable": false
    }
  ]
}
```

- Decide siempre por `code`; el `message` es diagnóstico y puede cambiar.
- `field` señala el campo, cabecera o parámetro rechazado (`jewel.stones.0.quantity`, `Idempotency-Key`, `cursor`).
- `diagnostic_id` aparece en fallos técnicos para que Zafirox localice la causa sin exponerla.
- Guarda `meta.request_id` (también en la cabecera `X-Request-Id`) en tus registros: es la referencia para soporte.

## Catálogo

| Código | HTTP | Reintentable | Causa |
| --- | --- | --- | --- |
| `AUTHENTICATION_REQUIRED` | 401 | no | Token ausente, mal formado, con firma inválida o vencido |
| `TOKEN_AUDIENCE_MISMATCH` | 401 | no | Token emitido para otro recurso |
| `CLIENT_CREDENTIALS_INVALID` | 401 | no | Credenciales incorrectas al pedir el token |
| `INVALID_GRANT` | 400 | no | `code` o `refresh_token` inválido, expirado, ya usado, de otro cliente o con `code_verifier`/`redirect_uri` distintos (token endpoint, `error: invalid_grant`) |
| `ACCOUNT_LOGIN_REJECTED` | 401 | no | La persona no pudo iniciar sesión en «Acceder con Zafirox» (`field`: `INVALID_CREDENTIALS`, `ACCOUNT_SUSPENDED`, `ACCOUNT_BLOCKED`); sólo lo ve la pantalla de cuentas |
| `GATEWAY_KEY_INVALID` | 403 | no | Paso reservado a la pantalla de cuentas invocado sin su llave; una integración nunca debería recibirlo |
| `SCOPE_REQUIRED` | 403 | no | El token no tiene el scope de la operación |
| `CLIENT_SCOPE_INVALID` | 403 | no | Actor fuera de la empresa, master inactivo o sujeto de escritura fuera del alcance |
| `CLIENT_ORIGIN_NOT_ALLOWED` | 403 | no | `Origin` no registrado para el cliente |
| `RESOURCE_NOT_FOUND` | 404 | no | El recurso no existe o no pertenece al alcance |
| `VALIDATION_ERROR` | 422 | no | Payload, parámetro, cursor o cabecera inválidos |
| `BUSINESS_RULE_VIOLATION` | 422 | no | Regla de negocio de órdenes (fecha no hábil, urgente sin autorización…) |
| `IDEMPOTENCY_CONFLICT` | 409 | no | Misma `Idempotency-Key` con otro payload |
| `CREATION_IN_PROGRESS` | 409 | sí | La misma creación sigue en curso |
| `ORDER_CREATE_VERSION_RETIRED` | 410 | no | Creación v1 retirada al publicar v2; cambia al contrato v2 |
| `order_policy.version` | 409 | no | Versión o SHA-256 de política no reconocido |
| `order_policy.field`, `order_policy.required` | 422 | no | Campo fuera del perfil o dato requerido ausente |
| `order_policy.weight`, `order_policy.measurement`, `order_policy.design` | 422 | no | Peso, medida o referencia de diseño inválidos |
| `order_policy.catalog`, `order_policy.text`, `order_policy.delivery` | 422 | no | Opción de catálogo, límite de texto o entrega inválida |
| `RATE_LIMITED` | 429 | sí | Límite por cliente superado; respeta `Retry-After` |
| `CLIENT_API_DISABLED` | 503 | no | La API está apagada por mantenimiento; no reintentes automáticamente, vuelve a intentarlo más tarde |
| `UPSTREAM_UNAVAILABLE` | 503 | sí | Dependencia técnica no disponible; lleva `diagnostic_id` |

## Qué reintentar

- Reintenta sólo errores `retryable: true`, timeouts y fallos de red, y únicamente en operaciones idempotentes: todas las lecturas y `POST /api/v2/client/orders` **con la misma `Idempotency-Key`**.
- Espera `Retry-After` segundos cuando el servidor lo envía (429, 503); si no, usa una espera creciente.
- Nunca reintentes 401, 403, 404, 409 `IDEMPOTENCY_CONFLICT` ni 422: el resultado será el mismo.

## Cómo lo hace el SDK

Cada clase de error extiende `ZafiroxApiError` y conserva `code`, `status`, `requestId`, `retryable`, `field`, `diagnosticId` y `retryAfterSeconds`:

| Clase | HTTP |
| --- | --- |
| `AuthenticationError` | 401 |
| `AuthorizationError` | 403 |
| `NotFoundError` | 404 |
| `ConflictError` | 409 |
| `ValidationError` | 422 |
| `RateLimitError` | 429 |
| `UpstreamError` | 5xx |
| `TransportError` | red (`NETWORK_ERROR`), timeout (`TIMEOUT`) o envelope inválido (`INVALID_ENVELOPE`) |

```js
import { ValidationError, RateLimitError, ZafiroxApiError } from '@zafirox/client-sdk';

try {
  await client.orders.create(entradaV2, { idempotencyKey: 'pedido-77' }); // SDK v2, pendiente de publicación
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Corrige el campo', error.field, error.message);
  } else if (error instanceof RateLimitError) {
    console.warn('Espera', error.retryAfterSeconds, 's');
  } else if (error instanceof ZafiroxApiError) {
    console.error(error.code, error.status, error.requestId);
  } else {
    throw error;
  }
}
```

Por defecto el SDK hace **dos intentos** con 250 ms de espera (o el `Retry-After` del servidor, tope 5 s). Ajusta `retry: { maxAttempts, backoffMs, maxRetryAfterMs }` en las opciones del cliente; `maxAttempts: 1` desactiva los reintentos.
