Este documento describe la superficie completa de la API pública y es lo que manda ante cualquier diferencia con las guías o con el explorador.
API pública Comges DTE — guía de integración
Público objetivo: desarrollador de un sistema externo (ERP, e-commerce, POS) que necesita emitir Documentos Tributarios Electrónicos al SII de Chile desde su propio software, sin usar nuestro portal.
Este documento es la fuente de verdad de la API pública. Las guías y la referencia interactiva del portal son tutoriales que apuntan acá; si alguna difiere de este documento, vale este documento.
Regla de este documento: acá solo se describe lo que existe y está desplegado. Cada ruta, cada campo y cada código de error fue verificado contra el código que corre en producción. Lo que todavía no existe está en Qué no existe todavía, explícito, para que nadie escriba código contra una promesa.
Referencia interactiva (OpenAPI, generada del mismo contrato):
/docs/referenceen el portal.
Índice
- Antes de empezar: dar de alta la empresa
- Lo mínimo para entender el modelo
- Autenticación con API Key
- Ambientes: certificación y producción
- Scopes (permisos de la key)
- Health check
- Prerequisitos: qué tiene que estar listo antes de emitir
- Emitir un DTE
- Consultar el estado (modelo asíncrono)
- Descargar XML y PDF - Enlaces firmados para el comprador
- Anular un documento
- Devolución parcial: notas de crédito por línea o por monto
- Exportación (110 / 111 / 112)
- Paso a producción (go-live)
- Errores: formato y códigos
- Idempotencia y reintentos
- Rate limiting y límites duros
- Webhooks - Catálogo de eventos - Vector de prueba fijo
- Ejemplo end-to-end con curl
- Qué no existe todavía
- Soporte
1. Antes de empezar: dar de alta la empresa
El alta de una empresa emisora no es self-service. No existe un endpoint público de registro: la ejecuta el equipo de Comges, y hasta que ese paso no está hecho no hay portal, no hay usuario, no hay API Key y no hay nada que integrar.
Esto es lo primero que hay que resolver, antes de escribir una línea de código.
Los cinco datos que hay que entregar
Para dar de alta a la empresa emisora hay que entregarnos, de esa empresa:
| Dato | Detalle |
|---|---|
| RUT de la empresa | El del contribuyente que va a emitir. |
| Clave tributaria del SII | La clave con la que la empresa entra al sitio del SII. |
Certificado digital (.pfx) | El certificado de firma electrónica del representante. Máximo 5 MB. |
| Clave del certificado | La contraseña del archivo .pfx. |
| RUT del titular del certificado | El RUT de la persona dueña del certificado. Se informa aparte: no se lee del archivo — los certificados chilenos no lo traen de forma confiable. |
⚠️ Estos datos son credenciales reales del contribuyente. Entregarlos es una decisión del cliente final, no del integrador. Coordinalo con él antes de prometer una fecha de puesta en marcha: en la práctica es el paso que más tarda de todo el proyecto.
Qué pasa en el alta
Con esos cinco datos, el alta:
- Valida el certificado y su clave, y valida la clave tributaria contra el sitio del SII.
- Trae del SII los datos de la empresa: razón social, correo, teléfono, oficina del SII, sucursales y actividades económicas (actecos).
- Crea la empresa con esos datos y guarda el certificado.
Si el certificado, su clave o la clave tributaria están mal, el alta falla completa: no queda una empresa a medio crear.
Lo que NO hace el alta (y hay que pedir aparte)
- El usuario administrador de la empresa se crea en un paso posterior. Sin usuario no hay quien entre al portal ni quien cree API Keys.
- La habilitación de cada tipo de documento (33, 34, 39, …) es un paso aparte. Ver §7 Prerequisitos.
- La certificación ante el SII para emitir en Producción es otro proceso, con su propia duración. Ver §14 Paso a producción.
El reloj del plan empieza acá
Toda empresa nueva se crea con plan Trial de 30 días, contados desde el alta, no desde que empieza la integración. Si el alta se hace mucho antes de que el ERP esté listo, el plan puede vencer en pleno desarrollo y toda emisión pasa a responder 403 plan.vencido.
Recomendación: pedí el alta cuando tengas el proyecto encaminado, y coordiná la vigencia del plan con soporte antes de arrancar.
A quién pedirlo
El alta, la creación del usuario administrador, la habilitación de tipos de documento y la extensión del plan las gestiona soporte de Comges: escribí a [email protected] con el RUT de la empresa y qué tipos de documento vas a emitir.
Quién puede crear la API Key
Una vez que la empresa existe y tiene usuario, las API Keys se administran desde el portal (/portal/configuracion/api-keys):
| Acción | Permiso que necesita el usuario del portal |
|---|---|
| Ver las keys existentes | empresa:api-keys:read |
| Crear, rotar, revocar o cambiar scopes | empresa:api-keys:write |
Si el usuario entra al portal y no ve la sección de API Keys, no es un bug: le falta el permiso. Se lo tiene que dar el administrador de la empresa.
2. Lo mínimo para entender el modelo
| Concepto | Qué significa acá |
|---|---|
| Base URL | https://api-publica.dtecomges.cl — todas las rutas de negocio cuelgan de /api/public/v1/*. |
| Credencial | Una API Key en el header X-Api-Key. No hay OAuth, no hay JWT, no hay sesiones. |
| Empresa emisora | Sale del claim de la key. Nunca se manda en el request. |
| Ambiente SII | Sale del claim de la key (pk_test_ → Certificación, pk_live_ → Producción). Nunca se elige en el body. |
| Emisión | Síncrona hasta el folio y la firma cuando hay folio disponible; asíncrona hacia el SII. Si no hay folio, la emisión se encola y devuelve 202 — ver §8. |
| Formato de error | RFC 7807 extendido: { code, title, status, detail?, field?, traceId? }. El campo estable para programar es code. Con excepciones — ver §15. |
| Server-to-server | La API no habilita CORS. Está pensada para que la llame tu backend. No pongas la key en un navegador ni en una app móvil: quien la tenga puede emitir documentos tributarios a nombre de tu cliente. |
Rutas públicas
17 rutas de negocio, más el health check. Base: https://api-publica.dtecomges.cl
DTE nacional
| Verbo y ruta | Scope | Qué hace |
|---|---|---|
POST /api/public/v1/dte | dte:emit:{tipo} | Emite un DTE nacional. Devuelve 201 o 202 + ticketId. |
GET /api/public/v1/dte/{id} | dte:read | Consulta el documento y su estado ante el SII. |
GET /api/public/v1/dte/{id}/xml | dte:read | Descarga el XML firmado. |
GET /api/public/v1/dte/{id}/pdf | dte:read | Descarga el PDF (?formato=carta o ?formato=80mm). |
POST /api/public/v1/dte/{id}/anular | dte:emit:61 | Anula el documento completo (NC 61, CodRef=1). |
GET /api/public/v1/dte/{id}/acreditable | dte:read | Saldo disponible para notas de crédito parciales. |
POST /api/public/v1/dte/{id}/nota-credito | dte:emit:61 | Nota de crédito parcial (CodRef=3). No anula. |
GET /api/public/v1/dte/pendientes/{ticketId} | dte:read | Resuelve el ticket que devolvió un 202. |
GET /api/public/v1/dte/pendientes | dte:read | Lista las emisiones encoladas esperando folio. |
Exportación
| Verbo y ruta | Scope | Qué hace |
|---|---|---|
POST /api/public/v1/exportacion | dte:emit:{110|111|112} | Emite un DTE de exportación. |
GET /api/public/v1/exportacion/{id} | dte:read | Consulta la exportación y su estado. |
GET /api/public/v1/exportacion/{id}/xml | dte:read | Descarga el XML firmado. |
GET /api/public/v1/exportacion/{id}/pdf | dte:read | Descarga el PDF (solo Carta, no admite ?formato=). |
Descargas firmadas — las únicas rutas sin API Key. Ver §10.
| Verbo y ruta | Scope | Qué hace |
|---|---|---|
GET /api/public/v1/descargas/dte/{id}/xml | ninguno | XML firmado, autorizado por el token del enlace. |
GET /api/public/v1/descargas/dte/{id}/pdf | ninguno | PDF (?formato=carta o ?formato=80mm). |
GET /api/public/v1/descargas/exportacion/{id}/xml | ninguno | XML firmado de la exportación. |
GET /api/public/v1/descargas/exportacion/{id}/pdf | ninguno | PDF de la exportación (solo Carta). |
Estas cuatro no las construís vos: llegan armadas y firmadas en
xmlUrlypdfUrldentro del cuerpo de la emisión. Son para reenviárselas al comprador, que no tiene tu API Key.
Operación
| Verbo y ruta | Scope | Qué hace |
|---|---|---|
GET /health | ninguno | Liveness. Público, sin API Key, sin rate limit. Ver §6. |
Una integración de facturación típica usa cinco: emitir, consultar, XML, PDF y anular.
3. Autenticación con API Key
Formato de la key
pk_{live|test}_{prefijo}_{secreto}
└─ ambiente └─ 8 chars └─ secreto base64url (32 bytes)Ejemplo (ilustrativo, no funcional): pk_test_a1b2c3d4_R0hKTlBRU1RVVldYWVpiY2RlZmdoaWprbG0
- El prefijo de 8 caracteres identifica la key y es el que aparece en el portal, en los logs y en la partición del rate limiter. No es secreto.
- El secreto se muestra una sola vez, al crear o rotar la key. No se puede recuperar: la base solo guarda su hash SHA-256.
- El segmento
live/testtiene que coincidir con el ambiente registrado de la key. Una clavepk_live_*presentada contra una key marcada como de certificación se rechaza con 401 (el prefijo no puede mentir sobre el ambiente).
Cómo se manda
Header canónico:
X-Api-Key: pk_test_a1b2c3d4_...Alternativa aceptada (útil si tu cliente HTTP solo maneja Authorization):
Authorization: Bearer pk_test_a1b2c3d4_...El servidor acepta Authorization: Bearer solo si el token empieza con pk_.
Qué pasa si falla
| Situación | Respuesta |
|---|---|
| Sin header | 401 con cuerpo {"code":"api-key.ausente","title":"..."} |
| Key mal formada, inexistente, revocada o expirada | 401 con el mismo cuerpo api-key.ausente |
| Key válida pero sin el scope del endpoint | 403 con {"code":"permiso.requerido","title":"Se requiere el permiso 'dte:read'.","status":403} |
El 401 es deliberadamente genérico: no distingue "no mandaste key" de "tu key no existe" ni de "tu key fue revocada". Es anti-enumeración — no queremos que alguien descubra prefijos válidos probando. Para programar, usá el status 401, no el sub-código: hoy todos los caminos de fallo de autenticación devuelven el mismo
code.
El 403 por scope faltante devuelve
permiso.requerido, con el código del permiso que falta dentro deltitle. Si tu código busca otro valor, esa rama nunca se va a ejecutar.
Obtener y rotar una key
Se crean desde el portal (/portal/configuracion/api-keys). Al crear se elige nombre, scopes y una expiración opcional. Quién puede hacerlo: ver §1.
⚠️ La rotación no tiene ventana de gracia. Al rotar una key, el secreto anterior deja de servir de inmediato: no hay período en que las dos versiones funcionen. Planificá la rotación como un despliegue coordinado (guardá el secreto nuevo, desplegá, recién ahí rotá), no como un cambio en caliente.
⚠️ La revocación tarda hasta 30 segundos en propagarse. El resultado positivo de validación se cachea 30 s. Una key revocada puede seguir emitiendo dentro de esa ventana. Si necesitás corte inmediato, revocá y verificá; para un incidente de seguridad real, escalá a soporte.
4. Ambientes: certificación y producción
| Prefijo | Ambiente | SII | Qué emite |
|---|---|---|---|
pk_test_ | Certificación | Maullín | Documentos de prueba. No tienen validez tributaria. |
pk_live_ | Producción | Palena | Documentos tributarios reales. |
El ambiente lo determina la key y nada más.
- No lo cambia el host: la misma URL sirve a los dos ambientes.
- No lo cambia el body. El campo
ambientedel cuerpo de emisión existe por compatibilidad, pero con una API Key se ignora si coincide y se rechaza si contradice:
// key pk_test_ (Certificación) + body {"ambiente":"Produccion"}
{
"code": "emisor.ambiente-no-coincide-con-key",
"title": "La API key opera en 'Certificacion' y el cuerpo pide 'Produccion'. El ambiente lo determina la key, no el request: usá una key del ambiente que necesitás.",
"status": 400,
"field": "ambiente"
}La forma correcta de trabajar con los dos ambientes es tener dos keys y elegir cuál usar en tu configuración. La recomendación operativa es no mandar ambiente en el body en absoluto.
⚠️ Una sola URL base.
https://api-publica.dtecomges.cles la única que se integra, para los dos ambientes. Si en alguna herramienta o colección importada aparece un servidor alternativo con un hostname de despliegue (algo terminado en.run.app), no lo uses: es un entorno interno de desarrollo, corre contra otra base de datos, tus keys devuelven 401 ahí y su URL cambia sin aviso.
El ambiente de la key se hereda del usuario que la creó y queda congelado en ella. Un usuario que opera en certificación nunca obtiene una key de producción; al crear la key se puede además restringirla explícitamente a certificación, pero nunca ampliarla. Para pasar a producción, ver §14.
5. Scopes (permisos de la key)
Cada key lleva una lista de scopes. El servidor valida el scope antes de reservar folio.
| Scope | Habilita |
|---|---|
dte:emit:33 | Emitir factura electrónica afecta |
dte:emit:34 | Emitir factura exenta |
dte:emit:39 | Emitir boleta electrónica |
dte:emit:41 | Emitir boleta exenta |
dte:emit:52 | Emitir guía de despacho |
dte:emit:56 | Emitir nota de débito |
dte:emit:61 | Emitir nota de crédito, anular documentos y emitir notas de crédito parciales |
dte:emit:110 / 111 / 112 | Emitir factura / ND / NC de exportación |
dte:read | Consultar documentos, tickets, saldo acreditable, y descargar XML y PDF |
dte:emit:43 / dte:emit:46 | No operativo. El scope se puede asignar, pero la emisión responde 501. Ver §8. |
Reglas que sorprenden si no se leen:
- Anular exige
dte:emit:61, no un scope propio. No existedte:anular. dte:readcubre todos los caminos de lectura (detalle, XML, PDF, ticket, acreditable). Sin él, todos responden403, incluido el PDF.- Tener
dte:emit:33no habilita a leer. Son scopes independientes. - Existen otros scopes asignables en el portal (plantillas, sobres, procesos) que no tienen ninguna ruta en esta API pública. Asignarlos no habilita nada acá.
- El scope
webhook:configurarse puede asignar, pero no habilita nada en esta API. Las suscripciones de webhooks se administran desde el portal, no por código: ninguna ruta bajo/api/public/v1/las crea, edita ni consulta. Ver §18.
Una key típica de integración de facturación lleva: dte:emit:33, dte:emit:61, dte:read.
⚠️ La misma falta de
dte:emit:61puede devolver tres códigos distintos, según qué capa corte primero:permiso.requerido(403),emisor.permiso-tipo-no-autorizado(403, al anular) odte.permiso-faltante(403, al emitir una nota de crédito parcial). Los tres significan lo mismo: falta el scope. Ramificá por elcode, no por el status: dentro del mismo 403 conviven causas que se resuelven de forma distinta —a la key le falta un scope (§15.1) vs. ese tipo todavía no está certificado en Producción (cert.tipo-no-aprobado-prod, §15.3) vs. el plan venció (plan.vencido, §15.2)—. Si programás una rama de "falta permiso", cubrí los tres códigos de scope.
6. Health check
GET https://api-publica.dtecomges.cl/health → 200 "Healthy"- Cuelga de la raíz del host, no de
/api/public/v1. - Público: no lleva
X-Api-Key. - Exento del rate limit: podés consultarlo tan seguido como quieras sin gastar tu cuota.
⚠️ Solo dice que el servicio responde. No verifica el SII, ni la base de datos, ni la disponibilidad de folios. Un
200acá no garantiza que una emisión vaya a funcionar.
Usalo para tus monitores de disponibilidad. Lo que no hay que hacer es usar GET /api/public/v1/dte/{id} como ping: consume cuota de rate limit por cada llamada.
7. Prerequisitos: qué tiene que estar listo antes de emitir
Cada fila es una condición que, si falta, hace fallar la emisión. La columna del código está para que puedas buscar acá el error que te devolvió la API.
| Prerequisito | Quién lo resuelve | Si falta |
|---|---|---|
| Empresa dada de alta | Soporte de Comges | No hay usuario, no hay portal, no hay key. Ver §1. |
| Tipo de documento habilitado para la empresa en ese ambiente | Soporte de Comges | 403 emisor.tipo-dte-no-habilitado |
| Certificación SII aprobada por tipo — solo en Producción | Equipo de Comges + SII | 403 cert.tipo-no-aprobado-prod. Si la verificación no responde: 503 cert.servicio-no-disponible (reintentable). Ver §14. |
| CAF vigente del tipo que vas a emitir | Automático (se piden al SII) o carga manual | Normalmente no falla: la emisión se encola y devuelve 202. Si el SII no autoriza timbraje: 422 caf.sin-autorizacion (terminal). Caso residual: 409 emisor.sin-folios. |
| CAF de tipo 61, si vas a anular o acreditar | Igual que el anterior | 400 o 409 emisor.sin-folios-nc |
| Casa matriz configurada | Se toma del SII en el alta | 422 emisor.empresa-sin-sucursal |
| Actividades económicas (actecos) activas | Se toman del SII en el alta | 422 emisor.empresa-sin-actecos |
| Certificado digital activo | Se carga en el alta | DTE nacional: 422 emisor.sin-certificado (no hay PFX activo) o 422 emisor.certificado-invalido (hay PFX pero no abre), sin consumir folio. Exportación: 409 exp.sin-certificado, pero ahí el folio ya se tomó. Ver §8. |
| Plan vigente | Comercial | 403 plan.vencido |
8. Emitir un DTE
POST /api/public/v1/dte
Content-Type: application/json
X-Api-Key: pk_test_...Un solo endpoint cubre todos los tipos nacionales. El tipo se elige con tipoDte: 33 factura, 34 factura exenta, 39 boleta, 41 boleta exenta, 52 guía de despacho, 56 nota de débito, 61 nota de crédito.
Tipos que esta API no emite
Los tipos de exportación (110/111/112) no van por acá:
POST /api/public/v1/dtelos rechaza con501 emisor.tipo-dte-no-implementado. UsáPOST /api/public/v1/exportacion.
Los tipos 43 (liquidación-factura) y 46 (factura de compra) no se emiten por esta API:
POST /api/public/v1/dteresponde501 emisor.tipo-dte-no-emisiblesin consumir folio. La 43 admite montos negativos y exige un bloque de comisiones propio; la 46 invierte la semántica del receptor (el RUT es el del vendedor) y lleva retención. Ninguna de las dos aritméticas está implementada, y emitirlas a medias produciría documentos mal formados que el SII rechazaría con el folio ya quemado. Sin fecha comprometida. Los scopesdte:emit:43ydte:emit:46existen y se pueden asignar, pero no habilitan nada.
El certificado digital tiene que estar activo
Firmar es obligatorio: no existe ningún camino que persista un DTE sin firma. Por eso la emisión nacional verifica el certificado antes de reservar el folio, y falla explícito:
| Situación | Respuesta | Folio |
|---|---|---|
| La empresa no tiene un PFX activo | 422 emisor.sin-certificado | no se consume |
| Hay un PFX cargado, pero no se pudo abrir (clave equivocada o certificado vencido) | 422 emisor.certificado-invalido | no se consume |
Los dos errores traen field: "certificado". Ninguno se arregla reintentando: hay que cargar un PFX vigente o corregir su clave desde el portal. El certificado vence cada 1–3 años, así que este error puede aparecer de un día para otro en una integración que venía funcionando: tratalo como un error operativo que hay que avisar, no como un 4xx de validación de tu request.
⚠️ En exportación el orden es distinto y peor.
POST /api/public/v1/exportacioncarga el PFX después de tomar el folio, así que responde409 exp.sin-certificado(sin PFX activo) o500 exp.error-pfx(PFX ilegible) con el folio ya consumido. Ver §15.17.
Cuerpo mínimo
{
"tipoDte": 33,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234",
"comuna": "Providencia"
},
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Consultoría técnica",
"cantidad": 1,
"precioUnitario": 1000000
}
]
}Campos del cuerpo
Raíz:
| Campo | Tipo | Obligatorio | Notas |
|---|---|---|---|
tipoDte | int | sí | Código SII. |
receptor | objeto | sí | Ver abajo. |
detalles | array | sí | 1 a 60 líneas (límite duro del SII). |
transactionId | uuid | no | Clave de idempotencia. Ver §16. |
ambiente | string | no | No usar con API Key. Ver §4. |
fechaEmision | string | no | dd-MM-yyyy o yyyy-MM-dd. Si se omite, hoy en Chile. No puede ser futura. |
formaPago | byte | no | 1 contado, 2 crédito, 3 sin costo. |
fechaVencimiento | string | no | Mismo formato que fechaEmision. En 33/34 a crédito, si falta se usa emisión + 30 días. |
medioPago | string/int | no | 1..6, o "Efectivo"/"Cheque"/"Transferencia"/"TarjetaCredito"/"TarjetaDebito"/"Otro", o código SII de 2 letras. Un valor desconocido se ignora (no tumba la emisión). |
indServicio | byte | no | Solo se emite en boletas 39/41; si falta, el servidor pone 3. El valor no se valida: ver Boleta electrónica (39) y boleta exenta (41). |
indTraslado | byte | condicional | Obligatorio en la guía 52, prohibido en el resto. Ver Guía de despacho (52). |
tipoDespacho | byte | no | Solo en la guía 52. 1/2/3. |
transporte | objeto | no | Datos de transporte y destino. Solo tiene sentido en la 52; en boletas se descarta. |
referencias | array | condicional | Obligatorio en 56 y 61. Máx. 40. |
impuestosAdicionales | array | no | Máx. 20. |
descuentosGlobales | array | no | Máx. 20. |
observaciones | string | no | Máx. 500. No viaja al SII; se imprime en el PDF. |
moneda, tasaCambio | string / decimal | no | Default PESO CL. |
empresaSucursalId, actecoIds, empresaActecoIdPrincipal | uuid(s) | no | Perfil de emisión. Si se omiten, el servidor elige casa matriz y actecos activos. |
customFields | objeto | no | Campos definidos para tu empresa. |
receptor:
| Campo | Obligatorio | Largo máx. | Notas |
|---|---|---|---|
rut | sí | — | Formato 12345678-5. Se normaliza automáticamente (se aceptan puntos, se guardan sin). RUT inválido → 400 emisor.rut-receptor-invalido. |
razonSocial | sí | 100 | |
giro | no | 40 | Exigido por el SII en factura 33. |
direccion | no | 70 | Exigida por el SII en factura 33. |
comuna | no | 20 | Exigida por el SII en factura 33. |
ciudad | no | 20 | |
contacto | no | 80 | |
rutSolicita | no | 20 | |
correos | no | — | Lista de correos adicionales a los que se envía copia (PDF + XML). No viaja al XML. |
detalles[]:
| Campo | Obligatorio | Largo máx. | Notas |
|---|---|---|---|
nroLinea | sí | — | 1..60. |
nombreItem | sí | 80 | |
cantidad, precioUnitario | sí | — | cantidad > 0 (hasta 6 decimales), precioUnitario >= 0 redondeado a peso entero. Ver Aritmética. |
descripcionItem | no | 1000 | |
codigoItem | no | 35 | |
tipoCodigo | no | 10 | |
unidadMedida | no | 4 | Ver la advertencia abajo. |
descuentoMonto, recargoMonto | no | — | Montos, no porcentajes. |
indExe | no | — | 0 afecto, 1 exento, 2 no facturable (ej. propina), 6 no facturable negativo. Manda sobre el booleano legado indExento. |
⚠️
unidadMedidatiene un máximo de 4 caracteres. Es un límite del formato del SII, no nuestro. Unidades corrientes comoUNIDAD,KILOSoLITROSno caben: usáUN,KG,LT,MT,CJA. Si tu ERP exporta la unidad como texto largo, mapeala antes de llamar. Una línea con la unidad demasiado larga devuelve400 emisor.detalle-largo-excedidoy ninguna línea del documento se emite.
referencias[] (obligatorio en notas de crédito y débito emitidas a mano):
| Campo | Obligatorio | Largo máx. |
|---|---|---|
nroLinea | sí | — |
tipoDocRef | sí | 3 |
folioRef | sí | 18 |
fechaRef | sí | — |
codRef | recomendado | — (1 anula, 2 corrige texto, 3 corrige monto) |
razonRef | no | 90 |
Para anular un documento no armes la referencia a mano: usá
POST /{id}/anular, que la resuelve el servidor. Para una devolución parcial, usáPOST /{id}/nota-credito.
Ningún largo de esta sección se trunca. Exceder cualquiera de los de arriba —
razonRefincluido— devuelve400antes de reservar folio, con el campo culpable enfield. El único campo del sistema que sí se recorta en silencio es elmotivodePOST /{id}/anular, que no es parte de este cuerpo.
Aritmética: cómo se calcula el documento
Los totales del documento — y los montoNeto / montoIva / montoTotal que devuelve la respuesta — los calcula el servidor, no vos. Si tu ERP usa otra aritmética, el total que te devolvemos no va a coincidir con el tuyo, y esa diferencia después impide anular el documento (emisor.montos-no-reproducibles, ver §11).
El precio unitario se redondea a peso entero (AwayFromZero) antes de calcular la línea:
montoLinea = round( cantidad × round(precioUnitario) − descuentoMonto + recargoMonto )Si trabajás con precios de más de dos decimales, mandá el precio ya redondeado a peso. cantidad sí admite decimales (hasta 6, formato del SII); los montos del documento son pesos enteros.
Cero y negativos se rechazan: cantidad > 0, precioUnitario >= 0, y descuentoMonto y recargoMonto no negativos. Si no, 400 emisor.detalle-monto-invalido, con todas las líneas problemáticas listadas de una vez en el formato L[n] que usa el SII.
Única excepción: en nota de crédito (61) y nota de débito (56) se admite
cantidad: 0— es la línea "dummy" con la que el SII anula un documento o corrige un texto.
Ejemplos por tipo de documento
Todos los cuerpos de abajo van al mismo POST /api/public/v1/dte.
Factura afecta (33)
El caso base. giro, direccion y comuna del receptor son obligatorios (el SII rechaza la factura sin ellos, y para entonces el folio ya se consumió: por eso se validan antes).
{
"tipoDte": 33,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234, of. 501",
"comuna": "Providencia",
"ciudad": "Santiago",
"contacto": "Marcela Ruiz - [email protected]",
"correos": ["[email protected]"]
},
"formaPago": 2,
"fechaVencimiento": "30-08-2026",
"detalles": [
{
"nroLinea": 1,
"codigoItem": "SRV-CONS-01",
"tipoCodigo": "INT1",
"nombreItem": "Consultoría técnica",
"descripcionItem": "Horas de consultoría del mes de julio 2026",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 1000000
},
{
"nroLinea": 2,
"nombreItem": "Licencia de software",
"cantidad": 3,
"unidadMedida": "UN",
"precioUnitario": 25000
}
]
}Totales que devuelve la respuesta:
| Campo | Valor |
|---|---|
montoNeto | 1 075 000 |
montoIva | 204 250 |
montoTotal | 1 279 250 |
formaPago: 2 (crédito) sin fechaVencimiento toma por defecto emisión + 30 días. Con formaPago: 1 (contado) o 3 (sin costo) no se genera vencimiento.
Factura exenta (34)
Mismos requisitos de receptor que la 33 (giro, direccion, comuna obligatorios).
⚠️ Marcá cada línea con
indExe: 1. El servicio no lo deduce del tipo de documento: en una 34 el IVA siempre sale 0, así que una línea sinindExesuma amontoNetoy el documento queda con neto positivo e IVA cero — descuadrado, y el SII lo rechaza con el folio ya consumido. Es la trampa propia de este tipo.
{
"tipoDte": 34,
"receptor": {
"rut": "77123456-9",
"razonSocial": "Instituto de Formación Andes SpA",
"giro": "Enseñanza",
"direccion": "Manuel Montt 980",
"comuna": "Ñuñoa"
},
"formaPago": 1,
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Curso de capacitación exento",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 350000,
"indExe": 1
},
{
"nroLinea": 2,
"nombreItem": "Material de estudio",
"cantidad": 12,
"unidadMedida": "UN",
"precioUnitario": 15000,
"indExe": 1
}
]
}| Campo | Valor |
|---|---|
montoNeto | 0 |
montoExento | 530 000 |
montoIva | 0 |
montoTotal | 530 000 |
Boleta electrónica (39) y boleta exenta (41)
Tres diferencias con la factura, todas propias del formato de boleta del SII:
precioUnitariova NETO. El sistema le suma el IVA. En la boleta afecta, el precio que viaja al SII es el precio al público (con IVA); vos mandás el neto y el servicio multiplica por 1,19 al construir el documento. Si mandás el precio con IVA ya incluido, la boleta sale por un 19 % de más.giro,contacto,ciudadyrutSolicitadel receptor se descartan. El receptor de boleta del SII es estricto y rechaza el envío completo si aparecen.direccionycomunasí se emiten si vienen.indServicioes propio de la boleta:1periódico con domicilio,2periódico sin domicilio,3boletas de venta y servicios (default si se omite),4espectáculos.
{
"tipoDte": 39,
"indServicio": 3,
"receptor": {
"rut": "66666666-6",
"razonSocial": "Consumidor final"
},
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Café de grano 250 g",
"cantidad": 2,
"unidadMedida": "UN",
"precioUnitario": 1500
},
{
"nroLinea": 2,
"nombreItem": "Sándwich del día",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 4200
}
]
}| Campo | Valor | De dónde sale |
|---|---|---|
montoNeto | 7 200 | 2 × 1 500 + 4 200 |
montoIva | 1 368 | |
montoTotal | 8 568 | lo que paga el cliente en caja |
Propinas y otros no facturables: una línea con indExe: 2 no genera neto ni IVA, y en la boleta sí suma al montoTotal (viaja además en montoNF). Es la diferencia con la factura, donde el no facturable queda fuera del montoTotal.
Boleta exenta (41): mismo cuerpo, con "tipoDte": 41 y indExe: 1 en todas las líneas (misma regla que la 34: el tipo no lo deduce solo). En la 41 el montoTotal incluye el componente no facturable, igual que en la 39.
indServiciono se valida: se escribe tal cual en el documento. Un valor fuera de rango no devuelve 400 acá — lo rechaza el SII después, con el folio ya consumido.
Guía de despacho (52)
Es el tipo con más campos propios, y los tres son exclusivos de él.
indTraslado — obligatorio, sin valor por defecto. Sin él: 400 emisor.ind-traslado-requerido. No se asume un default a propósito: emitir "traslado interno" por omisión falsearía el motivo del traslado ante el SII.
| Valor | Significado | ¿Emitible? |
|---|---|---|
1 | Operación constituye venta | sí |
2 | Ventas por efectuar | sí |
3 | Consignaciones | sí |
4 | Entrega gratuita | sí |
5 | Traslados internos | sí |
6 | Otros traslados no venta | sí |
7 | Guía de devolución | sí |
8 | Traslado para exportación | no → 400 emisor.ind-traslado-exportacion-no-soportado |
9 | Venta para exportación | no → 400 emisor.ind-traslado-exportacion-no-soportado |
8 y 9 exigen el bloque de aduana (puerto de embarque y desembarque, peso bruto, total de bultos) que esta operación no arma. Se rechazan antes de reservar folio, en vez de emitir un documento que el SII rechazaría con el folio ya quemado. Un valor fuera de 1..9 devuelve 400 emisor.ind-traslado-invalido.
tipoDespacho — opcional. 1 por cuenta del receptor, 2 por cuenta del emisor a instalaciones del cliente, 3 por cuenta del emisor a otras instalaciones. Fuera de 1..3: 400 emisor.tipo-despacho-invalido.
indTrasladoytipoDespachosolo existen en el 52. Mandarlos en otro tipo devuelve 400 (emisor.ind-traslado-no-aplica/emisor.tipo-despacho-no-aplica) — no se ignoran en silencio.
transporte — opcional, todos sus campos opcionales:
| Campo | Largo máx. | Notas |
|---|---|---|
patente | 8 | |
rutTransportista | — | RUT chileno válido; si no, 400 emisor.transporte-rut-invalido. |
rutChofer | — | RUT chileno válido. |
nombreChofer | 30 | |
direccionDestino | 70 | |
comunaDestino | 20 | |
ciudadDestino | 20 |
rutChofer y nombreChofer van juntos o no van. Informar solo uno devuelve 400 emisor.transporte-chofer-incompleto: el formato del SII exige los dos hijos dentro del bloque del chofer. Exceder cualquiera de los largos devuelve 400 emisor.transporte-largo-excedido (no se trunca). El bloque de transporte se acepta en cualquier tipo salvo boletas, donde se descarta; en la práctica solo tiene sentido en la 52.
{
"tipoDte": 52,
"indTraslado": 1,
"tipoDespacho": 2,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234",
"comuna": "Providencia"
},
"transporte": {
"patente": "KXPR23",
"rutTransportista": "76543210-3",
"rutChofer": "13579246-2",
"nombreChofer": "Pedro Salinas",
"direccionDestino": "Camino a Melipilla 5600, bodega 3",
"comunaDestino": "Maipú",
"ciudadDestino": "Santiago"
},
"detalles": [
{
"nroLinea": 1,
"codigoItem": "PROD-114",
"nombreItem": "Caja de repuestos modelo A",
"cantidad": 10,
"unidadMedida": "CJ",
"precioUnitario": 12000
}
]
}| Campo | Valor |
|---|---|
montoNeto | 120 000 |
montoIva | 22 800 |
montoTotal | 142 800 |
Guía de venta vs. traslado interno. Con indTraslado 1 o 2 la mercadería ya está vendida y el SII exige neto, tasa e IVA cuadrados: se calcula IVA sobre las líneas afectas, como en una factura. En un traslado interno (indTraslado: 5) lo habitual es marcar las líneas con indExe: 1, con lo que neto e IVA quedan en 0 y el documento solo mueve mercadería.
Nota de débito (56)
Exige al menos una referencia, y cada referencia exige codRef. Son dos validaciones distintas, con dos códigos distintos:
- Sin
referencias→400 emisor.referencia-requerida. - Con referencia pero sin
codRef(o con uncodRefdistinto de 1/2/3) →400 emisor.referencia-codref-invalido, con todas las líneas problemáticas listadas.
A diferencia de la factura, en la nota de débito y en la de crédito giro, direccion y comuna del receptor son opcionales.
codRef | Significado |
|---|---|
1 | Anula documento referenciado |
2 | Corrige texto del documento referenciado |
3 | Corrige montos |
fechaRef debe ser la fecha de emisión del documento referenciado, y tiene que caer entre 2002-08-01 y 2050-12-31; fuera de rango es 400 emisor.referencia-fecha-fuera-de-rango.
{
"tipoDte": 56,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A."
},
"referencias": [
{
"nroLinea": 1,
"tipoDocRef": "33",
"folioRef": "1048",
"fechaRef": "2026-07-27",
"codRef": 3,
"razonRef": "Cobro de intereses por mora"
}
],
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Intereses por mora factura 1048",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 25000
}
]
}| Campo | Valor |
|---|---|
montoNeto | 25 000 |
montoIva | 4 750 |
montoTotal | 29 750 |
Nota de crédito (61)
Mismas reglas de referencia que la 56 (referencias obligatoria, codRef obligatorio en cada línea, receptor sin giro/direccion/comuna obligatorios).
Antes de armar una 61 a mano, mirá si te sirve un endpoint dedicado. Para anular un documento completo está
POST /{id}/anular, y para una devolución parcialPOST /{id}/nota-credito. En los dos casos la referencia la arma el servidor leyendo el documento original. Armarla a mano ya rechazó una anulación real: el cliente mandófechaRefcon la fecha de hoy en lugar de la fecha de emisión del original, el SII rechazó la nota y el folio se quemó igual.La 61 a mano queda para lo que esos endpoints no cubren: por ejemplo corregir el texto de un documento (
codRef: 2) o acreditar un documento que no emitiste por esta API.
{
"tipoDte": 61,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A."
},
"referencias": [
{
"nroLinea": 1,
"tipoDocRef": "33",
"folioRef": "1048",
"fechaRef": "2026-07-27",
"codRef": 3,
"razonRef": "Descuento comercial acordado post facturación"
}
],
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Descuento comercial factura 1048",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 50000
}
]
}| Campo | Valor |
|---|---|
montoNeto | 50 000 |
montoIva | 9 500 |
montoTotal | 59 500 |
Corregir solo el texto (codRef: 2) es el único caso donde se admite cantidad: 0: la nota no mueve montos, solo declara la corrección.
{
"tipoDte": 61,
"receptor": { "rut": "96790240-3", "razonSocial": "Cliente Ejemplo S.A." },
"referencias": [
{
"nroLinea": 1,
"tipoDocRef": "33",
"folioRef": "1048",
"fechaRef": "2026-07-27",
"codRef": 2,
"razonRef": "Corrige giro del receptor"
}
],
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Corrige giro del receptor",
"cantidad": 0,
"precioUnitario": 0
}
]
}Casos de borde
Ítems exentos mezclados con afectos
Usá indExe por línea: 0 afecto (default), 1 exento, 2 no facturable (propina, cobro por cuenta de terceros), 6 no facturable negativo. Cualquier otro valor devuelve 400 emisor.detalle-indexe-invalido (los códigos 3, 4 y 5 del formato del SII no están soportados).
indExento: true sigue funcionando por compatibilidad, pero equivale a indExe: 1 y manda indExe cuando vienen los dos. En integraciones nuevas usá solo indExe.
{
"tipoDte": 33,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234",
"comuna": "Providencia"
},
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Servicio afecto a IVA",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 100000,
"indExe": 0
},
{
"nroLinea": 2,
"nombreItem": "Servicio exento de IVA",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 40000,
"indExe": 1
}
]
}| Campo | Valor |
|---|---|
montoNeto | 100 000 |
montoExento | 40 000 |
montoIva | 19 000 |
montoTotal | 159 000 |
Descuentos y recargos
Hay dos niveles, y no se comportan igual.
Por línea — descuentoMonto / recargoMonto. Son montos, nunca porcentajes, y el valor es el de la línea completa, no el de cada unidad. No pueden ser negativos. Se aplican dentro de la línea, con la fórmula de Aritmética.
{
"tipoDte": 33,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234",
"comuna": "Providencia"
},
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Producto con descuento por volumen",
"cantidad": 10,
"unidadMedida": "UN",
"precioUnitario": 5000,
"descuentoMonto": 5000
},
{
"nroLinea": 2,
"nombreItem": "Servicio con recargo por urgencia",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 20000,
"recargoMonto": 1500
}
]
}Línea 1: 10 × 5000 − 5000 = 45 000. Línea 2: 20 000 + 1 500 = 21 500.
| Campo | Valor |
|---|---|
montoNeto | 66 500 |
montoIva | 12 635 |
montoTotal | 79 135 |
Globales — descuentosGlobales[]. Máximo 20 entradas. Se aplican en cascada sobre las bases del documento, en el orden en que vienen: un segundo descuento del 10 % descuenta el 10 % de lo que quedó, no del total inicial.
| Campo | Obligatorio | Valores |
|---|---|---|
nroLinea | sí | 1..20 |
tipoMovimiento | sí | "D" descuento, "R" recargo — otro valor: 400 emisor.dr-tipo-movimiento-invalido |
tipoValor | sí | "%" porcentaje, "$" monto — otro valor: 400 emisor.dr-tipo-valor-invalido |
valor | sí | ≥ 0 — negativo: 400 emisor.dr-valor-negativo |
glosa | no | máx. 45 → 400 emisor.dr-glosa-excede-45 |
indExeDR | no | ausente/0 = afecta la base afecta; 1 = la base exenta; 2 = no facturables (no toca ninguna base tributaria) |
⚠️ Un documento con líneas afectas y exentas necesita una línea de descuento global por cada base. Con una sola entrada sin
indExeDRsolo se descuenta la parte afecta, y la exenta queda entera — el documento se emite igual, y el SII lo cuestiona porque los montos no le cuadran.
{
"tipoDte": 33,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234",
"comuna": "Providencia"
},
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Servicio afecto",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 100000,
"indExe": 0
},
{
"nroLinea": 2,
"nombreItem": "Servicio exento",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 40000,
"indExe": 1
}
],
"descuentosGlobales": [
{
"nroLinea": 1,
"tipoMovimiento": "D",
"tipoValor": "%",
"valor": 10,
"glosa": "Descuento comercial 10% - afectos"
},
{
"nroLinea": 2,
"tipoMovimiento": "D",
"tipoValor": "%",
"valor": 10,
"glosa": "Descuento comercial 10% - exentos",
"indExeDR": 1
}
]
}| Campo | Valor | Cálculo |
|---|---|---|
montoNeto | 90 000 | 100 000 − 10 % |
montoExento | 36 000 | 40 000 − 10 % |
montoIva | 17 100 | 19 % de 90 000 |
montoTotal | 143 100 |
En boleta (39/41) los descuentos globales se aplican sobre bases brutas, porque el detalle de la boleta va con IVA incluido. Un descuento global con
tipoValor: "$"en una boleta tiene que expresarse con IVA; en porcentaje da lo mismo.
Receptor sin RUT y boleta a consumidor final
receptor.rut y receptor.razonSocial son siempre obligatorios, en todos los tipos. No hay forma de emitir sin receptor: tanto omitir el RUT como mandarlo con el dígito verificador equivocado devuelven 400 emisor.rut-receptor-invalido, con field: "receptor.rut" y sin tocar el folio (se valida por módulo 11 antes de reservarlo).
Ojo con el orden de las palabras: el código que vas a recibir es
emisor.rut-receptor-invalido. Los códigosemisor.receptor-rut-requeridoyemisor.receptor-rut-invalidoexisten en el código fuente, pero son inalcanzables desdePOST /dte: el chequeo que devuelveemisor.rut-receptor-invalidocorre antes y se los come. No programes contra ellos.
Cuando no hay cliente identificado —el caso normal de una boleta en punto de venta— se usa el RUT genérico del SII para consumidor final:
{
"receptor": {
"rut": "66666666-6",
"razonSocial": "Consumidor final"
}
}razonSocial es texto libre: cualquier etiqueta legible sirve.
El formato del RUT es indiferente: 96790240-3, 96.790.240-3 y 967902403 se normalizan solos a la forma canónica. La k del dígito verificador se acepta en minúscula o mayúscula.
Otros RUT que conviene conocer: 55555555-5 es el receptor extranjero genérico y pertenece al flujo de exportación (§13), no a este endpoint.
Respuesta 201 — el camino normal
{
"id": "b5f8c2e1-7d93-4e8f-a12b-9c4d5e6f7a8b",
"tipoDte": 33,
"folio": 1048,
"ambiente": "Certificacion",
"fechaEmision": "2026-07-27",
"montoNeto": 1000000,
"montoExento": 0,
"montoIva": 190000,
"montoTotal": 1190000,
"montoNF": 0,
"estadoSii": "Pendiente",
"glosaSii": null,
"trackId": null,
"transactionId": "a3bb189e-8bf9-3888-9912-ace4e6543002",
"creadoEn": "2026-07-27T14:30:12Z",
"offsetUtcHoras": -4,
"glosa": "Consultoría técnica",
"receptorRut": "96790240-3",
"receptorRznSoc": "Cliente Ejemplo S.A.",
"montoAcreditado": 0,
"estadoAcreditacion": "SinAcreditar",
"anuladoEn": null,
"notaCreditoId": null,
"plantillaId": null,
"cotizacionIds": null,
"customFields": null,
"customFieldLabels": null,
"xmlBlobPath": "...",
"ted": "<TED version=\"1.0\"><DD>…</DD><FRMT algoritmo=\"SHA1withRSA\">…</FRMT></TED>",
"xmlUrl": "https://api-publica.dtecomges.cl/api/public/v1/descargas/dte/b5f8c2e1-.../xml?t=Vg9m…&exp=1769472000&e=7c1e…&a=Certificacion",
"pdfUrl": "https://api-publica.dtecomges.cl/api/public/v1/descargas/dte/b5f8c2e1-.../pdf?t=Kq2p…&exp=1769472000&e=7c1e…&a=Certificacion"
}Campos que conviene conocer:
| Campo | Para qué sirve |
|---|---|
id | Guardalo siempre. Es el identificador de todas las operaciones posteriores. |
folio | Definitivo. Podés imprimirlo, guardarlo y mostrarlo. |
estadoSii | Estado ante el SII, como texto. Ver §9. |
glosaSii | El motivo que informó el SII cuando el documento fue rechazado o quedó con reparos. null mientras no hay una respuesta con glosa. Es el único lugar donde leer por qué un documento quedó Rechazado. |
trackId | Identificador del envío ante el SII. null hasta que el documento sale. |
glosa | El primer nombreItem truncado a 80. Es contenido del documento, no una respuesta del SII — no confundir con glosaSii. |
montoNF | Monto no facturable (líneas con indExe 2 o 6, ej. propina). |
montoAcreditado / estadoAcreditacion | Cuánto se acreditó por notas de crédito parciales, y en qué estado quedó (SinAcreditar / Parcial / AcreditadoTotal / Anulado). Ver §12. |
anuladoEn / notaCreditoId | Marca de anulación completa. null = vigente. |
offsetUtcHoras | Offset de Chile al emitir. Con creadoEn (UTC) reconstruís la hora local exacta. |
customFields / customFieldLabels | Campos personalizados definidos para tu empresa. |
plantillaId / cotizacionIds | Trazabilidad interna del portal. En emisión por API vienen null. |
xmlBlobPath | Referencia interna de almacenamiento. No la uses: no es una URL descargable. Para el XML, usá GET /{id}/xml. |
ted | El bloque <TED> del documento, como XML. Es el timbre electrónico ya firmado con el CAF — el mismo que va dentro del XML y que se imprime como código PDF417. Sirve si armás tu propia representación impresa. Es el XML del timbre, no la imagen del barcode: el código de barras lo dibuja quien imprime. Solo viene en la emisión — ver abajo. |
xmlUrl / pdfUrl | Enlaces de descarga firmados, que se abren sin API Key. Son para reenviárselos al comprador. Vencen. Ver Enlaces firmados para el comprador. |
tedsolo viaja en la respuesta de la emisión. Al emitir, el timbre ya está en memoria y devolverlo no cuesta nada. EnGET /dte/{id}llega siemprenulla propósito: el TED vive en almacenamiento y leerlo penalizaría una consulta que hoy no toca disco. Si lo necesitás, guardalo cuando emitís — no se puede recuperar después por esta API. Para tener el timbre más tarde, descargá el XML completo, que lo contiene.En exportación (
POST /exportacion) el campo existe en el cuerpo pero llega siemprenull: ese camino todavía no expone el TED. El XML de la exportación sí lo trae.
Lo que todavía no ocurrió en un 201 es el envío al SII.
Respuesta 202 — la emisión quedó esperando folio
Este no es un caso raro: es el caso normal de un contribuyente nuevo. El SII entrega folios de a poco a quien recién empieza (a veces de a uno), así que al emitir varios documentos seguidos es esperable quedarse sin folio disponible en medio.
Cuando eso pasa, el documento ya fue validado y en vez de descartarlo lo encolamos: pedimos folios al SII y emitimos apenas llegan. La respuesta es 202 con un ticket:
{
"ticketId": "6f1c2b90-33a1-4a5e-8b7d-0c2e5f9a1d44",
"estado": "Pendiente",
"tipoDte": 33,
"ambiente": "Certificacion",
"posicionEnCola": 1,
"intentos": 0,
"documentoId": null,
"codigoError": null,
"mensaje": "No hay folios disponibles; se solicitaron al SII.",
"esErrorTerminal": false,
"resumen": "Factura 33 — Cliente Ejemplo S.A.",
"solicitadoEn": "2026-07-27T14:30:12Z",
"terminadoEn": null,
"offsetUtcHoras": -4
}Fijate en lo que no trae: no hay id, no hay folio.
⚠️ Tres formas clásicas de romperse acá:
- Leer
.idde la respuesta sin mirar el status. Con un202te quedaundefinedonull, y como es un 2xx tu manejo de errores no se entera: guardás un documento fantasma. Ramificá siempre por el status code (201vs202), no por la presencia de campos. Para los errores, en cambio, el campo estable es elcode(§15).- Tratar todo lo que no sea
201como un fallo y reemitir. El documento ya está encolado: reemitir consume otro folio, salvo que mandes el mismotransactionId.- Seguir el header
Location. No llega: la respuesta pública no reenvía ese header. Leé elticketIddel cuerpo.
Resolver el ticket
GET /api/public/v1/dte/pendientes/{ticketId} (scope dte:read)Devuelve el mismo cuerpo, actualizado. Los estados posibles de estado son:
estado | Qué significa | Qué hacer |
|---|---|---|
Pendiente | En la fila, todavía no se intentó. | Seguir consultando. |
Procesando | Se está emitiendo ahora. | Seguir consultando. |
Completado | Listo. Trae documentoId. | Consultá GET /dte/{documentoId} y guardá ese id como el del documento. |
Error | Falló. Mirá codigoError y esErrorTerminal. | Si esErrorTerminal es true, no reintentes: hay que actuar (típicamente, gestionar el timbraje ante el SII). Si es false, el sistema sigue reintentando solo. |
Cancelado | La emisión fue cancelada. | No se va a emitir. |
Ritmo de consulta sugerido: igual que el polling de estado — 5 s, 10 s, 20 s, 40 s, 60 s. Consume la misma cuota de rate limit (§17).
Listar los tickets abiertos
GET /api/public/v1/dte/pendientes?estado={n}&pagina=1&tamano=50 (scope dte:read)| Parámetro | Default | Notas |
|---|---|---|
estado | (todos) | Byte del estado. |
pagina | 1 | Menor a 1 se corrige a 1. |
tamano | 50 | Máximo 200; fuera de rango vuelve a 50. |
Devuelve un array plano, sin envoltorio de paginación: paginá hasta recibir menos elementos que tamano. Si la cola está deshabilitada en el despliegue, responde 200 con un array vacío (no un error).
Cuando el SII no autoriza timbraje
Si el SII directamente no autoriza folios para ese tipo de documento, la emisión no se encola: falla de inmediato con 422 caf.sin-autorizacion. Es terminal — reintentar no cambia nada, hay que gestionarlo ante el SII.
9. Consultar el estado (modelo asíncrono)
GET /api/public/v1/dte/{id} (scope dte:read)Devuelve el mismo cuerpo que la emisión, con estadoSii, glosaSii y trackId actualizados.
Pendiente → firmado y persistido, todavía no salió al SII (trackId null)
Enviado → el SII lo recibió, ya hay trackId, falta el veredicto
Aceptado | AceptadoConReparos | Rechazado → estado final
ErrorEnvio → error técnico de envío; reintenta solo desde nuestra cola
Anulado → anulado por nota de créditoSi llegás a
RechazadooAceptadoConReparos, el motivo está englosaSii. Es el campo que explica qué objetó el SII.
Ritmo de polling sugerido: primer intento a los 5 s, después backoff (10 s, 20 s, 40 s, 60 s). Cortá en cuanto llegues a un estado final. No hagas polling en loop cerrado: el rate limit es por key y lo vas a topar (§17).
Se puede evitar buena parte de este polling recibiendo un aviso firmado en tu servidor. El contrato está en §18, y la suscripción la das de alta vos, desde el portal.
Aislamiento entre empresas: un id que pertenece a otra empresa responde 404, no 403. La API no confirma la existencia de documentos ajenos.
⚠️ El
404de esta ruta y el deGET /{id}/xmlllegan con el cuerpo vacío, sincodenititle. No intentes leercodede un 404: ramificá por el status.
⚠️ No agregues parámetros de query que no estén documentados. La query se reenvía tal cual al servicio de emisión, y los dos parámetros que existen ahí se comportan distinto:
?empresaId=...falla fuerte:403 dte.cross-tenant-no-autorizado. Una API key nunca puede consultar otra empresa.?ambiente=Produccionfalla en silencio, y eso es peor. El override de ambiente solo lo honra un godadmin; para una API key se ignora sin avisar y la consulta se resuelve en el ambiente de la key. Con una key de certificación vas a recibir200con el documento de certificación (o404si eseidno existe ahí), nunca un error que te diga que el parámetro no se aplicó. Si tu código lo manda "por las dudas", vas a leer datos del ambiente equivocado creyendo que leíste producción.Ninguno de los dos hace falta: la key ya define empresa y ambiente, y no hay forma de cambiarlos desde el request.
10. Descargar XML y PDF
XML firmado
GET /api/public/v1/dte/{id}/xml → 200 application/xmlDevuelve el XML del DTE firmado, con su TED. La firma está garantizada: sin certificado digital activo el documento no se llega a emitir (§8). Es el archivo que corresponde archivar y el que se le entrega al receptor para intercambio.
El XML se sirve en ISO-8859-1 y así lo declara su cabecera. Si lo vas a procesar, respetá esa codificación: reinterpretarlo como UTF-8 rompe las tildes y la eñe, y con eso invalida su propia firma.
GET /api/public/v1/dte/{id}/pdf?formato=carta → 200 application/pdfformato | Resultado |
|---|---|
| omitido | Carta, siempre — también en boletas 39/41. |
carta | Hoja carta. |
80mm | Ticket térmico. |
Si necesitás el ticket térmico de una boleta, pedilo explícitamente con
?formato=80mm. Omitir el parámetro no lo elige por vos.
⚠️ Estos dos endpoints requieren API Key. No los pegues en un correo al receptor: quien tenga la key puede emitir a nombre de tu cliente. Para hacerle llegar el documento al comprador existen los enlaces firmados, acá abajo.
Ambos exigen el scope dte:read.
Enlaces firmados para el comprador
El problema que resuelven. Al comprador hay que entregarle su factura, y tu API Key no puede salir de tu backend. Hasta acá la única salida era descargar el archivo y redistribuirlo vos.
Por eso la respuesta de emisión trae xmlUrl y pdfUrl: enlaces que se abren sin API Key, listos para reenviar por correo o incrustar en tu portal de clientes. Lo que autoriza no es una credencial sino la firma del propio enlace, que lleva vencimiento.
GET /api/public/v1/descargas/dte/{id}/xml?t=…&exp=…&e=…&a=…
GET /api/public/v1/descargas/dte/{id}/pdf?t=…&exp=…&e=…&a=…&formato=carta
GET /api/public/v1/descargas/exportacion/{id}/xml?t=…&exp=…&e=…&a=…
GET /api/public/v1/descargas/exportacion/{id}/pdf?t=…&exp=…&e=…&a=…Usá la URL tal como viene. No la armes vos, no le cambies parámetros, no la re-firmes: el token cubre el documento y el recurso, así que cualquier retoque la invalida. Los parámetros:
| Parámetro | Qué es |
|---|---|
t | La firma. Es la credencial: tratala como un secreto mientras esté vigente. |
exp | Vencimiento, en segundos unix. Podés leerlo para saber hasta cuándo sirve el enlace sin tener que probarlo. |
e, a | Empresa y ambiente. Son criterio de búsqueda, no autorización: alterarlos solo consigue un 404. |
formato | Solo en el PDF de DTE nacional: carta (default) o 80mm. La exportación no lo admite. |
Dónde aparecen los tres campos
ted, xmlUrl y pdfUrl viajan en el cuerpo de cuatro respuestas:
| Respuesta | ted | xmlUrl / pdfUrl |
|---|---|---|
POST /dte → 201 | el TED del documento | firmadas |
GET /dte/{id} → 200 | null (ver §8) | firmadas, con vencimiento nuevo |
POST /exportacion → 201 | null (exportación no lo expone) | firmadas |
GET /exportacion/{id} → 200 | null | firmadas, con vencimiento nuevo |
Dónde no aparecen:
POST /dte→202. El ticket todavía no tiene documento: no hay nada que firmar ni que descargar. Cuando el ticket se resuelve, consultáGET /dte/{documentoId}y ahí sí vienen.POST /dte/{id}/anularyPOST /dte/{id}/nota-credito. El cuerpo trae la nota de crédito emitida, pero sin enlaces. Si necesitás mandarle la NC al comprador, consultala aparte conGET /dte/{notaCreditoId}y usá los enlaces de esa respuesta.- En cualquier respuesta de error.
Cuánto duran y qué pasa al vencer
30 días, por defecto, contados desde que el enlace se firma — no desde que se emitió el documento. En la respuesta de emisión son lo mismo; en la de una consulta posterior, el reloj arranca en esa consulta. El valor exacto viaja en exp, así que no hace falta suponerlo.
Es configuración del despliegue, no del cliente: si necesitás otra ventana, hablalo con soporte.
Pasado el vencimiento el enlace devuelve 403 descarga.token-vencido y deja de servir para siempre — no hay período de gracia.
Renovar un enlace vencido es gratis: volvé a consultar el documento.
GET /api/public/v1/dte/{id}(con tu API Key) devuelvexmlUrlypdfUrlrecién firmadas, con un vencimiento nuevo contado desde ese momento. Lo mismo vale paraGET /api/public/v1/exportacion/{id}. No hace falta reemitir nada ni pedir nada a soporte.
Por eso conviene generar el enlace cuando el comprador lo pide, y no guardar el de la emisión para reenviarlo meses después.
Errores
code | HTTP | Cuándo |
|---|---|---|
descarga.token-invalido | 403 | Falta t, está mal formado, o no corresponde a ese documento y ese recurso. También lo devuelve el enlace del XML si intentás usarlo para el PDF, y viceversa. |
descarga.token-vencido | 403 | La firma es correcta pero exp ya pasó. |
Ambos usan el mismo cuerpo de error que el resto de la API (§15), con field: "t". Con un token inválido la respuesta es idéntica exista o no el documento: el token se valida antes de consultar nada, así que probar ids al azar no revela cuáles son reales.
Un 404 acá significa lo de siempre: el documento no existe o no pertenece a esa empresa.
Seguridad: lo que hay que asumir
⚠️ La URL da acceso al documento a cualquiera que la tenga, hasta que vence. No hay login ni segundo factor detrás: el enlace es la llave. Eso es exactamente lo que lo hace útil para mandárselo al comprador, y también lo que obliga a tratarlo con cuidado.
- No la publiques en una página indexable, ni en un repositorio, ni en un ticket público. Un buscador que la encuentre expone la factura de tu cliente.
- Asumí que un reenvío de correo, un log de proxy o una captura de pantalla la propagan. El vencimiento existe justamente porque eso pasa.
- Del lado nuestro, el token no queda archivado: se enmascara antes de guardar la traza de la petición.
- No se puede revocar un enlace suelto. No hay ruta para eso, ni por API ni en el portal: el único corte disponible es rotar el secreto de firma del ambiente, lo que invalida de golpe todos los enlaces vigentes de todas tus emisiones. Si un enlace se filtró y el impacto lo justifica, escribí a soporte para evaluarlo; si no, el enlace deja de servir al vencer.
Cuota
Estas rutas sí consumen rate limit (§17) — son públicas, y sin límite serían un vector de abuso. Como no llevan API Key, la cuota se cuenta por IP del visitante, no contra la de tu integración: los compradores que abren sus facturas no te gastan el presupuesto de emisión.
Si los campos llegan en null
xmlUrl y pdfUrl vienen null cuando la funcionalidad no está configurada en ese ambiente (le falta el secreto de firma al despliegue). En ese caso los cuatro endpoints de descarga rechazan todo enlace con 403 descarga.token-invalido.
No es un error de tu request y no se arregla reintentando. Programá para el caso — if (respuesta.xmlUrl) — y si esperabas tener la funcionalidad, pedila a soporte. El resto de la emisión funciona igual: el documento se emitió, y GET /dte/{id}/xml con tu API Key sigue siendo el camino de siempre.
11. Anular un documento
POST /api/public/v1/dte/{id}/anular
X-Api-Key: pk_test_...
Content-Type: application/json (cuerpo opcional)Anular es emitir una Nota de Crédito tipo 61 que referencia al documento original con CodRef=1 ("anula documento completo") y marcarlo como anulado. Es una sola llamada.
¿Anular o acreditar parcial?
/anulardeja el documento sin efecto por su total. Si lo que querés es devolver parte del documento (unas líneas, o un monto) y que el resto siga vigente, no es esto: esPOST /{id}/nota-credito.
Lo importante: la referencia la arma el servidor
El cliente no manda tipoDocRef, folioRef ni fechaRef. El servidor lee el documento original y arma la referencia con su tipo, su folio y su fecha de emisión real.
Esto no es una comodidad, es la razón de existir del endpoint. Armar la NC a mano ya provocó una anulación rechazada por el SII: el cliente mandó
FchRefcon la fecha de hoy en vez de la fecha de emisión del original. El SII rechazó la nota de crédito y el folio se quemó igual.
La NC además espeja el cuerpo del original (detalles, impuestos adicionales, descuentos y recargos globales) para que los montos cuadren peso a peso.
Cuerpo (opcional)
{
"motivo": "Error en el RUT del receptor"
}| Campo | Notas |
|---|---|
motivo | Glosa que viaja como RazonRef al SII. Máx. 90 caracteres; si es más largo se trunca, no falla. Si se omite: "Anula documento". |
transactionId | Aceptado por compatibilidad, no influye en nada. Ver la nota de idempotencia. |
El cuerpo entero es opcional: POST sin body es válido.
Idempotencia: reintentar es seguro
La anulación es idempotente por documento. La clave es el {id} de la URL, no un campo que mandes vos. Consecuencia práctica:
- Reintentar el mismo
POST(timeout de red, retry automático de tu cliente HTTP, doble click de un usuario) devuelve la misma nota de crédito. - No se emite una segunda NC. No se quema otro folio.
- No necesitás mandar
transactionIdpara que esto funcione — de hecho, mandarlo no cambia nada.
Respuesta 201
{
"documentoAnulado": {
"id": "b5f8c2e1-7d93-4e8f-a12b-9c4d5e6f7a8b",
"tipoDte": 33,
"folio": 1048,
"fechaEmision": "2026-07-27",
"estadoSii": "Aceptado"
},
"notaCredito": {
"id": "7c1e9a44-2f60-4a1d-8e33-5b0c9d2f1a77",
"tipoDte": 61,
"folio": 312,
"montoTotal": 1190000,
"estadoSii": "Pendiente",
"trackId": null
}
}Dos cosas que suelen confundir:
documentoAnulado.estadoSiisigue diciendoAceptado. No es un bug. Para el SII el documento original sigue aceptado: la anulación es comercial (una NC 61 conCodRef=1), no un cambio de estado en el SII. Lo que marca la anulación es el campoanuladoEndel documento (visible enGET /dte/{id}), junto connotaCreditoId.- La nota de crédito nace
Pendiente, como cualquier emisión: viaja al SII en segundo plano. Si querés confirmar que el SII la aceptó, hacé polling sobreGET /api/public/v1/dte/{notaCredito.id}.
Qué se puede anular
| Condición | Regla |
|---|---|
| Tipo | 33, 34, 39, 41, 52, 56. Una NC 61 no se anula con otra NC; exportación tiene su propio flujo. |
| Estado SII | Enviado, Aceptado o AceptadoConReparos. Un documento que todavía no salió al SII (Pendiente) o que fue rechazado no se anula. |
| Folios | Tiene que haber CAF vigente de tipo 61. |
| Ambiente | La NC se emite en el mismo ambiente del documento. No se acepta override. |
Errores propios de la anulación
Ver la tabla completa en §15; los específicos son emisor.documento-ya-anulado, emisor.tipo-no-anulable, emisor.estado-no-anulable, emisor.sin-folios-nc, emisor.montos-no-reproducibles, emisor.anulacion-nc-descartada y emisor.anulacion-marca-fallida.
12. Devolución parcial: notas de crédito por línea o por monto
Cuando el cliente devuelve parte de lo facturado —dos de cinco unidades, un descuento posterior, o hay que corregir un dato sin tocar montos— no corresponde anular. Corresponde una nota de crédito parcial, con CodRef=3 (o CodRef=2 si solo se corrige texto), que rebaja el documento sin dejarlo sin efecto.
POST /{id}/anular | POST /{id}/nota-credito | |
|---|---|---|
| Referencia SII | CodRef=1 (anula documento completo) | CodRef=3 (corrige monto) — salvo el modo texto, que usa CodRef=2 (corrige texto) |
| Efecto | El documento queda anulado (anuladoEn se llena) | El documento sigue vigente por el saldo restante |
| Monto | El total del documento | El que vos indiques, hasta el saldo disponible |
| Veces | Una sola vez por documento | Varias notas parciales sobre el mismo documento |
| Idempotencia | Automática, por documento | Por transactionId que mandás vos |
| Referencia al original | La arma el servidor | La arma el servidor |
| Scope | dte:emit:61 | dte:emit:61 |
El flujo recomendado es siempre el mismo: consultar el saldo, después emitir.
12.1 Consultar el saldo acreditable
GET /api/public/v1/dte/{id}/acreditable (scope dte:read)Te dice qué queda disponible del documento y cómo se puede acreditar. Sirve especialmente si tu ERP no lleva su propio registro de devoluciones: el servidor ya lo lleva, y así no se desincronizan.
{
"documento": {
"id": "b5f8c2e1-7d93-4e8f-a12b-9c4d5e6f7a8b",
"tipoDte": 33,
"folio": 1048,
"fechaEmision": "2026-07-27",
"montoNeto": 1000000,
"montoExento": 0,
"montoTotal": 1190000,
"montoAcreditado": 200000,
"saldoAcreditable": 800000,
"estadoAcreditacion": "Parcial",
"porcentajeAcreditado": 20
},
"puedeAcreditar": true,
"motivoNoAcreditar": null,
"modosDisponibles": ["lineas", "monto", "texto"],
"advertenciaPlazo": null,
"lineas": [
{
"nroLinea": 1,
"nombreItem": "Consultoría técnica",
"descripcionItem": null,
"codigoItem": null,
"unidadMedida": null,
"indExe": 0,
"cantidadOriginal": 5,
"precioUnitario": 200000,
"montoItem": 1000000,
"montoEfectivo": 1000000,
"cantidadAcreditada": 1,
"montoAcreditado": 200000,
"cantidadDisponible": 4,
"montoDisponible": 800000,
"acreditable": true,
"motivoNoAcreditable": null
}
],
"notasCreditoPrevias": [
{
"id": "…", "folio": 310, "fechaEmision": "2026-07-28",
"montoTotal": 238000, "montoNetoImputado": 200000,
"codRef": 3, "razonRef": "Devolución de 1 unidad", "estadoSii": "Aceptado",
"lineasImputadas": [{ "nroLineaOrigen": 1, "cantidad": 1, "monto": 200000 }]
}
]
}Lo que hay que entender de esta respuesta:
| Campo | Qué significa |
|---|---|
documento.saldoAcreditable | El saldo va en NETO, no en el total con IVA: (montoNeto + montoExento) − montoAcreditado. Si acreditás por monto, ese monto se compara contra este número. |
documento.estadoAcreditacion | SinAcreditar / Parcial / AcreditadoTotal / Anulado. AcreditadoTotal no es lo mismo que Anulado: el primero es la acumulación de notas parciales (el SII nunca vio una anulación), el segundo es una anulación explícita. |
puedeAcreditar / motivoNoAcreditar | Si es false, el motivo viene en texto legible. |
modosDisponibles | Qué modos acepta este documento hoy. Puede no incluir lineas (ver abajo). |
advertenciaPlazo | Texto de advertencia cuando pasaron más de 90 días desde la emisión (plazo del Art. 70 del DL 825 para rebajar el débito fiscal). No bloquea: la nota se emite igual, pero fuera de plazo no da derecho a la rebaja. null si está en plazo. |
lineas[].montoDisponible / cantidadDisponible | Lo que queda por acreditar de cada línea. Es contra esto que se valida el modo lineas. |
lineas[].montoEfectivo | El neto real que la línea aporta. null en líneas no facturables o todavía sin procesar. |
notasCreditoPrevias | Historial de lo ya acreditado, con el detalle de qué línea imputó cada nota. |
Si
modosDisponiblesno incluye"lineas", es porque los montos de las líneas de ese documento no son reproducibles a partir decantidad × precio(típico de documentos importados de otro sistema). Podés acreditar igual, pormonto.
⚠️ Un
404 emisor.acreditacion-no-habilitadaen esta ruta NO significa que la ruta no exista: significa que el módulo de notas de crédito parciales está apagado para este despliegue. Si lo necesitás, pedilo a soporte.
12.2 Emitir la nota de crédito parcial
POST /api/public/v1/dte/{id}/nota-credito (scope dte:emit:61)
Content-Type: application/json{
"modo": "lineas",
"motivo": "Devolución de 2 unidades",
"lineas": [
{ "nroLinea": 1, "cantidad": 2 }
],
"transactionId": "1f0a7d2c-5c3e-4c1f-9b6a-2d8e4f0b7c31"
}| Campo | Obligatorio | Notas |
|---|---|---|
modo | sí | "lineas", "monto" o "texto". Otro valor → 400 emisor.modo-invalido. |
motivo | no | Glosa que viaja como RazonRef (máx. 90, se trunca). |
lineas | solo en modo lineas | [{ nroLinea, cantidad }]. La cantidad se valida contra cantidadDisponible. |
monto | solo en modo monto | Monto neto a acreditar. Se valida contra saldoAcreditable. |
transactionId | no, pero recomendado | Ver la nota de idempotencia. |
Los tres modos:
| Modo | Para qué | Efecto sobre el saldo |
|---|---|---|
lineas | Devolución de ítems concretos. | Descuenta de cada línea indicada. |
monto | Rebaja comercial, descuento posterior. | Descuenta del saldo del documento, sin imputar a líneas. |
texto | Corregir datos (razón social, giro, dirección) sin mover montos. Emite con CodRef=2, no CodRef=3. | Ninguno: emite una nota por monto 0. Se puede usar incluso sobre un documento ya acreditado por completo. |
La referencia al documento original la arma el servidor, igual que en /anular. Vos nunca mandás tipoDocRef, folioRef ni fechaRef.
12.3 Idempotencia: acá transactionId SÍ importa
A diferencia de /anular —donde un documento se anula a lo sumo una vez, así que la clave se deriva del documento— acá pueden existir N notas parciales legítimas sobre el mismo folio, incluso idénticas (dos devoluciones de 1 unidad el mismo día). El servidor no puede distinguir un reintento de una devolución nueva.
- Mandá un
transactionIdpropio, generado antes de la primera llamada. Con él, reintentar devuelve la misma nota; sin él, un reintento tras un timeout puede emitir una segunda nota y acreditar de más. - Si se omite, el servidor genera uno interno: la operación funciona, pero perdés la protección contra reintentos.
- El guard de saldo sigue impidiendo acreditar más de lo disponible en cualquier caso.
12.4 Respuesta 201
{
"notaCredito": { "id": "…", "tipoDte": 61, "folio": 313, "montoTotal": 476000, "estadoSii": "Pendiente", "…": "…" },
"documentoOrigen": {
"id": "b5f8c2e1-…", "tipoDte": 33, "folio": 1048,
"montoAcreditado": 600000, "saldoAcreditable": 400000,
"estadoAcreditacion": "Parcial", "porcentajeAcreditado": 60
},
"imputaciones": [
{ "nroLineaOrigen": 1, "cantidad": 2, "monto": 400000 }
]
}notaCredito tiene el mismo formato que cualquier documento emitido (§8), incluido su id para hacerle polling. documentoOrigen viene con el saldo ya actualizado. En modo monto, imputaciones trae una sola entrada con nroLineaOrigen: null.
12.5 Errores
code | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
emisor.acreditacion-no-habilitada | 404 | El módulo está apagado en este despliegue. | Pedirlo a soporte. No es una ruta inexistente. |
emisor.documento-no-encontrado | 404 | El id no existe o es de otra empresa. | Verificar el id. |
emisor.modo-invalido | 400 | modo no es lineas, monto ni texto. | Corregir. |
emisor.tipo-no-acreditable | 400 | El tipo no admite nota de crédito por esta vía. Acreditables: 33, 34, 39, 41, 52, 56. | No reintentar. |
emisor.estado-no-acreditable | 400 | El documento no está Enviado, Aceptado ni AceptadoConReparos, o no tiene montos que acreditar. | Si está Pendiente, esperar a que salga al SII. |
emisor.acreditacion-sin-saldo | 409 | Ya fue acreditado por completo. | Solo queda el modo texto. |
emisor.documento-ya-anulado | 409 | El documento fue anulado; no se acredita. | Ninguna acción. |
emisor.documento-descartado | 409 | El documento fue descartado por administración. | Escalar a soporte. |
emisor.acreditacion-excede-saldo | 400 | El monto o la cantidad pedida supera lo disponible. field indica la línea o monto. | Consultar /acreditable y ajustar. No reserva folio. |
emisor.acreditacion-en-curso | 409 | Hay otra petición emitiendo con el mismo transactionId ahora mismo. | Reintentar en unos segundos. |
emisor.acreditacion-replay-inconsistente | 409 | Ese transactionId ya acreditó, pero su nota no se pudo recuperar. | No reintentar. Escalar a soporte con el transactionId. |
emisor.sin-folios-nc | 400 / 409 | Sin folios de tipo 61. | Ver §15.12. |
emisor.ambiente-invalido | 400 | ambiente no reconocido. | No mandarlo. |
dte.permiso-faltante | 403 | Falta el scope dte:emit:61. | Agregar el scope. |
13. Exportación (110 / 111 / 112)
POST /api/public/v1/exportacion
GET /api/public/v1/exportacion/{id}
GET /api/public/v1/exportacion/{id}/xml
GET /api/public/v1/exportacion/{id}/pdfMismo modelo de autenticación y de errores, pero es un contrato distinto: el receptor es extranjero, los montos van en moneda extranjera y hay un bloque de aduana obligatorio.
Diferencias que importan de entrada:
- El receptor extranjero usa el RUT genérico
55555555-5. - Si la moneda no es peso chileno, el servidor genera automáticamente el bloque de totales en CLP equivalente a partir de la tasa de cambio.
- Los datos de aduana (país de destino, puerto de embarque y desembarque, modalidad y cláusula de venta, vía de transporte, bultos) salen de catálogos oficiales de aduana. Los códigos no son de invención libre.
- Los códigos de error usan el prefijo
exp.en vez deemisor.. estadoSiies un string, igual que en el DTE nacional ("Pendiente","Enviado","Aceptado","AceptadoConReparos","Rechazado","Anulado","ErrorEnvio"). No compares contra números.- El PDF de exportación es solo Carta y no admite
?formato=: el bloque de aduana, arancel, moneda extranjera y receptor extranjero no caben en un ticket de 80 mm. - Anulación: una exportación no se anula con
POST /dte/{id}/anularni conPOST /exportacion/{id}/anular(esa ruta no existe). Se emite una NC de exportación (112) referenciando el original. - Enlaces firmados:
POST /exportacionyGET /exportacion/{id}traenxmlUrlypdfUrligual que el DTE nacional, apuntando a/descargas/exportacion/.... Mismas reglas de vencimiento y de seguridad (§10). El PDF firmado tampoco admite?formato=. tedllega siemprenullen exportación: ese camino todavía no expone el timbre por separado. Está dentro del XML.
Los catálogos de aduana todavía no tienen endpoint público. Hoy se consultan desde el portal. Si necesitás integrarte a exportación, pedí el volcado de los catálogos a soporte — ver Qué no existe todavía.
El contrato campo por campo del cuerpo de exportación está en la referencia OpenAPI (/docs/reference, sección Exportación), generada del mismo contrato que valida el servidor.
14. Paso a producción (go-live)
Emitir con una key pk_live_* no es solo cambiar la key. Son tres pasos, y dos dependen de nosotros.
Paso 1 — Certificación ante el SII, por cada tipo de documento
El SII exige un proceso de certificación por contribuyente y por tipo de DTE. Lo ejecuta el equipo de Comges; el integrador no lo dispara por API, y no hay endpoint público para consultarlo ni iniciarlo.
Mientras un tipo no esté aprobado, emitirlo en Producción devuelve 403 cert.tipo-no-aprobado-prod, tipo por tipo. Es decir: podés tener la 33 aprobada y la 61 no, y descubrirlo recién al intentar tu primera anulación real.
Pedí la certificación de todos los tipos que vas a emitir, incluida la 61 si vas a anular o acreditar. Es el olvido más común.
Si la verificación de certificación no responde, la emisión falla con 503 cert.servicio-no-disponible: eso sí es reintentable con backoff.
Paso 2 — Cambiar el ambiente del usuario
El ambiente de la key se hereda del usuario que la crea. Para que un usuario pueda crear keys de producción, un administrador de la empresa (con el permiso empresa:usuarios:ambiente) tiene que cambiarle el ambiente operativo desde el portal.
Ese cambio cierra las sesiones abiertas de ese usuario: tiene que volver a entrar.
Paso 3 — Crear una API Key nueva
⚠️ Rotar una key
pk_test_NUNCA la convierte enpk_live_. La rotación conserva el ambiente de la key: solo cambia el secreto. Si rotás tu key de certificación esperando pasar a producción, vas a seguir emitiendo contra Maullín — documentos de prueba, sin validez tributaria — sin ningún error que te avise.
Hay que crear una key nueva después del paso 2, con los mismos scopes.
Checklist de go-live
- Pendiente: Certificación SII aprobada para cada tipo que vas a emitir (depende de Comges).
- Pendiente: Plan de la empresa vigente y con fecha suficiente (depende de Comges).
- Pendiente: Certificado digital de la empresa activo y no vencido (depende de Comges). Si vence, toda emisión corta con
422 emisor.sin-certificado/emisor.certificado-invalido(§8). - Pendiente: Todos los tipos habilitados para la empresa en Producción (depende de Comges).
- Pendiente: Ambiente del usuario cambiado a Producción (lo hace el administrador de la empresa).
- Pendiente: Key nueva
pk_live_*creada (no rotada) y desplegada en tu configuración. - Pendiente: Tu código manda
transactionIden toda emisión. - Pendiente: Tu código ramifica por el
codedel error (§15) y maneja el202(§8). - Pendiente: Tu timeout HTTP es mayor al nuestro (§17).
- Pendiente: Verificado que las primeras emisiones reales llegan a
Aceptado, y que si no, leésglosaSii.
15. Errores: formato y códigos
El cuerpo estándar
La mayoría de los errores llega con este cuerpo:
{
"code": "emisor.sin-folios",
"title": "No hay folios disponibles para tipo 33 ambiente Certificacion. Suba un CAF nuevo.",
"detail": null,
"status": 409,
"field": "tipoDte",
"traceId": null
}Los campos que no aplican viajan como null, no se omiten.
| Campo | Uso |
|---|---|
code | Estable. Es el campo contra el que hay que programar. |
title | Legible para humanos. La redacción puede cambiar entre versiones. |
detail | Explicación más larga, cuando el error la tiene. La mayoría la deja en null. |
status | Igual al status HTTP. |
field | Campo del cuerpo que causó el rechazo, cuando aplica. Puede venir con notación de índice (detalles[2].unidadMedida) o con varios campos separados por coma. |
traceId | Identificador de la petición. Hoy solo viaja en las respuestas 500; en los errores de negocio (4xx) llega null. Para pedir soporte, ver más abajo qué mandar. |
Respuestas que NO usan ese cuerpo
Estas son las excepciones. Si tu manejo de errores asume que siempre hay code, estas lo rompen:
| Situación | HTTP | Cuerpo real | Qué hacer |
|---|---|---|---|
| JSON malformado, tipo de dato equivocado, campo obligatorio ausente (rechazo del deserializador, antes de llegar a la lógica) | 400 | { "type", "title": "One or more validation errors occurred.", "status", "errors": { … }, "traceId" } — sin code | Leer el mapa errors: cada clave es la ruta del campo. |
| Error interno | 500 | { "type", "title", "status", "traceId" } — sin code | Reintentar con backoff y el mismo transactionId. Si persiste, soporte con el traceId. |
Documento inexistente en GET /dte/{id} y GET /dte/{id}/xml | 404 | Cuerpo vacío | Verificar el id. Un id de otra empresa también da 404. |
| La petición superó los 60 s de la plataforma | 504 | HTML, no JSON | Reintentar con backoff y el mismo transactionId. Ver §17. |
Además, dos respuestas traen el cuerpo recortado: el 401 y el 429 vienen con code y title (el 429 también con status) pero sin traceId; y el 403 plan.vencido trae campos extra (type, detail, fechaVencimiento).
Cómo leer las tablas
- ¿Reintentar? —
nosignifica que el mismo request va a fallar igual: hay que corregir algo.sí (backoff)significa que la causa es transitoria. - ¿Folio? — lo más importante cuando algo falla en una emisión.
no= el documento se rechazó antes de tomar un folio del CAF, así que no perdiste nada y podés corregir y reenviar.sí= el folio ya se consumió (o quedó comprometido) y ese número no vuelve.
Regla general de la emisión nacional: todas las validaciones del cuerpo, del receptor, del detalle, de la guía de despacho, de las fechas, de los largos y de los prerequisitos de la empresa —el certificado digital incluido— corren antes de reservar folio. Si recibiste un 400, un 403, un 422 o un 501 de
POST /dte, no se consumió folio.⚠️
POST /exportacionno cumple esa regla del todo. Sus validaciones de formato también corren antes del folio, pero el certificado digital se carga después de reservarlo: por eso409 exp.sin-certificadoy500 exp.error-pfxllegan con el folio ya consumido. La columna «¿Folio?» de §15.17 lo marca caso por caso.
15.1 Autenticación, cuota y permisos
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
api-key.ausente | 401 | Falta el header, o la key no sirve: inválida, revocada, expirada o con formato malo. El 401 es genérico a propósito — no distingue el motivo. | no | Revisar el header X-Api-Key y el secreto. Si la key fue rotada, actualizar el valor guardado. |
permiso.requerido | 403 | La key está bien, pero le falta el scope que exige la ruta (dte:read en las consultas, dte:emit:61 en anulación y nota de crédito). | no | Emitir con una key que tenga ese scope. |
emisor.permiso-tipo-no-autorizado | 403 | Falta dte:emit:{tipo} para el tipo que estás emitiendo. Es el mismo problema que el anterior, pero detectado en la capa de emisión. | no | Emitir con una key que tenga dte:emit:{tipo}. |
dte.permiso-faltante | 403 | Falta dte:emit:61 en POST /dte/{id}/nota-credito. Tercera variante del mismo problema. | no | Emitir con una key que tenga dte:emit:61. |
api-publica.rate-limit | 429 | Se excedió el límite de peticiones (se cuenta por key y también por IP). | sí | Esperar los segundos que indica el header Retry-After y reintentar. |
descarga.token-invalido | 403 | Solo en las rutas de descarga firmada. Falta el token t, está mal formado, o no corresponde a ese documento y ese recurso. También aparece cuando la funcionalidad no está configurada en el ambiente. | no | Usar la URL tal como vino en xmlUrl / pdfUrl, sin modificarla. Ver §10. |
descarga.token-vencido | 403 | Solo en las rutas de descarga firmada. La firma es válida pero el enlace ya venció. | no | Pedir uno nuevo: GET /dte/{id} devuelve xmlUrl y pdfUrl recién firmadas. |
Los tres 403 de scope (
permiso.requerido,emisor.permiso-tipo-no-autorizado,dte.permiso-faltante) describen la misma causa: la key no tiene el permiso. Cuál de los tres llega depende de qué capa corte primero, así que si programás una rama de "falta permiso", cubrí los tres.
15.2 Estado de la cuenta
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
plan.vencido | 403 | El plan de la empresa venció. El cuerpo trae además detail y fechaVencimiento. | no | Renovar el plan. Alcance real: bloquea emisión, anulación, notas de crédito y las consultas de documentos. Los dos endpoints de PDF siguen respondiendo con el plan vencido. |
demo.solo-lectura | 403 | La key pertenece a la empresa de demostración, que es compartida y de solo lectura. Solo bloquea escrituras (POST). | no | Usar una key de una empresa real. |
15.3 Prerequisitos de la empresa
Nada de esto se puede resolver desde el ERP: son datos o habilitaciones de la empresa emisora. Ninguno consume folio — los dos controles del certificado también corren antes de tomar el folio, a propósito: la firma ocurre después de reservarlo.
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
emisor.tipo-dte-no-habilitado | 403 | El tipo de documento no está habilitado para esa empresa. | no | Pedir la habilitación del tipo a soporte. |
cert.tipo-no-aprobado-prod | 403 | Solo en Producción. Ese tipo de documento todavía no tiene la certificación aprobada por el SII. El title incluye el estado actual del proceso. | no | Completar la certificación del tipo ante el SII antes de emitirlo en producción (ver §14). En Certificación este error no aparece. |
cert.servicio-no-disponible | 503 | No se pudo verificar el estado de certificación, así que la emisión en Producción se bloquea por seguridad. | sí (backoff) | Reintentar en unos minutos. Si persiste, soporte. |
emisor.empresa-sin-sucursal | 422 | La empresa no tiene casa matriz configurada; sin dirección de origen no se puede armar el documento. | no | Configurar la sucursal en el portal. |
emisor.empresa-sin-actecos | 422 | La empresa no tiene actividades económicas activas. | no | Configurar los actecos en el portal. |
emisor.perfil-empresa-no-disponible | 503 | No se pudo resolver el perfil de la empresa (razón social, giro, sucursales, actecos). | sí (backoff) | Reintentar. Si persiste, soporte. |
emisor.sin-certificado | 422 | La empresa no tiene un certificado digital (PFX) activo. Todo DTE se firma antes de guardarse, así que sin certificado no se puede emitir. | no | Cargar un PFX vigente. |
emisor.certificado-invalido | 422 | Hay un PFX cargado pero no se pudo abrir: clave equivocada o certificado vencido. | no | Revisar la vigencia del certificado y su clave. |
15.4 Tipo de documento y ambiente
Ninguno consume folio.
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
emisor.tipo-dte-invalido | 400 | tipoDte no es un código conocido del SII. | no | Corregir el valor. |
emisor.tipo-dte-no-emisible | 501 | Se pidió un 43 (Liquidación-Factura) o un 46 (Factura de Compra). Esta API no los emite: su aritmética específica no está implementada. No se consumió folio. | no | No usar 43 ni 46. Los scopes dte:emit:43 y dte:emit:46 existen y se pueden conceder, pero no habilitan nada. Ver §8. |
emisor.tipo-dte-no-implementado | 501 | Se mandó un 110/111/112 a POST /dte. | no | Usar POST /api/public/v1/exportacion. |
emisor.ambiente-no-coincide-con-key | 400 | El cuerpo pide un ambiente distinto al de la key. El ambiente lo fija la key y no se puede cambiar desde el request. | no | Sacar ambiente del cuerpo, o usar la key del ambiente que corresponde. |
emisor.ambiente-invalido | 400 | ambiente no es Certificacion ni Produccion. | no | Corregir, o mejor, no mandar el campo. |
15.5 Validación del cuerpo — receptor
Ninguno consume folio. Todos son 400.
code | Qué significa | Qué hacer |
|---|---|---|
emisor.rut-receptor-invalido | El RUT del receptor está vacío o no pasa el dígito verificador. Es el único que vas a recibir por este motivo. | Corregir el RUT. Los genéricos del SII (66666666-6 consumidor final, 55555555-5 extranjero) son válidos. |
emisor.receptor-incompleto | Faltan direccion, comuna o giro, que el SII exige para ese tipo de documento. El field lista los que faltan. | Completar los campos. |
emisor.receptor-largo-excedido | Un campo del receptor supera el largo máximo del SII. El field dice cuál. Ver los largos en §8. | Acortar. Estos campos no se truncan solos: un giro comercial chileno normal pasa fácil de 40 caracteres. |
emisor.receptor-correo-invalido | Alguno de los correos de copia no tiene forma de correo. | Corregir o quitar. |
emisor.receptor-correos-largo-excedido | La lista de correos de copia excede el largo permitido. | Reducir la cantidad. |
Los códigos
emisor.receptor-rut-requeridoyemisor.receptor-rut-invalidoaparecen en el código fuente pero no son alcanzables desdePOST /dte: el guard que devuelveemisor.rut-receptor-invalidocorre primero. Programá contra ese código, o contrafield == "receptor.rut". Ver §15.19.
15.6 Validación del cuerpo — detalle
Ninguno consume folio. Todos son 400.
code | Qué significa | Qué hacer |
|---|---|---|
emisor.detalles-vacios | detalles vacío. | Mandar al menos una línea. |
emisor.detalles-excede-60 | Más de 60 líneas (límite duro del SII). | Partir el documento. |
emisor.detalle-indexe-invalido | indExe fuera de los valores soportados (0, 1, 2, 6). | Corregir. |
emisor.detalle-monto-invalido | Cantidad, precio, descuento o recargo de una línea fuera de rango o incoherentes. | Corregir la línea que indica field. |
emisor.detalle-largo-excedido | Un campo de una línea supera el largo del SII. Ver los largos en §8. | Acortar. Ojo con unidadMedida: son 4 caracteres, así que UN, KG o LT sirven, pero UNIDAD, KILOS y LITROS no. |
15.7 Validación del cuerpo — referencias, impuestos, descuentos y recargos
Ninguno consume folio. Todos son 400.
code | Qué significa | Qué hacer |
|---|---|---|
emisor.referencias-excede-40 | Más de 40 referencias. | Reducir. |
emisor.referencia-requerida | Una nota de crédito (61) o de débito (56) sin ninguna referencia al documento que corrige o anula. El SII las exige. | Agregar la referencia. (Para anular, es más seguro usar POST /{id}/anular: la referencia la arma el servidor.) |
emisor.referencia-codref-invalido | codRef fuera de 1 (anula), 2 (corrige texto) o 3 (corrige montos). | Corregir. |
emisor.referencia-fecha-fuera-de-rango | Algún fechaRef cae fuera del rango que el SII reconoce (desde 2002-08-01 hasta 2050-12-31). El title lista todas las líneas con problema. | Corregir la fecha del documento referenciado. |
emisor.referencia-largo-excedido | Un campo de una referencia supera su largo. Límites: tipoDocRef 3, folioRef 18, razonRef 90. | Acortar. |
emisor.impuestos-excede-20 | Más de 20 impuestos adicionales. | Reducir. |
emisor.impuesto-codigo-invalido | codigoImpuesto no existe en la tabla del SII (rango 14..53, más el 271). | Corregir. |
emisor.impuesto-monto-negativo | Un impuesto adicional con monto negativo: bajaría el total del documento. | Corregir. |
emisor.descuentos-globales-excede-20 | Más de 20 descuentos o recargos globales. | Reducir. |
emisor.dr-tipo-movimiento-invalido | tipoMovimiento no es "D" (descuento) ni "R" (recargo). | Corregir. |
emisor.dr-tipo-valor-invalido | tipoValor no es "%" ni "$". | Corregir. |
emisor.dr-valor-negativo | Valor de descuento o recargo negativo. | Corregir. |
emisor.dr-glosa-excede-45 | Glosa de más de 45 caracteres. | Acortar. |
15.8 Validación del cuerpo — fechas y forma de pago
Ninguno consume folio. Todos son 400.
code | Qué significa | Qué hacer |
|---|---|---|
emisor.fecha-emision-invalida | Formato de fechaEmision no reconocido. | Usar dd-MM-yyyy o yyyy-MM-dd. Lo más simple es omitir el campo: por defecto se usa la fecha de hoy en Chile. |
emisor.fecha-emision-futura | Fecha posterior a hoy en Chile. El SII las rechaza. | Corregir u omitir el campo. |
emisor.fecha-emision-anterior-minima | Fecha anterior a la fecha mínima que acepta el SII. | Corregir. |
emisor.fecha-vencimiento-invalida | Formato de fechaVencimiento no reconocido. | Corregir. |
emisor.fecha-vencimiento-anterior | El vencimiento es anterior a la emisión. | Corregir. |
emisor.forma-pago-invalida | formaPago fuera de los valores del SII (1 contado, 2 crédito, 3 sin costo). | Corregir. |
15.9 Validación del cuerpo — sucursal, actecos y campos propios
Ninguno consume folio.
code | HTTP | Qué significa | Qué hacer |
|---|---|---|---|
emisor.sucursal-no-encontrada | 400 | empresaSucursalId no existe en esa empresa. | Corregir u omitir (se usa la casa matriz). |
emisor.actecos-excede-4 | 400 | Más de 4 actividades económicas (límite del SII). | Reducir u omitir. |
emisor.acteco-no-pertenece | 400 | Uno de los actecos declarados no pertenece a esa empresa. | Corregir u omitir. |
emisor.acteco-principal-fuera-de-lista | 400 | El acteco principal no está entre los declarados. | Corregir. |
custom-field-obligatorio | 400 | Falta un campo propio marcado como obligatorio para esa empresa. | Completar customFields. |
custom-field-desconocido | 400 | customFields trae una clave que no está definida para esa empresa. | Quitarla. |
custom-field-tipo-invalido | 400 | El valor de un campo propio no corresponde a su tipo. | Corregir el valor. |
custom-field-payload-grande | 400 | El bloque customFields supera el tamaño permitido. | Reducir. |
Los cuatro códigos de campos propios no llevan prefijo de familia. Solo aparecen si la empresa tiene campos propios configurados.
15.10 Validación del cuerpo — guía de despacho (52)
Estos nueve solo aplican al tipo 52. Ninguno consume folio. Todos son 400.
code | Qué significa | Qué hacer |
|---|---|---|
emisor.ind-traslado-requerido | Falta indTraslado, que es obligatorio en la guía. | Mandarlo (1..7). |
emisor.ind-traslado-invalido | indTraslado fuera del rango válido. | Corregir. |
emisor.ind-traslado-no-aplica | Se mandó indTraslado en un documento que no es una guía. | Quitarlo. |
emisor.ind-traslado-exportacion-no-soportado | Se pidió indTraslado 8 o 9 (traslado para exportación). Esta API no los soporta. | No usar 8 ni 9. |
emisor.tipo-despacho-invalido | tipoDespacho fuera de 1/2/3. | Corregir. |
emisor.tipo-despacho-no-aplica | Se mandó tipoDespacho en un documento que no es una guía. | Quitarlo. |
emisor.transporte-chofer-incompleto | Se mandó rutChofer sin nombreChofer, o al revés. Van los dos o ninguno. | Completar el par. |
emisor.transporte-rut-invalido | rutTransportista o rutChofer no pasa el dígito verificador. | Corregir. |
emisor.transporte-largo-excedido | Un campo del bloque transporte supera su largo. El field dice cuál. | Acortar. |
15.11 Cotizaciones de origen
Solo aparecen si el cuerpo trae cotizacionIds. Ninguno consume folio.
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
emisor.cotizacion-no-verificable | 503 | No se pudo verificar la cotización de origen. La emisión se bloquea a propósito antes de tomar folio. | sí (backoff) | Reintentar. Si persiste, emitir sin cotizacionIds. |
emisor.cotizacion-no-encontrada | 404 | La cotización no existe o es de otra empresa. | no | Corregir el id. |
emisor.cotizacion-no-emitible | 409 | La cotización no está abierta ni ganada, o está vencida. | no | Revisar el estado de la cotización. |
15.12 Folios
Es el grupo donde más importa saber si perdiste un folio. En ninguno de estos se consumió folio: el folio se toma después de todas las validaciones y, si algo falla más adelante, el servidor lo libera.
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
emisor.sin-folios | 409 | No quedan folios de ese tipo en ese ambiente y tampoco se pudo pedir más. | no | Cargar un CAF nuevo para ese tipo. |
caf.sin-autorizacion | 422 | El SII no autoriza timbraje de ese tipo de documento para esa empresa. Es terminal: reintentar no lo va a cambiar. | no | Requiere gestión ante el SII. |
emisor.sin-folios-nc | 400 | No hay folios de tipo 61 para emitir la nota de crédito (anulación o nota parcial). | no | Cargar un CAF de tipo 61. |
emisor.sin-folios-nc | 409 | Variante: se pidió el folio 61 al SII y no llegó a tiempo. La anulación no queda en cola, a diferencia de la emisión normal. | sí (backoff) | Reintentar el mismo POST /anular en unos minutos. |
emisor.caf-interno-no-configurado | 503 | Problema de configuración del lado nuestro: la reserva de folios no está disponible. | sí (backoff) | Soporte con el traceId. |
emisor.sin-folioses hoy un camino residual. Cuando no hay folio disponible, el sistema se los pide al SII y espera. Si el folio llega, recibís tu 201 normal. Si no llega a tiempo, la emisión se encola y recibís un 202 conticketId(ver Respuesta202): el documento ya está validado y se va a emitir solo. Solo si el SII dice que no autoriza ese tipo vas a ver el 422caf.sin-autorizacion.
15.13 Consulta de documentos y de tickets
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
| (sin cuerpo) | 404 | El documento no existe, o es de otra empresa. GET /dte/{id} y GET /dte/{id}/xml devuelven 404 vacío. | no | Verificar el id. |
dte.cross-tenant-no-autorizado | 403 | Se mandó ?empresaId= en la query. Una API key nunca puede consultar otra empresa. | no | Quitar el parámetro: la empresa sale de la key. |
dte.ambiente-no-autorizado | 401 | No se pudo resolver el ambiente de la petición. Sale solo si falta el claim de ambiente, no por mandar ?ambiente=. Con una API key no debería ocurrir: el ambiente siempre viaja en la key (y ante la duda se asume Certificación). | no | Si aparece, es un problema de la credencial — soporte. |
emisor.ticket-no-encontrado | 404 | El ticketId no existe, o es de otra empresa (se devuelve 404 y no 403 para no revelar su existencia). | no | Verificar el ticketId que devolvió el 202. |
emisor.pendientes-no-disponible | 503 | La cola de emisiones pendientes no está habilitada en ese despliegue. | sí (backoff) | Soporte. |
15.14 Códigos que llegan dentro del ticket del 202
Estos no son errores HTTP: viajan en el campo codigoError del cuerpo que devuelve GET /dte/pendientes/{ticketId}, junto con mensaje y esErrorTerminal. La respuesta del endpoint es 200.
codigoError | esErrorTerminal | Qué significa | Qué hacer |
|---|---|---|---|
caf.sin-folios | false | Todavía no hay folios; el SII aún no entregó. El ticket sigue en cola. | Seguir consultando. |
caf.sin-autorizacion | true | El SII no autoriza folios de ese tipo. El ticket no se va a emitir nunca. | Gestión ante el SII. Volver a emitir después. |
emisor.payload-ilegible | true | No se pudo recuperar el contenido de la emisión encolada. | Volver a emitir. Soporte si se repite. |
emisor.error-inesperado | false | Error inesperado al emitir; se reintenta solo. | Seguir consultando. |
emisor.error | true | Comodín cuando el error de emisión no trae código propio. | Leer mensaje. |
| cualquier código de las tablas anteriores | true | El documento encolado falló por un problema de negocio (datos inválidos, tipo no habilitado…). | Corregir y volver a emitir. |
esErrorTerminal: truesignifica: no reintentes ese ticket. Confalse, el sistema lo vuelve a tomar solo.
15.15 Anulación — POST /dte/{id}/anular
code | HTTP | Qué significa | ¿Folio? | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|---|
emisor.documento-no-encontrado | 404 | El id no existe o es de otra empresa. | no | no | Verificar el id. |
emisor.documento-ya-anulado | 409 | El documento ya tiene una anulación previa que no pasó por este endpoint. | no | no | Ninguna acción: ya está anulado. Un reintento del mismo POST /anular no da este error: devuelve la misma nota de crédito. |
emisor.tipo-no-anulable | 400 | Ese tipo no se anula con nota de crédito (por ejemplo, una NC 61, o un documento de exportación). | no | no | Para exportación, emitir una NC 112. |
emisor.estado-no-anulable | 400 | El documento está Pendiente, Rechazado o ErrorEnvio. Solo se anula lo que ya llegó al SII (Enviado, Aceptado, AceptadoConReparos). | no | no | Si está Pendiente, esperar a que llegue al SII y reintentar. |
emisor.montos-no-reproducibles | 409 | Alguna línea tiene un monto que no se deriva de cantidad × precio − descuento + recargo (típico de documentos importados). Emitir igual daría una nota por un importe distinto al que anula. | no | no | Probá POST /{id}/nota-credito en modo monto, que no depende de las líneas. No armes la nota de crédito a mano: la referencia al original (tipoDocRef, folioRef, fechaRef) es exactamente lo que estos endpoints existen para resolver. |
emisor.anulacion-nc-descartada | 409 | La nota de crédito que anulaba este documento fue descartada por administración. | no | no | No se re-emite automáticamente. Escalar a soporte. |
emisor.anulacion-marca-fallida | 500 | La nota de crédito se emitió, pero no se pudo marcar el original como anulado. | sí, ya consumido | sí | Reintentar el mismo POST /anular: el camino idempotente termina de marcar sin emitir otra nota. |
Y además: emisor.sin-folios-nc (§15.12) y dte.cross-tenant-no-autorizado (§15.13).
15.16 Nota de crédito parcial — GET /dte/{id}/acreditable y POST /dte/{id}/nota-credito
Ninguno consume folio: el saldo se valida antes de reservar.
code | HTTP | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|
emisor.acreditacion-no-habilitada | 404 | La funcionalidad de notas de crédito parciales no está habilitada. Un 404 acá no significa "ruta inexistente". | no | Pedir la habilitación a soporte. |
emisor.documento-no-encontrado | 404 | El id no existe o es de otra empresa. | no | Verificar el id. |
emisor.documento-descartado | 409 | El documento fue descartado por administración. | no | Escalar a soporte. |
emisor.documento-ya-anulado | 409 | El documento ya está anulado por completo: no queda nada que acreditar. | no | Ninguna. |
emisor.acreditacion-sin-saldo | 409 | El documento ya fue acreditado por completo. | no | Consultar GET /{id}/acreditable para ver el saldo. |
emisor.tipo-no-acreditable | 400 | Ese tipo de documento no admite nota de crédito parcial. | no | — |
emisor.estado-no-acreditable | 400 | El documento está en un estado que no admite nota de crédito. | no | Esperar a que llegue al SII. |
emisor.modo-invalido | 400 | modo no es lineas, monto ni texto. | no | Corregir. |
emisor.modo-no-disponible | 400 | Ese modo no aplica a ese documento. | no | Consultar modosDisponibles en GET /{id}/acreditable. |
emisor.lineas-requeridas | 400 | modo: "lineas" sin el arreglo lineas. | no | Mandar las líneas. |
emisor.linea-inexistente | 400 | Una nroLinea no existe en el documento original. | no | Corregir. |
emisor.linea-no-facturable | 400 | Se intentó acreditar una línea que no es facturable. | no | Quitarla. |
emisor.cantidad-invalida | 400 | Cantidad a acreditar fuera de rango para esa línea. | no | Corregir. |
emisor.monto-requerido | 400 | modo: "monto" sin el campo monto. | no | Mandar el monto. |
emisor.acreditacion-excede-saldo | 400 | Lo pedido supera el saldo disponible (del documento o de una línea). El field señala la línea culpable. No se reservó folio. | no | Consultar GET /{id}/acreditable antes de emitir. |
emisor.acreditacion-en-curso | 409 | Hay otra petición emitiendo con el mismo transactionId en este momento. | sí (backoff) | Reintentar en unos segundos con el mismo transactionId. |
emisor.acreditacion-replay-inconsistente | 409 | Ese transactionId ya acreditó, pero su nota de crédito no se pudo recuperar. | no | No reintentar con otro transactionId: acreditarías dos veces. Escalar a soporte con el transactionId. |
Y además: dte.permiso-faltante (§15.1) y emisor.sin-folios-nc (§15.12).
El saldo de
GET /{id}/acreditableviene en neto, no en total con IVA.
15.17 Exportación (exp.*)
Mismo criterio que la emisión nacional: todo lo que sea 400, 403 o 422 ocurre antes de tomar folio. Las excepciones están marcadas.
code | HTTP | Qué significa | ¿Folio? | ¿Reintentar? | Qué hacer |
|---|---|---|---|---|---|
exp.tipo-dte-invalido | 400 | tipoDte no es 110, 111 ni 112. | no | no | Corregir. |
exp.tipo-dte-no-habilitado | 403 | Ese tipo de exportación no está habilitado para la empresa en ese ambiente. | no | no | Pedir la habilitación a soporte. |
exp.ambiente-no-coincide-con-key | 400 | El cuerpo pide un ambiente distinto al de la key. | no | no | Sacar ambiente del cuerpo. |
exp.ambiente-invalido | 400 | ambiente no es Certificacion ni Produccion. | no | no | Corregir. |
exp.detalles-vacios | 400 | Sin líneas de detalle. | no | no | Mandar al menos una. |
exp.detalles-excede-60 | 400 | Más de 60 líneas. | no | no | Partir el documento. |
exp.referencias-excede-40 | 400 | Más de 40 referencias. | no | no | Reducir. |
exp.bultos-excede-10 | 400 | Más de 10 bultos. | no | no | Reducir. |
exp.comisiones-excede-20 | 400 | Más de 20 comisiones. | no | no | Reducir. |
exp.dr-excede-20 | 400 | Más de 20 descuentos o recargos globales. | no | no | Reducir. |
exp.nd-nc-sin-referencia | 400 | Una nota de débito (111) o de crédito (112) de exportación sin referencia al documento original. | no | no | Agregar la referencia. |
exp.referencia-codref-requerido | 400 | Falta codRef en una referencia. | no | no | Agregarlo. |
exp.referencia-codref-invalido | 400 | codRef fuera de los valores válidos. | no | no | Corregir. |
exp.receptor-ciudad-requerida | 400 | Falta la ciudad del receptor extranjero. | no | no | Completar. |
exp.receptor-direccion-requerida | 400 | Falta la dirección del receptor extranjero. | no | no | Completar. |
exp.moneda-invalida | 400 | La moneda no pertenece al catálogo del SII. | no | no | Usar un literal del SII (DOLAR USA, EURO, PESO CL…). Si no está en el listado, OTRAS MONEDAS. |
exp.fecha-emision-invalida | 400 | Formato de fecha no reconocido. | no | no | Corregir. |
exp.fecha-emision-futura | 400 | Fecha posterior a hoy en Chile. | no | no | Corregir. |
exportacion.* | 400 | Familia completa de reglas del formato SII de exportación. El code nombra la regla concreta y el field el campo culpable. Ver el detalle abajo. | no | no | Corregir el campo que indica field. |
exp.contrato-invalido | 400 | Comodín cuando una regla del formato no trae código propio. | no | no | Leer title y field. |
exp.empresa-incompleta | 422 | Faltan datos maestros de la empresa exigidos para exportación (razón social, giro, dirección, comuna, sucursal o actecos). | no | no | Completar en el portal. |
exp.perfil-empresa-no-disponible | 503 | No se pudo resolver el perfil de la empresa. | no | sí (backoff) | Reintentar. |
exp.caf-interno-no-configurado | 503 | La reserva de folios no está disponible. | no | sí (backoff) | Soporte. |
exp.sin-folios | 409 | No quedan folios de ese tipo de exportación. | no | no | Cargar un CAF. |
exp.sin-certificado | 409 | La empresa no tiene certificado digital activo. Se detecta después de tomar el folio. | sí, ya consumido | no | Cargar el PFX antes de volver a emitir. |
exp.error-pfx | 500 | El certificado digital no se pudo abrir (vencido o ilegible). Después de tomar el folio. | sí (se intenta liberar) | no | Revisar el certificado. |
exp.error-xml | 500 | Falló la construcción o la firma del XML. | sí (se intenta liberar) | sí (backoff) | Reintentar con el mismo transactionId. Si persiste, soporte. |
exp.error-persistencia | 500 | Falló el guardado del documento. | sí (se intenta liberar) | sí (backoff) | Reintentar con el mismo transactionId. |
exp.not-found | 404 | La exportación no existe o es de otra empresa. | — | no | Verificar el id. |
exp.xml-no-disponible | 404 | El documento existe pero todavía no tiene XML firmado. | — | sí (backoff) | Reintentar en unos segundos. |
exp.error-descarga-xml | 500 | No se pudo recuperar el XML almacenado. | — | sí (backoff) | Soporte con el traceId. |
exp.cross-tenant | 403 | Se mandó ?empresaId= en la query. | — | no | Quitar el parámetro. |
exp.ambiente-no-autorizado | 401 | No se pudo resolver el ambiente. | — | no | No debería ocurrir con una API key vigente. Soporte. |
exp.sin-contexto | 401 | No se pudo resolver la empresa de la petición. | — | no | Ídem. |
Y además: emisor.permiso-tipo-no-autorizado (§15.1) cuando falta dte:emit:{110|111|112}.
La familia exportacion.*
Son las reglas del formato oficial del SII para documentos de exportación: hoy son 109. Todas responden 400 sin consumir folio, y el code nombra exactamente la regla que falló, con el campo en field. Se agrupan por bloque del documento:
| Prefijo | Cubre |
|---|---|
exportacion.aduana.* y exportacion.aduana-requerida | Bloque de aduana: modalidad y cláusula de venta, país de recepción y de destino, vía de transporte, puertos de embarque y desembarque, transportista, flete, seguro, pesos y tara. |
exportacion.detalle.*, exportacion.detalles-* | Líneas: cantidad, precio, código arancelario, descuentos y recargos, unidad de medida, indicador de exención, cantidad y unidad de referencia. |
exportacion.receptor.*, exportacion.receptor-requerido | Receptor extranjero: nombre, giro, dirección, ciudad, nacionalidad, número de identificación, correo. |
exportacion.referencia.*, exportacion.referencias-maximo, exportacion.referencia-110-faltante*, exportacion.dus-duplicado | Referencias: tipo, folio, fecha, razón y código de referencia; DUS obligatorio y sin duplicar. |
exportacion.moneda.*, exportacion.moneda-requerida | Moneda y tasa de cambio. |
exportacion.bulto.*, exportacion.bultos-maximo | Bultos: código, cantidad, marcas, sellos, contenedor. |
exportacion.comision.*, exportacion.comisiones-maximo | Comisiones y otros cargos. |
exportacion.dr-global.*, exportacion.dr-global-maximo | Descuentos y recargos globales. |
exportacion.fecha-emision-*, exportacion.ind-servicio-invalido, exportacion.ambiente-* | Cabecera: fecha de emisión, indicador de servicio, ambiente. |
No hace falta programar contra cada uno: todos son 400 de validación, ninguno consume folio, y ninguno se arregla reintentando. Lo accionable está en field y en title.
15.18 Resumen: qué reintentar y qué no
| Situación | ¿Reintentar? |
|---|---|
| 400 y 422 de validación | No. El request está mal; reintentar da lo mismo. |
| 401 y 403 | No. Es un problema de credencial, de scope o de configuración de la empresa. |
| 404 | No. |
409 de folios (emisor.sin-folios, emisor.sin-folios-nc 400) | No hasta cargar un CAF. |
409 emisor.acreditacion-en-curso, emisor.sin-folios-nc 409 | Sí, con backoff. |
422 caf.sin-autorizacion | No. Requiere gestión ante el SII. |
403 descarga.token-invalido / descarga.token-vencido | No. El enlace no se arregla reintentando: hay que generar uno nuevo con GET /dte/{id}. |
| 429 | Sí, respetando Retry-After. Ver §17. |
| 500, 503, 504 y timeouts de red | Sí, con backoff exponencial y siempre con el mismo transactionId. |
202 con ticketId | No es un error. El documento ya está validado y encolado. Consultá el ticket; no reemitas sin transactionId o vas a quemar otro folio. |
15.19 Códigos que no existen
Ninguno de los códigos de esta tabla lo devuelve la API. Aparecen en documentación vieja o en ejemplos de terceros; si tu código tiene una rama para alguno, esa rama no se ejecuta nunca.
| Código que no existe | Qué devuelve la API en su lugar |
|---|---|
api-key.scope-insuficiente | permiso.requerido (y sus dos variantes de §15.1). |
api-key.invalida | Todo 401 llega como api-key.ausente, sin distinguir el motivo. |
caf.agotado | emisor.sin-folios (409) o caf.sin-autorizacion (422). No hay ningún caf.agotado: la falta de folios no viaja con ese nombre. |
emisor.receptor-rut-requerido / emisor.receptor-rut-invalido | emisor.rut-receptor-invalido (400). Existen en el código fuente pero son inalcanzables desde POST /dte (§15.5). |
15.20 Qué reportar cuando algo no calza
Como traceId hoy solo viaja en las respuestas 500, para cualquier otro error hay que mandar otros datos. La lista completa está en Soporte.
16. Idempotencia y reintentos
Un timeout de red no te dice si el documento se emitió o no. Sin idempotencia, reintentar duplica documentos y quema folios. Hay tres mecanismos, uno por operación:
Emisión: transactionId
Generá un UUID antes de la primera llamada, guardalo junto a tu orden/factura interna, y mandalo en el cuerpo:
{ "transactionId": "a3bb189e-8bf9-3888-9912-ace4e6543002", "tipoDte": 33, "...": "..." }Si reintentás con el mismo valor, el servidor devuelve el documento ya emitido en vez de emitir uno nuevo. No consume otro folio. Y si la primera llamada devolvió un 202, reintentar con el mismo transactionId devuelve el mismo ticketId: no encola un segundo documento.
La deduplicación corre después de las validaciones: un cuerpo inválido devuelve 400 aunque repita un transactionId ya usado.
transactionIdes opcional pero prácticamente obligatorio en producción. Sin él, un timeout te deja sin forma de saber si reintentar es seguro. Y es lo único que te protege si tratás por error un202como un fallo y reemitís.
Anulación: automática, por documento
No necesita transactionId. La clave es el {id} del documento: un documento se anula a lo sumo una vez, así que reintentar el mismo POST /{id}/anular devuelve siempre la misma NC. Ver §11.
Nota de crédito parcial: transactionId, y acá sí hace falta
Un mismo documento puede recibir varias notas parciales legítimas, incluso idénticas. El servidor no puede distinguir un reintento de una devolución nueva. Mandá tu propio transactionId. Ver §12.3.
17. Rate limiting y límites duros
Rate limit
Dos capas, ambas con ventana deslizante de 60 segundos, y se aplican las dos a la vez:
| Capa | Límite | Partición |
|---|---|---|
| Por credencial | 120 requests / 60 s | Prefijo de la API Key presentada |
| Global por IP | 300 requests / 60 s | IP de origen |
Al exceder cualquiera de las dos:
HTTP/1.1 429 Too Many Requests
Retry-After: 15
Content-Type: application/json
{"code":"api-publica.rate-limit","title":"Demasiadas solicitudes para esta API key. Reintenta en unos segundos.","status":429}El header Retry-After siempre viene (en segundos). Respetalo: reintentar antes solo gasta cuota.
Consecuencias prácticas:
- El límite por IP no es un simple respaldo del otro: está siempre activo. Si tu ERP multiempresa usa 5 keys desde el mismo servidor, no tenés 5 × 120 = 600 requests/min: topás a 300 por la IP.
- El polling de estado y el de tickets entran en la misma cuota que la emisión. Con backoff (5/10/20/40/60 s) un documento consume ~5 requests; en loop cerrado consume el presupuesto de una key entera.
- Las descargas firmadas de §10 también consumen cuota, pero como no llevan API Key se cuentan por IP del visitante: los compradores que abren sus documentos no gastan el presupuesto de tu integración.
GET /healthno consume cuota. Usalo para monitores.- Si necesitás más throughput, hablalo con soporte antes de subir a producción — el límite es configurable por despliegue, no por cliente.
Límites duros del documento (los pone el SII, no nosotros)
Cantidades:
| Qué | Límite |
|---|---|
| Líneas de detalle | 60 |
| Referencias | 40 |
| Impuestos adicionales | 20 |
| Descuentos/recargos globales | 20 |
| Actecos declarados | 4 |
| Bultos (exportación) | 10 |
Largos de texto — exceder cualquiera devuelve 400 antes de reservar folio. La única excepción es el motivo de anulación y de nota de crédito, que se trunca en silencio:
| Campo | Largo | Código de error |
|---|---|---|
receptor.razonSocial | 100 | emisor.receptor-largo-excedido |
receptor.giro | 40 | emisor.receptor-largo-excedido |
receptor.direccion | 70 | emisor.receptor-largo-excedido |
receptor.comuna | 20 | emisor.receptor-largo-excedido |
receptor.ciudad | 20 | emisor.receptor-largo-excedido |
receptor.contacto | 80 | emisor.receptor-largo-excedido |
receptor.rutSolicita | 20 | emisor.receptor-largo-excedido |
detalles[].nombreItem | 80 | emisor.detalle-largo-excedido |
detalles[].descripcionItem | 1000 | emisor.detalle-largo-excedido |
detalles[].codigoItem | 35 | emisor.detalle-largo-excedido |
detalles[].tipoCodigo | 10 | emisor.detalle-largo-excedido |
detalles[].unidadMedida | 4 | emisor.detalle-largo-excedido |
referencias[].tipoDocRef | 3 | emisor.referencia-largo-excedido |
referencias[].folioRef | 18 | emisor.referencia-largo-excedido |
referencias[].razonRef | 90 | emisor.referencia-largo-excedido |
descuentosGlobales[].glosa | 45 | emisor.dr-glosa-excede-45 |
motivo de anulación / nota de crédito | 90 | (se trunca, no falla) |
observaciones | 500 | (no viaja al SII) |
Los tres que más rompen con datos comerciales chilenos perfectamente normales:
unidadMedida(4),receptor.giro(40) yreceptor.comuna(20). Mapealos o recortalos en tu ERP antes de llamar.
Timeouts
| Camino | Timeout del servidor |
|---|---|
| Emisión, consulta, XML, anulación, nota de crédito | 45 s |
| 60 s |
Configurá el timeout de tu cliente HTTP por encima de esos valores (p. ej. 60 s / 75 s). Un timeout de tu lado más corto que el nuestro es la receta clásica para creer que algo falló cuando en realidad se emitió — y por eso transactionId no es opcional.
⚠️ En el camino del PDF podés recibir un
504que no es nuestro: si la petición llega al techo de la plataforma, la respuesta es una página de error HTML, sincodenitraceId. No intentes parsearlo como JSON. Tratalo como resultado indeterminado y reintentá.
18. Webhooks
En vez de pollear GET /dte/{id} hasta que el estado se estabilice, podés recibir un POST firmado en tu servidor cada vez que pasa algo que te interesa: un documento cambia de estado ante el SII, llega un DTE de un proveedor, o se te están acabando los folios.
Estado: implementado, en autoservicio, contrato del cuerpo congelado.
Las suscripciones se administran desde el portal, en Portal → Webhooks (
/portal/webhooks), con el permisowebhook:configurar. Ya no hay que escribirle a soporte para dar de alta un webhook, ni para rotar el secreto, ni para ver por qué falló una entrega.Lo que no existe es administrarlas por código: ninguna ruta bajo
/api/public/v1/crea, edita, lista ni borra suscripciones, ni rota el secreto, ni lee el historial de entregas. El scopewebhook:configurarse puede tildear al crear una key, pero no hay ninguna ruta de esta API que lo consuma: la administración va por sesión del portal. Dar de alta un webhook es configuración, no runtime.El contrato del cuerpo y de la firma no cambia sin versionar el header. Podés escribir tu receptor contra esta sección y no tocarlo más.
Los webhooks son una optimización, no un reemplazo del polling. Si un aviso no llega —porque tu endpoint estuvo caído más de 31 horas, o porque la suscripción se apagó sola— el estado real sigue estando en GET /dte/{id}. Un receptor bien hecho trata el webhook como "llegó antes" y deja el polling como red.
Catálogo de eventos
Son 17 tipos y el catálogo es cerrado: un tipo que no esté acá no se publica. Los nombres tienen la forma dominio.hecho y van en pasado — el webhook informa algo que ya ocurrió.
Al suscribirte elegís una lista de tipos, o * para recibir todos, incluidos los que agreguemos más adelante. Agregar un tipo nuevo es retrocompatible (nadie lo recibe hasta que se suscribe); renombrar uno no lo es, y por eso no lo hacemos sin versionar.
Tres cosas que valen para todo el catálogo:
- El cuerpo tiene siempre la misma forma. Lo que cambia entre un tipo y otro es qué campos de
datosvienen con valor y cuáles vienennull. No hay tipos con estructura propia. datos.extraes una bolsa abierta y siempre está presente (aunque sea{}). Ahí viaja lo propio de los eventos que no hablan de un documento.- No asumas orden de llegada. Un
dte.enviadoy eldte.aceptadoque le sigue pueden llegar en cualquier orden. Decidí pordatos.estadoSii, no por el orden en que te llegaron.
Emisión
Tu propio documento, antes de que el SII opine. Sirven para numerar la venta sin esperar el ciclo tributario completo.
tipo | Cuándo se dispara | Qué trae en datos |
|---|---|---|
dte.emitido | El documento existe: folio asignado, XML firmado y persistido. Se publica antes de ir al SII. | documentoId, tipoDte, folio, montoTotal, fechaEmision, rutReceptor y razonSocialReceptor. estadoSii llega 0 (Pendiente) y trackId en null: todavía no hay sobre. En una NC/ND llega además la referencia (codRef + documentoReferenciado). |
dte.anulado | Se anuló un documento con una nota de crédito 61 (codRef 1) y el original quedó marcado como anulado. | El documento ORIGINAL, con estadoSii 5 (Anulado). codRef y documentoReferenciado llegan en null —ver el aviso de abajo— y el folio de la nota de crédito va en detalle, legible. |
⚠️
documentoReferenciadoapunta siempre en la misma dirección: es el documento al que referencia el DTE del evento. Nunca al revés. Una NC/ND trae ahí su documento original; undte.anulado—cuyo DTE es el original, que no referencia a nadie— lo trae ennull.Si leés el par
codRef+documentoReferenciadode forma genérica ("este evento corrige a aquel documento"), esta convención te da siempre el sentido correcto. La relación nota de crédito → original te llega igual, y estructurada, en los eventos de la propia nota.
Ciclo SII
El recorrido del documento ante el SII. Son los que reemplazan al polling de §9: en vez de repetir la consulta, esperás el aviso.
tipo | Cuándo se dispara | ¿Terminal? | Qué trae en datos |
|---|---|---|---|
dte.enviado | El sobre se subió al SII y el SII devolvió trackId. A partir de acá conviene esperar el estado final en vez de pollear. | No | trackId ya con valor y estadoSii 1 (Enviado). |
dte.aceptado | El SII aceptó el documento. | Sí | estadoSii 2 y detalle con lo que respondió el SII. |
dte.aceptado_con_reparos | El SII lo aceptó pero observó algo. Es válido tributariamente: el documento existe y tiene folio. Va aparte de dte.aceptado para que puedas revisarlo sin mirar el detalle de cada aceptado. | Sí | estadoSii 2 (Aceptado) y detalle con el reparo. ⚠️ No es 4: el documento queda Aceptado en la base y el evento manda el mismo estado, para que no veas dos verdades. Lo que distingue el reparo es el tipo del evento, no estadoSii — si ramificás por estadoSii === 4 nunca entrás a esa rama. |
dte.rechazado | El SII rechazó el documento. | Sí | estadoSii 3 y detalle con el motivo del rechazo. |
dte.error_envio | El sobre no se pudo subir al SII (sin token, error HTTP, timeout). No es terminal: el documento sigue emitido y el envío se reintenta solo. | No | estadoSii en null, trackId en null y detalle con el error del intento. ⚠️ No es 6: el documento sigue emitido y su estado no cambió, así que el evento no afirma ninguno. Ramificá por el tipo del evento. Ver Limitación conocida. |
En los cuatro tipos que hablan de una NC o una ND, codRef y documentoReferenciado vienen poblados desde las líneas de referencia del documento. Sin codRef no podés distinguir "NC 61 aceptada porque anularon la factura" de "NC 61 aceptada porque devolvieron tres unidades". Si tu ERP registra devoluciones, es el campo que te importa.
Recepción de proveedores
Documentos que otros te emiten a vos, y los acuses que respondés. Los publica el módulo de recepción; no tienen nada que ver con tus emisiones.
tipo | Cuándo se dispara | Qué trae en datos |
|---|---|---|
recepcion.documento_recibido | Llegó un DTE de un proveedor y quedó persistido, después del dedupe. | tipoDte, folio, montoTotal y fechaEmision del documento del proveedor. estadoSii y trackId en null: el ciclo ante el SII es del emisor, no tuyo. |
recepcion.documento_aceptado | Se envió el acuse comercial de aceptación del documento del proveedor. | El documento del proveedor que se aceptó. |
recepcion.documento_rechazado | Se envió el acuse comercial de rechazo. | El documento del proveedor, con el motivo en detalle. |
recepcion.documento_reclamado | Se reclamó el documento dentro de los 8 días hábiles. Todavía no se publica — ver el aviso de Eventos declarados que aún no se publican. | — |
recepcion.acuse_tacito | Venció el plazo legal sin acuse y quedó registrada la aceptación tácita. Lo dispara un proceso programado, no una acción tuya. | El documento del proveedor que quedó aceptado por vencimiento. |
recepcion.recibo_mercaderia | Se registró el recibo de mercaderías de la Ley 19.983. | El documento del proveedor sobre el que se firmó el recibo. |
⚠️ Acá los RUT se invierten, y es el error más común de toda la sección. En los eventos
recepcion.*,datos.rutEmisores el proveedor ydatos.rutReceptorsos vos —datos.razonSocialReceptores tu razón social, no la del proveedor—. En emisión y en ciclo SII es al revés.empresaRut, en cambio, es siempre el tuyo: identifica de quién es la suscripción, no quién emitió el documento.⚠️
datos.documentoIdde un eventorecepcion.*no sirve enGET /dte/{id}. Es el identificador del documento recibido, que vive en el módulo de recepción; esa ruta consulta tus emisiones y va a responder404. Para cruzarlo contra tus registros usá la ternarutEmisor+tipoDte+folio.
Operativos
Avisos de infraestructura tributaria: lo que se te va a acabar antes de dejarte sin emitir. No hablan de un documento tuyo, así que casi todo el contenido útil viaja en datos.extra.
tipo | Cuándo se dispara | Qué trae en datos |
|---|---|---|
caf.folios_bajos | Quedan pocos folios disponibles para un tipo de DTE. El umbral es configurable, y un barrido diario lo evalúa. | tipoDte del CAF y extra con foliosDisponibles, tipoDte y umbral. folio, trackId y estadoSii en null. |
caf.por_vencer | Un CAF vence pronto: hay que reobtener folios antes de quedarse sin timbrar. El aviso es por CAF, no por tipo. | tipoDte y extra con folioDesde, folioHasta, fechaVencimiento (AAAA-MM-DD), diasRestantes y cafId. |
certificado.por_vencer | El certificado digital de la empresa vence pronto. Todavía no se publica — ver el aviso de abajo. | — |
⚠️
datos.documentoIdde un eventocaf.*no es un documento. Es un identificador sintético que usamos para no avisarte lo mismo dos veces el mismo día. No lo consultes enGET /dte/{id}: no existe. El resto de los campos de documento (folio,trackId,estadoSii,montoTotal,fechaEmision,rutEmisor,rutReceptor) llegan ennull.El dedupe de
caf.folios_bajoses por tramo de severidad y por día: si por la mañana quedaban 12 folios y por la tarde quedan 0, te llegan los dos avisos. Lo que no te llega es el mismo tramo repetido dentro del día.
Prueba
tipo | Cuándo se dispara | Qué trae en datos |
|---|---|---|
webhook.prueba | Lo dispara el botón "Probar" del portal contra una suscripción puntual. | Datos de relleno con la forma completa del payload, y extra con {"prueba":true}. |
webhook.pruebano se suscribe: se dispara. Es el único tipo del catálogo que no aparece en el selector de eventos —si lo declarás en la lista de una suscripción, el alta responde400—. Y cuando probás una suscripción, esa suscripción lo recibe siempre, aunque su lista de eventos sea sólodte.aceptado: si no fuera así, el botón "Probar" no serviría justo en las suscripciones acotadas, que son las que más ganas dan de probar. Tampoco se deduplica: cada prueba llega como un evento nuevo.
Eventos declarados que aún no se publican
Regla de este documento: acá sólo se describe lo que existe. Dos tipos del catálogo se pueden elegir en el selector, pero hoy no los publica nadie. Si te suscribís a ellos no vas a recibir nada, y no hay ningún error que te lo avise.
tipo | Estado real |
|---|---|
certificado.por_vencer | Sin productor. El tipo existe en el catálogo y se puede suscribir, pero ningún servicio lo publica todavía: le tocaría al servicio que conoce la vigencia del certificado. Mientras tanto, la fecha de vencimiento del PFX se mira en el portal. |
recepcion.documento_reclamado | Sin punto de publicación. La constante está declarada en el módulo de recepción, pero el reclamo al SII no pasa hoy por un camino que publique el evento. |
Los dos van a empezar a llegar sin aviso previo el día que se implementen —que un tipo empiece a publicarse es retrocompatible—, así que si ya los tenés suscritos, tu receptor debería ignorarlos sin romperse en vez de asumir que nunca llegan.
Cuerpo del POST
Content-Type: application/json. Este es el cuerpo exacto que se firma.
{
"id": "0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11",
"tipo": "dte.aceptado",
"version": 1,
"ocurridoEn": "2026-08-26T12:30:00Z",
"ambiente": "Produccion",
"empresaRut": "76665850-4",
"datos": {
"documentoId": "8a1f5c30-77d2-4e19-b6c4-5d09e2f31a88",
"tipoDte": 33,
"folio": 1234,
"trackId": 987654321,
"estadoSii": 2,
"estadoSiiNombre": "Aceptado",
"detalle": "Aceptado por el SII",
"codRef": null,
"codRefNombre": null,
"documentoReferenciado": null,
"rutEmisor": "76665850-4",
"rutReceptor": "96790240-3",
"razonSocialReceptor": "Cliente Ejemplo S.A.",
"montoTotal": 119000,
"fechaEmision": "2026-08-26",
"extra": {}
}
}| Campo | Qué es |
|---|---|
id | Identificador del evento. Es estable entre reintentos: es la clave por la que tenés que deduplicar. Viaja también en X-Comges-Event-Id. |
tipo | Uno de los 17 tipos del catálogo. Viaja también en X-Comges-Event-Type, así que podés rutear sin parsear el cuerpo. |
version | Versión del formato del cuerpo. Hoy siempre 1. |
ocurridoEn | Momento real del hecho, en UTC (ISO 8601). No es el momento del intento de entrega. |
ambiente | Certificacion o Produccion. Una suscripción vive en un solo ambiente. |
empresaRut | RUT de tu empresa, la dueña de la suscripción. En los eventos recepcion.* sigue siendo el tuyo, no el del proveedor. |
datos.documentoId | En emisión y ciclo SII, el mismo id que devolvió la emisión y que usás en GET /dte/{id}. En recepcion.* es el id del documento recibido (no consultable por esa ruta), y en caf.* es un ancla de deduplicación, no un documento. null si el evento no tiene sujeto. |
datos.tipoDte | Código SII del tipo, como número (33, 34, 39, 41, 52, 56, 61, 110, 111, 112). |
datos.folio | Folio del documento. |
datos.trackId | Identificador del sobre en el SII. null mientras no haya sobre subido. |
datos.estadoSii | Código numérico del estado (ver tabla). |
datos.estadoSiiNombre | El mismo estado en texto. |
datos.detalle | Texto legible: lo que respondió el SII, el motivo del error, o la descripción del aviso operativo. Puede venir null. |
datos.codRef | Solo cuando el DTE del evento referencia a otro (NC y ND): 1 anula el documento completo, 2 corrige texto, 3 corrige monto (devolución parcial). null en el resto. |
datos.codRefNombre | El mismo codRef en texto: Anula documento, Corrige texto, Corrige monto. |
datos.documentoReferenciado | El documento al que apunta el DTE del evento: { "tipoDte": "33", "folio": "1234" }. null cuando no referencia a nadie. Ojo con los tipos — ver el aviso de abajo. |
datos.rutEmisor | Quién emitió el documento. En emisión y ciclo SII sos vos; en recepcion.* es el proveedor. |
datos.rutReceptor | Quién lo recibe. En emisión es tu cliente; en recepcion.* sos vos. |
datos.razonSocialReceptor | Razón social del receptor, para no tener que ir a buscarla. |
datos.montoTotal | Monto total del documento, en la moneda del documento, como número. |
datos.fechaEmision | Fecha de emisión, AAAA-MM-DD. Es una fecha de negocio, sin hora ni zona: no la parsees como instante o vas a mostrar el día anterior. |
datos.extra | Diccionario abierto con lo propio de cada evento que no habla de un documento (caf.*, certificado.*, webhook.prueba). Llega {} cuando no hay nada. Leelo defensivamente: puede crecer sin previo aviso. |
⚠️ Dentro de
documentoReferenciado,tipoDteyfolioson strings. Se serializan como{ "tipoDte": "33", "folio": "1234" }, entre comillas, mientras quedatos.tipoDteydatos.foliodel nivel de arriba son números. La asimetría es real y está congelada: unevento.datos.documentoReferenciado.tipoDte === 33en JavaScript dafalse. Compará como texto, o convertí explícitamente.
Códigos de estadoSii:
| Código | Nombre |
|---|---|
0 | Pendiente |
1 | Enviado |
2 | Aceptado |
3 | Rechazado |
4 | AceptadoConReparos |
5 | Anulado |
6 | ErrorEnvio |
versionsigue en1y los campos nuevos son aditivos.rutEmisor,rutReceptor,razonSocialReceptor,montoTotal,fechaEmisionyextrase agregaron sin subir la versión: agregar claves a un objeto JSON no rompe a ningún consumidor, y subirla habría obligado a cada integrador a revisar su código para no ganar nada. Tu parser tiene que ignorar lo que no conoce en vez de fallar, porque vamos a seguir agregando.
Ejemplo: un evento de recepción
El mismo cuerpo, con los RUT invertidos. Acá rutEmisor es el proveedor que te facturó y rutReceptor sos vos:
{
"id": "c81d4a90-5f27-4b6e-9d33-0a71e5c8b402",
"tipo": "recepcion.documento_recibido",
"version": 1,
"ocurridoEn": "2026-08-26T09:14:05Z",
"ambiente": "Produccion",
"empresaRut": "76665850-4",
"datos": {
"documentoId": "1d9b7f44-2c60-4a8e-b0f1-6e3d5a90c7b2",
"tipoDte": 33,
"folio": 55120,
"trackId": null,
"estadoSii": null,
"estadoSiiNombre": null,
"detalle": "Documento recibido de un proveedor",
"codRef": null,
"codRefNombre": null,
"documentoReferenciado": null,
"rutEmisor": "96790240-3",
"rutReceptor": "76665850-4",
"razonSocialReceptor": "Mi Empresa SpA",
"montoTotal": 476000,
"fechaEmision": "2026-08-25",
"extra": {}
}
}empresaRut y rutReceptor son el mismo RUT —el tuyo—, y razonSocialReceptor es tu razón social. Si tu integración da por hecho que rutEmisor es siempre tu empresa, este evento te va a registrar la compra como si fuera una venta.
Ejemplo: un aviso operativo y el uso de extra
Los eventos que no hablan de un documento dejan casi todo el bloque de documento en null y ponen lo suyo en extra:
{
"id": "2b6f0c81-9e34-4d17-8a52-c470b1f9e335",
"tipo": "caf.folios_bajos",
"version": 1,
"ocurridoEn": "2026-08-26T12:20:00Z",
"ambiente": "Produccion",
"empresaRut": "76665850-4",
"datos": {
"documentoId": "6f1c0a55-2d38-4b71-9e04-8a5c3d2f7b19",
"tipoDte": 33,
"folio": null,
"trackId": null,
"estadoSii": null,
"estadoSiiNombre": null,
"detalle": "Quedan 12 folio(s) disponibles para el tipo 33 (umbral de aviso: 50).",
"codRef": null,
"codRefNombre": null,
"documentoReferenciado": null,
"rutEmisor": null,
"rutReceptor": null,
"razonSocialReceptor": null,
"montoTotal": null,
"fechaEmision": null,
"extra": {
"foliosDisponibles": 12,
"tipoDte": 33,
"umbral": 50
}
}
}Las claves de extra dependen del tipo: caf.folios_bajos manda foliosDisponibles, tipoDte y umbral; caf.por_vencer manda folioDesde, folioHasta, fechaVencimiento, diasRestantes, tipoDte y cafId. Leé extra con ?. o su equivalente: es el único bloque del payload que puede sumar claves nuevas sin que cambie nada más.
Headers
| Header | Contenido |
|---|---|
X-Comges-Signature | t=<unix>,v1=<hex minúscula de 64 caracteres> — ver abajo. |
X-Comges-Timestamp | Segundos Unix UTC. Es el mismo valor que el t= de la firma, y entra en el material firmado. |
X-Comges-Event-Id | Id del evento. Estable entre reintentos → deduplicá por acá. |
X-Comges-Event-Type | El tipo del evento, para rutear sin parsear el cuerpo. |
X-Comges-Delivery-Id | Id de la entrega. Cambia por suscripción, no por intento. |
X-Comges-Delivery-Attempt | Número de intento, 1 en la primera entrega. |
A esos se suman los headers extra que hayas declarado en la suscripción, si los hay. Ninguno de ellos puede pisar los X-Comges-* ni el Content-Type.
Verificar la firma
v1 = HMAC_SHA256(secreto, "{timestamp}.{cuerpoCrudo}")Tres reglas que no se pueden saltar:
- Usá los bytes crudos del request, no el JSON re-serializado. Cualquier cambio de espaciado, de orden de claves o de escapado rompe la comparación.
- El timestamp entra en el material firmado. Sin él, cualquiera que capture una entrega podría reproducirla para siempre. Con él, podés rechazar todo lo que tenga más de N minutos (5 es un valor razonable) y la firma sólo sirve dentro de esa ventana.
- Compará en tiempo constante (
crypto.timingSafeEqual,hmac.compare_digest,hash_equals). Un===sobre strings filtra información por el tiempo de respuesta.
Es el mismo esquema que usa Stripe, así que si ya tenés código para eso, sirve tal cual.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRETO = process.env.COMGES_WEBHOOK_SECRET
// Importante: body crudo, NO express.json()
app.post('/webhooks/comges', express.raw({ type: 'application/json' }), (req, res) => {
const firma = req.header('X-Comges-Signature') ?? ''
const partes = Object.fromEntries(
firma.split(',').map((p) => p.split('=', 2)),
)
const t = Number(partes.t)
const recibida = partes.v1 ?? ''
// 1) Ventana de tolerancia: descarta reproducciones viejas.
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) {
return res.status(400).send('timestamp fuera de ventana')
}
// 2) HMAC sobre "{t}.{cuerpoCrudo}".
const esperada = crypto
.createHmac('sha256', SECRETO)
.update(`${t}.${req.body.toString('utf8')}`)
.digest('hex')
// 3) Comparación en tiempo constante.
const a = Buffer.from(esperada, 'utf8')
const b = Buffer.from(recibida, 'utf8')
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('firma inválida')
}
const evento = JSON.parse(req.body.toString('utf8'))
// 4) Deduplicar: la entrega es at-least-once.
if (yaProcesado(evento.id)) return res.sendStatus(200)
// 5) Responder rápido y procesar aparte.
encolarParaProcesar(evento)
res.sendStatus(200)
})import hmac, hashlib, time
def verificar(secreto: str, cuerpo_crudo: bytes, header_firma: str, tolerancia: int = 300) -> bool:
partes = dict(p.split("=", 1) for p in header_firma.split(","))
t = int(partes["t"])
if abs(time.time() - t) > tolerancia:
return False
esperada = hmac.new(
secreto.encode("utf-8"),
f"{t}.".encode("utf-8") + cuerpo_crudo,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(esperada, partes.get("v1", ""))Vector de prueba fijo
Para que puedas verificar tu implementación sin recibir nada, acá va un caso cerrado: si tu código produce este mismo hexadecimal, tu verificación está bien. Si no, el problema es tuyo y no de la entrega.
| Dato | Valor |
|---|---|
| Secreto | whsec_documentacion_vector_de_prueba |
X-Comges-Timestamp | 1774704312 |
| Cuerpo | 568 bytes UTF-8, una sola línea, sin salto final (es el bloque de abajo) |
| Material firmado | 1774704312. + el cuerpo, concatenados sin nada en medio |
v1 esperado | 66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd |
| Header completo | X-Comges-Signature: t=1774704312,v1=66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd |
El cuerpo es el mismo ejemplo dte.aceptado de más arriba, pero tal como viaja: compacto, sin saltos de línea y sin espacios entre claves. Copialo entero, en una sola línea:
{"id":"0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11","tipo":"dte.aceptado","version":1,"ocurridoEn":"2026-08-26T12:30:00Z","ambiente":"Produccion","empresaRut":"76665850-4","datos":{"documentoId":"8a1f5c30-77d2-4e19-b6c4-5d09e2f31a88","tipoDte":33,"folio":1234,"trackId":987654321,"estadoSii":2,"estadoSiiNombre":"Aceptado","detalle":"Aceptado por el SII","codRef":null,"codRefNombre":null,"documentoReferenciado":null,"rutEmisor":"76665850-4","rutReceptor":"96790240-3","razonSocialReceptor":"Cliente Ejemplo S.A.","montoTotal":119000,"fechaEmision":"2026-08-26","extra":{}}}⚠️ Byte a byte. Si tu editor le agrega un salto de línea al final, o si reindentás el JSON antes de firmarlo, el digest cambia por completo y no vas a poder reproducir el valor de arriba. Es exactamente el error que después aparece en producción como "la firma nunca coincide".
Reproducilo en la consola:
SECRETO='whsec_documentacion_vector_de_prueba'
TIMESTAMP='1774704312'
CUERPO='{"id":"0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11","tipo":"dte.aceptado","version":1,"ocurridoEn":"2026-08-26T12:30:00Z","ambiente":"Produccion","empresaRut":"76665850-4","datos":{"documentoId":"8a1f5c30-77d2-4e19-b6c4-5d09e2f31a88","tipoDte":33,"folio":1234,"trackId":987654321,"estadoSii":2,"estadoSiiNombre":"Aceptado","detalle":"Aceptado por el SII","codRef":null,"codRefNombre":null,"documentoReferenciado":null,"rutEmisor":"76665850-4","rutReceptor":"96790240-3","razonSocialReceptor":"Cliente Ejemplo S.A.","montoTotal":119000,"fechaEmision":"2026-08-26","extra":{}}}'
printf '%s' "$TIMESTAMP.$CUERPO" | openssl dgst -sha256 -hmac "$SECRETO" -r
# 66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd *stdinO en Python, contra la misma función que vas a usar en tu receptor:
import hmac, hashlib
SECRETO = "whsec_documentacion_vector_de_prueba"
TIMESTAMP = 1774704312
CUERPO = b'{"id":"0f4c9d2e-3b71-4a55-9c18-2f6e0b7d4a11","tipo":"dte.aceptado","version":1,"ocurridoEn":"2026-08-26T12:30:00Z","ambiente":"Produccion","empresaRut":"76665850-4","datos":{"documentoId":"8a1f5c30-77d2-4e19-b6c4-5d09e2f31a88","tipoDte":33,"folio":1234,"trackId":987654321,"estadoSii":2,"estadoSiiNombre":"Aceptado","detalle":"Aceptado por el SII","codRef":null,"codRefNombre":null,"documentoReferenciado":null,"rutEmisor":"76665850-4","rutReceptor":"96790240-3","razonSocialReceptor":"Cliente Ejemplo S.A.","montoTotal":119000,"fechaEmision":"2026-08-26","extra":{}}}'
v1 = hmac.new(
SECRETO.encode("utf-8"),
f"{TIMESTAMP}.".encode("utf-8") + CUERPO,
hashlib.sha256,
).hexdigest()
assert v1 == "66188afa87cae22d8c7e2f642da1df88a459ade615cbf6c8e7303b2ab27165fd"Deduplicación: la entrega es at-least-once
El mismo X-Comges-Event-Id puede llegar más de una vez —un reintento después de que tu respuesta se perdió en la red, por ejemplo—. Guardá los ids procesados y devolvé 200 sin volver a hacer nada si ya lo viste.
Tampoco asumas orden. Si un documento pasa a dte.enviado y enseguida a dte.aceptado, los dos avisos pueden llegar en cualquier orden. Usá datos.estadoSii para decidir, no el orden de llegada.
Qué responder
| Tu respuesta | Qué hacemos |
|---|---|
2xx | Entrega exitosa. No hay más intentos. |
408 o 429 | Reintentamos según la escalera. Son las dos únicas excepciones dentro de los 4xx. |
Cualquier otro 4xx | Agotamos la entrega de inmediato. Un 404 o un 401 no se arregla repitiendo el mismo POST; no tiene sentido castigarte 31 horas. |
5xx, timeout o error de red | Reintentamos según la escalera. |
Respondé rápido y procesá en segundo plano: el POST tiene un timeout de 10 segundos. Si tardás más, el intento cuenta como fallido y te vas a comer un duplicado cuando reintentemos.
Reintentos
7 intentos en total: la entrega inicial más 6 reintentos.
| Intento | Espera desde el anterior | Acumulado |
|---|---|---|
| 1 | Inmediato | — |
| 2 | 1 minuto | 1 min |
| 3 | 5 minutos | 6 min |
| 4 | 15 minutos | 21 min |
| 5 | 1 hora | 1 h 21 min |
| 6 | 6 horas | 7 h 21 min |
| 7 | 24 horas | ≈ 31 horas |
Después del séptimo la entrega queda agotada: no se vuelve a intentar sola, pero se puede reintentar a mano desde el portal, de a una o en lote.
La escalera sólo corre para lo que tiene sentido reintentar. Un 5xx, un timeout o un error de red la recorren entera. Un 4xx permanente —400, 401, 403, 404— agota la entrega en el primer intento. Las dos excepciones son 408 y 429, que sí escalan: los dos dicen "ahora no, probá después".
⚠️ 20 fallos consecutivos apagan la suscripción. Son unos 3 eventos agotando la escalera entera; a esa altura el endpoint no está "con un problemita". La suscripción queda deshabilitada, con el motivo y la fecha registrados, y dejamos de intentar.
Se reactiva desde el portal, y al reactivarla el contador de fallos consecutivos vuelve a cero, así que no se apaga de nuevo con el primer tropiezo. El orden que funciona: arreglás el endpoint → lo probás con el botón "Probar" → reactivás la suscripción → reintentás en lote lo que quedó agotado. Mientras tanto, el estado real sigue disponible por polling.
Evidencia: qué queda archivado de cada intento
De cada intento —no sólo del último— se archivan dos JSON que podés ver y descargar desde el portal:
- El request: la URL, el método, todos los headers (incluida la firma completa) y el cuerpo exacto que salió. Es el material con el que se depura una verificación HMAC que no cierra: tenés el cuerpo crudo y el
v1que calculamos nosotros. El secreto nunca se guarda ahí. - La respuesta: status, headers y cuerpo de lo que devolviste. El cuerpo se guarda siempre, también en los
2xx, con un tope de 64 KB (si te pasás, se recorta y queda marcado como truncado).
La latencia que ves medida es la de tu endpoint: se corta al recibir tus headers de respuesta, sin contar la lectura del cuerpo ni nuestro trabajo posterior. La evidencia se conserva un año.
Administrar la suscripción desde el portal
Todo esto lo hace el cliente solo, en Portal → Webhooks (/portal/webhooks), con el permiso webhook:configurar:
- Crear y editar suscripciones: URL
httpsde destino, ambiente y descripción. - Elegir qué eventos recibir, del catálogo completo agrupado por categoría, o
*para todos. - Filtrar por tipo de DTE (ver abajo).
- Agregar headers extra para atravesar tu gateway (ver abajo).
- Generar o fijar tu propio secreto, y rotarlo.
- Pausar, reactivar o eliminar una suscripción.
- Probar el endpoint: dispara un
webhook.pruebacontra ese destino y te devuelve el resultado del intento —status, milisegundos, error y el cuerpo que contestaste— en el momento, sin esperar ningún backoff. - Ver el historial de entregas con la evidencia de cada intento.
- Reintentar, de a una o en lote (tope de 500 entregas por corrida).
- Ver métricas: entregadas, fallidas y pendientes, tasa de éxito y latencia de tu endpoint, por período, por tipo de evento y por suscripción.
Filtro por tipo de DTE
Además de elegir qué eventos querés, podés acotar sobre qué documentos. El filtro es una lista de códigos SII —33,34,61, por ejemplo—; vacío significa todos.
- Se aplica además de la lista de eventos, no en su lugar: primero se mira si el tipo de evento te interesa, después si
datos.tipoDtepasa el filtro. - Si el evento no trae
tipoDte, el filtro no aplica y el evento se entrega igual. Es el caso decertificado.por_vencer: no hay documento que filtrar, y silenciar un aviso operativo por tener un filtro de documentos puesto sería el peor resultado posible. - El caso que resuelve: un ERP que sólo procesa facturas no tiene por qué recibir —ni verificar, ni encolar, ni descartar— cada boleta de un punto de venta con miles de emisiones por día.
Headers extra
Si tu endpoint vive detrás de un gateway que exige su propia autenticación, podés declarar headers fijos que se agregan a todas las entregas de esa suscripción —el caso típico es un Authorization: Bearer …—.
- No pueden pisar ningún header
X-Comges-*ni elContent-Type. Al guardar, uno de esos nombres se rechaza con400; al entregar, se ignora. Si pudieras sobrescribirX-Comges-Signatureestarías anulando tu propia verificación, y si pudieras sobrescribirContent-Typeromperías el parseo del cuerpo. - Máximo 10 headers, con nombres alfanuméricos y guiones (
[A-Za-z0-9-]), y 1000 caracteres de tope en total. - Van en claro en el request y quedan visibles en el historial de entregas, igual que el resto de los headers.
- No reemplazan la firma. Sirven para atravesar tu infraestructura, no para autenticarnos. Lo que prueba que el
POSTsalió de nosotros y que el cuerpo no se tocó esX-Comges-Signature: verificala siempre, aunque tu gateway ya haya validado tu propio header.
El secreto de firma
- Autogenerado o propio. El portal lo genera con prefijo
whsec_, o traés el tuyo: mínimo 16 caracteres. Si ya manejás secretos en un vault, poné el que ya tenés. - Se muestra una sola vez, al crearlo o al rotarlo. Después siempre viaja enmascarado (
whsec_••••abcd). Guardalo apenas lo recibís: no hay forma de volver a verlo, sólo de rotarlo. - La rotación no tiene ventana de gracia. No conviven el secreto viejo y el nuevo: la entrega siguiente ya va firmada con el nuevo. Actualizá tu receptor antes de rotar, no después —o dejalo aceptando los dos durante el despliegue y sacá el viejo cuando la rotación esté hecha.
- El secreto nunca viaja en el request del webhook ni queda guardado en la evidencia. Sólo lo tenemos nosotros para firmar y vos para verificar.
Requisitos del endpoint de destino
La URL la elegís vos y la llama nuestro backend, así que la validamos con criterio estricto — al registrarla y otra vez antes de cada POST, porque el DNS puede cambiar en el medio.
httpsobligatorio. No aceptamoshttp.- Sin direcciones internas: nada de loopback, link-local, multicast ni rangos privados (RFC 1918, CGNAT, IPv6 ULA). Si tu host resuelve a una de esas, la entrega no sale.
- Sin credenciales embebidas en la URL (
https://usuario:clave@host). Autenticá con la firma HMAC, o con un header extra si tu gateway lo exige. - No seguimos redirects. Un
302no se sigue: devolvé2xxen la URL registrada.
Limitación conocida
dte.error_envio se notifica una sola vez por documento. Si el mismo sobre falla al subir otra vez, no volvés a recibir el aviso. Es deliberado —evita spamear mientras un sobre reintenta solo—, pero significa que ves el primer fallo y no los siguientes. Como dte.error_envio no es terminal, el documento va a terminar en un estado final igual y ese sí te llega.
19. Ejemplo end-to-end con curl
Copiable y ejecutable. Emite una factura 33, resuelve el 202 si la emisión queda esperando folio, consulta el estado hasta que el SII se pronuncia, descarga XML y PDF, y anula el documento dos veces para mostrar que la anulación es idempotente.
Tres cosas que hace bien y que conviene copiar a tu integración:
- Ramifica por el código HTTP, no por la presencia de campos. Con un
202,jq -r '.id'devuelve la cadena"null"y el script seguiría pidiendo/dte/null/xmlsin darse cuenta. - Resuelve el
ticketIddel202leyéndolo del cuerpo. Seguir el headerLocationno sirve: no llega (§8). - Comprueba el estado antes de anular. Un
emisor.estado-no-anulablees un resultado esperable, no un incidente para soporte.
Requiere curl, jq y uuidgen.
#!/usr/bin/env bash
# Ejemplo end-to-end de la API pública Comges DTE.
# Requisitos: curl, jq, uuidgen.
set -euo pipefail
BASE="https://api-publica.dtecomges.cl"
API_KEY="pk_test_XXXXXXXX_reemplazame"
CUERPO=$(mktemp)
trap 'rm -f "$CUERPO"' EXIT
# Hace la petición, deja el cuerpo en $CUERPO e imprime el código HTTP.
peticion() {
local metodo="$1" ruta="$2" json="${3:-}"
local args=(-sS -o "$CUERPO" -w '%{http_code}'
-X "$metodo" "$BASE$ruta"
-H "X-Api-Key: $API_KEY")
if [ -n "$json" ]; then
args+=(-H 'Content-Type: application/json' -d "$json")
fi
curl "${args[@]}"
}
abortar() {
echo "✗ $1" >&2
jq . "$CUERPO" >&2 2>/dev/null || cat "$CUERPO" >&2
exit 1
}
# transactionId: se genera UNA vez y se reusa en todos los reintentos.
TX=$(uuidgen | tr 'A-Z' 'a-z')
# ─── 1. Emitir una factura 33 ────────────────────────────────────────────────
echo "→ Emitiendo factura 33 (transactionId=$TX)"
BODY=$(cat <<JSON
{
"transactionId": "$TX",
"tipoDte": 33,
"receptor": {
"rut": "96790240-3",
"razonSocial": "Cliente Ejemplo S.A.",
"giro": "Comercio al por mayor",
"direccion": "Av. Providencia 1234",
"comuna": "Providencia"
},
"detalles": [
{
"nroLinea": 1,
"nombreItem": "Consultoría técnica",
"cantidad": 1,
"unidadMedida": "UN",
"precioUnitario": 1000000
}
]
}
JSON
)
STATUS=$(peticion POST "/api/public/v1/dte" "$BODY")
jq . "$CUERPO"
DOC_ID=""
case "$STATUS" in
201)
# Camino normal: había folio disponible, el documento ya está emitido.
DOC_ID=$(jq -r '.id // empty' "$CUERPO")
;;
202)
# No había folio: la emisión quedó encolada y el SII ya fue notificado.
# Le pasa a todo contribuyente nuevo, al que el SII autoriza folios de a pocos.
TICKET=$(jq -r '.ticketId // empty' "$CUERPO")
[ -n "$TICKET" ] || abortar "202 sin ticketId"
echo "→ Emisión encolada esperando folio. ticketId=$TICKET"
for espera in 5 10 20 30 60 60; do
sleep "$espera"
TSTATUS=$(peticion GET "/api/public/v1/dte/pendientes/$TICKET")
[ "$TSTATUS" = "200" ] || abortar "consulta del ticket devolvió HTTP $TSTATUS"
ESTADO_TICKET=$(jq -r '.estado' "$CUERPO")
echo "→ ticket estado=$ESTADO_TICKET (posición en cola: $(jq -r '.posicionEnCola' "$CUERPO"))"
case "$ESTADO_TICKET" in
Completado)
DOC_ID=$(jq -r '.documentoId // empty' "$CUERPO")
break
;;
Error|Cancelado)
abortar "el ticket terminó en $ESTADO_TICKET: $(jq -r '.codigoError // "sin código"' "$CUERPO")"
;;
esac
done
;;
*)
abortar "la emisión devolvió HTTP $STATUS"
;;
esac
# Sin id no hay nada más que hacer: cortar acá evita pedir /dte/null/xml.
[ -n "$DOC_ID" ] && [ "$DOC_ID" != "null" ] \
|| abortar "no se resolvió el id del documento (¿el ticket sigue en cola?)"
STATUS=$(peticion GET "/api/public/v1/dte/$DOC_ID")
[ "$STATUS" = "200" ] || abortar "consulta del documento devolvió HTTP $STATUS"
FOLIO=$(jq -r '.folio' "$CUERPO")
echo "→ Documento $DOC_ID, folio $FOLIO (el folio ya es definitivo)"
# ─── 2. Polling del estado en el SII (con backoff) ───────────────────────────
# Estados finales: Aceptado, AceptadoConReparos, Rechazado.
# 'Enviado' NO es final —el SII todavía no dio veredicto—, pero ya permite anular,
# así que corta el bucle para no seguir gastando cuota de rate limit.
ESTADO=""
for espera in 5 10 20 40 60; do
sleep "$espera"
STATUS=$(peticion GET "/api/public/v1/dte/$DOC_ID")
[ "$STATUS" = "200" ] || abortar "consulta de estado devolvió HTTP $STATUS"
ESTADO=$(jq -r '.estadoSii' "$CUERPO")
echo "→ estadoSii=$ESTADO"
case "$ESTADO" in
Enviado|Aceptado|AceptadoConReparos|Rechazado) break ;;
esac
done
if [ "$ESTADO" = "Rechazado" ]; then
echo "✗ El SII rechazó el documento: $(jq -r '.glosaSii // "sin glosa"' "$CUERPO")" >&2
fi
# ─── 3. Descargar XML firmado y PDF ──────────────────────────────────────────
STATUS=$(curl -sS -o "dte-$FOLIO.xml" -w '%{http_code}' \
"$BASE/api/public/v1/dte/$DOC_ID/xml" -H "X-Api-Key: $API_KEY")
[ "$STATUS" = "200" ] || { echo "✗ XML devolvió HTTP $STATUS" >&2; exit 1; }
STATUS=$(curl -sS -o "dte-$FOLIO.pdf" -w '%{http_code}' \
"$BASE/api/public/v1/dte/$DOC_ID/pdf?formato=carta" -H "X-Api-Key: $API_KEY")
[ "$STATUS" = "200" ] || { echo "✗ PDF devolvió HTTP $STATUS" >&2; exit 1; }
echo "→ Guardados dte-$FOLIO.xml y dte-$FOLIO.pdf"
# ─── 4. Anular — solo si el documento está en un estado anulable ─────────────
# Anulable: tipos 33, 34, 39, 41, 52 y 56, en estado Enviado, Aceptado o
# AceptadoConReparos, y con CAF vigente de tipo 61.
case "$ESTADO" in
Enviado|Aceptado|AceptadoConReparos) ;;
*)
echo "→ Estado '$ESTADO': el documento todavía no es anulable. Fin del ejemplo."
exit 0
;;
esac
echo "→ Anulando"
STATUS=$(peticion POST "/api/public/v1/dte/$DOC_ID/anular" '{"motivo":"Prueba de integración"}')
[ "$STATUS" = "201" ] || abortar "la anulación devolvió HTTP $STATUS"
jq . "$CUERPO"
NC_ID=$(jq -r '.notaCredito.id' "$CUERPO")
NC_FOLIO=$(jq -r '.notaCredito.folio' "$CUERPO")
echo "→ Nota de crédito $NC_ID folio $NC_FOLIO"
# ─── 5. Reintentar la anulación: devuelve LA MISMA NC, no emite otra ─────────
STATUS=$(peticion POST "/api/public/v1/dte/$DOC_ID/anular" '{"motivo":"Prueba de integración"}')
[ "$STATUS" = "201" ] || abortar "el reintento de anulación devolvió HTTP $STATUS"
NC_ID2=$(jq -r '.notaCredito.id' "$CUERPO")
if [ "$NC_ID" = "$NC_ID2" ]; then
echo "✓ Idempotente: misma NC ($NC_ID). No se quemó un segundo folio."
else
echo "✗ Se emitieron dos NC distintas — reportar a soporte." >&2
exit 1
fiPara producción: cambiar la key por una pk_live_*. La URL no cambia — el ambiente lo define la key (§4).
Qué no existe todavía
Esta sección existe para que nadie escriba código contra una promesa.
| Función | Estado real |
|---|---|
| Administrar webhooks por API (crear, editar o listar una suscripción desde código) | Ninguna ruta bajo /api/public/v1/* administra suscripciones, rota el secreto ni lee el historial de entregas. El alta sí es autoservicio, pero desde el portal (/portal/webhooks, permiso webhook:configurar): ver §18. El scope webhook:configurar se puede tildear al crear una key, pero no hay ruta pública que lo consuma. |
Los eventos certificado.por_vencer y recepcion.documento_reclamado | Están en el catálogo y se pueden suscribir, pero hoy no los publica nadie: quien se suscriba no recibe nada y no hay error que lo avise. Ver §18. |
| Emisión de los tipos 43 y 46 | POST /dte responde 501 emisor.tipo-dte-no-emisible sin consumir folio. Los scopes existen pero no habilitan nada. Sin fecha comprometida. |
Listado de documentos (GET /api/public/v1/dte con filtros) | No existe. Los documentos se consultan por id. Guardá el id que devuelve la emisión. (Lo que sí existe es GET /dte/pendientes, que lista tickets de emisiones encoladas, no documentos.) |
| Cancelar un ticket encolado | No hay ruta pública. Un ticket se resuelve solo o queda en error. |
| Emisión en lote | No existe en la superficie pública. Un documento por request. |
| Emisión por plantilla | Existe en el portal, no en la API pública. El scope plantillas:emitir se puede asignar, pero no hay ruta que lo consuma. |
| Envío de sobres al SII a demanda | El envío es automático y en segundo plano. No hay endpoint público para forzarlo. Los scopes dte:sobre:* y dte:proceso:read no tienen ruta pública. |
| Links de descarga públicos y permanentes | No existen, y no van a existir. Lo que sí hay son enlaces firmados con vencimiento (xmlUrl / pdfUrl, §10): se abren sin API Key pero caducan, y renovarlos es volver a consultar el documento. Un link eterno queda descartado por diseño. |
| Revocar un enlace de descarga ya emitido | No hay ruta pública. Un enlace filtrado deja de servir cuando vence; si necesitás cortarlo antes, escribí a soporte. |
Recuperar el ted de un documento ya emitido | No existe. ted solo viaja en la respuesta de la emisión; en las consultas llega null. Guardalo al emitir, o sacalo del XML firmado, que lo contiene. |
| Alta de empresa por API | No existe. La ejecuta soporte. Ver §1. |
| Consultar o disparar la certificación SII por API | No existe. Ver §14. |
| Catálogos de aduana por API (país, puerto, moneda, modalidad y cláusula de venta) | No hay endpoint público. Se consultan en el portal o se piden a soporte. |
Anulación de exportación por POST /anular | No aplica: se anula emitiendo una NC 112. No existe POST /exportacion/{id}/anular. |
| PDF de exportación en 80 mm | No existe. Exportación se renderiza solo en Carta. |
| Ventana de gracia en la rotación de keys | No existe. Rotar invalida el secreto anterior de inmediato. |
| Convertir una key de certificación en una de producción | No existe. Hay que crear una key nueva. Ver §14. |
Soporte
Ante un error 5xx, un 504 sin cuerpo JSON o un comportamiento que no calce con este documento, escribí a [email protected] con:
- El
codede la respuesta (recordá: los500no traencode). - El
traceId, si vino. En los errores 4xx de negocio vienenull— no lo esperes. - El prefijo de la key (los 8 caracteres, nunca el secreto).
- El
transactionIdque usaste, y elticketIdsi fue un202. - El
iddel documento, si ya se emitió. - Timestamp aproximado en UTC.