# Errores

> Códigos de error de la API de Simplo, clases de error del SDK por status HTTP y las reglas de reintento automático.

Toda falla del SDK lanza una subclase de `SimploError`. Captura estrecho
cuando puedas actuar sobre el caso, amplio en el resto:

```ts
import Simplo, { RateLimitError, SimploError, ValidationError } from '@simplohq/sdk';

const simplo = new Simplo();

try {
  await simplo.dtes.emit(params);
} catch (error) {
  if (error instanceof ValidationError) {
    // Input malo — corrige la request, no la reintentes tal cual.
    for (const issue of error.details ?? []) {
      console.error(`${issue.field}: ${issue.code} — ${issue.message}`);
    }
  } else if (error instanceof RateLimitError) {
    // El SDK ya reintentó; frena al llamador.
    console.error(`Rate limit. Reintenta en ${error.retryAfter ?? '?'}s`);
  } else if (error instanceof SimploError) {
    console.error(error.status, error.code, error.message, error.requestId);
  } else {
    throw error; // error de programación — no lo tragues
  }
}
```

## Propiedades del error

| Propiedad   | Tipo                  | Descripción                                                          |
| ----------- | --------------------- | -------------------------------------------------------------------- |
| `status`    | `number \| undefined` | Status HTTP (ausente en fallas de conexión)                          |
| `code`      | `string \| undefined` | Código de la API legible por máquina, ej. `"VALIDATION_ERROR"`       |
| `message`   | `string`              | Descripción legible por humanos                                      |
| `requestId` | `string \| undefined` | ID de la request — inclúyelo al contactar soporte                    |
| `details`   | `ValidationIssue[]`   | Issues a nivel de campo (`field`, `code`, `message`) en errores de validación |
| `action`    | `string \| undefined` | Siguiente paso recomendado por la API, cuando existe                 |
| `docUrl`    | `string`              | Link a la documentación relevante                                    |

## Clases de error por status

| Status  | Clase                 | ¿Reintentar?           | Causas típicas                                              |
| ------- | --------------------- | ---------------------- | ----------------------------------------------------------- |
| 400/422 | `ValidationError`     | No — corrige la request | Dígito verificador de RUT inválido, campos requeridos ausentes |
| 401     | `AuthenticationError` | No — corrige credenciales | API key errónea o revocada ([Autenticación](/comienza/autenticacion/)) |
| 403     | `PermissionError`     | No                     | La key no tiene el permiso o rol requerido                  |
| 404     | `NotFoundError`       | No                     | ID equivocado, o recurso de otra empresa                    |
| 409     | `ConflictError`       | Depende (ver abajo)    | Folio duplicado; idempotency key aún en vuelo               |
| 429     | `RateLimitError`      | Automático             | Demasiadas requests (los límites son por empresa)           |
| 5xx     | `ServerError`         | Automático (si es seguro) | Problema transitorio de la API                           |
| —       | `ConnectionError`     | Automático (si es seguro) | Falla de red, DNS, timeout                               |
| —       | `WebhookVerificationError` | No — rechaza la entrega | Firma mala, timestamp vencido ([Webhooks](/referencia/webhooks/)) |

## El sobre de error, capa por capa

Los errores vienen de **dos capas** y sus sobres no son idénticos:

- **Plataforma** (auth, allowlist, rate limit, idempotencia requerida):
  `{ code, message, error, severity, action?, context? }`.
- **Motor de facturación** (validaciones de emisión, folios, estados): pasa
  por el proxy con su propio shape — trae `code`, `message`, `action` y
  `severity`/`context` cuando aplica.

Reglas de manejo robustas: decide por **status HTTP + `code`** cuando existe,
tolera campos desconocidos, y **nunca parsees `message`** como si fuera un
contrato. Dos comportamientos reales a tener presentes:

- Algunas validaciones del motor responden hoy con mensaje crudo y código
  genérico — p. ej. omitir `ind_servicio` en una boleta da
  `400` con `"IndServicio is required for boleta"`, no un código de
  catálogo. Y en **producción** algunos errores tipados del motor pueden
  llegar como texto plano donde el sandbox devuelve el código estructurado
  (`RUT_INVALIDO`, `CAF_NO_DISPONIBLE`) — divergencia conocida, fix
  pendiente. Diseña tu manejo para funcionar aunque solo tengas el status.
- Los largos de `razon_social` tienen dos capas: el motor valida el tope del
  SII (100 caracteres, receptor y emisor) y responde `400
  RAZON_SOCIAL_INVALIDA` nombrando el campo y el límite; strings todavía más
  largos (> 200) los corta antes la validación de shape de la plataforma con
  un `400` genérico `{"code":"BAD_REQUEST","message":"invalid request"}`
  **sin nombrar el campo** — si ves ese, revisa primero los largos de tus
  strings.

## Códigos que vas a ver como integrador

| Código                        | Descripción                                                                                                          |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `401`                         | Credencial inválida, revocada o expirada. La respuesta no distingue el motivo (anti-enumeración): revisa el header y el prefijo `sk_simplo_`. |
| `409 IDEMPOTENCY_IN_PROGRESS` | La misma `Idempotency-Key` aún se está procesando. Espera y consulta el estado en vez de reintentar de inmediato.     |
| `409` (folio duplicado)       | Conflicto de folio al emitir.                                                                                        |
| `422 IDEMPOTENCY_BODY_MISMATCH` | Reutilizaste una `Idempotency-Key` con un body distinto al original. Usa una key nueva por cada emisión distinta.  |
| `422 BILLING_METADATA_NOT_SUPPORTED` | Mandaste `metadata` con contenido en la emisión. El campo todavía no se persiste, así que lo rechazamos en vez de descartarlo en silencio — quítalo y correlaciona contra el `id` que devuelve la emisión. `{}` y `null` sí pasan. Ver [Emisión](/referencia/emision/). |
| `429`                         | Rate limit: 60 llamadas/min por empresa en la facturación por API key (120/min en MCP). Espera y reintenta — ver [Límites](/referencia/limites/). |
| `503` (idempotencia)          | Servicio de idempotencia temporalmente no disponible al emitir. Reintenta con la **misma** key.                       |

## Códigos de validación de emisión (motor de facturación)

Validaciones que corren **antes de asignar folio** — el mismo camino de
emisión en sandbox y producción, así que un `400` de estos en sandbox es
exactamente lo que verías en producción (donde además evitan quemar un folio
real en un documento que el SII rechazaría):

| Código                  | Status | Qué hacer                                                                                                        |
| ----------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `EMISOR_INCOMPLETO`     | 400    | A tu empresa le falta giro, dirección o comuna (el `context.missing_fields` lista cuáles). El SII exige estos datos del emisor en el XML (`GiroEmis`/`DirOrigen`/`CmnaOrigen`) — complétalos en la app o vía API y reintenta. Aplica a todos los tipos, boletas incluidas. |
| `RAZON_SOCIAL_INVALIDA` | 400    | `razon_social` (del receptor o de tu empresa) supera los **100 caracteres** del esquema del SII (`RznSocLargaType`). Acórtala; el mensaje trae el largo recibido. |
| `MONTO_INVALIDO`        | 400    | Cantidad/precio negativos, o `monto_total` final ≤ 0 en un documento con montos. Corrige las líneas — el campo exacto viene en `context.field`. |
| `REFERENCIA_INVALIDA`   | 400    | Una NC/ND (56/61 **y también las de exportación 111/112**) referencia un tipo+folio que no existe para tu empresa. Verifica `tipo_doc_ref`/`folio_ref` contra el documento original. |
| `RECEPTOR_INCOMPLETO`   | 400    | En documentos tipo factura (33/34/43/46/56/61) el receptor necesita `direccion` y `comuna` — el SII real rechaza sin ellas (`HED-3-845`/`HED-3-846`). El `context.missing_fields` lista cuáles faltan. Boletas (39/41) y exportación (110/111/112) no las exigen. |
| `DETALLE_DESCUADRADO`   | 400    | Enviaste un `monto_item` que no cuadra con `cantidad × precio` (± descuentos/recargos, tolerancia ±1 peso de redondeo). El error trae el valor esperado y el recibido por línea. Si quieres fijar el monto final directo, omite `precio` y manda solo `monto_item`. |
| `IND_TRASLADO_REQUERIDO`| 400    | Las guías de despacho (52) requieren `ind_traslado` según la tabla del SII (1=operación constituye venta … 9=venta para exportación). Nota: una guía de **venta** (1 o 9) además debe llevar montos — con precio 0 responde `MONTO_INVALIDO`. |
| `EXPORT_TPO_CAMBIO_REQUERIDO` | 400 | Documento de exportación en moneda extranjera sin `tpo_cambio`: el SII real rechaza el fallback 1:1 («Los Valores del Encabezado No Cuadran»). Manda `tpo_cambio` (> 0); `mnt_export` explícito o `forma_pago_exp: 21` (sin pago) siguen siendo válidos sin tasa. |

## Códigos de la plataforma (API keys y sandbox)

Estos códigos vienen de la plataforma Simplo (no del motor de facturación) y
tienen siempre una salida clara:

| Código                                     | Status | Qué hacer                                                                                              |
| ------------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------ |
| `BILLING_API_KEY_ROUTE_NOT_ALLOWED`        | 403    | La ruta es solo-sesión. Una key `billing:dte` solo alcanza las 6 rutas DTE — ver [la tabla del quickstart](/comienza/quickstart/#qué-puede-hacer-tu-api-key). No es una cuenta sin habilitar. |
| `BILLING_API_KEY_IDEMPOTENCY_KEY_REQUIRED` | 400    | Agrega el header `Idempotency-Key` a tu POST por key (`/billing/dte` y `/billing/dte/send-boletas`).                                            |
| `BILLING_API_KEY_NOT_ENABLED`              | 403    | Tu empresa no tiene habilitada la emisión en **producción** por key — escribe a [hola@simplo.cl](mailto:hola@simplo.cl). El sandbox no necesita esto. |
| `BILLING_SANDBOX_API_KEY_NOT_ENABLED`      | 403    | Tu plan no incluye el sandbox de facturación.                                                           |
| `COMPANY_RUT_REQUIRED_FOR_SANDBOX`         | 422    | Al crear una key sandbox: tu empresa aún no tiene RUT — complétalo en la app y reintenta.               |
| `COMPANY_RAZON_SOCIAL_REQUIRED_FOR_SANDBOX`| 422    | Al crear una key sandbox: falta la razón social — complétala en la app y reintenta.                     |
| `SANDBOX_COMPANY_FLOW_REMOVED`             | 410    | Llamaste el flujo antiguo `POST /api/v1/sandbox/companies`, que ya no existe: el sandbox es un modo de tu propia empresa — crea una key sandbox en Desarrolladores. |
| `SANDBOX_NOT_AVAILABLE`                    | 503    | El sandbox no está disponible en este momento — reintenta más tarde.                                    |

## Reintentos automáticos

El SDK reintenta en `429`, `5xx` y errores de red con backoff exponencial y
jitter, respetando el header `Retry-After`. Por defecto: 2 reintentos;
sobrescríbelo con `maxRetries` en el cliente o por request.

**Regla de seguridad:** un `POST` solo se reintenta cuando lleva una
`Idempotency-Key`. `dtes.emit` genera una automáticamente (UUID v4), así que
los reintentos de emisión nunca pueden duplicar un documento. Otros POSTs
(`folios.upload`, `webhooks.create`) no se reintentan automáticamente —
vuelve a llamarlos tú si hace falta; ambos son seguros de re-ejecutar (un CAF
duplicado devuelve `ConflictError`).

```ts
const simplo = new Simplo({ maxRetries: 3, timeout: 30_000 });
await simplo.dtes.list({}, { maxRetries: 0 }); // override por request
```

## Relacionado

- [Emisión](/referencia/emision/) — idempotencia en detalle
- [Límites](/referencia/limites/) — el rate limit por empresa

---

Versión HTML: https://docs.simplo.cl/referencia/errores/ · Índice para agentes: https://docs.simplo.cl/llms.txt
