Emisión
Emitir un DTE con la API de Simplo — el endpoint POST /api/v1/billing/dte, la Idempotency-Key obligatoria y el flujo asíncrono hacia el SII.
El endpoint
Sección titulada «El endpoint»| Método | Path | Descripción |
|---|---|---|
POST |
/api/v1/billing/dte |
Emitir un DTE (Idempotency-Key obligatoria). Responde 201 con estado firmado; el envío al SII es asíncrono. |
GET |
/api/v1/billing/dte |
Listar DTEs con paginación por cursor y filtros por tipo, estado, fecha y receptor. |
GET |
/api/v1/billing/dte/{id} |
Detalle y estado actual de un DTE (útil para polling). |
GET |
/api/v1/billing/dte/{id}/pdf |
Representación impresa (PDF con timbre PDF417). |
GET |
/api/v1/billing/dte/{id}/xml |
XML firmado del documento. |
POST |
/api/v1/billing/dte/send-boletas |
Enviar un conjunto de boletas al SII en un envelope EnvioBOLETA (Idempotency-Key obligatoria por API key). |
Estas seis rutas son, además, toda la superficie que una API key
billing:dte puede llamar — el resto de /api/v1/billing/* es solo-sesión
(ver la tabla del quickstart).
Con el SDK (aún no publicado), todo esto será simplo.dtes.emit, .list,
.retrieve, .pdf y .xml — ver el quickstart.
Reglas que hacen correcta una emisión
Sección titulada «Reglas que hacen correcta una emisión»-
Los montos son pesos chilenos (CLP) enteros. Sin centavos, sin floats para dinero. En facturas
precioes neto y la API agrega el IVA (19%); en boletas (39/41)precioes bruto (IVA incluido) y el motor lo desglosa por dentro. -
emitretorna antes del veredicto del SII. Persistedte.id, tratafirmadocomo “en vuelo” y resuelve el estado final con polling deGET /api/v1/billing/dte/{id}hasta un estado terminal (aceptado,rechazado,con_reparosoerror_envio). (La entrega de webhooks aún no está activa.) -
Las boletas (tipo 39 y 41) exigen
ind_servicio— usa3(boleta de venta y servicios). Omitirlo responde400 IND_SERVICIO_REQUERIDO, conaction: "set_ind_servicio"ycontext.field. Las facturas (33) no lo llevan. -
Guarda
dte.idydte.folioen tu lado (ej. en la fila de la orden) inmediatamente después de emitir. -
La API valida los montos por ti — y nombra el campo.
precionegativo,cantidad: 0y total en cero responden400 MONTO_INVALIDOconcontext.field(detalle[0].precio) yreceived_value; no se emite ni se quema folio. Medido el 2026-09-04. Igual conviene validar en tu lado para no gastar un round-trip. -
No envíes
metadata— ahora te lo decimos con un error. El campo todavía no se persiste, así que no volvería enGET /api/v1/billing/dte/{id}. Antes se aceptaba y se descartaba en silencio; desde el 2026-09-10 unametadatacon contenido responde422 BILLING_METADATA_NOT_SUPPORTEDy no se emite nada. Preferimos que pierdas un round-trip hoy a que pierdas tu id de correlación sin enterarte.metadata: {}ymetadata: nullsiguen pasando (no pierden nada). Tu correlación vive en tu base, keyed por eldte.idque devuelve la emisión.Si recibes este 422 al reintentar una emisión que pudo haber salido antes de este cambio: no mintees una key nueva — eso emitiría el documento dos veces (y quitar
metadatareintentando con la misma key daríaIDEMPOTENCY_BODY_MISMATCH). Busca primero el documento enGET /api/v1/billing/dtepor folio/monto/fecha; no hay búsqueda porIdempotency-Key, así que es una revisión manual, no una recuperación automática. Esta ventana se cierra sola: las keys completadas se retienen 24 horas. -
Boletas de monto alto: identifica al pagador (Res. Ex. SII 44/2025). Una boleta de monto alto debe identificar a quien paga; el consumidor final genérico
66666666-6no basta. Simplo no valida esta regla: la emisión se acepta igual y la responsabilidad tributaria queda en el emisor. No la validamos porque exigiría resolver el umbral en pesos, y un valor vencido o no disponible bloquearía emisiones legítimas — negarse a vender es peor falla que la que se evita.Dónde se identifica al pagador en nuestra API: en
receptor, que ya es obligatorio en toda emisión — mandas ahí el RUT real y larazon_socialen vez de66666666-6. No necesitas ningún campo nuevo.Lo que no afirmamos: ni el umbral exacto en pesos ni su fecha de valorización, ni que identificar al receptor sea por sí solo suficiente para cumplir la resolución (hay requisitos adicionales para software de mercado que nuestro contrato no expone hoy). Confirma el alcance con tu asesor tributario o con el texto de la resolución: no publicamos una garantía de cumplimiento que no podemos verificar.
-
Puedes emitir en paralelo. La carrera de asignación de folios que producía un
400 CAF_NO_DISPONIBLEespurio está corregida: el allocator espera el lock en vez de saltárselo. Medido el 2026-09-04 contra producción: 20 emisiones simultáneas → 20 folios únicos, cero espurios. Recuerda que el límite sigue siendo 60 req/min por empresa. -
No bloquees el flujo del usuario esperando la aceptación del SII — puede tardar. Emite, responde y reconcilia de forma asíncrona.
Boletas: el conjunto va en UN envelope
Sección titulada «Boletas: el conjunto va en UN envelope»Emitir con auto_send manda un envelope por documento. Cuando necesitas
que un conjunto de boletas viaje al SII en un solo EnvioBOLETA —el caso
del cierre de turno, y el que exige el Set de Pruebas de certificación— la
emisión y el envío son dos pasos separados:
POST /api/v1/billing/dtepor cada boleta, conauto_send: false. Cada una responde201con suidy sufolio, en estadofirmado.POST /api/v1/billing/dte/send-boletasuna vez, con todos losiddel paso anterior. Ese es el envelope.
Ejemplo completo: tres boletas, un envelope
Sección titulada «Ejemplo completo: tres boletas, un envelope»set -euo pipefailIDS=()
# Usa TUS identificadores de negocio, iguales en cada reintento del MISMO# cierre. Si cambias la key reemites; si repites la key con otro payload, 422.RUN_ID="caja-01-2026-09-10-turno-tarde"
# Paso 1 — emitir cada boleta SIN enviarla (auto_send: false)for N in 1 2 3; do RESP=$(curl -sX POST https://api.simplo.cl/api/v1/billing/dte \ -H "X-Api-Key: $SIMPLO_API_KEY" \ -H "Idempotency-Key: $RUN_ID-boleta-$N" \ -H 'Content-Type: application/json' \ -d '{ "tipo_dte": 39, "ind_servicio": 3, "auto_send": false, "receptor": {"rut":"66666666-6","razon_social":"Consumidor final", "giro":"Particular","direccion":"Sin direccion", "comuna":"Santiago","ciudad":"Santiago"}, "detalle": [{"nombre":"Cafe","cantidad":1,"precio":2500}] }') ID=$(jq -r '.id // empty' <<<"$RESP") # Aborta AQUÍ si una boleta no se emitió. Seguir de largo mete un id vacío # en la lista y terminas enviando un envelope incompleto sin darte cuenta. [ -n "$ID" ] || { echo "boleta $N falló: $RESP" >&2; exit 1; } IDS+=("$ID")done
# Paso 2 — un SOLO envelope con las tresENVIO=$(curl -sX POST https://api.simplo.cl/api/v1/billing/dte/send-boletas \ -H "X-Api-Key: $SIMPLO_API_KEY" \ -H "Idempotency-Key: $RUN_ID-envio" \ -H 'Content-Type: application/json' \ -d "$(jq -nc '{dte_ids: $ARGS.positional}' --args "${IDS[@]}")")
# Paso 3 — comprueba que viajaron TODAS (ver "Lo que te va a morder")[ "$(jq -r '.num_dtes' <<<"$ENVIO")" = "${#IDS[@]}" ] \ || { echo "enviamos MENOS boletas de las pedidas: $ENVIO" >&2; exit 1; }echo "$ENVIO"Respuesta:
{ "envio_id": "6f1c0a2e-...", "track_id": "918273645", "estado": "enviado", "num_dtes": 3}El track_id es del envelope completo, no de cada boleta. Guárdalo, pero
ten claro que ninguna ruta pública lo acepta como consulta: sirve para
soporte y para hablar con el SII. El veredicto de cada boleta lo sigues como
siempre con GET /api/v1/billing/dte/{id}.
Lo que te va a morder a las 3 AM
Sección titulada «Lo que te va a morder a las 3 AM»- Entre 1 y 500 boletas por envelope. Con 501 falla el envío completo: parte la jornada en tandas antes de llamar, no después del error.
- Manda
dte_idsdistintos y comparanum_dtescontra el largo de tu lista. Con200puedes haber enviado menos de las que pediste: los ids que no existen en tu empresa se omiten (si faltan todos, ahí sí es400), y los ids repetidos colapsan aunque los documentos existan. El paso 3 del ejemplo hace exactamente esa comparación. - Solo entran boletas (tipo 39 y 41). Un
idde factura 33 en la lista aborta el envío completo con400. - Solo entran boletas en estado
firmadooerror_envio. Una boleta ya enviada aborta el envío completo con400. - El
track_ides del envelope; ninguna ruta pública lo acepta como consulta. Guárdalo para soporte y para hablar con el SII. El veredicto por documento se consulta conGET /api/v1/billing/dte/{id}(campostracking/sii_status). Elestado: "enviado"de esta respuesta es el resultado de la subida, no el veredicto final del SII.
Reintentos: léelo antes de reintentar
Sección titulada «Reintentos: léelo antes de reintentar»Esta es la parte que preferimos decirte incómoda a decírtela tranquilizadora. El envío no es atómico. El motor lee los estados, sube al SII y recién después persiste el resultado y mueve los documentos; los fallos al actualizar cada documento solo quedan en el log. De ahí salen tres hechos:
- Un
400no prueba que no se subió nada. Un fallo posterior a la subida responde400con las boletas todavía enfirmado. - Que las boletas sigan en
firmadotampoco lo prueba, por lo mismo. - Dos llamadas solapadas pueden ambas leer
firmadoy ambas subir. No hay un candado que lo impida, y laIdempotency-Key—obligatoria por API key— no tiene replay en esta ruta: la misma key vuelve a ejecutar.
Por lo tanto, ante cualquier error o timeout: no reintentes a ciegas.
Consulta GET /api/v1/billing/dte/{id} de las boletas involucradas y, si el
estado no te deja decidir, escala a soporte con tus dte_ids, el envio_id y
la Idempotency-Key que usaste, en vez de volver a enviar. Una boleta enviada
dos veces es un problema tributario; una llamada de soporte, no.
estado: "ambiguo"significa que el SII respondió que ya había recibido ese envelope (código 99). Las boletas quedan como estaban. La reconciliación periódica puede resolver el envelope, pero no garantiza propagar un estado terminal a cada documento, y sintrack_idsolo agota reintentos: no asumas que se arregla solo. Guardaenvio_id,track_idy tusdte_ids, revisa el estado de los documentos y escala si no avanza.- Esta ruta no emite nada: no crea documentos y no quema folios. Solo empaqueta y envía boletas que tu empresa ya emitió, y solo las tuyas.
Idempotencia
Sección titulada «Idempotencia»La emisión exige un header Idempotency-Key — un UUID es lo recomendado,
pero sirve cualquier string único y estable (no hay largo mínimo/máximo
aplicado). Con el SDK:
-
dtes.emitgenera unaIdempotency-Key(UUID) automáticamente por llamada — un reintento en vuelo nunca puede duplicar una factura. No construyas una capa extra de dedup alrededor de llamadas individuales. -
Para dedup entre procesos (un job reintentado por tu cola), pasa tu propia key estable:
await simplo.dtes.emit(params, { idempotencyKey: `order-${orderId}` });La misma key + el mismo body dentro de 24 horas devuelve el resultado original. Una emisión repetida (misma key, mismo body) llega marcada con el header de respuesta
X-Idempotency-Replay: true. Las keys completadas se retienen por 24 horas. -
Nunca reutilices una idempotency key con un body distinto — eso es un
ValidationErrorconcode: 'IDEMPOTENCY_BODY_MISMATCH'.
code |
Status | Significado |
|---|---|---|
IDEMPOTENCY_IN_PROGRESS |
409 | La misma key se está procesando ahora mismo — espera y consulta el resultado |
IDEMPOTENCY_BODY_MISMATCH |
422 | La key se reutilizó con un body distinto — usa una key nueva |
Si al emitir recibes un 503 por el servicio de idempotencia temporalmente no
disponible, reintenta con la misma key.
Relacionado
Sección titulada «Relacionado»- Tipos de DTE — códigos y requisitos por tipo
- Estados — el ciclo de vida completo
- Errores — clases de error y reintentos
- Límites — rate limits por empresa