Facturiadocs
Errores

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"
  }
}
CampoQué es
typeLa familia del error. Determina qué hacer
codeEl error específico, estable. Programa contra esto
messageProsa legible por una persona. No programes contra esto
hintQué hacer al respecto, para este code exacto
retriable¿Puede funcionar el request idéntico más rato, sin cambiarlo?
paramEl campo del request que causó el problema, cuando aplica
doc_urlUn enlace a la explicación de este código
request_idEl identificador de esta llamada exacta
empresaEl RUT en cuyo contexto ocurrió
existing_idEn conflictos, el id del objeto que ya existe
detailsDatos estructurados del caso: el pending_action que falta, el checkout_url de un 402
siiPresente 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

typeHTTPSignificaQué hacer
invalid_request_error400Entrada mal formada o incompletaArregla el request. Reintentar igual no sirve
invalid_request_error402Falta pagar: subscription_required o plan_limit_reachedManda a una persona a error.details.checkout_url. Ver abajo
authentication_error401Llave ausente, revocada o mal formadaRevisa la credencial
permission_error403Llave válida, no permitida: scope, lista blanca de empresas, modoRevisa el scope o la empresa. error.message nombra el que falta
not_found_error404No existe ese objeto bajo esta llaveRevisa el id y el modo de la llave
conflict_error409Conflicto de estado: reuso de idempotencia, folio duplicado, empresa ya existenteMira existing_id
unprocessable_error422Bien formado pero viola una regla del dominio: folios agotados, ventana de acuse cerrada, empresa que aún no emiteEs una regla de negocio, no un bug de formato
rate_limit_error429Más lento; mira Retry-AfterEspera lo que dice el header
sii_error502El SII rechazó o falló la llamada de arriba. error.sii trae el estado y la glosa crudosVer Códigos
api_error500 / 503Nuestro. 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/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: 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_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, en una forma que tu build puede consumir.

En esta página