

Facturación electrónica Nodejs para Ecuador (paso a paso, 2026)
Integra facturación electrónica SRI en Node.js con el SDK oficial de Factuplan: webhooks, manejo de errores y repo facturación electrónica Ecuador en GitHub.
Tu app en Node.js necesita emitir facturas electrónicas autorizadas por el SRI de Ecuador. La buena noticia: con el SDK oficial de Factuplan lo puedes resolver en una tarde, sin tocar XMLs, sin firmar XAdES-BES manualmente y sin pelear con la API SOAP del SRI.
En este tutorial vas a ver cada paso: pre-requisitos, instalación, configuración, emisión de la primera factura, manejo de webhooks y troubleshooting. Todo con código real probado en producción.
Pre-requisitos
Antes de escribir una línea de código, necesitas:
1. Cuenta en Factuplan
Crea cuenta gratuita en app.factuplan.com.ec. El plan Free te da 25 facturas/mes sin costo, sin tarjeta de crédito. Suficiente para desarrollo y proyectos pequeños.
2. RUC activo
Tu negocio o el de tu cliente debe tener RUC activo en el SRI. Si todavía no tienes RUC, hay una guía completa para sacarlo.
3. Firma electrónica vigente
Para producción necesitas un certificado P12 emitido por una entidad acreditada en Ecuador (Banco Central, UANATACA, Security Data, ANF AC). Lo cargas una sola vez en tu workspace de Factuplan y ya. Para desarrollo en pruebas no se necesita firma real.
4. Node.js 18+
node --version # debe ser v18.x o superiorSi usas una versión anterior, el SDK no funciona.
5. API key
Una vez dentro de Factuplan: Developer → Create API key.
ak_test_*para pruebas (comprobantes ficticios, se borran cada hora).ak_live_*para producción (facturas reales firmadas y enviadas al SRI).
Guárdala en .env:
FACTUPLAN_API_KEY=ak_test_xxxxxxxxxxxxxxxxxxFACTUPLAN_RUC=0950194407001Paso 1 — Crear el proyecto
mkdir mi-app-facturacioncd mi-app-facturacionnpm init -ynpm install factuplan dotenvnpm install -D @types/node typescript tsxnpx tsc --init --target es2022 --module nodenext --moduleResolution nodenext --strictSi prefieres CommonJS o JavaScript puro, salta el último comando y
tsx. El SDK funciona en ambos.
Estructura mínima:
mi-app-facturacion/├── .env├── .gitignore├── package.json├── tsconfig.json└── src/ ├── factuplan.ts # cliente del SDK ├── emitir-factura.ts # ejemplo básico └── webhooks.ts # endpoint webhooksEn .gitignore añade siempre:
.envnode_modules/dist/Paso 2 — Inicializar el cliente
import "dotenv/config"import { Factuplan } from "factuplan"
if (!process.env.FACTUPLAN_API_KEY) { throw new Error("FACTUPLAN_API_KEY no está definida en .env")}
export const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY, { ruc: process.env.FACTUPLAN_RUC!,})Tip: importa este archivo desde cualquier módulo. El SDK reutiliza la misma conexión HTTP, no se crea un nuevo cliente por request.
Paso 3 — Emitir tu primera factura
import { factuplan } from "./factuplan.js"
async function main() { const factura = await factuplan.invoices.create({ customer: { identificationType: "RUC", identification: "0993378150001", legalName: "Cliente Demo S.A.", email: "facturas@cliente-demo.ec", saveToContacts: true, }, items: [ { code: "SERV-001", description: "Desarrollo web mensual", quantity: 1, unitPrice: 500, taxType: "IVA_RATE", tax: 15, }, ], payments: [ { method: "20", // Transferencia bancaria (catálogo SRI) amount: 575, // 500 + IVA 15% }, ], sendEmail: true, })
console.log("✅ Factura emitida") console.log("Clave de acceso:", factura.accessKey) console.log("RIDE PDF:", factura.pdfUrl) console.log("XML:", factura.xmlUrl) console.log("Estado:", factura.status) // PENDING | AUTHORIZED}
main().catch(console.error)Ejecútalo:
npx tsx src/emitir-factura.tsDeberías ver algo como:
✅ Factura emitidaClave de acceso: 1305202401099337815000110010010000056781234567811RIDE PDF: https://api.factuplan.com.ec/pdfs/inv_abc123.pdfXML: https://api.factuplan.com.ec/xmls/inv_abc123.xmlEstado: AUTHORIZEDEn pruebas, la autorización del SRI es simulada e instantánea. En producción tarda típicamente 2-5 segundos.
Paso 4 — Manejo de errores
En producción siempre vas a encontrar errores. El SDK los lanza tipados:
import { FactuplanError, FactuplanValidationError, FactuplanAuthError, FactuplanRateLimitError, FactuplanSRIError,} from "factuplan"import { factuplan } from "./factuplan.js"
async function emitirConManejo() { try { const factura = await factuplan.invoices.create({ /* ... */ }) return { ok: true, factura } } catch (err) { if (err instanceof FactuplanValidationError) { // Datos del request inválidos (400/422) return { ok: false, kind: "validacion", code: err.code, details: err.details } } if (err instanceof FactuplanAuthError) { // API key inválida (401) return { ok: false, kind: "auth", message: "Revisa tu FACTUPLAN_API_KEY" } } if (err instanceof FactuplanRateLimitError) { // 429 — cuota mensual o rate limit return { ok: false, kind: "limite", retryAfter: err.retryAfter } } if (err instanceof FactuplanSRIError) { // El SRI rechazó (código del catálogo SRI) return { ok: false, kind: "sri", sriCode: err.sriCode, sriMessage: err.sriMessage } } if (err instanceof FactuplanError) { // Otro error de la API return { ok: false, kind: "api", status: err.statusCode, message: err.message } } // Error de red u otro throw err }}Errores más frecuentes en producción
| Error | Causa típica | Solución |
|---|---|---|
FactuplanAuthError | API key faltante o de producción usada en pruebas | Revisar .env y el prefijo de la key |
FactuplanValidationError con INVOICE_4002 | RUC del cliente inválido | Validar dígito verificador antes de enviar |
FactuplanSRIError código 50 | Secuencial duplicado | Reintentar con siguiente secuencial automático |
FactuplanSRIError código 39 | Certificado P12 con RUC distinto al emisor | Cargar el P12 correcto del emisor |
FactuplanRateLimitError | Pasaste tu cuota mensual o picos | Esperar retryAfter o subir de plan |
FactuplanError 500 | Caída temporal | El SDK reintenta automáticamente |
Paso 5 — Webhooks
Para apps reales no basta con “fire and forget”. Configura webhooks para enterarte de:
invoice.authorized— el SRI autorizó.invoice.rejected— el SRI rechazó (necesitas reaccionar).credit_note.authorized— nota de crédito aprobada.waybill.authorized— guía de remisión aprobada.
Configurar el endpoint en Factuplan
- Dashboard → Developer → Webhooks.
- Agrega tu URL (https://tu-app.com/webhooks/factuplan).
- Copia el webhook secret (lo necesitas para verificar firmas).
Implementar el endpoint con Express
import "dotenv/config"import express from "express"import { Factuplan } from "factuplan"import { factuplan } from "./factuplan.js"
const app = express()
app.post( "/webhooks/factuplan", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["x-factuplan-signature"] as string
try { const event = Factuplan.webhooks.constructEvent( req.body, signature, process.env.FACTUPLAN_WEBHOOK_SECRET!, )
switch (event.type) { case "invoice.authorized": console.log("✅ Autorizada:", event.data.accessKey) // Tu lógica: marcar pedido como facturado, notificar al cliente, etc. break case "invoice.rejected": console.error("❌ Rechazada por SRI:", event.data.sriError) // Tu lógica: avisar al ops team, intentar corrección automática, etc. break case "credit_note.authorized": console.log("✅ NC autorizada:", event.data.accessKey) break }
res.json({ received: true }) } catch (err) { console.error("Firma de webhook inválida") res.status(400).send("Invalid signature") } },)
const PORT = process.env.PORT || 3000app.listen(PORT, () => console.log(`Webhook listener en :${PORT}`))Importante: usa
express.raw()para este endpoint (noexpress.json()). La verificación HMAC necesita el body sin parsear.
Probar webhooks localmente
Usa ngrok o cloudflared tunnel para exponer localhost:3000 a internet:
ngrok http 3000# Copia el URL https que te da y úsalo en el dashboard de FactuplanPaso 6 — Patrón para apps web (Next.js)
Si estás en Next.js App Router, usa Server Actions:
"use server"
import { factuplan } from "@/lib/factuplan"import { revalidatePath } from "next/cache"
export async function emitirFacturaAction(formData: FormData) { try { 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: [ { description: formData.get("descripcion") as string, quantity: 1, unitPrice: Number(formData.get("precio")), taxType: "IVA_RATE", tax: 15, }, ], sendEmail: true, })
revalidatePath("/facturas") return { ok: true, accessKey: factura.accessKey, pdfUrl: factura.pdfUrl } } catch (err) { return { ok: false, error: (err as Error).message } }}Y en el componente cliente:
import { emitirFacturaAction } from "@/actions/emitir-factura"
export default function NuevaFactura() { return ( <form action={emitirFacturaAction}> <input name="ruc" required /> <input name="legalName" required /> <input name="email" type="email" required /> <input name="descripcion" required /> <input name="precio" type="number" step="0.01" required /> <button>Emitir</button> </form> )}Paso 7 — Patrón para apps backend (NestJS)
import { Injectable } from "@nestjs/common"import { Factuplan } from "factuplan"
@Injectable()export class FacturacionService { private readonly factuplan: Factuplan
constructor() { this.factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: process.env.FACTUPLAN_RUC!, }) }
async emitirFactura(data: EmisionDTO) { return this.factuplan.invoices.create({ customer: { identificationType: data.identificationType, identification: data.identification, legalName: data.legalName, email: data.email, }, items: data.items.map((item) => ({ description: item.description, quantity: item.quantity, unitPrice: item.unitPrice, taxType: "IVA_RATE", tax: 15, })), sendEmail: true, }) }}Paso 8 — Testing
Para tests unitarios sin llamar al SRI real, usa mocks de Jest/Vitest:
import { describe, it, expect, vi } from "vitest"
vi.mock("factuplan", () => ({ Factuplan: vi.fn().mockImplementation(() => ({ invoices: { create: vi.fn().mockResolvedValue({ id: "inv_mock", accessKey: "1305202401099337815000110010010000056781234567811", status: "AUTHORIZED", pdfUrl: "https://example.com/mock.pdf", xmlUrl: "https://example.com/mock.xml", }), }, })),}))
describe("emitirFactura", () => { it("emite con datos válidos", async () => { const { emitirFactura } = await import("../src/emitir.js") const result = await emitirFactura({ /* ... */ }) expect(result.accessKey).toHaveLength(49) })})Para tests end-to-end contra Factuplan, usa siempre la API key de pruebas (ak_test_*) y no la de producción.
Buenas prácticas en producción
1. Idempotencia
Si tu sistema reintenta una emisión por timeout, evita facturas duplicadas usando un idempotency key:
const factura = await factuplan.invoices.create({ // ...}, { idempotencyKey: `order-${pedidoId}-attempt-${intento}`,})Factuplan detecta el key y, si ya emitió una factura con ese identificador, devuelve la misma sin crear nueva.
2. Reintentos con accessKey ya generado
Si la red se cae después del POST pero antes de recibir la respuesta, no reintentes ciegamente. Consulta primero el estado:
const status = await factuplan.invoices.findByIdempotencyKey(key)if (status) return status3. Backoff exponencial
Por defecto el SDK ya lo hace, pero si tu lógica está afuera del SDK (por ejemplo, llamadas a tu propia DB que fallan), implementa backoff:
async function withBackoff<T>(fn: () => Promise<T>, maxAttempts = 5): Promise<T> { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fn() } catch (err) { if (attempt === maxAttempts - 1) throw err const delay = Math.min(1000 * Math.pow(2, attempt), 30000) await new Promise((r) => setTimeout(r, delay)) } } throw new Error("unreachable")}4. Logs estructurados
Loguea siempre accessKey, status, sriCode y requestId para poder debuggear:
import pino from "pino"const log = pino()
try { const factura = await factuplan.invoices.create(payload) log.info({ accessKey: factura.accessKey, status: factura.status }, "factura emitida")} catch (err) { log.error({ err, payload }, "error emitiendo factura")}5. Variables de entorno por ambiente
Usa keys distintas en development, staging y producción:
FACTUPLAN_API_KEY=ak_test_dev_xxxFACTUPLAN_WEBHOOK_SECRET=whsec_dev_xxx
# .env.productionFACTUPLAN_API_KEY=ak_live_prod_xxxFACTUPLAN_WEBHOOK_SECRET=whsec_prod_xxxTroubleshooting
”FactuplanAuthError: API key inválida”
Causa: la key no existe, fue revocada o se usó key de prod con dominio de prueba.
Solución: regenerar en el dashboard de Factuplan y actualizar .env.
”RUC del cliente inválido”
El SDK valida el dígito verificador antes de enviar. Si tu RUC fue mal escrito o le falta el 001 al final, el error es local.
Solución: validar con la herramienta de validación de RUC o usar el SDK con el wrapper interno.
”Timeout esperando autorización del SRI”
En horas pico el SRI puede tardar más de lo normal. El SDK reintenta automáticamente, pero si tu HTTP timeout es muy corto (5 segundos), no llega.
Solución: subir timeout a 30 segundos o esperar el webhook invoice.authorized en lugar de bloquear.
”EAI_AGAIN” o “ECONNRESET”
Errores de red transitorios.
Solución: el SDK ya reintenta. Si persiste, revisa la conectividad de tu servidor con api.factuplan.com.ec.
Repositorio de ejemplo
El repo de referencia con todos estos snippets se publicará en GitHub pronto. Por ahora puedes clonar el quickstart desde el hub de developers (próximamente en factuplan.com.ec/desarrolladores).
Resumen práctico
- Instala
factuplanydotenven tu proyecto Node.js 18+. - Crea API key de pruebas en
app.factuplan.com.ec. - Inicializa el cliente con API key + RUC.
- Emite tu primera factura en 10 líneas con
factuplan.invoices.create({ ... }). - Configura webhooks para reaccionar a estados de autorización del SRI.
- Maneja errores tipados (
FactuplanError,FactuplanSRIError, etc.). - Usa idempotency keys, backoff, logs estructurados y variables de entorno por ambiente.
Crea tu API key gratis → y empieza con el plan freemium (25 facturas/mes sin tarjeta).
Referencia completa de la API: factuplan.com.ec/docs/api Referencia del SDK: factuplan.com.ec/docs/sdk Colección Postman: descarga desde tu dashboard de Factuplan.
Preguntas frecuentes
¿Puedo emitir facturas electrónicas en Node.js sin pasar por un servicio como Factuplan?
Técnicamente sí, pero implica resolver por tu cuenta: 1) Generación de XMLs según la ficha técnica del SRI v2.32 con todos los campos obligatorios y reglas de cascada IVA/ICE; 2) Firma electrónica XAdES-BES con tu certificado P12 (cargar el P12, extraer la cadena de certificados, firmar el XML con el algoritmo correcto); 3) Comunicación con los webservices SOAP del SRI (recepción y autorización) en lugar de REST; 4) Manejo de la clave de acceso de 49 dígitos con dígito verificador módulo 11; 5) Generación del RIDE PDF con QR. Es viable en proyectos donde tienes recursos para mantener todo eso, pero la mayoría de empresas usan un servicio como Factuplan para evitarse meses de desarrollo y mantenimiento continuo cada vez que el SRI publica una nueva ficha técnica.
¿Cómo manejo timeouts cuando el SRI tarda en autorizar?
El esquema offline del SRI puede tardar desde 2-5 segundos en horarios normales hasta varios minutos en cierres de mes o picos de declaración. Hay tres patrones recomendados en Node.js: 1) HTTP timeout amplio (30-60 segundos) si tu UI puede esperar; 2) Respuesta asíncrona: emites la factura, devuelves al cliente status PENDING con la access key, y procesas la autorización vía webhook invoice.authorized en background; 3) Polling con timeout: emites, esperas X segundos, si no autorizó haces poll cada N segundos hasta que el SRI responda. La opción 2 (webhooks) es la más limpia para producción porque desacopla la UX del tiempo del SRI y no bloquea tu servidor con peticiones largas.
¿Cuál es el RUC para usar en pruebas con el SDK?
Para pruebas debes usar el RUC asociado a tu workspace de Factuplan al crearlo. Las API keys de prueba (prefijo ak_test_) generan comprobantes ficticios que NO se envían al SRI real (se mantienen en el sandbox interno de Factuplan y se borran cada hora). Puedes usar cualquier RUC válido sintácticamente para el cliente receptor en tus pruebas, por ejemplo el RUC de prueba estándar 0993378150001. En producción debes usar el RUC real de tu negocio, con la firma electrónica P12 vigente cargada en tu workspace, y los comprobantes se envían al SRI real con autorización efectiva.
¿Qué pasa si el SRI rechaza una factura? ¿Cómo lo manejo en Node.js?
Cuando el SRI rechaza, el SDK lanza FactuplanSRIError con propiedades sriCode (código del catálogo SRI) y sriMessage (descripción). Patrón recomendado: capturar el error, loguear con el accessKey y sriCode para tener trazabilidad, decidir según el código si reintentar automáticamente (por ejemplo error 50 secuencial duplicado: el SDK usa siguiente secuencial) o requerir intervención humana (por ejemplo error 39 firma no coincide con el RUC del emisor: necesitas cargar el P12 correcto). Si tu flujo requiere reaccionar después de la respuesta inicial, usa webhooks: cuando llegue invoice.rejected con event.data.sriError tu sistema decide la acción correctiva sin bloquear el cliente original.
¿Cómo configuro webhooks de Factuplan en una app Node.js detrás de un proxy o load balancer?
El webhook se verifica con HMAC del body crudo (no parseado) usando el header x-factuplan-signature contra tu webhook secret. Si tu app está detrás de NGINX, AWS ALB o Cloudflare, debes asegurar que: 1) El body llegue sin modificación al handler (no usar middleware que reparse JSON antes del verificador); 2) En Express, usar express.raw({ type: 'application/json' }) específicamente en la ruta del webhook, ANTES que cualquier express.json() global; 3) El path del webhook esté expuesto en el ALB/proxy sin reescritura del body; 4) Si usas serverless (Vercel, Cloudflare Workers), accede al request.body como Buffer antes de hacer await req.json(). Si la verificación falla con cuerpo válido, casi siempre es porque algún middleware reparseó el body.
¿Puedo emitir facturas con consumidor final desde Node.js?
Sí. En la propiedad customer del SDK, usa identificationType FINAL_CONSUMER en lugar de RUC o CEDULA, e identification con valor '9999999999999' (trece nueves) o '13 nueves' según la práctica vigente. legalName puede ser 'CONSUMIDOR FINAL'. El SRI permite consumidor final hasta el monto máximo establecido en la normativa vigente (verifica el monto actualizado en la guía de monto a consumidor final). Para montos superiores debes solicitar al cliente RUC o cédula. Algunos comercios (restaurantes, farmacias, comercio masivo) emiten todo a consumidor final por velocidad, lo cual es legal mientras no excedan el monto tope por transacción.
¿Es seguro guardar la API key de Factuplan en variables de entorno del servidor?
Sí, siempre que el servidor esté correctamente configurado. Buenas prácticas: 1) Usar un gestor de secretos (AWS Secrets Manager, Vercel Environment Variables, Doppler, 1Password Secrets Automation) en lugar de archivos .env en producción; 2) Rotar la API key periódicamente (cada 3-6 meses); 3) Usar keys distintas por ambiente (dev, staging, prod) con permisos granulares cuando estén disponibles; 4) Auditar los logs de uso de la API key en el dashboard de Factuplan para detectar acceso anómalo; 5) Tener un procedimiento documentado de revocación rápida (botón en el dashboard) si sospechas filtración; 6) Nunca commitear .env al repositorio (siempre en .gitignore). Lo que NO debes hacer es exponer la key en NEXT_PUBLIC_, VITE_PUBLIC_ ni en código JavaScript del lado del cliente.
Equipo Factuplan
Especialista en facturación electrónica
Equipo editorial de Factuplan, especializado en facturación electrónica y normativa tributaria del SRI.
Conocer al equipoEmpieza a facturar electrónicamente hoy
1 mes gratis al comprar tu firma electrónica con FirmaOK. Sin tarjeta de crédito, sin compromiso.
