Ir al contenido
SimploSimplo Docs

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.

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.

  1. Los montos son pesos chilenos (CLP) enteros. Sin centavos, sin floats para dinero. En facturas precio es neto y la API agrega el IVA (19%); en boletas (39/41) precio es bruto (IVA incluido) y el motor lo desglosa por dentro.

  2. emit retorna antes del veredicto del SII. Persiste dte.id, trata firmado como “en vuelo” y resuelve el estado final con polling de GET /api/v1/billing/dte/{id} hasta un estado terminal (aceptado, rechazado, con_reparos o error_envio). (La entrega de webhooks aún no está activa.)

  3. Las boletas (tipo 39 y 41) exigen ind_servicio — usa 3 (boleta de venta y servicios). Omitirlo responde 400 IND_SERVICIO_REQUERIDO, con action: "set_ind_servicio" y context.field. Las facturas (33) no lo llevan.

  4. Guarda dte.id y dte.folio en tu lado (ej. en la fila de la orden) inmediatamente después de emitir.

  5. La API valida los montos por ti — y nombra el campo. precio negativo, cantidad: 0 y total en cero responden 400 MONTO_INVALIDO con context.field (detalle[0].precio) y received_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.

  6. No envíes metadata — ahora te lo decimos con un error. El campo todavía no se persiste, así que no volvería en GET /api/v1/billing/dte/{id}. Antes se aceptaba y se descartaba en silencio; desde el 2026-09-10 una metadata con contenido responde 422 BILLING_METADATA_NOT_SUPPORTED y no se emite nada. Preferimos que pierdas un round-trip hoy a que pierdas tu id de correlación sin enterarte. metadata: {} y metadata: null siguen pasando (no pierden nada). Tu correlación vive en tu base, keyed por el dte.id que 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 metadata reintentando con la misma key daría IDEMPOTENCY_BODY_MISMATCH). Busca primero el documento en GET /api/v1/billing/dte por folio/monto/fecha; no hay búsqueda por Idempotency-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.

  7. 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-6 no 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 la razon_social en vez de 66666666-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.

  8. Puedes emitir en paralelo. La carrera de asignación de folios que producía un 400 CAF_NO_DISPONIBLE espurio 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.

  9. No bloquees el flujo del usuario esperando la aceptación del SII — puede tardar. Emite, responde y reconcilia de forma asíncrona.

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:

  1. POST /api/v1/billing/dte por cada boleta, con auto_send: false. Cada una responde 201 con su id y su folio, en estado firmado.
  2. POST /api/v1/billing/dte/send-boletas una vez, con todos los id del paso anterior. Ese es el envelope.
Ventana de terminal
set -euo pipefail
IDS=()
# 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 tres
ENVIO=$(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}.

  • 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_ids distintos y compara num_dtes contra el largo de tu lista. Con 200 puedes haber enviado menos de las que pediste: los ids que no existen en tu empresa se omiten (si faltan todos, ahí sí es 400), 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 id de factura 33 en la lista aborta el envío completo con 400.
  • Solo entran boletas en estado firmado o error_envio. Una boleta ya enviada aborta el envío completo con 400.
  • El track_id es 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 con GET /api/v1/billing/dte/{id} (campos tracking / sii_status). El estado: "enviado" de esta respuesta es el resultado de la subida, no el veredicto final del SII.

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 400 no prueba que no se subió nada. Un fallo posterior a la subida responde 400 con las boletas todavía en firmado.
  • Que las boletas sigan en firmado tampoco lo prueba, por lo mismo.
  • Dos llamadas solapadas pueden ambas leer firmado y ambas subir. No hay un candado que lo impida, y la Idempotency-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 sin track_id solo agota reintentos: no asumas que se arregla solo. Guarda envio_id, track_id y tus dte_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.

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.emit genera una Idempotency-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 ValidationError con code: '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.