Emitir en lote

Esta guía es para la facturación masiva: el cierre de mes, la corrida de suscripciones, la migración desde otro proveedor. Son los momentos en que una integración emite decenas o cientos de documentos seguidos, y también los momentos en que un dato mal mapeado se repite en todos ellos.

Cada factura que el SII rechaza consume su folio igual que una aceptada. Si el error es sistemático (un receptor sin comuna, un giro que viajó dentro de la razón social, una referencia sin fecha), un lote de 200 documentos puede terminar con 200 folios consumidos y cero facturas válidas. La receta de esta guía evita ese escenario con cuatro reglas:

  1. Validar antes de emitir, sin consumir nada.
  2. Emitir uno y esperar la aceptación del SII antes de soltar el resto.
  3. Detenerse al primer rechazo.
  4. Una Idempotency-Key por documento, y respetar Retry-After.

Prerequisitos

  • Empresa configurada en producción con folios cargados para el tipo de documento.
  • API Key creada.
  • Opcional, pero recomendado: un webhook suscrito a dte.rechazado, emision.pausada y emision.reanudada.

1. Validar todo el lote sin emitir

POST /v1/dte/validar/lote ejecuta sobre cada documento exactamente la misma validación que la emisión, pero no crea jobs, no asigna folios y no descuenta cuota. Devuelve un resultado por documento, en el mismo orden en que los enviaste.

bash
curl -X POST "https://api.folyo.cl/v1/dte/validar/lote" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"documentos": [
  {
    "tipo_dte": 33,
    "receptor": {
      "rut": "76.543.210-K",
      "razon_social": "Cliente SpA",
      "giro": "Comercio al por menor",
      "direccion": "Calle Los Leones 456",
      "comuna": "Las Condes"
    },
    "detalle": [{ "nombre": "Plan mensual", "cantidad": 1, "precio": 50000, "monto": 50000 }]
  },
  {
    "tipo_dte": 33,
    "receptor": {
      "rut": "77.111.222-3",
      "razon_social": "Otro Cliente Ltda",
      "direccion": "Av. Providencia 1234, Providencia"
    },
    "detalle": [{ "nombre": "Plan mensual", "cantidad": 1, "precio": 50000, "monto": 50000 }]
  }
]
}'

Respuesta para el ejemplo anterior:

json
{
  "ok": true,
  "data": {
    "total": 2,
    "validos": 1,
    "resultados": [
      { "indice": 0, "valido": true, "field_errors": [], "advertencias": [] },
      {
        "indice": 1,
        "valido": false,
        "codigo": "RECEPTOR_INCOMPLETO",
        "mensaje": "Faltan el giro y la comuna del receptor. El SII exige estos datos del receptor en este tipo de documento y rechaza el documento si no vienen. Complete el dato y vuelva a emitir; no se consumió folio.",
        "field_errors": [
          {
            "field": "receptor.giro",
            "message": "Obligatorio para el DTE 33 según el Formato DTE del SII; sin este dato el SII rechaza el documento."
          },
          {
            "field": "receptor.comuna",
            "message": "Obligatorio para el DTE 33 según el Formato DTE del SII; sin este dato el SII rechaza el documento."
          }
        ],
        "advertencias": []
      }
    ]
  }
}

El segundo documento trae la comuna dentro de la dirección y no trae giro. El SII exige cada dato en su campo propio; Folyo lo rechaza antes de que exista un folio. codigo y mensaje son los mismos que respondería la emisión, y las rutas de field_errors son relativas a cada documento (indice ya dice cuál es). Corrige el mapeo en tu sistema y vuelve a validar hasta que validos sea igual a total.

Validar no es emitir

Un valido: true confirma que el documento cumple las reglas de formato y las obligaciones del SII que Folyo revisa en local. No reserva folio ni garantiza la aceptación del SII, que depende de información que solo el SII tiene (la situación tributaria del receptor, por ejemplo). Por eso el paso siguiente existe.

2. Emitir el primero y esperar la aceptación del SII

Antes de soltar el lote, emite un solo documento y espera a que el SII lo acepte. Una factura aceptada confirma que la configuración de la empresa, los folios y el mapeo de datos están bien; si algo falla, lo descubres con un folio consumido en vez de doscientos.

Emite con una Idempotency-Key propia:

http
POST https://api.folyo.cl/v1/dte/emitir
X-API-Key: tu_api_key
Idempotency-Key: cierre-2026-10-cliente-76543210
Content-Type: application/json

{ "tipo_dte": 33, "receptor": { ... }, "detalle": [ ... ] }

Luego espera dos cosas distintas:

  1. Que el job termine en completed (polling a GET /v1/dte/emision/{job_id} o webhook dte.emitido). Significa que el SII recibió el documento y le asignó un Track ID. Todavía no es la aceptación.
  2. Que el SII lo acepte. Consulta el envío con GET /v1/dte/estado/{track_id}: cuando el SII termina de procesarlo, detalle informa por tipo de documento cuántos quedaron aceptados y cuántos rechazados. Si el SII lo rechaza, además llega el webhook dte.rechazado con el track_id, el RUT del receptor y el motivo. En Folyo Platform también puedes leer el estado del documento (aceptado_sii o rechazado_sii) en GET /v1/documentos.
typescript
const job = await folyo.dte.emitirYEsperar(primero, {
idempotencyKey: "cierre-2026-10-cliente-76543210",
});
const trackId = job.result?.track_id;
if (!trackId) throw new Error("El job terminó sin Track ID");

// completed = recibido por el SII con Track ID. Ahora esperamos el veredicto.
for (;;) {
const envio = (await folyo.dte.getEstadoEnvio(trackId)) as {
  detalle?: { aceptados: number; rechazados: number; reparos: number }[];
};
const resumen = envio.detalle?.[0];
if (resumen && resumen.rechazados > 0) throw new Error("El SII rechazó el primer documento");
if (resumen && resumen.aceptados + resumen.reparos > 0) break;
await new Promise((r) => setTimeout(r, 60_000));
}

Cada consulta de GET /v1/dte/estado/{track_id} cuenta en la cuota de consultas al SII de tu plan: espácialas al menos un minuto.

El SII suele responder en minutos, pero en horas de alta carga puede demorar más de una hora. Es una demora del SII, no un error: no reemitas un documento porque siga en enviado. Si el primero queda rechazado_sii, el motivo que informa el SII te dice qué corregir; arregla el mapeo, valida de nuevo y repite este paso.

3. Emitir el resto y parar al primer rechazo

Con el primer documento aceptado, emite los demás. Dos recomendaciones:

  • Una Idempotency-Key por documento, derivada de tu propio identificador (el número de la orden, el id de la suscripción y el período). Si tu proceso se cae a la mitad y lo vuelves a correr, los documentos ya emitidos devuelven el mismo job_id sin consumir otro folio.
  • Detente al primer rechazo. Un rechazado_sii en medio de una corrida casi siempre es un problema que se repite en los que vienen. Deja de emitir, lee el motivo del webhook dte.rechazado (o del documento en GET /v1/documentos?estado=rechazado_sii si usas Folyo Platform), corrige y retoma con las mismas claves.

Puedes usar POST /v1/dte/emitir/lote para encolar hasta 100 documentos por llamada: cada documento lleva su idempotency_key propia y ninguno se encola si alguno falla la validación. Si prefieres controlar el ritmo, emítelos de a uno con POST /v1/dte/emitir.

typescript
import { FolyoEmisionPausadaError, FolyoRateLimitError } from "@folyo/sdk";

for (const doc of restantes) {
const key = `cierre-2026-10-${doc.referencia_interna}`;
try {
  await folyo.dte.emitir(doc.request, { idempotencyKey: key });
} catch (err) {
  if (err instanceof FolyoRateLimitError) {
    // 429: espera lo que indica Retry-After y reintenta con la MISMA clave.
    await new Promise((r) => setTimeout(r, (err.retryAfter ?? 5) * 1000));
    await folyo.dte.emitir(doc.request, { idempotencyKey: key });
    continue;
  }
  if (err instanceof FolyoEmisionPausadaError) {
    // 423: la emisión de la empresa está en pausa. No reintentes: ver sección siguiente.
    console.error("Emisión en pausa:", err.freno?.motivo, err.freno?.detalle?.ejemplos_motivo);
    break;
  }
  throw err;
}
}

4. Reintentos y límites

RespuestaQué significaQué hacer
429 RATE_LIMITEDDemasiadas peticiones por segundo en tu cuenta.Esperar Retry-After y reintentar con la misma Idempotency-Key.
429 DEMASIADAS_EMISIONES_EN_CURSOLa empresa tiene demasiados documentos todavía en proceso. No se creó job ni se consumió folio.Esperar Retry-After y reintentar con la misma Idempotency-Key. Baja el ritmo del lote.
503 con Retry-AfterEl SII o la cola no están disponibles.Reintentar con la misma Idempotency-Key cuando indique el header. Los SDK oficiales lo hacen solos.
423 EMISION_PAUSADALa emisión de la empresa está en pausa.No reintentar automáticamente. Ver la sección siguiente.

La regla que resume todo: nunca cambies la Idempotency-Key al reintentar. Un reintento con la misma clave y el mismo cuerpo es seguro siempre; un reintento con una clave nueva es una segunda emisión.

Qué hacer ante 423 EMISION_PAUSADA

Si el SII empieza a rechazar los documentos de tu empresa de forma sistemática, Folyo pausa su emisión para que no sigas consumiendo folios en documentos que van a ser rechazados, y avisa al titular de la cuenta por correo, en el dashboard y por el webhook emision.pausada. El equipo de Folyo también puede pausar una empresa a mano, por ejemplo durante un incidente.

Mientras dura la pausa, POST /v1/dte/emitir, el lote y la facturación de guías responden 423 con este cuerpo:

json
{
  "ok": false,
  "error": "Se pausó la emisión de facturas, notas y guías de Comercial Ejemplo SpA porque el SII rechazó varios documentos seguidos (6 en los últimos 30 minutos). Revise el motivo de los rechazos en Documentos, corrija los datos y reanude la emisión desde el panel. Los documentos que quedaron en cola no se enviarán hasta reanudar.",
  "code": "EMISION_PAUSADA",
  "freno": {
    "alcance": "afectos",
    "motivo": "rechazos_sii",
    "por": "sistema",
    "desde": "2026-10-30T04:12:09Z",
    "detalle": {
      "rechazos": 6,
      "aceptados": 0,
      "ventana_min": 30,
      "ejemplos_motivo": ["Comuna del receptor no corresponde o no informada"]
    }
  }
}

Nada se perdió: no se creó documento ni se consumió folio, y la Idempotency-Key quedó libre. Los documentos que ya estaban encolados esperan sin procesarse. Haz esto:

  1. Detén la corrida. Un 423 no se resuelve reintentando; la pausa no se levanta sola.
  2. Lee freno.detalle.ejemplos_motivo: son las glosas con que el SII rechazó los documentos. Casi siempre apuntan al dato que hay que corregir.
  3. Corrige el mapeo y valida de nuevo con POST /v1/dte/validar/lote.
  4. Reanuda desde el dashboard: Configuración de la empresa, botón Reanudar emisión. Requiere un usuario owner o admin. GET /v1/empresas muestra en emision_pausada si la pausa sigue vigente, y el webhook emision.reanudada avisa cuando se levanta.
  5. Retoma con las mismas claves. Los documentos pendientes salen solos al reanudar; los que no alcanzaste a enviar se emiten con la misma Idempotency-Key que ya tenías asignada.

Si se pausa otra vez

Si la empresa se vuelve a pausar poco después de que la reanudaste, la siguiente reanudación la hace el equipo de Folyo, para revisar la causa junto a ti. Escribe a soporte con el motivo corregido.

Resumen

  • Valida el lote completo con POST /v1/dte/validar/lote hasta que validos sea igual a total.
  • Emite un documento, espera completed y después la aceptación del SII.
  • Emite el resto con una Idempotency-Key por documento y detente al primer rechazado_sii.
  • Ante 429 o 503, espera Retry-After y reintenta con la misma clave.
  • Ante 423 EMISION_PAUSADA, detente, corrige, reanuda desde el dashboard y retoma con las mismas claves.
  • Los documentos que el SII rechaza no cuentan en la cuota mensual de tu plan, pero sí consumen su folio: la prevención es la única forma de no perderlos.