# El sobre de error (/docs/errores)

Un error es un código HTTP más un solo objeto `error`. No hay sobre de éxito: las respuestas exitosas
**son** el recurso.

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "receptor_incomplete",
    "message": "Receptor 76543212-K is missing `receptor.giro`, `receptor.address.street` and `receptor.address.comuna`, and tipo 33 cannot be filed without <GiroRecep>, <DirRecep>, <CmnaRecep>. Facturia looked: this empresa has emitted no earlier document to 76543212-K carrying them. …",
    "hint": "`error.param` names exactly which field to send. Facturia fills a receptor’s razón social, giro, dirección and comuna from this empresa’s own earlier documents to the same RUT; when there is no earlier one, they have to arrive with the request. Better here than an SII rejection with the folio already spent.",
    "retriable": false,
    "param": "receptor.giro",
    "doc_url": "https://docs.facturia.cl/errors/receptor_incomplete",
    "request_id": "req_01K2R7Q9M4T2VB",
    "empresa": "77928532-4"
  }
}
```

| Campo         | Qué es                                                                                     |
| ------------- | ------------------------------------------------------------------------------------------ |
| `type`        | La familia del error. Determina qué hacer                                                  |
| `code`        | El error específico, estable. Programa contra esto                                         |
| `message`     | Prosa legible por una persona. **No** programes contra esto                                |
| `hint`        | Qué hacer al respecto, para este `code` exacto                                             |
| `retriable`   | ¿Puede funcionar el request **idéntico** más rato, sin cambiarlo?                          |
| `param`       | El campo del request que causó el problema, cuando aplica                                  |
| `doc_url`     | Un enlace a la explicación de este código                                                  |
| `request_id`  | El identificador de esta llamada exacta                                                    |
| `empresa`     | El RUT en cuyo contexto ocurrió                                                            |
| `existing_id` | En conflictos, el id del objeto que ya existe                                              |
| `details`     | Datos estructurados del caso: el `pending_action` que falta, el `checkout_url` de un `402` |
| `sii`         | Presente en `sii_error` — el estado crudo del SII                                          |

### `hint` y `retriable` vienen en la respuesta [#hint-y-retriable-vienen-en-la-respuesta]

Son exactamente el mismo `hint` y el mismo `retriable` que publica `GET /v1/error_codes` para ese
`code`, incorporados en el sobre para que no tengas que hacer una segunda llamada de 75 KB a buscar
qué hacer. No se escriben en el punto donde se lanza el error: se buscan por `code` en el mismo
registro que sirve el diccionario, así que la respuesta y el diccionario no se pueden contradecir.

Faltan **solo** en el caso raro de un código acuñado después de que tu cliente se publicó y que
todavía no está en tu copia local del diccionario. Trátalo como «lee `message` y `doc_url`», nunca
como una falla.

> **`hint` viene en inglés, a propósito**
>
> El diccionario de errores siempre fue inglés y es el mismo objeto en el cable y en
> `GET /v1/error_codes`; traducirlo forkearía el registro por idioma o haría que la respuesta y el
> diccionario dijeran cosas distintas. El español de cada código vive en su ficha de este sitio —
> la que apunta `doc_url` — y en el panel.

## Los nueve tipos [#los-nueve-tipos]

| `type`                  | HTTP      | Significa                                                                                                          | Qué hacer                                                         |
| ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| `invalid_request_error` | 400       | Entrada mal formada o incompleta                                                                                   | Arregla el request. Reintentar igual no sirve                     |
| `invalid_request_error` | **402**   | Falta pagar: `subscription_required` o `plan_limit_reached`                                                        | Manda a una persona a `error.details.checkout_url`. Ver abajo     |
| `authentication_error`  | 401       | Llave ausente, revocada o mal formada                                                                              | Revisa la credencial                                              |
| `permission_error`      | 403       | Llave válida, no permitida: scope, lista blanca de empresas, modo                                                  | Revisa el scope o la empresa. `error.message` nombra el que falta |
| `not_found_error`       | 404       | No existe ese objeto bajo esta llave                                                                               | Revisa el id y el modo de la llave                                |
| `conflict_error`        | 409       | Conflicto de estado: reuso de idempotencia, folio duplicado, empresa ya existente                                  | Mira `existing_id`                                                |
| `unprocessable_error`   | 422       | Bien formado pero viola una regla del dominio: folios agotados, ventana de acuse cerrada, empresa que aún no emite | Es una regla de negocio, no un bug de formato                     |
| `rate_limit_error`      | 429       | Más lento; mira `Retry-After`                                                                                      | Espera lo que dice el header                                      |
| `sii_error`             | 502       | El SII rechazó o falló la llamada de arriba. `error.sii` trae el estado y la glosa crudos                          | Ver [Códigos](/docs/errores/codigos)                              |
| `api_error`             | 500 / 503 | Nuestro. Nada se emitió                                                                                            | Reintenta con backoff                                             |

Son nueve **familias** y diez estados: el `402` no es una décima familia, es el único caso en que un
`invalid_request_error` sale con un código HTTP distinto de `400`, porque «te falta pagar» no es un
request mal formado y merecía su propio estado.

### El `402`, en detalle [#el-402]

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "subscription_required",
    "message": "This live act needs an active subscription for this empresa, and it has none.",
    "retriable": false,
    "details": {
      "empresa": "emp_01K2R7Q4XW9M3B8ZC5YHTVJD6N",
      "empresa_rut": "77928532-4",
      "empresa_name": "Connect SpA",
      "account_plan": "starter",
      "subscription_status": "canceled",
      "grace_until": null,
      "checkout_url": "https://app.facturia.cl/…"
    }
  }
}
```

**`402 subscription_required` se aplica de verdad desde el 2026-08-24.** La suscripción es **por
empresa** ($19.990 + IVA al mes por cada RUT que emite), así que cada acto se decide con la
suscripción de la empresa que nombra el request: un RUT vencido nunca detiene a otro de la misma
cuenta. Se bloquean exactamente cinco actos:

* `POST /v1/documents`
* `POST /v1/documents/{document_id}/credit_note`
* `POST /v1/documents/{document_id}/send`
* `POST /v1/folios/requests`
* `POST /v1/empresas/{empresa_id}/onboarding/go_live`

Las lecturas, la respuesta comercial a un documento recibido (plazo legal de 8 días), las
declaraciones tributarias (plazos legales) y **todo el modo de prueba** no se bloquean nunca. Una
suscripción que vence sigue emitiendo durante 7 días de gracia antes de que esto empiece; una cuenta
con `plan: house` pasa por todas sus empresas.

Lo accionable está en `error.details&#x60;: **`checkout_url` apunta a esa empresa**, así que un agente que
recibe un `402` no tiene que improvisar dónde manda a pagar a su humano. Ficha:
[`subscription_required`](/docs/errores/subscription_required).

**`402 plan_limit_reached` está publicado y hoy no lo emite nada.** El plan es uno solo y con
documentos ilimitados: no existe una cuota de documentos que se pueda agotar. El código sigue
publicado para que un cliente que ya sabe manejar un `402` tenga una definición estable contra la
cual programar. Su ficha lo dice igual de claro:
[`plan_limit_reached`](/docs/errores/plan_limit_reached).

## El `request_id` [#el-request_id]

> **Loguéalo siempre**
>
> `request_id` aparece en **cada** respuesta, exitosa o no, en el header `Facturia-Request-Id`. Con él
> podemos encontrar la llamada exacta. Sin él, una conversación de soporte empieza por reconstruir qué
> pasó.
>
> Un cliente HTTP bien hecho lo guarda junto al resultado, no solo cuando algo falla.

## Cómo manejar errores en la práctica [#cómo-manejar-errores-en-la-práctica]

```js
async function llamar(path, init) {
  const res = await fetch(`https://api.facturia.cl/v1${path}`, init);
  const requestId = res.headers.get('Facturia-Request-Id');

  if (res.ok) return { data: await res.json(), requestId };

  const { error } = await res.json();

  // `hint` viene en la respuesta: no hace falta ir a buscarlo al diccionario.
  // Falta solo si el código es más nuevo que tu cliente, así que hay fallback.
  log.warn({ requestId, code: error.code, quéHacer: error.hint ?? error.message });

  // El único caso en que el estado importa más que el tipo: falta pagar, y
  // `details.checkout_url` es lo que hay que ponerle delante a una persona.
  if (res.status === 402) {
    throw new PagoRequerido(error, error.details?.checkout_url, requestId);
  }

  // `retriable` responde «¿puede funcionar este MISMO request más rato?» para
  // cualquier código, incluidos los que salieron después de este switch.
  switch (error.type) {
    case 'rate_limit_error':
      // El header dice cuánto. No adivines.
      return { retryAfter: Number(res.headers.get('Retry-After') ?? 1), requestId };

    case 'api_error':
      // Nada se emitió. Reintenta con la MISMA Idempotency-Key.
      throw new Reintentable(error, requestId);

    case 'sii_error':
      // Puede ser transitorio. `error.sii` dice qué respondió el SII.
      throw new Reintentable(error, requestId);

    case 'invalid_request_error':
    case 'unprocessable_error':
      // Reintentar el mismo cuerpo va a fallar igual. Arréglalo o escálalo.
      throw new NoReintentable(error, requestId);

    default:
      // Un tipo que no reconoces: `retriable` sigue siendo una respuesta.
      throw error.retriable
        ? new Reintentable(error, requestId)
        : new NoReintentable(error, requestId);
  }
}
```

> **Trampa — Un 4xx no quema tu Idempotency-Key; un 5xx sí es seguro de reintentar**
>
> Los requests que fallan con 4xx **no** consumen la llave de idempotencia: arregla el cuerpo y
> reintenta con la misma llave. Los fallos 5xx también son seguros de reintentar con la misma llave —
> esa es exactamente la situación para la que existe.
>
> Lo que no debes hacer es generar una llave nueva al reintentar un timeout: ahí es donde nacen las
> facturas duplicadas.

## El diccionario legible por máquina [#el-diccionario-legible-por-máquina]

```bash
curl https://api.facturia.cl/v1/error_codes
```

Sin autenticación. Devuelve cada código de Facturia, su contraparte del SII y si conviene reintentar.
Es el mismo contenido que [Códigos de rechazo](/docs/errores/codigos), en una forma que tu build
puede consumir.
