Referencia · API v1
La API para emitir DTE desde tu sistema
Una API REST sobre el mismo motor certificado del portal: JSON de entrada, documento timbrado de salida. Sin SOAP, sin XML a mano, sin pelear con los tres encodings del SII.
URL base
https://app.facturalibre.cl/api/v1
Autenticación
Cada empresa tiene sus claves, que emitimos nosotros y se muestran una sola vez. Van en el header Authorization:
Authorization: Bearer fl_live_su_clave- La clave es secreta y de servidor: nunca la pongas en un navegador, una app móvil o un repositorio. La API no sirve CORS a propósito.
- Si se filtra, revócala desde tu panel: deja de servir al instante.
GET /pingverifica la clave y responde con la empresa y el ambiente — el primer request de toda integración.- Rotación sin downtime: tu empresa admite hasta 5 claves activas a la vez. Para rotar sin cortar la emisión: crea la clave nueva, despliégala en tu sistema, verifica que emite, y recién entonces revoca la anterior. Las claves se administran desde Mi negocio → Integraciones y API.
Ambientes
El prefijo de la clave declara su ambiente, y solo opera si coincide con el de la empresa: fl_test_ emite contra la certificación del SII (maullin, sin efectos tributarios) y fl_live_ contra producción. Una clave de prueba filtrada jamás emite documentos reales, y al pasar tu empresa a producción las fl_test_ dejan de operar solas. Cada clave ve únicamente los documentos de su ambiente.
Modo prueba
Para ensayar tu integración —incluso contra tu clave de producción— sin quemar folios reales, usa los endpoints de validación: POST /eventos/validar y POST /documentos/validar. Aceptan el mismo cuerpo que sus gemelos de emisión, corren toda la validación y el cálculo de totales, y te devuelven qué documento se emitiría — tipo, decisión, montos y receptor — sin tomar folio, sin firmar y sin tocar el SII. Cero efectos secundarios, no cuentan contra tu límite de emisión.
curl -X POST https://app.facturalibre.cl/api/v1/eventos/validar \
-H "Authorization: Bearer fl_live_su_clave" \
-H "Content-Type: application/json" \
-d '{ "tipo": "venta", "origen": "mi-sistema", "referencia": "prueba-1",
"items": [{ "nombre": "Plan", "cantidad": 1, "precioUnitario": 12990 }] }'
# → 200 OK, SIN tomar folio ni emitir:
{
"validado": true,
"documento": { "tipoDte": 39, "totales": { "neto": 10916, "iva": 2074, "total": 12990 }, … },
"decision": { "tipoDte": 39, "motivo": "cliente sin datos tributarios completos → boleta" },
"nota": "Validación sin efecto: no se tomó folio ni se emitió."
}Úsalos para verificar que tu payload es correcto y que tu código maneja bien la respuesta antes de emitir de verdad. Si además quieres emitir documentos reales de prueba (que sí aparecen en el SII de certificación), pídenos una empresa en ambiente de certificación con su clave fl_test_.
Emitir un documento
POST /documentos emite facturas (33), guías de despacho (52), facturas exentas (34), boletas (39/41), facturas de compra (46) y notas de crédito y débito (61/56). La llamada no espera al SII: valida, asigna folio, timbra, firma y responde 201 con el documento en firmado. El envío sale al instante por nuestra cola y el veredicto llega por webhook — tu request nunca queda colgado.
curl -X POST https://app.facturalibre.cl/api/v1/documentos \
-H "Authorization: Bearer fl_live_su_clave" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-8841" \
-d '{
"tipoDte": 33,
"receptor": {
"rut": "76543210-3",
"razonSocial": "Cliente SpA",
"giro": "Comercio",
"direccion": "Providencia 1234",
"comuna": "Providencia",
"email": "facturas@cliente.cl"
},
"lineas": [
{ "nombre": "Plan mensual", "cantidad": 1, "precioUnitario": 100000 }
]
}'HTTP/1.1 201 Created
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
{
"documento": {
"id": "5f0c…",
"tipoDte": 33,
"folio": 124,
"estado": "firmado",
"ambiente": "produccion",
"fechaEmision": "2026-08-11",
"montos": { "neto": 100000, "exento": 0, "iva": 19000, "total": 119000 },
"trackId": null,
"createdAt": "2026-08-11T14:03:22.000Z"
},
"enviado": false,
"aviso": "El envío al SII está en curso; el veredicto llega por webhook."
}Los precios de boletas (39/41) van con IVA incluido; los de facturas (33/34) son netos. El IVA lo calculamos nosotros en el servidor. Las notas (61/56) exigen referencias al documento que modifican. Desde TypeScript:
const res = await fetch("https://app.facturalibre.cl/api/v1/documentos", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FACTURALIBRE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `pedido-${pedido.id}`,
},
body: JSON.stringify({
tipoDte: 39, // boleta electrónica
lineas: [{ nombre: "Entrada general", cantidad: 2, precioUnitario: 12000 }],
}),
})
if (res.status === 201 || res.status === 200) {
const { documento } = await res.json()
// documento.folio ya está asignado; el veredicto del SII llega por webhook
}Las guías de despacho (52) llevan además el objeto traslado — obligatorio en la guía, opcional en facturas que amparan traslado de bienes (Resolución 154, vigente desde el 1-nov-2026): tipo de traslado (indTraslado 1 venta, 2 ventas por efectuar, 3 consignación, 4 entrega gratuita, 5 traslado interno, 6 otros no venta, 7 devolución), datos del transporte (patente, transportista, chofer, destino) y salida/llegada. Una guía sin venta (traslado interno, entrega gratuita) puede ir con precios en 0: se emite con total 0, como exige el SII.
{
"tipoDte": 52,
"receptor": {
"rut": "76543210-3",
"razonSocial": "Cliente SpA",
"giro": "Comercio",
"direccion": "Providencia 1234",
"comuna": "Providencia"
},
"lineas": [
{ "nombre": "Caja de repuestos", "cantidad": 4, "unidad": "UN", "precioUnitario": 25000 }
],
"traslado": {
"indTraslado": 1,
"tipoDespacho": 2,
"transporte": {
"patente": "ABCD12",
"chofer": { "rut": "12345678-5", "nombre": "Juan Pérez" },
"dirDestino": "Bodega Norte, Camino Interior 5",
"cmnaDestino": "Lampa",
"fchSalida": "2026-08-17",
"hraSalida": "08:30"
}
}
}La factura de compra (46) invierte los roles: tú emites por algo que compraste y el receptor es tu proveedor. Lleva retención total del IVA (código 15): los precios van netos, el IVA se declara completo y se retiene entero, y el total del documento es el neto — lo que le pagas al proveedor. El IVA retenido lo declara tu empresa en su F29 (código 39); en la respuesta viene como montos.ivaRetenido. Es el documento del cambio de sujeto — por ejemplo, servicios de proveedores extranjeros que facturan sin IVA. Las notas de crédito o débito sobre una 46 aún no están disponibles.
Una factura (33/34/46) exige el receptor completo: RUT, razón social, giro, dirección y comuna. Si falta alguno, la emisión —y /validar— la rechazan con el campo señalado, antes de tomar folio. Una boleta (39/41) no necesita receptor. El rut se acepta con o sin puntos y con dígito verificador K mayúscula o minúscula (76543210-3, 78.459.548-K); lo normalizamos.
Largos máximos por campo (del esquema del SII). Si te pasas, el error te dice cuál — con su ruta, p. ej. campo: “lineas → línea 1 → nombre”:
| Campo | Límite |
|---|---|
| lineas | máximo 60 líneas por documento |
| lineas[].nombre | 80 caracteres |
| lineas[].descripcion | 1.000 caracteres (opcional) |
| lineas[].codigo | 35 caracteres (opcional) |
| lineas[].unidad | 4 caracteres (opcional) |
| receptor.razonSocial | 100 caracteres |
| receptor.giro | 40 caracteres — se TRUNCA a 40, no rebota |
| receptor.direccion | 70 caracteres |
| receptor.comuna | 20 caracteres |
| receptor.ciudad | 20 caracteres (opcional) |
| receptor.email | 80 caracteres (opcional) |
| observaciones | 500 caracteres (opcional) |
| referencias | máximo 40; folio hasta 18 caracteres |
| Idempotency-Key | 8 a 80 caracteres |
Eventos: emitir sin saber de DTE
POST /eventos es la puerta para sistemas que no hablan tributario: un ecommerce, un POS, un Zapier. Le cuentas la venta — los ítems al precio final que pagó el cliente (con IVA) y quién compró — y nosotros decidimos el documento: factura si el cliente trae RUT, razón social, giro, dirección y comuna; boleta en cualquier otro caso (y la variante exenta si todos los ítems lo son). La decisión tomada viaja en la respuesta, en decision, para tu propio registro.
curl -X POST https://app.facturalibre.cl/api/v1/eventos \
-H "Authorization: Bearer fl_live_su_clave" \
-H "Content-Type: application/json" \
-d '{
"tipo": "venta",
"origen": "mi-sistema",
"referencia": "orden-1042",
"cliente": { "nombre": "Juana Pérez", "email": "juana@correo.cl" },
"items": [
{ "nombre": "Suscripción mensual", "cantidad": 1, "precioUnitario": 12990 }
]
}'HTTP/1.1 201 Created
{
"documento": { "id": "8a1b…", "tipoDte": 39, "folio": 35012, "estado": "firmado", … },
"decision": {
"tipoDte": 39,
"motivo": "cliente sin datos tributarios completos → boleta"
},
"enviado": false
}- La idempotencia es del evento:
(origen, referencia)emite UNA vez, reintentes lo que reintentes. La referencia es tu número de orden. - Puedes forzar con
documento: "boleta"o"factura"(esta última exige los datos tributarios del cliente). - Si al convertir precios con IVA a netos de factura el redondeo mueve el total en algún peso, te lo decimos en
aviso— nunca en silencio. - Nuestro plugin de WooCommerce usa exactamente este endpoint: cualquier integración puede hacer lo mismo con una llamada.
Folios: nos encargamos nosotros
No tienes que gestionar folios. Los folios (los CAF que autoriza el SII) los solicitamos, cargamos y reabastecemos nosotros, de forma 100% automática, por cada tipo de documento. No pides folios, no cargas CAF, no vigilas el stock: tu sistema solo emite.
- Reabastecimiento automático: cuando el stock de un tipo baja, pedimos el siguiente CAF al SII solos, antes de que se agote. Es un proceso continuo de nuestro lado, invisible para ti y para tu cliente.
- Si en un instante puntual una factura no encuentra folio, no falla: queda en estado
en_esperay la emitimos solas en cuanto hay folios (típicamente minutos). TuPOSTresponde 201 con ese estado; el veredicto final llega por webhook. No reintentes: ya está encolada. - El único límite es la cantidad de timbraje que el SII te tenga autorizada —algo que depende de tu situación ante el SII, no de esta API—. Si alguna vez hay que gestionarlo, lo vemos nosotros contigo; tu integración no necesita hacer nada.
Idempotencia
Manda Idempotency-Key (8–80 caracteres, p. ej. el id de tu pedido) en cada emisión. Reintentar con la misma clave jamás emite dos veces ni consume otro folio. La garantía es un índice único en la base, no una caché — sobrevive a instancias paralelas.
Cuándo es seguro reintentar con la misma clave: casi siempre. Ante cualquier duda —un 500, un timeout, una respuesta que no llegó— repite el request con la misma Idempotency-Key. Si el documento ya avanzó, te devolvemos ese mismo documento (200) en vez de crear otro. Solo hay un caso en que debes usar una clave nueva, y te lo decimos explícitamente con un 422: cuando un intento anterior alcanzó a tomar el folio pero falló de forma irreversible. Ese folio queda registrado (se declara anulado) y no se reutiliza — con una clave nueva emites un documento nuevo, sin duplicar en el SII. Nunca tienes que adivinar: o te devolvemos el documento, o te pedimos la clave nueva.
| Estado del intento previo | Qué pasa al reintentar con la misma clave |
|---|---|
| enviado / aceptado / rechazado | Te devolvemos ese documento (200). No dupliques. |
| firmado | Emitido, esperando al SII. Te devolvemos ese documento (200). |
| en_espera | Sin folios, se emitirá solo. Te devolvemos ese documento (200). |
| error/borrador con folio tomado | 422: usa una clave nueva (ese folio no se reutiliza). |
Consultar y listar
GET /documentos/{id} devuelve el documento; GET /documentos lista con filtros (estado, tipoDte, folio, receptorRut, desde/hasta, limit hasta 100) y paginación por cursor: usa el siguienteCursor de cada respuesta hasta que venga null.
Para reconciliar cuando perdiste la respuesta de un POST, filtra por tu clave: GET /documentos?idempotencyKey=pedido-8841 devuelve el documento de esa clave (o una lista vacía si nunca se emitió). Cada documento incluye su idempotencyKey en la respuesta.
| Estado | Qué significa |
|---|---|
| firmado | Folio y timbre asignados; el envío al SII está en curso |
| enviado | El SII lo recibió (hay trackId); espera veredicto |
| aceptado | Aceptado por el SII — terminal |
| aceptado_reparos | Aceptado con reparos — terminal, revisa siiGlosa |
| rechazado | Rechazado por el SII — terminal, el folio quedó consumido |
| en_espera | Sin folios disponibles; se emitirá solo (solo facturas) |
| error | No se pudo completar la emisión; revisa siiGlosa |
| anulado | Anulado por una nota de crédito posterior |
PDF y XML
GET /documentos/{id}/pdf entrega la representación impresa (con timbre PDF417) y GET /documentos/{id}/xml el DTE firmado (?sobre=1 para el sobre de envío completo). Mientras el documento esté en proceso (por ejemplo firmado o en_espera) responden 409; espera el webhook de aceptación o vuelve a consultar. El PDF también le llega solo por correo al receptor si la emisión trae su email. Si prefieres entregarlo tú, manda "enviarEmail": false en cada emisión: el documento se emite igual y no sale ningún correo de nuestra parte.
Para no depender de recordar el flag en cada request, puedes apagar el envío a nivel de toda la empresa desde Mi negocio → Integraciones y API → Correo al receptor. Con eso, ninguna emisión de la empresa manda correo al receptor, aunque el request pida enviarEmail: true — defensa en profundidad para cuando eres tú quien entrega los documentos.
Webhooks
Registramos hasta 3 endpoints https por empresa. Cada evento llega firmado con tu secreto whsec_ (se muestra una vez al crear el endpoint):
POST https://tu-sistema.cl/webhooks/facturalibre
FacturaLibre-Firma: t=1754920000,v1=ab12…
FacturaLibre-Evento: dte.aceptado
FacturaLibre-Entrega: 9c41… ← dedupe: procesa cada id una sola vez
{
"id": "9c41…",
"evento": "dte.aceptado",
"creadoEn": "2026-08-11T14:05:10.000Z",
"datos": {
"documento": {
"id": "5f0c…",
"tipoDte": 33,
"folio": 124,
"estado": "aceptado",
"ambiente": "produccion",
"fechaEmision": "2026-08-11",
"receptor": { "rut": "76543210-3", "razonSocial": "Cliente SpA", "giro": "Comercio", "direccion": "Providencia 1234", "comuna": "Providencia", "email": "facturas@cliente.cl" },
"montos": { "neto": 100000, "exento": 0, "iva": 19000, "total": 119000 },
"trackId": "12355834916",
"siiGlosa": null,
"idempotencyKey": "pedido-8841",
"createdAt": "2026-08-11T14:03:22.000Z"
}
}
}| Evento | Cuándo llega |
|---|---|
| dte.en_espera | Sin folios en el instante: quedó en cola y se emitirá solo — espera el dte.aceptado, no reintentes |
| dte.enviado | El SII recibió el documento (trae trackId) |
| dte.aceptado | Veredicto: aceptado |
| dte.aceptado_reparos | Veredicto: aceptado con reparos |
| dte.rechazado | Veredicto: rechazado |
| dte.error | La emisión no se pudo completar |
El cuerpo es el mismo para los cinco eventos: { id, evento, creadoEn, datos: { documento } }. Lo único que cambia es el string evento. El documento es el mismo objeto que devuelve GET /documentos/{id}. En dte.rechazado y dte.aceptado_reparos, el motivo del SII viene como texto en datos.documento.siiGlosa (es null cuando no hay glosa):
{
"id": "9c41…",
"evento": "dte.rechazado",
"creadoEn": "2026-08-11T14:07:02.000Z",
"datos": { "documento": {
"id": "5f0c…", "folio": 124, "estado": "rechazado",
"siiGlosa": "DTE con folio fuera de rango autorizado", // ← el motivo del SII, texto
…
} }
}Verifica SIEMPRE la firma antes de procesar — sobre el body crudo, tal como llegó (re-serializar el JSON cambia los bytes y rompe la verificación):
import { createHmac, timingSafeEqual } from "node:crypto"
export function verificarFirma(req: { headers: Headers; bodyCrudo: string }): boolean {
const firma = req.headers.get("facturalibre-firma") ?? "" // "t=1754920000,v1=ab12…"
const partes = Object.fromEntries(firma.split(",").map((p) => p.split("=", 2)))
const t = Number(partes["t"])
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false // tolerancia 5 min
const esperado = createHmac("sha256", process.env.FACTURALIBRE_WHSEC!)
.update(`${t}.${req.bodyCrudo}`) // el body CRUDO, sin re-serializar
.digest("hex")
const a = Buffer.from(esperado)
const b = Buffer.from(partes["v1"] ?? "")
return a.length === b.length && timingSafeEqual(a, b)
}- Responde 2xx rápido (antes de 5 segundos) y procesa después; otra respuesta cuenta como fallo.
- Reintentamos con backoff durante ~44 horas. Deduplica por
FacturaLibre-Entrega: el mismo id puede llegar más de una vez. - Tras demasiados fallos seguidos el endpoint se desactiva solo y avisamos por correo a los dueños de la empresa. Puedes reactivarlo desde el panel cuando tu servicio vuelva a estar arriba.
Desde Mi negocio → Integraciones y API gestionas los webhooks a mano: ves el log de últimas entregas con su estado, código HTTP y error; reintentas una entrega puntual (para depurar «¿me llegó y lo perdí, o nunca salió?»); rotas el secreto whsec_ sin recrear el endpoint (el nuevo se muestra una vez; actualízalo en tu servidor); y reactivas un endpoint caído.
Boletas de honorarios (BHE)
Para cuentas de persona natural (plan Honorarios). La BHE no es un DTE: la emite el SII a nombre del titular, con la retención vigente calculada por el propio SII, y no tiene ambiente de prueba — solo aceptan claves fl_live_ y cada POST crea un documento tributario real. El contrato es síncrono: la respuesta ya trae el número y el código de barras.
POST /v1/boletas-honorarios
{
"receptorRut": "76543210-K",
"receptorNombre": "Empresa Cliente SpA",
"receptorDomicilio": "Av. Providencia 1234",
"comunaCodigo": 15103,
"lineas": [{ "glosa": "Asesoría agosto 2026", "monto": 500000 }],
"fecha": { "dia": 18, "mes": 8, "anio": 2026 },
"retiene": "receptor",
"emailReceptor": "pagos@cliente.cl"
}retiene:receptor(la empresa que paga retiene — lo normal) oemisor. Los montos son brutos en CLP enteros; la retención y el líquido los calcula el SII y vuelven en la respuesta.GET /v1/boletas-honorarios?anio=&mes=lista lo emitido;GET /v1/boletas-honorarios/:id/pdfbaja el PDF oficial del SII.- Manda
Idempotency-Key(header o campo del cuerpo): el mismo valor devuelve la misma boleta, nunca una segunda — es tu seguro contra reintentos. - Un 502 con
ambigua: true— o un timeout/504, trátalo igual — significa que el SII no confirmó la emisión: no reintentes a ciegas sin Idempotency-Key — consulta el listado primero, o duplicarás la boleta. - Un 409 significa que la clave tributaria guardada dejó de servir (el titular la cambió en el SII): se repone desde el portal.
Errores y límites
Todos los errores responden { "error": "…" } (y campo cuando la validación sabe señalarlo). La emisión (POST /documentos y /eventos) tiene un límite por minuto por clave; su estado viaja en los headers X-RateLimit-Limit / -Remaining / -Reset en la respuesta de emisión y en todo 429 (que además trae Retry-After). Los GET (consultas, PDF, XML) no consumen esa cuota, así que no llevan esos headers.
| Código | Qué significa |
|---|---|
| 400 | Cuerpo o parámetros inválidos; `campo` indica dónde |
| 401 | Clave ausente, no válida o revocada |
| 402 | La suscripción de la empresa no está al día |
| 403 | La clave no calza con el ambiente de la empresa, o la empresa no puede emitir |
| 404 | El documento no existe para esta clave (empresa y ambiente) |
| 409 | El recurso aún no está disponible (p. ej. PDF de un documento en proceso) |
| 422 | El SII o el motor rechazó la emisión; `error` trae el motivo |
| 429 | Límite de peticiones; respeta `Retry-After` |
| 500 | Error nuestro; reintenta con la misma Idempotency-Key |
| 503 | No se pudo verificar la clave; reintenta en un momento |
¿Listo para integrar?
Escríbenos y te entregamos tu clave fl_test_ para partir contra el ambiente de certificación del SII, sin consumir folios reales.