El sobre de error
La forma exacta de un error, los nueve tipos, qué hacer con cada uno y por qué el request_id importa.
Un error es un código HTTP más un solo objeto error. No hay sobre de éxito: las respuestas exitosas
son el recurso.
{
"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
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
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 |
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
{
"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/documentsPOST /v1/documents/{document_id}/credit_notePOST /v1/documents/{document_id}/sendPOST /v1/folios/requestsPOST /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: 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.
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.
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
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
curl https://api.facturia.cl/v1/error_codesSin 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, en una forma que tu build puede consumir.