SDK TypeScript para facturación electrónica en Ecuador (SRI)
SDK oficial Node/TypeScript para emitir facturas electrónicas en Ecuador con type safety. Quickstart, ejemplos, manejo de errores y comparativa con cURL.
Si trabajas con Node.js o TypeScript y necesitas emitir facturas electrónicas en Ecuador desde tu app, llamar a la API SOAP del SRI directamente es una pesadilla: XMLs gigantes, firmas XAdES-BES, certificados P12, manejo de respuestas mal documentadas. El SDK oficial de Factuplan resuelve eso con una librería tipada que te deja emitir una factura en menos de 10 líneas.
En este post te muestro cómo arrancar con el SDK TypeScript de Factuplan (factuplan en npm), por qué un SDK tipado le gana a cURL directo, y los patrones de error que vas a encontrar en producción.
¿Por qué usar un SDK y no llamar a la API con cURL?
Si llamas directamente a una API REST con cURL o fetch, ganas en simplicidad inicial pero pierdes:
- Type safety: no sabes qué campos son obligatorios hasta que el servidor te devuelve 400.
- Autocompletado: tienes que abrir docs cada vez que escribes un endpoint.
- Errores tipados: tienes que distinguir a mano un 401 de un 429 o de un error de validación.
- Manejo de respuestas: cada endpoint tiene un sobre
{ data, meta }que tienes que parsear manualmente.
El SDK de Factuplan resuelve todo eso de fábrica.
Comparativa: emitir factura con cURL vs SDK
Opción A: cURL crudo (60+ líneas de bash, sin type safety)
curl https://api-rest.factuplan.com.ec/v1/developer/invoices \ -H "X-API-Key: $FACTUPLAN_API_KEY" \ -H "x-taxpayer-ruc: 0950194407001" \ -H "Content-Type: application/json" \ -d '{ "customer": { "identificationType": "RUC", "identification": "0993378150001", "legalName": "Cliente S.A.", "email": "facturas@cliente.ec" }, "items": [{ "code": "SERV-001", "description": "Servicio de consultoría", "quantity": 1, "unitPrice": 150.00, "taxType": "IVA_RATE", "tax": 15 }], "sendEmail": true }'Sin autocomplete, sin validación. Si te equivocas en identificationType o taxType, te enteras cuando la API responde 400.
Opción B: SDK TypeScript (10 líneas, todo tipado)
import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001",})
const factura = await factuplan.invoices.create({ customer: { identificationType: "RUC", identification: "0993378150001", legalName: "Cliente S.A.", email: "facturas@cliente.ec", }, items: [ { code: "SERV-001", description: "Servicio de consultoría", quantity: 1, unitPrice: 150, taxType: "IVA_RATE", tax: 15, }, ], sendEmail: true,})
console.log(factura.accessKey) // 49 dígitos de clave de acceso SRIEl editor te autocompleta identificationType con "RUC" | "CEDULA" | "PASSPORT" | "FINAL_CONSUMER" | "EXTERIOR" y taxType con "IVA_0" | "IVA_RATE" | "NOT_TAXABLE" | "EXEMPT". Si pones cualquier otra cosa, el TypeScript compiler te lo marca antes de ejecutar.
Instalación
El SDK soporta Node.js 18+, ESM y CJS, cero dependencias externas:
# npmnpm install factuplan
# pnpmpnpm add factuplan
# yarnyarn add factuplanSi usas Deno o Bun, también funciona porque el paquete está publicado con exports ESM compatibles.
Crear una API key
Antes de escribir código, necesitas una API key del workspace:
- Crea cuenta en app.factuplan.com.ec.
- Anda a Developer → Create API key.
- Copia la clave (
ak_test_*oak_live_*). Se muestra una sola vez. - Guárdala en una variable de entorno (
FACTUPLAN_API_KEY).
Nunca la subas al repositorio. Nunca la expongas en el frontend.
| Prefijo | Entorno | Comportamiento |
|---|---|---|
ak_test_* | Pruebas | Comprobantes ficticios, eliminados cada hora |
ak_live_* | Producción | Comprobantes reales firmados y enviados al SRI |
Empieza siempre en pruebas. Cuando todo funcione, intercambia la API key por la de producción sin tocar código.
Inicializar el cliente
import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // RUC del contribuyente emisor})El SDK adjunta automáticamente la cabecera X-API-Key y x-taxpayer-ruc en cada request. No tienes que pensar en autenticación.
Si vas a usar operaciones que no requieren emisor (consultar tu uso del mes, por ejemplo), puedes omitir el ruc:
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!)
const usage = await factuplan.usage()console.log(usage) // la API responde { currentMonth: { documents, cost }, plan, monthlyQuota, monthlyUsed }Hello world: emitir una factura en 10 líneas
import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001",})
const factura = await factuplan.invoices.create({ customer: { identificationType: "RUC", identification: "0993378150001", legalName: "Empresa Demo S.A.", email: "facturas@empresademo.ec", saveToContacts: true, }, items: [ { code: "SERV-001", description: "Servicio de consultoría técnica", quantity: 1, unitPrice: 150, taxType: "IVA_RATE", tax: 15, }, ], sendEmail: true,})
console.log("Clave de acceso SRI:", factura.accessKey)console.log("Estado:", factura.status) // DRAFT: la emisión sigue en segundo plano
// El enlace al RIDE se pide aparte, cuando la factura ya está en COMPLETED:// const { url } = await factuplan.invoices.downloadPdf(factura.id)Eso es todo. create() responde enseguida con la factura en DRAFT y Factuplan se encarga, en segundo plano, de:
- Generar el XML SRI.
- Firmarlo con tu certificado P12 (cargado previamente en tu workspace).
- Enviarlo al SRI para autorización.
- Generar el RIDE PDF con tu branding.
- Mandar el email al cliente (salvo que envíes
sendEmail: false).
Cuando termina, la factura queda en COMPLETED.
Type safety: el corazón del SDK
Todo el SDK está tipado con TypeScript. Esto significa que:
1. Autocompletado en tu editor
Al escribir factuplan.invoices. tu editor te lista todos los métodos disponibles:
factuplan.invoices.create(...)factuplan.invoices.get(id)factuplan.invoices.list({ page, limit, status, devReference })factuplan.invoices.void(id, reason) // anular (solo en Factuplan, no ante el SRI)2. Validación en tiempo de compilación
const factura = await factuplan.invoices.create({ customer: { identificationType: "DNI", // ❌ Error TS: Type "DNI" is not assignable // El SDK solo acepta "RUC" | "CEDULA" | "PASSPORT" | "FINAL_CONSUMER" | "EXTERIOR" identification: "0993378150001", legalName: "Empresa Demo S.A.", email: "facturas@empresademo.ec", }, // ...})3. Tipado de respuestas
const factura = await factuplan.invoices.create({ /* ... */ })
// factura es typed: tiene id, accessKey, sequential, status, total y, si enviaste devReference, devReference y duplicateconst status: string = factura.status // "DRAFT" al crear; termina en "COMPLETED" (o REJECTED / RETURNED / ERROR)4. Campos opcionales documentados
// Punto de emisión: 3 formas de indicarlo. El SDK no las modela como unión: son tres campos// opcionales y la combinación la valida la API (400 si falta uno de los dos códigos).// Conceptualmente:type EmissionPoint = | { emissionPointId: string } // Por UUID | { establishment: string; emissionPoint: string } // Por códigos SRI | {} // Auto-detect
const factura = await factuplan.invoices.create({ emissionPointId: "0199c2d4-5d21-7e0b-a4c8-9b1f3e7d2a60", // ✓ válido // o // establishment: "001", emissionPoint: "001", // ✓ válido // o // (nada) // ✓ auto-detecta si solo hay un punto activo // ...})
Operaciones disponibles
El SDK cubre el ciclo completo de facturación:
Clientes (customers)
// Crearconst cliente = await factuplan.customers.create({ identificationType: "RUC", identification: "0993378150001", legalName: "Empresa Demo S.A.", email: "facturas@empresademo.ec",})
// Listar con paginaciónconst { data: clientes, meta } = await factuplan.customers.list({ page: 1, limit: 20, search: "empresa",})
// Obtenerconst guardado = await factuplan.customers.get(cliente.id)
// Actualizarawait factuplan.customers.update(cliente.id, { email: "nuevo@empresademo.ec",})
// Eliminar (lo desactiva: isActive pasa a false)await factuplan.customers.delete(cliente.id)Productos (products)
const producto = await factuplan.products.create({ code: "SERV-001", name: "Servicio de consultoría", unitPrice: 150, type: "SERVICE", // PRODUCT | SERVICE taxType: "IVA_RATE", // IVA_0 | IVA_RATE | NOT_TAXABLE | EXEMPT})Facturas (invoices)
const factura = await factuplan.invoices.create({ /* ... */ })
// Anular: marca la factura como VOIDED en Factuplan. No emite nota de crédito// ni la anula ante el SRI: eso lo haces tú (por ejemplo con creditNotes.create).// Solo se puede anular una factura en AUTHORIZED o COMPLETED.await factuplan.invoices.void(factura.id, "Error en datos del cliente")Notas de crédito (creditNotes)
const notaCredito = await factuplan.creditNotes.create({ invoiceAccessKey: "1305202401099337815000110010010000056781234567811", // clave de acceso de la factura autorizada reason: "Devolución parcial de mercadería", items: [ /* ... */ ], devReference: "nc-pedido-1001", // opcional: hace idempotente la emisión})Guías de remisión
El SDK no tiene un recurso waybills y la API de desarrolladores no tiene una ruta para emitirlas ni listarlas. Las guías de remisión se emiten desde el panel de Factuplan, o enviando tu propio XML <guiaRemision>: sin firmar, con factuplan.invoices.signAndAuthorize({ xml }) (POST /v1/developer/sign-and-authorize); ya firmado, con factuplan.documents.authorizeSigned({ xml }) (POST /v1/developer/authorize-signed, solo con API key de producción). En ambos casos recibes la respuesta del SRI en la misma llamada: no se genera PDF ni se envía correo.
Retenciones (withholdings), notas de débito (debitNotes) y liquidaciones de compra (purchaseSettlements)
Disponibles con los mismos métodos: create, list, get, getStatus, downloadXml, downloadPdf y void.
Manejo de errores
El SDK lanza errores tipados que puedes diferenciar con instanceof:
import { Factuplan, FactuplanError, AuthenticationError, RateLimitError,} from "factuplan"
try { const factura = await factuplan.invoices.create({ /* ... */ })} catch (err) { if (err instanceof AuthenticationError) { // 401 — API key inválida, revocada o expirada console.error("Autenticación falló") } else if (err instanceof RateLimitError) { // 429 — superaste el límite de peticiones: espera y repite la misma petición console.error("Límite alcanzado:", err.message) } else if (err instanceof FactuplanError) { // Cualquier otro error de la API: 400 validación (GENERAL_0002), // 403 plan o cupo, 409 devReference (API_10011 / API_10012), 5xx… console.error("Error:", err.statusCode, err.code, err.message, err.details) } else { // Error no relacionado al SDK (red, timeout a los 30 s, etc.) throw err } // Un rechazo del SRI no llega aquí: la emisión es asíncrona y aparece // después como estado REJECTED / RETURNED o en el webhook invoice.rejected.}Reintentos y timeout
El SDK no reintenta por su cuenta: cada llamada hace una sola petición y la corta a los 30 segundos. Si quieres reintentar ante un 5xx, un 429 por límite de peticiones o un error de red, hazlo tú, y en las emisiones envía siempre la misma devReference (un campo del body: no hay ningún header de idempotencia) para que un reintento nunca emita dos veces:
import { Factuplan, FactuplanError, RateLimitError, type CreateInvoiceInput } from "factuplan"
const factuplan = new Factuplan(apiKey, { ruc: "0950194407001", timeout: 30000, // ms, default 30000})
async function emitirConReintentos(payload: CreateInvoiceInput, devReference: string) { for (let intento = 1; ; intento++) { try { // El mismo contenido y la MISMA devReference en cada intento return await factuplan.invoices.create({ ...payload, devReference }) } catch (err) { const temporal = err instanceof RateLimitError || // 429 por límite de peticiones !(err instanceof FactuplanError) || // red o timeout: no llegó ninguna respuesta err.statusCode >= 500 || err.statusCode === 408 if (!temporal || intento === 5) throw err // 400, 401, 403, 409… no cambian al repetir await new Promise((r) => setTimeout(r, 1000 * 2 ** (intento - 1))) } }}
const factura = await emitirConReintentos(payload, "pedido-1001")if (factura.duplicate) { // Un intento anterior sí llegó: no se emitió nada nuevo. // Mira factura.status para saber en qué quedó.}Con la misma referencia y el mismo contenido, la API responde con la factura que ya existía y duplicate: true. Con la misma referencia y otro contenido responde 409 (API_10011), y si esa factura fue anulada, 409 (API_10012).
Webhooks
Si tienes un endpoint que recibe eventos de Factuplan (autorización exitosa, rechazo, etc.), el SDK ofrece verificación de firma:
import { verifyWebhookSignature } from "factuplan"
// Express/Next.js handlerapp.post("/webhooks/factuplan", express.raw({ type: "application/json" }), (req, res) => { const valida = verifyWebhookSignature( req.body, // Buffer crudo req.headers["x-factuplan-signature"], process.env.FACTUPLAN_WEBHOOK_SECRET!, )
if (!valida) { res.status(400).send("Webhook signature inválida") return }
const event = JSON.parse(req.body.toString("utf-8"))
switch (event.event) { case "invoice.authorized": // event.data: receiptId, type, accessKey, authorizationNumber, documentNumber, total… console.log("Factura autorizada:", event.data.accessKey) break case "invoice.rejected": console.log("Rechazada por SRI:", event.data.reason) break // ... }
res.json({ received: true })})Comparativa con HTTP directo
| Aspecto | cURL/fetch directo | SDK TypeScript |
|---|---|---|
| Autocompletado | ❌ No | ✅ Sí |
| Reintentos | Manual | Manual (el SDK no reintenta; usa devReference para que sean seguros) |
| Manejo de errores | try/catch genérico | ✅ Errores tipados |
| Type safety | ❌ Cero | ✅ 100% |
| Curva de aprendizaje | Alta (XMLs, firmas) | Baja (10 líneas) |
| Tamaño del bundle | 0 (nativo) | ~30 KB minified |
| Sostenibilidad | Tú mantienes el código | Factuplan mantiene el SDK |
Si tu proyecto es scriptcito de un solo uso, cURL está bien. Si vas a producción, SDK siempre.
Uso desde Next.js o aplicaciones web
El SDK nunca debe usarse desde el frontend (filtraría tu API key). Úsalo desde:
- Server Actions o Route Handlers de Next.js.
- Backend de Express, NestJS, Fastify, Koa, Hono.
- Funciones serverless con runtime de Node.js, como AWS Lambda. En runtimes edge (Cloudflare Workers, Vercel Edge Functions) depende de su compatibilidad con Node.js: el SDK importa el módulo
cryptode Node y usaBuffer. - Scripts CLI internos.
Ejemplo: Next.js Server Action
"use server"
import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: process.env.FACTUPLAN_RUC!,})
export async function emitirFactura(formData: FormData) { const factura = await factuplan.invoices.create({ customer: { identificationType: "RUC", identification: formData.get("ruc") as string, legalName: formData.get("legalName") as string, email: formData.get("email") as string, }, items: [ { code: "SERV-001", description: "Servicio web", quantity: 1, unitPrice: Number(formData.get("precio")), taxType: "IVA_RATE", tax: 15, }, ], sendEmail: true, })
return { id: factura.id, accessKey: factura.accessKey, status: factura.status }}¿Dónde está el código fuente?
El SDK es open source (MIT). El repositorio está disponible para ver, reportar issues o contribuir. Próximamente publicaremos el link del repo en el hub de developers de Factuplan.
Resumen práctico
- El SDK
factuplanpara Node.js/TypeScript te deja emitir facturas electrónicas al SRI Ecuador en menos de 10 líneas. - Es tipado de punta a punta — el compilador te marca errores antes de ejecutar.
- Trae errores tipados y la verificación de firma de los webhooks; los reintentos los haces tú, con
devReferencepara que sean seguros. - Se instala con
npm install factuplany soporta Node 18+, ESM y CJS. - Es muy superior a llamar a cURL directo si vas a producción: menos código, menos bugs, mejor mantenibilidad.
Si estás integrando facturación electrónica en tu app, crea una API key gratis y empieza con el SDK en pruebas. Sin tarjeta, sin compromisos. Cuando estés listo, intercambias la API key por la de producción.
Repositorio y referencia: ver documentación técnica del SDK en factuplan.com.ec/docs/sdk. Soporte para developers: próximamente hub completo en factuplan.com.ec/desarrolladores.
Preguntas frecuentes
¿Qué versión de Node.js requiere el SDK de Factuplan?
El SDK oficial de Factuplan (paquete factuplan en npm) requiere Node.js 18 o superior. Está publicado como módulo dual ESM y CJS, por lo que funciona tanto en proyectos modernos con ECMAScript Modules como en proyectos legacy con CommonJS. También funciona en Deno y Bun gracias a su compatibilidad con los exports estándar. El paquete tiene cero dependencias externas, lo que minimiza el tamaño del bundle y elimina problemas de versionado transitivo.
¿El SDK funciona en TypeScript puro o solo en JavaScript?
El SDK está escrito en TypeScript y publica sus declaraciones de tipos (.d.ts) junto con el código. Puedes usarlo desde TypeScript con tipado completo de punta a punta (incluyendo uniones de literales para los valores permitidos y autocompletado en VSCode/WebStorm) o desde JavaScript puro con la misma API y los tipos disponibles vía JSDoc. La validación en tiempo de compilación funciona automáticamente en TypeScript; en JavaScript los errores aparecen en runtime al hacer la llamada HTTP. Recomendamos TypeScript para producción.
¿Cómo manejo errores del SRI con el SDK TypeScript?
El SDK lanza errores tipados que puedes diferenciar con instanceof: AuthenticationError para 401 (API key inválida), RateLimitError para 429 y FactuplanError como base para cualquier otro error de la API, con statusCode, code, message y details (por ejemplo 400 con GENERAL_0002 para datos inválidos, o 409 con API_10011 cuando la devReference ya pertenece a otro documento). El SDK no reintenta por su cuenta. Un rechazo del SRI no llega como excepción: la emisión es asíncrona, create() responde con el comprobante en DRAFT y el rechazo aparece después como estado REJECTED o RETURNED; lo ves consultando el estado con getStatus(id) o en el webhook invoice.rejected, cuyo data.reason trae los mensajes del SRI.
¿Puedo usar el SDK desde el frontend de mi aplicación web?
No, el SDK debe usarse solo desde el backend (servidor Node.js, Server Actions, funciones serverless, etc.). Usar el SDK desde el frontend expondría tu API key de Factuplan en el bundle JavaScript que se descarga al navegador, lo cual permitiría a cualquier usuario emitir facturas en nombre de tu empresa y consumir tu cuota mensual. La regla es: API key SIEMPRE en variable de entorno del servidor (FACTUPLAN_API_KEY), nunca en NEXT_PUBLIC_ ni VITE_PUBLIC_ ni en código del cliente. Para flujos donde el usuario completa un formulario y necesitas emitir factura, hazlo desde un Server Action (Next.js), una API route (Express) o una función serverless.
¿Qué diferencia hay entre usar el SDK y llamar a la API REST directa con fetch?
Funcionalmente puedes hacer todo con fetch o cURL, pero el SDK añade: 1) Type safety completo, el compilador TypeScript te avisa de campos faltantes o valores inválidos antes de ejecutar; 2) Autocompletado en el editor para nombres de métodos, parámetros y enums; 3) Parsing de los sobres data/meta y de las respuestas paginadas; 4) Helper para verificar firmas de webhooks; 5) Errores tipados que facilitan el manejo con instanceof. El SDK no reintenta por su cuenta ni valida identificaciones en local: la validación la hace la API (responde 400) y los reintentos los decides tú, con devReference para que no dupliquen. Para scripts puntuales fetch está bien; para producción el SDK ahorra horas y previene bugs.
¿Hay otros SDKs de Factuplan para Python, PHP, Go o .NET?
El SDK oficial actual es para Node.js/TypeScript (paquete factuplan en npm). Para otros lenguajes la integración se hace contra la API REST directa, que está completamente documentada en factuplan.com.ec/docs/api con ejemplos en cURL, Python, PHP, Go, Ruby y .NET. La colección de Postman oficial también está disponible para acelerar la integración en cualquier lenguaje. Si tu proyecto justifica un SDK oficial en otro lenguaje, puedes solicitarlo en el roadmap público de developers de Factuplan; los SDKs en Python y PHP están en evaluación según la demanda de la comunidad.
¿El SDK soporta webhooks y verificación de firma?
Sí. El SDK exporta la función verifyWebhookSignature(rawBody, signatureHeader, secret), que verifica la firma HMAC del header x-factuplan-signature (formato t=<timestamp>,v1=<firma>) contra tu webhook secret (el whsec_… que se muestra al crear el webhook en el dashboard de Factuplan) y devuelve true o false; no lanza una excepción, y rechaza entregas con más de 5 minutos. Después parseas tú el body. Los eventos se llaman {comprobante}.{resultado}: invoice, credit_note, debit_note, waybill, withholding, purchase_settlement o el genérico receipt, con authorized, rejected o error. El nombre viene en el campo event y los datos en data, con la misma forma para todos los tipos: receiptId, type, accessKey, authorizationNumber, documentNumber, total, customerName y customerIdentification, más devReference y environment si el documento tiene referencia, y reason en los eventos rejected y error.
¿Te sirvió este artículo? Marca a Factuplan como fuente preferida y aparecerá más en tus resultados de Google.
Añadir a fuentes preferidas
¿Aún no tienes firma electrónica?
Sin ella no puedes emitir comprobantes ante el SRI. Consíguela 100% en línea.
Compra tu firma electrónica