@folyo/sdk
v0.2.1
Published
SDK oficial de Folyo para facturación electrónica chilena (DTE + SII). Del código al SII.
Maintainers
Readme
@folyo/sdk
SDK oficial de Folyo para facturación electrónica chilena (DTE + SII). Del código al SII. Sin escalas.
- TypeScript nativo, tipos derivados del OpenAPI oficial de la API.
- ESM + CJS, sin dependencias de runtime (usa el
fetchglobal de Node 18+). - Manejo de errores tipado, reintentos con backoff y emisión asíncrona con polling.
- Redacción automática de credenciales: el cliente y los errores nunca exponen
tu API key, tu JWT ni material sensible (clave SII,
.pfx, CAF, secretos).
Instalación
pnpm add @folyo/sdk
# o
npm install @folyo/sdk
# o
yarn add @folyo/sdk
# o
bun add @folyo/sdkRequiere Node.js >= 18.
Quickstart: emitir una factura electrónica (DTE 33)
import { Folyo } from "@folyo/sdk";
const folyo = new Folyo({ apiKey: process.env.FOLYO_API_KEY! });
// Emisión asíncrona + espera del resultado por polling.
const job = await folyo.dte.emitirYEsperar(
{
tipo_dte: 33, // Factura Electrónica
receptor: {
rut: "12.345.678-9",
razon_social: "Cliente SpA",
giro: "Comercio",
direccion: "Av. Siempre Viva 123",
comuna: "Santiago",
},
detalle: [
{
nombre: "Servicio de consultoría",
cantidad: 1,
precio: 100000,
monto: 100000, // requerido
},
],
// Referencias opcionales. `tipo_doc_ref` acepta códigos del SII (ej. 801 =
// orden de compra) o un código de texto de hasta 3 caracteres acordado con
// el receptor, como "HES" (Hoja de Entrada de Servicios). Para facturar al
// MOP usa `tipo_doc_ref` "UDP" (equivale al 802), el código de la unidad de
// pago que entrega el mandante en `folio_ref` y su descripción en `razon_ref`.
referencia: [
{ tipo_doc_ref: 801, folio_ref: "OC-2026-1487" },
{ tipo_doc_ref: "HES", folio_ref: "4500123456" },
{ tipo_doc_ref: "UDP", folio_ref: "2020", razon_ref: "NIVEL_CENTRAL_SUBSECRETARIA" },
],
},
{ idempotencyKey: crypto.randomUUID() },
);
console.log(job.estado); // "completed"
console.log(job.result?.track_id, job.folio);Nota:
completedsignifica que el SII recibió el documento (tienes folio y track_id). El veredicto (aceptación o rechazo) llega después: el documento queda en estadoenviadoy Folyo lo consulta automáticamente hasta resolverlo. El SII puede demorar desde minutos hasta más de una hora; no reemitas por eso — suscríbete al webhook de cambio de estado.
Emisión cruda (solo encolar) + polling manual
const encolada = await folyo.dte.emitir(body, { idempotencyKey: crypto.randomUUID() });
console.log(encolada.job_id, encolada.folio);
// más tarde, o desde un webhook `dte.emitido`:
const job = await folyo.dte.getEmision(encolada.job_id!);Impuestos adicionales y retenciones por línea
Una línea puede llevar impuestos con el código del impuesto adicional del
DL 825 (art. 42). Tú mandas el código; Folyo resuelve la tasa desde su catálogo,
calcula el monto sobre la misma base que el IVA y lo desglosa en el documento.
import type { CodigoImpuestoAdicional } from "@folyo/sdk";
const cervezas: CodigoImpuestoAdicional = "26";
const job = await folyo.dte.emitirYEsperar(
{
tipo_dte: 33,
receptor: { rut: "12.345.678-9", razon_social: "Distribuidora SpA", giro: "Comercio" },
detalle: [
{
nombre: "Cerveza lager 330 ml",
cantidad: 120,
precio: 12500,
monto: 1500000,
impuestos: [{ codigo: cervezas }], // cervezas y otras bebidas alcohólicas: 20,5%
},
],
},
{ idempotencyKey: crypto.randomUUID() },
);
// Neto 1.500.000 + IVA 285.000 + impuesto 307.500 = total 2.092.500
for (const imp of job.result?.impuestos ?? []) {
console.log(imp.codigo, imp.glosa, imp.tasa, imp.monto, imp.retencion);
// "26" "Cervezas y otras bebidas alcohólicas" 20.5 307500 false
}| Código | Impuesto | Tasa |
|---|---|---|
| 24 | Licores, piscos y whisky (incluye aguardientes y vinos licorosos) | 31,5% |
| 25 | Vinos | 20,5% |
| 26 | Cervezas y otras bebidas alcohólicas | 20,5% |
| 27 | Bebidas analcohólicas y minerales | 10% |
| 271 | Bebidas analcohólicas con azúcar elevada | 18% |
| 15 | IVA retenido total genérico por cambio de sujeto | 19% |
| 30/32/33/34/36/37/48 | Retención parcial: legumbres, ganado, madera, trigo, arroz, hidrobiológicos y frambuesas | 10/8/8/4/10/10/14% |
| 31/38/39/41/47 | Retención total: silvestres, chatarra, PPA, construcción y cartones | 19% IVA |
| 23/44/45 | Suntuarios | 15/15/50% |
| 28/35/51/52 | Específicos de diésel, gasolina y gases | monto fijo, sin tasa |
| 17 | IVA anticipado de faenamiento | 5% de la base especial faenamiento |
| 18/19 | IVA anticipado de carne y harina | 5/12% |
| 46 | IVA retenido oro | 100% del IVA, solo 33 y NC/ND 56/61 referenciarias |
Los códigos se validan por tipo de DTE. Las retenciones parciales van en una
factura de compra 46 o en su NC/ND 56/61 de referencia. Para que una de
ellas retenga el IVA completo, manda el código base y ndf: true: Folyo usa la
variante total (30→301, 32→321, 33→331, 34→341, 36→361, 37→371,
48→481) y omite iva_no_retenido. La API no consulta nóminas SII; quien emite
debe cumplir la calidad de agente retenedor que exija el régimen.
Envía normalmente el código parcial con ndf: true, sin tasa ni monto:
Folyo lo normaliza a la variante total. También puede enviarse la variante total
canónica directamente con ndf: true; sólo para revalidación admite tasa: 19
y un monto que cuadre exactamente con la base del código.
Una línea exenta no puede llevar impuesto. Con impuestos o retenciones, un
descuento/recargo global sólo puede ser porcentual: se prorratea en cada base
antes de calcular los montos. Un descuento global en pesos responde
400 INVALID_REQUEST antes de reservar folio. El tipo 43 sólo admite
adicionales, nunca retenciones ni iva_no_retenido. Los códigos 17 y 46
siguen cerrados. Mientras una familia no esté validada en certificación para tu
empresa, producción responde 422 IMPUESTO_PENDIENTE_CERT.
El código 15 conserva su compatibilidad histórica sin descuento ni recargo
global. Si se combina con uno porcentual, producción exige que el código 15
esté habilitado explícitamente; de lo contrario responde
422 IMPUESTO_PENDIENTE_CERT.
El código 17 exige faenamiento en cada línea afectada: codigo_cpcs
(1701 a 1706), cantidad_cabezas positiva y monto_base_faena positivo.
Ambas cantidades usan hasta 6 decimales y se normalizan a micro-unidades cuyo
valor escalado no excede 9007199254740991 (máximo nominal
9007199254.740991); la base y su suma no superan 9007199254740991.
La línea usa cantidad en KG; Folyo deriva el CPCS, Retenedor, QtyRef UN,
el impuesto 17 y monto_base del resultado. Solo aplica a 33 y a 56/61 que
referencian una 33 compatible; no admite descuentos o recargos globales ni
mezcla con otros impuestos. El emisor debe contar previamente con acreditación
operativa como agente retenedor: Folyo no hace lookup automático ni afirma una
aprobación digital persistida; producción se habilita después de certificación.
El código 46 es una retención total de oro para
33 y sus 56/61 referenciarias; no se usa en la Factura de Compra DTE 46.
tasa y monto son opcionales y normalmente no se envían: el catálogo de
Folyo resuelve la tasa vigente. Si entregas una tasa, debe ser finita, estar
entre 0,01% y 100% y tener como máximo dos decimales; debes enviarla con el
mismo valor en todas las líneas que llevan ese código, o en ninguna. Si
entregas monto, debes entregarlo en todas esas líneas y la suma debe ser
exactamente round(baseDelCodigo * tasa / 100): no se acepta redondear cada
línea por separado. monto es obligatorio para los específicos fijos
28, 35, 51 y 52, que no llevan tasa.
Para la retención total del IVA en una factura de compra (46), la forma
recomendada es impuestos: [{ codigo: "15" }] en cada línea afecta, sin
tasa ni monto: el código 15 siempre retiene el IVA total al 19% y produce
el mismo XML que retencion_iva_total: true, que queda obsoleto pero sigue
funcionando.
Para los servicios agrícolas de la Res. Ex. SII 83/2026, el uso del código 15
es una inferencia pendiente de confirmación tributaria y certificación. La
capacidad técnica no acredita que el emisor cumpla los requisitos de ese
régimen; la API no consulta ni acredita esa condición ante el SII.
El desglose también vuelve en impuestos del listado de documentos, junto con
monto_neto, monto_exento, monto_iva e iva_no_retenido cuando la
retención fue parcial:
const [doc] = await folyo.dte.listDocumentos({ limite: 1 });
for (const imp of doc?.impuestos ?? []) {
console.log(imp.codigo, imp.glosa, imp.tasa, imp.monto);
}Autenticación
Dos esquemas, mutuamente excluyentes:
// API key (recomendado server-side; no expira; fija tenant y empresa).
const folyo = new Folyo({ apiKey: "tu-api-key" });
// JWT (sesión de usuario).
const folyo = new Folyo({ token: "access-token-jwt" });El SDK envía la API key tal cual en el header
X-API-Key(no asume prefijo). Con API key la empresa queda fijada por la key.
Configuración
const folyo = new Folyo({
apiKey: process.env.FOLYO_API_KEY!,
baseURL: "https://api.folyo.cl", // por defecto; usa http://localhost:8080 en local
timeoutMs: 30000, // timeout por request
maxRetries: 2, // reintentos ante 429 / 503 idempotentes
userAgent: "mi-app/1.0", // sufijo opcional del User-Agent
// fetch: customFetch, // inyectable (tests, proxies)
});Recursos disponibles
| Namespace | Métodos |
|---|---|
| folyo.dte | emitir, emitirYEsperar, getEmision, listDocumentos, downloadXml, downloadPdf, regeneratePdf, getEstado, getEstadoEnvio, getEmitidos, getRecibidos, getContribuyente, getSituacionTributaria |
| folyo.folios | info, cargarCaf, solicitar |
| folyo.rcv | periodos, get, sync, resumenIva |
| folyo.clientes | list, upsert, importar, buscarPorRut, update, delete |
| folyo.empresa | list, seleccionar |
| folyo.acuse | registrar, pendientes, estado |
| folyo.rcof | enviar, resumen |
| folyo.webhooks | list, create, update, delete |
| folyo.apiKeys | list, create, delete |
Algunos endpoints (clientes, RCV, plantillas, listado de documentos) requieren "panel operativo" y pueden devolver
403en planes solo-API.
Idempotencia
Para dte.emitir / dte.emitirYEsperar, pasa una idempotencyKey (8-64 chars
[a-zA-Z0-9_-], UUID v4 recomendado). Un reenvío con la misma key y el mismo
cuerpo devuelve el mismo job_id sin quemar un folio nuevo. Además habilita
el reintento seguro ante 503 del lado del SDK.
const key = crypto.randomUUID();
await folyo.dte.emitir(body, { idempotencyKey: key });
// reintento seguro con la misma key → mismo job_id
await folyo.dte.emitir(body, { idempotencyKey: key });Una misma key con un cuerpo distinto produce 409 IDEMPOTENCY_KEY_CONFLICT
(FolyoValidationError).
Manejo de errores
Todos los errores heredan de FolyoError y exponen status, code y requestId
(nunca el cuerpo de la request).
import {
FolyoError,
FolyoAuthError, // 401
FolyoRateLimitError, // 429 (.retryAfter en segundos)
FolyoQuotaError, // 402 / 403 (PLAN_LIMIT, PAYMENT_REQUIRED, ...)
FolyoValidationError, // 400 / 409 / 422
FolyoConnectionError, // red / timeout
FolyoSiiUnavailableError, // 502 / 503 / 504 (.retryAfter)
} from "@folyo/sdk";
try {
await folyo.dte.emitir(body, { idempotencyKey: crypto.randomUUID() });
} catch (err) {
if (err instanceof FolyoRateLimitError) {
console.warn(`Rate limit; reintenta en ${err.retryAfter}s`);
} else if (err instanceof FolyoQuotaError) {
console.error(`Cuota/plan: ${err.code}`); // PLAN_LIMIT, PAYMENT_REQUIRED...
} else if (err instanceof FolyoSiiUnavailableError) {
console.error("El SII no está disponible, reintenta más tarde.");
} else if (err instanceof FolyoError) {
console.error(`${err.code ?? err.status}: ${err.message} (req ${err.requestId})`);
}
}Reintentos automáticos
El SDK reintenta con backoff exponencial (respetando Retry-After) ante 429 y
503 solo en operaciones idempotentes: cualquier GET, y POST /dte/emitir
solo si entregaste una Idempotency-Key. Ajusta con maxRetries.
Seguridad
- El cliente y todos los objetos de error redactan la credencial a
***al serializar (JSON.stringify) o al imprimirse (util.inspect). - Los errores nunca incluyen el cuerpo de la request (que puede traer la clave
del SII o un
.pfx). - Sin telemetría ni logging por defecto.
Licencia
MIT — Folyo Technologies SpA.
