# Errores y límites

El contrato de errores es estable: códigos fijos, un formato único y una
señal explícita de si conviene reintentar. Esta página es la tabla completa
de la plataforma; cada servicio enlaza aquí y solo agrega la forma de sus
códigos propios.

## El formato

Todo error responde el mismo body. Respuesta real de un request sin token:

<!-- ejemplo error_unauthorized:  -->
Respuesta real (2026-09-28).

```json
{
  "code": "UNAUTHORIZED",
  "message": "missing bearer token",
  "request_id": "c95a96e7-1636-40ac-b422-24c0f88030fc",
  "retryable": false
}
```
<!-- /ejemplo -->

Programe contra `code`, que es
contractual; `message` es descriptivo y puede mejorar con el tiempo.
`retryable` indica si el mismo request puede tener éxito al reintentarse sin
cambiar nada: `true` solo en `RATE_LIMITED`, `DOWNLOAD_QUOTA`, `INTERNAL` y
`SERVICE_UNAVAILABLE`; los tres con `Retry-After` dicen cuánto esperar.

## Los códigos

La columna **dominio** marca los códigos que solo un servicio produce; el
resto es de la plataforma y puede aparecer en cualquier endpoint.

| `code` | HTTP | dominio | cuándo ocurre |
|---|---|---|---|
| `INVALID_PARAMS` | 400 | todos | Parámetro faltante o mal formado, `from > to`, rango mayor a 5 años, `as_of_revision` fuera de rango, RUC que no tiene 11 dígitos, o `page_token` corrupto o de otra consulta. |
| `UNAUTHORIZED` | 401 | todos | Token ausente, inválido, revocado o expirado. El servicio nunca repite el token en la respuesta. |
| `FORBIDDEN` | 403 | todos | El token no puede usar esa ruta: un token de colector en `/v1`; `GET /v1/ruc/by-dni/{dni}` mientras el prerrequisito legal E1 esté pendiente; la descarga de un artefacto del padrón con un token sin la marca `descarga_artefactos` ([Consulta RUC](consulta-ruc.md#evidencia)). |
| `UNSUPPORTED_CURRENCY` | 404 | tipo de cambio | Moneda que la API sabe que no sirve (ejemplo real: `currency=EUR`). |
| `UNSUPPORTED_SERIES` | 404 | tipo de cambio | Serie distinta de `publicado` y `cierre`. |
| `NO_OBSERVATIONS` | 404 | tipo de cambio, revisiones | Consulta válida pero sin observaciones: rango sin datos o serie aún sin cobertura ([Tipo de cambio](tipo-de-cambio.md#consulta-historica)); en `/v1/revisions`, `at` anterior a la primera revisión. Distinto por diseño de `UNSUPPORTED_CURRENCY`: "no hay datos" no es "no sirvo esa moneda". |
| `NOT_FOUND` | 404 | todos | Observación, evidencia o artefacto inexistente para el id pedido. |
| `NO_DATA_AT_REVISION` | 404 | consulta RUC | La `as_of_revision` pedida es anterior a la primera publicación del dominio ([Consulta RUC](consulta-ruc.md#consulta-historica)). Todo dominio con publicaciones lo hereda. |
| `RATE_LIMITED` | 429 | todos | Límite de requests del token excedido. Trae `Retry-After` con los segundos a esperar. |
| `DOWNLOAD_QUOTA` | 429 | todos | Cuota diaria de bytes de descarga del token agotada (`/v1/evidence/{id}/descarga`). Trae `Retry-After`. |
| `INTERNAL` | 500 | todos | Error interno. El detalle queda en los registros del servicio (referenciable por `request_id`), nunca en el body. |
| `SERVICE_UNAVAILABLE` | 503 | todos | La base de datos no está disponible (por ejemplo, durante un reinicio). Trae `Retry-After`. |

Un dominio nuevo que necesite un código propio lo declara en esta tabla
antes de producirlo; el [registro de cambios](registro-de-cambios.md) fecha
cada alta.

## Límites de uso

- **60 requests por minuto** y **5 000 por día** por token; cada token
  identifica una instalación.
- Ante `429`, espere lo que indique `Retry-After`.
- Su consumo del mes, por dominio, está en `GET /v1/usage` (ver
  [Empezar](empezar.md#su-consumo-v1-usage)).
- Servicio en desarrollo, sin acceso público por ahora: estos son los
  límites con los que está desplegado.

## Cuota de descarga

Los bytes de evidencia (`GET /v1/evidence/{id}/descarga`) tienen una cuota
diaria por token, aparte del límite de requests:

| cuota | bytes por día |
|---|---|
| por token | 20 GB (propuesto a partir del egreso medido del archivo: 19,8 MB/s; equivale a unos 50 ZIP del padrón al día) |
| credencial de evaluación interna | 2 GB (unos cinco ZIP del padrón) |

La ventana es deslizante de 24 horas. Una captura pequeña (el
`tipoCambio.txt`) cuenta por sus bytes al servirse; un artefacto grande (el
ZIP del padrón) cuenta por su tamaño al emitirse el redirect, y se rechaza
con `DOWNLOAD_QUOTA` si no cabe entero en lo que queda. Los metadatos
(`GET /v1/evidence/{id}`) nunca cuentan. La descarga es parte del plan del
token, sin sobrecargo.

## Paginación a revisión fija

Dos endpoints paginan, con el mismo mecanismo:

| endpoint | máximo por página | orden |
|---|---|---|
| `GET /v1/rates` | 366 ítems | fecha ascendente |
| `GET /v1/ruc/{ruc}/history` | 500 transiciones | ordinal ascendente |

Si hay más, viene `next_page_token`: un token opaco que lleva embebida la
*revisión de la primera página* y la consulta a la que pertenece; se pasa
en `page_token`.

- Recorrer todas las páginas entrega un **snapshot consistente**: aunque el
  dataset avance entre página y página, todas responden a la misma revisión.
- El token solo vale para la misma consulta (moneda/serie/rango; RUC);
  usarlo en otra responde `INVALID_PARAMS`.
- `total` (tasas) y `total_transiciones` (historia) son el total a esa
  revisión, no el tamaño de la página. En la historia, `estado_inicial`
  viaja solo en la primera página.

Límite adicional: el rango `from`–`to` de tasas no puede exceder 5 años por
consulta.

## Caché

| respuesta | política |
|---|---|
| Respuesta anclada a revisión (`as_of_revision`, `page_token`) del tipo de cambio | `immutable` + `ETag` (SHA-256 del body) — cacheable indefinidamente. |
| `/v1/rates`, `/v1/rates/latest` sin anclar | `no-store` — el dataset puede avanzar; no la guarde. |
| `/v1/ruc/{ruc}`, `by-dni`, `/history`, `/v1/ruc-coverage` y `/v1/evidence/{sha256}` (metadatos del padrón), ancladas o no | `private, no-store`, sin `ETag`, `Vary: Authorization` salvo `ruc-coverage` (adenda a ESP-006 A.7, desde R0): cada respuesta lleva valores propios del pedido. `If-None-Match: *` sobre un `200` → `304` sin cuerpo y con los mismos encabezados; otro valor → `200`; un error se responde aunque traiga la precondición. La estabilidad del contenido anclado es propiedad del dato, no de la caché. |
| `/v1/evidence/{id}/descarga` de una captura del tipo de cambio | `immutable` + `ETag` = SHA-256 de los bytes. |
| `/v1/evidence/{uuid}` (metadatos de una captura) | `no-store` — el enlace firmado cambia en cada emisión. |
| `/v1/observations/{id}` | `no-store` — la observación es inmutable, pero su *representación* incluye la cadena de supersesión y los eventos, que pueden crecer. |

## Alertas y salud: la plataforma se vigila a sí misma

Estos endpoints valen para todo dominio y no tienen página de servicio:

- `GET /v1/alerts` — incidentes operativos abiertos (`open=true`, por
  defecto) o todos: fuente no disponible, invariante fallida,
  discrepancia entre fuentes, plausibilidad baja, cambio de formato,
  verificación fallida del padrón. Cada alerta tiene ciclo de vida
  (`abierta` → `reconocida` → `resuelta`) y la misma condición no se
  duplica. Un consumidor que quiera reaccionar consulta aquí; el servicio no
  empuja notificaciones en v1.
- `GET /v1/health/live` — vivo o no; el único endpoint sin token junto con
  el calendario. No toca la base de datos.
- `GET /v1/health/ready` — listo para servir: responde `{"status":
  "ready"}` solo si la base de datos contesta.
- `GET /v1/health/freshness` — la frescura de todas las series del tipo de
  cambio, el conteo de alertas abiertas y el estado del reloj del servidor
  (`ntp`: `sincronizado`, `no_sincronizado` o `no_verificado`; el timestamp
  de las revisiones depende de ese reloj). La frescura del padrón RUC viaja
  en sus propias respuestas.
- `GET /v1/annotations` — anotaciones humanas (valor, actor, justificación,
  revisión), filtrables por moneda, serie y rango. Jamás forman parte de la
  vigencia de una serie oficial ni reemplazan una observación: son un
  registro paralelo, visible aquí y con `include_annotations=true` en
  `/v1/rates`.
