Guía
Modelo asíncrono y polling
La emisión responde antes de que el SII conteste. Esta guía explica por qué, qué recibís en ese momento y cómo averiguar en qué terminó el documento.
Cómo funciona
No hay modos que elegir: no existen parámetros sendMode ni responseFormat. Pero la emisión tiene dos respuestas exitosas posibles, y tu código tiene que manejar las dos:
| Respuesta | Cuándo | Qué trae |
|---|---|---|
| 201 Created | Había folio disponible. Es el caso normal en régimen. | El documento completo: id, folio, estadoSii. |
| 202 Accepted | No había folio: el documento es válido, quedó encolado y ya se pidieron folios al SII. | Un ticket: ticketId. No trae id ni folio. |
1. La emisión responde al instante
Reservamos el folio del CAF, armamos el XML, lo firmamos, generamos el timbre y respondemos 201. El folio que recibís es definitivo. Si no hay folio, en vez de fallar respondemos 202 con un ticket.
2. El envío al SII ocurre después
El documento viaja al SII en segundo plano. Su estadoSii converge a Aceptado o Rechazado sin que hagas nada.
El ciclo completo
1. Emitir
curl -X POST https://api-publica.dtecomges.cl/api/public/v1/dte \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tipoDte": 39,
"transactionId": "e2f0b1c4-77aa-4f2b-9d31-8c5a0e6b1d20",
"receptor": { "rut": "66666666-6", "razonSocial": "Cliente" },
"detalles": [
{ "nroLinea": 1, "nombreItem": "Producto retail", "cantidad": 1, "precioUnitario": 9990 }
],
"indServicio": 3
}'2. Respuesta inmediata
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "8b4c2f1e-9a3d-4e8f-b21c-5d6e7f8a9b0c",
"tipoDte": 39,
"folio": 1049,
"ambiente": "Certificacion",
"fechaEmision": "2026-07-27",
"montoNeto": 9990,
"montoExento": 0,
"montoIva": 1898,
"montoTotal": 11888,
"montoNF": 0,
"estadoSii": "Pendiente",
"trackId": null,
"creadoEn": "2026-07-27T14:30:15Z",
"offsetUtcHoras": -4
}precioUnitario va NETO, incluso en boletaEn una boleta afecta (39) el servidor recarga el 19 % sobre lo que le mandás: el PrcItem que viaja al XML es round(precioUnitario × 1,19). Por eso el ejemplo de arriba manda 9990 y totaliza $11.888, no $9.990.
Si lo que tenés es el precio al público(el que cobra la caja), dividilo por 1,19 antes de mandarlo. Mandar el precio bruto tal cual emite la boleta un 19 % arriba — y el folio ya quedó consumido.
2b. Partiendo del precio al público
El mismo producto, pero al revés: querés que la boleta totalice $9.990 exactos. Enviás el neto — round(9990 / 1,19) = 8395 — y la respuesta trae montoNeto: 8395, montoIva: 1595 y montoTotal: 9990.
# Objetivo: que la boleta totalice $9.990 en la caja.
# precioUnitario va NETO -> se envía round(9990 / 1,19) = 8395.
curl -X POST https://api-publica.dtecomges.cl/api/public/v1/dte \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tipoDte": 39,
"receptor": { "rut": "66666666-6", "razonSocial": "Cliente" },
"detalles": [
{ "nroLinea": 1, "nombreItem": "Producto retail", "cantidad": 1, "precioUnitario": 8395 }
],
"indServicio": 3
}'folio del 201 está reservado del CAF y no va a cambiar. Podés imprimirlo, guardarlo en tu base y entregarlo al cliente aunque el envío al SII todavía no haya ocurrido.La otra respuesta: 202 y un ticket
Cuando no hay folio disponible, la emisión no falla: el documento ya fue validado, se encola y se piden folios al SII automáticamente. La respuesta es un 202 con un ticketId.
Le pasa sobre todo a las empresas recién dadas de alta: el SII autoriza timbrajes de a pocos folios al principio, así que es el caso que más probablemente te toque en tu primera integración real.
El cuerpo del 202 no trae id ni folio. Si tu código hace respuesta.id sin mirar el status, se guarda un documento sin identificador y, como el status es 2xx, ningún manejo de errores lo atrapa.
Y si lo tratás como fallo y reemitís, consumís otro folio — salvo que hayas mandado un transactionId, que es exactamente para lo que sirve. Ramificá por status: 201 y 202 son dos éxitos distintos.
HTTP/1.1 202 Accepted
Content-Type: application/json
{
"ticketId": "3f21a9c8-0e6b-4d51-8a77-1c2b3d4e5f60",
"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 Demo S.A.",
"solicitadoEn": "2026-07-27T14:30:12Z",
"terminadoEn": null,
"offsetUtcHoras": -4
}ticketId del cuerpo. El header Location no llega al cliente de la API pública.Resolver el ticket
Consultá GET /api/public/v1/dte/pendientes/{ticketId} (scope dte:read) hasta que estado sea terminal. Con Completado llega el documentoId, y desde ahí el flujo sigue igual que un 201.
| estado | Significado | ¿Terminal? |
|---|---|---|
| Pendiente | En la cola, esperando que el SII entregue folios. | No |
| Procesando | Hay folio y se está emitiendo el documento en este momento. | No |
| Completado | El documento se emitió. El campo documentoId trae su id. | Sí |
| Error | La emisión falló. Mirá codigoError y esErrorTerminal para saber si conviene actuar o esperar. | Sí |
| Cancelado | El ticket fue cancelado y no va a emitirse. | Sí |
curl https://api-publica.dtecomges.cl/api/public/v1/dte/pendientes/3f21a9c8-0e6b-4d51-8a77-1c2b3d4e5f60 \
-H "X-Api-Key: $API_KEY"
# Cuando el SII entrega el folio:
# { ..., "estado": "Completado", "documentoId": "8b4c2f1e-..." }
#
# Y con ese documentoId ya podés consultar el DTE como cualquier otro:
# GET /api/public/v1/dte/8b4c2f1e-...Si el ticket termina en Error, mirá esErrorTerminal: si es true no se arregla esperando (por ejemplo, el SII no autoriza folios de ese tipo para la empresa) y hay que actuar. El codigoError te dice cuál es el caso.
async function emitir(cuerpo, apiKey) {
const res = await fetch('https://api-publica.dtecomges.cl/api/public/v1/dte', {
method: 'POST',
headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
// transactionId SIEMPRE: es lo que hace seguro cualquier reintento.
body: JSON.stringify(cuerpo),
})
if (res.status === 201) return { emitido: await res.json() }
if (res.status === 202) {
// El documento es válido y está encolado. NO reemitir.
const { ticketId } = await res.json()
return { ticketId }
}
throw new Error(`Emisión fallida: ${res.status}`)
}
// Resolver el ticket: pollear hasta un estado terminal.
async function resolverTicket(ticketId, apiKey) {
const TERMINALES = ['Completado', 'Error', 'Cancelado']
for (const espera of [5000, 10000, 20000, 40000, 60000]) {
await new Promise((r) => setTimeout(r, espera))
const res = await fetch(
`https://api-publica.dtecomges.cl/api/public/v1/dte/pendientes/${ticketId}`,
{ headers: { 'X-Api-Key': apiKey } },
)
const ticket = await res.json()
if (ticket.estado === 'Completado') return ticket.documentoId
if (TERMINALES.includes(ticket.estado)) {
// esErrorTerminal: true significa que no se arregla esperando.
throw new Error(`${ticket.codigoError}: ${ticket.mensaje}`)
}
}
return null // sigue en cola: reintentá el polling más tarde
}422 en el acto con caf.sin-autorizacion en vez de encolar: encolar sería condenar el documento a una espera infinita.3. Consultar el estado
curl https://api-publica.dtecomges.cl/api/public/v1/dte/8b4c2f1e-9a3d-4e8f-b21c-5d6e7f8a9b0c \
-H "X-Api-Key: $API_KEY"4. Estado final
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "8b4c2f1e-9a3d-4e8f-b21c-5d6e7f8a9b0c",
"tipoDte": 39,
"folio": 1049,
"estadoSii": "Aceptado",
"trackId": 2394857620,
"montoTotal": 11888,
"creadoEn": "2026-07-27T14:30:15Z",
"offsetUtcHoras": -4
}Valores de estadoSii
El estado es un texto, tanto en el DTE nacional como en exportación (GET /api/public/v1/exportacion/{id}). Compará siempre contra la cadena, nunca contra un número.
| estadoSii | Significado | ¿Final? |
|---|---|---|
| Pendiente | Firmado y persistido, todavía no salió al SII. Es el estado con el que responde la emisión. | No |
| Enviado | El SII recibió el sobre y asignó trackId. Falta el veredicto. | No |
| Aceptado | El SII lo aceptó conforme. | Sí |
| Rechazado | El SII lo rechazó. Revisá el detalle en el portal. | Sí |
| AceptadoConReparos | Aceptado con observaciones. Es válido, pero conviene revisarlo. | Sí |
| Anulado | Anulado mediante una nota de crédito posterior. | Sí |
| ErrorEnvio | Falla técnica de envío (timeout, 5xx del SII). Se reintenta automáticamente desde la cola. | No |
Polling recomendado
Primer intento a los 5 s y después backoff: 10 s, 20 s, 40 s, 60 s. Frená en cuanto llegues a un estado final. Cuidá el rate limit si tenés muchos documentos en vuelo: conviene un worker que recorra los pendientes en lote, y no un poll agresivo por documento.
const FINALES = ['Aceptado', 'AceptadoConReparos', 'Rechazado']
async function esperarEstadoFinal(id, apiKey) {
const esperas = [5000, 10000, 20000, 40000, 60000, 60000]
for (const espera of esperas) {
await new Promise((r) => setTimeout(r, espera))
const res = await fetch(`https://api-publica.dtecomges.cl/api/public/v1/dte/${id}`, {
headers: { 'X-Api-Key': apiKey },
})
const dte = await res.json()
if (FINALES.includes(dte.estadoSii)) return dte
// "Pendiente" y "Enviado" son estados de tránsito: seguir esperando.
// "ErrorEnvio" es reintentable del lado nuestro; también se sigue esperando.
}
throw new Error('El documento no alcanzó un estado final en el tiempo esperado')
}No hay forma de contratar notificaciones push por autoservicio: la API pública no expone una ruta para registrar suscripciones. Diseñá tu integración sobre polling.
El contrato de webhooks existe y está congelado, así que si te bloquea el polling podés pedirlo por soporte y adelantar el receptor: los detalles están en la guía de webhooks.
Descargar XML y PDF
Ambos están disponibles desde la emisión, sin esperar al SII. Estos dos endpoints piden la misma API Key, así que son para tu backend. Para hacérselos llegar al receptor está la otra vía: los enlaces firmados que la propia emisión devuelve, acá abajo.
# El XML y el PDF existen desde la emisión: no hace falta
# esperar el veredicto del SII para descargarlos.
curl https://api-publica.dtecomges.cl/api/public/v1/dte/{id}/xml \
-H "X-Api-Key: $API_KEY" -o dte.xml
curl "https://api-publica.dtecomges.cl/api/public/v1/dte/{id}/pdf?formato=80mm" \
-H "X-Api-Key: $API_KEY" -o dte.pdfEl cuerpo de la emisión trae xmlUrl y pdfUrl: enlaces que se abren sin API Key, para reenviarle el documento a tu comprador. Lo que los protege es su firma, que vence a los 30 días. Usalos tal como vienen — modificarlos los invalida — y no los publiques en una página indexable: hasta que caducan, quien tenga la URL accede al documento.
Acá importa el modelo asíncrono: el 202 no los trae, porque todavía no hay documento. Aparecen cuando el ticket se resuelve y consultás GET /api/public/v1/dte/{documentoId}. Esa misma consulta es también la forma de renovar un enlace vencido: devuelve enlaces recién firmados, sin reemitir nada. Todo el detalle está en la referencia.
Los documentos de exportación (110/111/112) también tienen su descarga: GET /api/public/v1/exportacion/{id}/pdf con scope dte:read. La única diferencia es que no acepta ?formato=: solo hay plantilla Carta, porque el documento lleva datos de aduana, código arancelario y moneda extranjera que no entran en 80 mm.
POST /api/public/v1/exportacion/{id}/anular. La anulación en un paso está disponible solo para el DTE nacional. Para revertir una exportación hay que emitir la nota de crédito de exportación (112) referenciando el documento original.¿Siguiente? → Errores y reintentos para manejar bien el caso ambiguo del timeout. El detalle completo de cada cuerpo de respuesta está en la referencia de la API, que es la fuente de verdad.