Guía
Errores y manejo de fallos
Los errores siguen una extensión propia de RFC 7807 Problem Details. Esta guía explica cómo leerlos y cuándo reintentar; la tabla completa de códigos vive en la referencia de la API, que es la fuente de verdad.
Formato estándar
Los campos son: code (identificador estable, en minúsculas con puntos — es el que hay que conmutar en el código), title (texto legible), status, y opcionalmente detail, field (el campo del request que causó el problema) y traceId (inclúyelo si necesitas abrir un ticket con soporte).
type ni instance, y los errores de validación no vienen como un mapa errors: el campo culpable viaja en field.HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"code": "emisor.permiso-tipo-no-autorizado",
"title": "No tiene el permiso 'dte:emit:33' requerido para emitir este tipo de documento.",
"status": 403,
"field": "tipoDte"
}Programá contra el code, no contra el status
El code es el único campo estable: el title es texto para humanos y puede cambiar de redacción sin aviso. El status agrupa causas que se resuelven de formas opuestas: dentro de un mismo 403conviven «a la key le falta un scope» (lo arregla el administrador de la empresa en el portal) y «ese tipo de documento todavía no está certificado en producción» (lo destraba el equipo de Comges). Ramificar por status te deja sin saber cuál de las dos te tocó.
Códigos garantizados
Estos son los códigos que vas a ver con más frecuencia y que llegan siempre con el mismo valor. No son todos: la tabla completa está en la referencia de la API.
| HTTP | Código | Descripción | Reintentar |
|---|---|---|---|
| 401 | api-key.ausente | Único código de 401. Cubre header ausente y también key inexistente, revocada o expirada: es deliberadamente genérico para no confirmar qué keys existen. | No |
| 403 | permiso.requerido | A la key le falta el scope que exige la ruta (por ejemplo dte:read en una consulta o dte:emit:61 en una anulación). El title nombra el permiso exacto. | No |
| 403 | emisor.permiso-tipo-no-autorizado | A la key le falta dte:emit:{tipoDte} en la emisión. El campo field indica "tipoDte". | No |
| 501 | emisor.tipo-dte-no-emisible | Los tipos 43 (liquidación-factura) y 46 (factura de compra) no se emiten por esta API. Se corta antes de reservar folio: no se consume ninguno. | No |
| 422 | caf.sin-autorizacion | El SII no autoriza timbrar folios de ese tipo para la empresa. Es terminal: requiere gestión ante el SII, no se resuelve reintentando. | No |
| 429 | api-publica.rate-limit | Superaste el límite de requests. Viene siempre con header Retry-After. | Con Retry-After |
Los rechazos de autenticación se escriben antes de que se arme el cuerpo de error completo: llegan con code y title solamente. No asumas que todos los campos del formato estándar están presentes en todas las respuestas — parseá defensivo.
No existe un api-key.invalida de cara al cliente: una key inexistente, una revocada y una expirada devuelven todas api-key.ausente.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"code": "api-key.ausente",
"title": "Se requiere API key en el header X-Api-Key."
}Otros status que vas a ver
El resto de las respuestas las produce el motor de emisión. Usá esta tabla para decidir la estrategia (reintentar o no) y el code para decidir la acción.
| HTTP | Significado | Reintentar |
|---|---|---|
| 202 | No es un error. La emisión es válida pero todavía no hay folio disponible y ya se pidieron al SII. El cuerpo trae un ticketId, no un id ni un folio. Ver la guía del modelo asíncrono. | No |
| 400 | Cuerpo inválido o regla de negocio incumplida (RUT mal formado, un texto que excede el largo máximo del SII, falta una referencia en una nota de crédito…). Corregí el cuerpo antes de reintentar. | No |
| 404 | El documento no existe o pertenece a otra empresa. La API no distingue los dos casos a propósito. | No |
| 409 | Conflicto de estado: el documento ya fue anulado, la nota de crédito que lo anulaba fue descartada, los montos del original no se pueden reproducir, o la cotización referenciada no está en un estado emitible. Cada caso necesita una acción distinta: leé el code. | No |
| 422 | Falta un prerequisito de la empresa que la API no puede resolver sola: el SII no autoriza folios de ese tipo, no hay certificado digital cargado, faltan actecos o sucursal. Requiere gestión, no reintento. | No |
| 5xx | Error interno o servicio no disponible. Reintentar con backoff, siempre con el mismo transactionId. | Sí |
201 aunque después el SII rechace el documento: el envío es asíncrono. El rechazo se ve como estadoSii: "Rechazado" al consultar GET /api/public/v1/dte/{id}, no como un 4xx.Estrategia de retry
Reintenta sólo 5xx y 429. Nunca reintentes un 400, 403, 404, 409 o 422 sin corregir antes el request o resolver el prerequisito — volverás a recibir el mismo error.
- En
429usá siempre el headerRetry-After: la API lo envía en todos los rechazos por rate limit. - En 5xx, backoff exponencial desde 1 s, tope de 30 s, máximo 5 intentos.
- Reintentá siempre con el mismo
transactionId: es lo que evita duplicar el documento y consumir otro folio.
async function emitirConReintento(body, apiKey, intento = 0) {
const res = await fetch('https://api-publica.dtecomges.cl/api/public/v1/dte', {
method: 'POST',
headers: {
'X-Api-Key': apiKey,
'Content-Type': 'application/json',
},
// transactionId hace que el reintento NO duplique el documento
// ni consuma otro folio: si ya se emitió, devuelve el mismo.
body: JSON.stringify(body),
})
// 202: la emisión es válida pero quedó esperando folio del SII.
// NO es un error y NO hay que reemitir: se resuelve con el ticket.
if (res.status === 202) {
const ticket = await res.json()
return { pendiente: true, ticketId: ticket.ticketId }
}
if (res.ok) return res.json()
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') ?? '15')
await new Promise((r) => setTimeout(r, retryAfter * 1000))
return emitirConReintento(body, apiKey, intento + 1)
}
if (res.status >= 500 && intento < 5) {
await new Promise((r) => setTimeout(r, Math.min(1000 * 2 ** intento, 30_000)))
return emitirConReintento(body, apiKey, intento + 1)
}
const problema = await res.json()
// El campo estable es 'code'. El status agrupa causas distintas:
// dentro de un mismo 403 conviven "falta el scope" y "el tipo no está
// certificado en producción", que se resuelven de formas opuestas.
throw new Error(`${problema.code}: ${problema.title}`)
}Los endpoints de emisión aceptan un campo opcional transactionId (UUID que generás vos). Si reintentás con el mismo valor recibís el documento original — nunca se crean duplicados ni se consume un folio extra. Es lo correcto ante un timeout de red, donde no sabés si el request llegó.
Reusar un transactionId con un cuerpo distinto no devuelve un conflicto: devuelve el documento que se emitió la primera vez. Generá un UUID nuevo por cada documento nuevo.