SDK para Node.js
El SDK oficial de Factuplan (factuplan en npm, v0.22.0) te permite emitir
facturas electrónicas, notas de crédito, notas de débito, comprobantes de
retención y liquidaciones de compra autorizados por el SRI del Ecuador desde
tu backend, en JavaScript o TypeScript. Las guías de remisión no tienen un
método de emisión propio: emítelas desde el panel, o envía tu propio XML
<guiaRemision> con invoices.signAndAuthorize (sin firmar) o con
documents.authorizeSigned (ya firmado, solo ak_live_*).
- Node.js 18+ · ESM & CJS · cero dependencias.
- Tipado completo en TypeScript.
- Dos entornos: pruebas (
ak_test_*) y producción (ak_live_*). - Compatible con Postman: descarga la colección desde tu cuenta.
Importante: los comprobantes creados con una API key de pruebas se eliminan automáticamente cada hora. Usa pruebas solo para desarrollo.
Instalación
Sección titulada «Instalación»Instala el SDK con el gestor de paquetes que prefieras:
# npmnpm install factuplan
# pnpmpnpm add factuplan
# yarnyarn add factuplanCrear una API key
Sección titulada «Crear una API key»- Inicia sesión en app.factuplan.com.ec.
- Anda a Developer → Crear API key.
- Copia la clave generada — solo se muestra una vez.
- Guárdala en una variable de entorno (
FACTUPLAN_API_KEY), nunca la subas al repositorio.
Las API keys son por workspace. El prefijo indica el entorno:
| Prefijo | Entorno | Comportamiento |
|---|---|---|
ak_test_* | Pruebas | Comprobantes ficticios, eliminados cada hora |
ak_live_* | Producción | Comprobantes reales firmados y enviados al SRI |
Autenticación
Sección titulada «Autenticación»El SDK adjunta automáticamente la API key y el RUC del contribuyente en cada
petición (x-taxpayer-ruc). Solo tienes que inicializar el cliente:
import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001",})Si vas a usar únicamente operaciones que no requieren contribuyente (por
ejemplo, listar tu uso del mes), puedes omitir el ruc:
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!)Seguridad: nunca expongas tu API key en el frontend. Úsala solo desde tu servidor.
Clientes
Sección titulada «Clientes»CRUD de los clientes (receptores) que aparecerán en tus comprobantes.
Crear cliente
Sección titulada «Crear cliente»const cliente = await factuplan.customers.create({ identificationType: "RUC", // RUC | CEDULA | PASSPORT | FINAL_CONSUMER | EXTERIOR identification: "0993378150001", // RUC=13 dígitos · CEDULA=10 dígitos legalName: "Empresa Demo S.A.", email: "facturas@empresademo.ec", // obligatorio address: "Guayaquil, Ecuador", // opcional phone: "+593958657236", // opcional})| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
identificationType | enum | Sí | RUC, CEDULA, PASSPORT, FINAL_CONSUMER, EXTERIOR |
identification | string | Sí | RUC: 13 dígitos terminados en 001 · cédula: 10 dígitos. No se valida el dígito verificador |
legalName | string | Sí | Razón social o nombre completo |
email | string | Sí | Para envío automático de comprobantes |
address | string | No | Aparece en el RIDE |
phone | string | No | Formato libre |
Listar clientes
Sección titulada «Listar clientes»Devuelve resultados paginados con un objeto meta que incluye el total.
const { data: clientes, meta } = await factuplan.customers.list({ page: 1, limit: 20, search: "empresa",})
console.log(meta.total, "clientes encontrados")Obtener, actualizar y eliminar
Sección titulada «Obtener, actualizar y eliminar»const cliente = await factuplan.customers.get("cust_id")
await factuplan.customers.update("cust_id", { email: "nuevo@empresademo.ec",})
await factuplan.customers.delete("cust_id")Productos
Sección titulada «Productos»CRUD del catálogo de ítems facturables. Funciona igual para productos tangibles y servicios.
Crear producto
Sección titulada «Crear producto»const producto = await factuplan.products.create({ code: "SERV-001", name: "Servicio de consultoría", unitPrice: 150.0, type: "SERVICE", // PRODUCT | SERVICE taxType: "IVA_RATE", // IVA_0 | IVA_RATE | NOT_TAXABLE | EXEMPT description: "Hora de consultoría técnica", // opcional})| Campo | Tipo | Notas |
|---|---|---|
code | string | Identificador único por contribuyente (máx. 25 caracteres) |
name | string | Aparece en el RIDE |
unitPrice | number | Hasta 4 decimales de precisión |
type | enum | PRODUCT, SERVICE |
taxType | enum | IVA_0, IVA_RATE, NOT_TAXABLE, EXEMPT |
description | string | Opcional |
Listar, actualizar y eliminar
Sección titulada «Listar, actualizar y eliminar»const { data: productos } = await factuplan.products.list({ search: "consultoría",})
await factuplan.products.update("prod_id", { unitPrice: 175.0 })
await factuplan.products.delete("prod_id")Facturas
Sección titulada «Facturas»Emisión completa de facturas electrónicas: el SDK genera el XML, lo firma con tu certificado, lo envía al SRI, genera el RIDE PDF y, opcionalmente, manda el correo al cliente.
Crear factura
Sección titulada «Crear factura»const factura = await factuplan.invoices.create({ // Punto de emisión: elige UNA de las 3 opciones // 1) Por UUID: emissionPointId: "ep_id", // 2) Por códigos SRI: // establishment: "001", // emissionPoint: "001", // 3) Omitir todo: auto-detecta si solo hay un punto de emisión activo
// Cliente: sus datos en línea, o { customerId: "cust_id" } si ya lo creaste customer: { identificationType: "RUC", identification: "0993378150001", legalName: "Cliente S.A.", email: "facturas@cliente.ec", saveToContacts: true, }, // Ítems: sus datos en línea, o { productId: "prod_id", quantity: 1 } para // usar un producto de tu catálogo (toma su código, nombre, precio e impuesto) items: [ { code: "SERV-001", description: "Servicio de consultoría", quantity: 1, unitPrice: 1.0, discount: 0, taxType: "IVA_RATE", tax: 15, // 0, 5, 8, 12, 14, 15 (default: 15) }, ], payments: [ { method: "20", amount: 1.15, term: 30, timeUnit: "dias", }, ], additionalInfo: { Vendedor: "Juan Pérez" }, // sendEmail: false, // opcional, default: true. Con false no se envía el correo; la factura igual termina en COMPLETED // devReference: "pedido-1001", // opcional: tu propia referencia (1 a 100 caracteres); hace idempotente la emisión})
console.log(factura.id) // ID internoconsole.log(factura.accessKey) // Clave de acceso SRI (49 dígitos)console.log(factura.sequential) // Secuencial del comprobanteconsole.log(factura.duplicate) // Solo si enviaste devReference: true si ya existía un comprobante con esa referenciadevReference evita emitir dos veces el mismo comprobante cuando repites una
petición. El detalle está en
Idempotencia con devReference.
Punto de emisión
Sección titulada «Punto de emisión»Tres formas de resolver el punto de emisión, en orden de prioridad:
// customer, items y payments: los mismos objetos del ejemplo de "Crear factura"// 1) Por UUID (referencia directa)factuplan.invoices.create({ emissionPointId: "ep_id", customer, items, payments })
// 2) Por códigos SRI del establecimiento + punto de emisiónfactuplan.invoices.create({ establishment: "001", emissionPoint: "001", customer, items, payments })
// 3) Auto-detección: si solo tienes un punto activo, omítelofactuplan.invoices.create({ customer, items, payments })Tipos de impuesto (taxType)
Sección titulada «Tipos de impuesto (taxType)»taxType | Código SRI | Descripción |
|---|---|---|
IVA_RATE | depende del tax | Aplica una tarifa de IVA específica |
IVA_0 | 0 | IVA 0% |
NOT_TAXABLE | 6 | No objeto de IVA |
EXEMPT | 7 | Exento de IVA |
Tarifas de IVA (tax)
Sección titulada «Tarifas de IVA (tax)»Usa tax cuando taxType sea IVA_RATE. Default: 15.
tax | Código SRI | Descripción |
|---|---|---|
0 | 0 | IVA 0% |
5 | 5 | IVA 5% |
8 | 8 | IVA 8% |
12 | 2 | IVA 12% |
14 | 3 | IVA 14% |
15 | 4 | IVA 15% (default) |
// Item con IVA reducido al 5%items: [ { code: "PROD-001", description: "Producto con IVA reducido", quantity: 1, unitPrice: 100.0, taxType: "IVA_RATE", tax: 5, },]Formas de pago (payments)
Sección titulada «Formas de pago (payments)»Cada elemento del arreglo payments lleva el código SRI en method, el
amount y, opcionalmente, term + timeUnit. Si omites payments se aplica
'01' (Sin utilización del sistema financiero) por defecto.
La suma de los
amountdebe ser igual al total de la factura con impuestos (se tolera una diferencia de hasta 0.01). De lo contrario la API responde400con el códigoINVOICE_4014y no envía nada al SRI.
| Código | Descripción |
|---|---|
01 | Sin utilización del sistema financiero |
15 | Compensación de deudas |
16 | Tarjeta de débito |
17 | Dinero electrónico |
18 | Tarjeta prepago |
19 | Tarjeta de crédito |
20 | Otros con utilización del sistema financiero |
21 | Endoso de títulos |
timeUnit | Descripción |
|---|---|
dias | Días |
meses | Meses |
anios | Años |
Redondeo y precisión
Sección titulada «Redondeo y precisión»unitPriceadmite hasta 4 decimales — útil para productos con costos unitarios bajos.- Los totales finales (subtotal, IVA, total) se redondean automáticamente a 2 decimales según el estándar del SRI.
Ejemplos:
Cantidad: 1000 × Precio: 0.1234 = Subtotal: 123.40Base imponible: 10.05 × 0.15 = 1.5075 → IVA: 1.51Total a pagar (Base + IVA): 11.56Consultar estado
Sección titulada «Consultar estado»const estado = await factuplan.invoices.getStatus("factura_id")// estado.status termina en COMPLETED | ERROR | REJECTED (o VOIDED si lo anulas).// Mientras tanto pasa por DRAFT | PROCESSING | GENERATING_XML | SIGNING | SENDING_SRI |// RECEIVED_SRI | AUTHORIZED | GENERATING_PDF | SENDING_EMAIL.// AUTHORIZED es un estado de paso: el SRI ya autorizó, pero el PDF puede no estar listo.
if (estado.status === "COMPLETED") { console.log(estado.authorizationNumber)}Descargar XML y PDF
Sección titulada «Descargar XML y PDF»URLs pre-firmadas que expiran en 5 minutos.
const { url: xmlUrl } = await factuplan.invoices.downloadXml("id")const { url: pdfUrl } = await factuplan.invoices.downloadPdf("id")Importar por clave de acceso
Sección titulada «Importar por clave de acceso»Importa una factura ya autorizada por el SRI usando su clave de 49 dígitos. Útil para sincronizar comprobantes emitidos fuera de Factuplan.
Para que la factura quede guardada, el cliente del SDK debe tener ruc y la clave de
acceso debe ser de ese mismo contribuyente; sin ruc solo se verifica (la
respuesta trae id: null). Importar dos veces la misma clave responde 400
con el código INVOICE_4006. Se descuenta un documento de tu plan solo si el
SRI la tiene autorizada, se guarde o no.
Solo
ak_live_*. No disponible con claves de pruebas.
const factura = await factuplan.invoices.importByAccessKey({ accessKey: "0104202601099337815000110010010000000011234567890",})Consultar comprobante externo por clave de acceso
Sección titulada «Consultar comprobante externo por clave de acceso»POST /developer/receipts/query — consulta cualquier comprobante autorizado
por el SRI (factura, nota de crédito, guía de remisión), sin importar qué
sistema lo emitió. Si el contribuyente emisor no existe en tu workspace, se
crea automáticamente con firma vacía.
Solo
ak_live_*. No disponible con claves de pruebas.
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
accessKey | string | Sí | Clave de acceso SRI (49 dígitos) |
save | boolean | No (default true) | Si false, no guarda en BD ni genera PDF |
Cómo se descuenta el contador: el crédito se descuenta cuando el comprobante está autorizado, sin importar el valor de
save. Si no está autorizado, no se descuenta ningún crédito.
// Consultar y guardar (default)const comp = await factuplan.invoices.queryExternalByAccessKey({ accessKey: "0104202601019999999900110010010000000011234567813",})console.log(comp.id, comp.status, comp.xmlBase64)
// Solo verificar sin guardarconst verificado = await factuplan.invoices.queryExternalByAccessKey({ accessKey: "0104202601019999999900110010010000000011234567813", save: false,})console.log(verificado.id) // nullconsole.log(verificado.xmlBase64) // XML autorizadoAnular factura
Sección titulada «Anular factura»Anula un comprobante a nivel del sistema: su estado pasa a VOIDED. Solo
se pueden anular comprobantes en estado AUTHORIZED o COMPLETED.
Importante: la anulación es solo dentro de Factuplan. Notificar la anulación ante el SRI es responsabilidad tuya.
const result = await factuplan.invoices.void( "factura_id", "Error en los datos del cliente",)
console.log(result.status) // "VOIDED"console.log(result.voidReason) // Motivo ingresadoconsole.log(result.message) // Confirmación| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID del comprobante anulado |
status | string | VOIDED |
accessKey | string | Clave de acceso del comprobante |
voidReason | string | Motivo de la anulación |
message | string | Mensaje de confirmación |
Secuencial de facturas
Sección titulada «Secuencial de facturas»Fija el contador de secuencial de facturas de un punto de emisión. El valor que
envías es el último secuencial usado: la siguiente factura sale con ese
valor más uno (si envías 100, la próxima es la 000000101).
const result = await factuplan.invoices.updateSequential( "001", // branchCode — código del establecimiento "001", // emissionCode — código del punto de emisión 100, // último secuencial usado: la próxima factura será la 101)
console.log(result.invoiceSequential) // 100console.log(result.updatedAt) // ISO 8601| Campo | Tipo | Descripción |
|---|---|---|
branchCode | string | Código del establecimiento |
emissionCode | string | Código del punto de emisión |
invoiceSequential | number | Nuevo valor del secuencial aplicado |
updatedAt | string | Fecha de actualización (ISO 8601) |
Nota: este método afecta únicamente el secuencial de facturas. Para notas de crédito y de débito usa
creditNotes.updateSequentialydebitNotes.updateSequential, con los mismos argumentos.
Ejemplo completo
Sección titulada «Ejemplo completo»import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // RUC del contribuyente emisor: obligatorio para emitir})
async function crearFactura() { // 1. Buscar o crear cliente const { data: clientes } = await factuplan.customers.list({ search: "0993378150001", }) const cliente = clientes[0] ?? (await factuplan.customers.create({ identificationType: "RUC", identification: "0993378150001", legalName: "Mi Cliente S.A.", email: "facturas@cliente.ec", }))
// 2. Crear factura (auto-detecta el punto de emisión) const factura = await factuplan.invoices.create({ customer: { customerId: cliente.id }, items: [ { code: "PROD-001", description: "Laptop Dell XPS 15", quantity: 1, unitPrice: 1500.0, taxType: "IVA_RATE", tax: 15, }, ], payments: [ { method: "19", amount: 1725.0, // Total con IVA term: 6, timeUnit: "meses", }, ], })
// 3. Polling hasta que el SRI responda let estado do { await new Promise((r) => setTimeout(r, 3000)) estado = await factuplan.invoices.getStatus(factura.id) } while (!["COMPLETED", "ERROR", "REJECTED"].includes(estado.status))
if (estado.status === "COMPLETED") { const { url } = await factuplan.invoices.downloadPdf(factura.id) console.log("PDF:", url) }}
crearFactura()Firmar XML
Sección titulada «Firmar XML»Si tu sistema ya genera el XML de factura sin firmar, puedes enviarlo directamente a Factuplan para que lo firme con tu certificado y lo autorice ante el SRI.
Con una API key de producción necesitas cupo disponible en tu plan: se comprueba antes de firmar, y solo se descuenta un documento si el SRI lo autoriza. Reenviar el mismo comprobante no lo vuelve a descontar. En el entorno de pruebas no consume cupo.
import { Factuplan } from "factuplan"import { readFileSync } from "fs"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // contribuyente con cuyo certificado se firma})
const xml = readFileSync("./factura-sin-firmar.xml", "utf-8")
const result = await factuplan.invoices.signAndAuthorize({ xml })
console.log(result.authorized) // true si el SRI autorizó el comprobanteconsole.log(result.authorizationNumber) // Número de autorización del SRI (o null)console.log(result.authorizationDate) // Fecha de autorización (o null)console.log(result.messages) // Mensajes del SRI; los motivos si no autorizó// Si el SRI no lo autorizó, no se descuenta ningún documento de tu plan.
// La respuesta ya es la del SRI: este modo no guarda el comprobante en Factuplan,// así que no hay id, ni estado que consultar, ni PDF (RIDE) que descargar.if (result.authorized && result.authorizedXml) { // XML autorizado, en base64. Guárdalo en tu sistema. const xmlAutorizado = Buffer.from(result.authorizedXml, "base64").toString("utf-8") console.log("XML autorizado:", xmlAutorizado)}| Campo | Tipo | Descripción |
|---|---|---|
authorized | boolean | true si el SRI autorizó el comprobante |
authorizationNumber | string | null | Número de autorización del SRI |
authorizationDate | string | null | Fecha de autorización |
authorizedXml | string | null | XML autorizado, en base64 |
messages | array | Mensajes del SRI; los motivos del rechazo cuando authorized es false |
invoices.signAndAuthorize acepta cualquier tipo de comprobante (factura, nota
de crédito, nota de débito, guía de remisión, retención o liquidación de
compra). No genera PDF ni envía correo, y el comprobante no queda guardado en
Factuplan: no se puede consultar con getStatus ni descargar después.
Notas de crédito
Sección titulada «Notas de crédito»import { Factuplan } from 'factuplan';
const factuplan = new Factuplan('ak_live_...', { ruc: '0950194407001' });
// Emitir nota de crédito// La factura original debe existir en Factuplan (mismo contribuyente) en estado// AUTHORIZED o COMPLETED; si no, la API responde 400 con el código CN_5001.// Si se emitió en otro sistema, impórtala antes con invoices.importByAccessKey.const creditNote = await factuplan.creditNotes.create({ invoiceAccessKey: '2312202301179214289100110010020000001231234567811', reason: 'Error en datos del cliente', items: [ { code: 'SERV-001', description: 'Servicio de desarrollo web', quantity: 1, unitPrice: 100.00, taxType: 'IVA_RATE', }, ], // devReference: 'nc-1001', // opcional: tu propia referencia; hace idempotente la emisión});
console.log(creditNote.id); // '0199c2f4-7b1e-7c3a-9f2d-5a6b7c8d9e0f'console.log(creditNote.accessKey); // '0412202404...'console.log(creditNote.status); // 'DRAFT'
// Consultar estado: espera a que termine (COMPLETED, ERROR o REJECTED).// Mientras está en proceso, el XML o el PDF pueden no existir todavía (404), y solo se puede anular en AUTHORIZED o COMPLETED (400 en otro estado).let status;do { await new Promise((r) => setTimeout(r, 3000)); status = await factuplan.creditNotes.getStatus(creditNote.id);} while (!['COMPLETED', 'ERROR', 'REJECTED'].includes(status.status));
// Descargar XML y PDFconst xml = await factuplan.creditNotes.downloadXml(creditNote.id);const pdf = await factuplan.creditNotes.downloadPdf(creditNote.id);
// Anular (solo a nivel del sistema — la anulación ante el SRI es tu responsabilidad)const voided = await factuplan.creditNotes.void(creditNote.id, 'Error en precio');| Método | Descripción |
|---|---|
creditNotes.create(input) | Emite una nota de crédito |
creditNotes.get(id) | Obtiene el detalle |
creditNotes.getStatus(id) | Consulta el estado de procesamiento |
creditNotes.downloadXml(id) | URL pre-firmada del XML (5 min) |
creditNotes.downloadPdf(id) | URL pre-firmada del PDF RIDE (5 min) |
creditNotes.void(id, reason) | Anula a nivel de sistema |
Firmar XML
Sección titulada «Firmar XML»Si tu sistema ya genera el XML de nota de crédito sin firmar, envíalo con
invoices.signAndAuthorize: aunque el método está en invoices, acepta
cualquier tipo de comprobante. Factuplan lo firma con tu certificado y lo
autoriza ante el SRI en la misma petición; la factura original no tiene que
existir en Factuplan.
Con una API key de producción necesitas cupo disponible en tu plan: se comprueba antes de firmar, y solo se descuenta un documento si el SRI lo autoriza. Reenviar el mismo comprobante no lo vuelve a descontar. En el entorno de pruebas no consume cupo.
import { Factuplan } from "factuplan"import { readFileSync } from "fs"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // contribuyente con cuyo certificado se firma})
const xml = readFileSync("./nota-credito-sin-firmar.xml", "utf-8")const result = await factuplan.invoices.signAndAuthorize({ xml })
console.log(result.authorized) // true si el SRI autorizó la nota de créditoconsole.log(result.authorizationNumber) // Número de autorización del SRI (o null)console.log(result.messages) // Mensajes del SRI; los motivos si no autorizóLa respuesta trae los mismos campos que en Firmar XML de
Facturas (authorized, authorizationNumber, authorizationDate,
authorizedXml y messages). Este modo no guarda el comprobante en Factuplan:
no hay id, ni estado que consultar con creditNotes.getStatus, ni PDF.
Notas de débito
Sección titulada «Notas de débito»Una nota de débito registra cargos adicionales sobre una factura ya autorizada: ajustes de precio, mora, flete no facturado, etc.
Emitir nota de débito
Sección titulada «Emitir nota de débito»import { Factuplan } from 'factuplan';
const factuplan = new Factuplan('ak_live_...', { ruc: '0950194407001' });
// La factura original debe existir en Factuplan (mismo contribuyente) en estado// AUTHORIZED o COMPLETED; si no, la API responde 400 con el código DN_5101.const debitNote = await factuplan.debitNotes.create({ invoiceAccessKey: '2312202301179214289100110010020000001231234567811', reasons: [ { description: 'Ajuste por flete no facturado', amount: 25.00, taxType: 'IVA_RATE', // IVA_RATE (15%) | IVA_0 | NOT_TAXABLE | EXEMPT }, ], // additionalInfo: { Referencia: 'OC-4521' }, // opcional // devReference: 'nd-1001', // opcional: tu propia referencia; hace idempotente la emisión})
console.log(debitNote.id); // '0199c2f4-7b1e-7c3a-9f2d-5a6b7c8d9e0f'console.log(debitNote.accessKey); // '0412202405...'console.log(debitNote.status); // 'DRAFT'Consultar estado
Sección titulada «Consultar estado»const status = await factuplan.debitNotes.getStatus(debitNote.id);// status.status termina en COMPLETED | ERROR | REJECTED (o VOIDED si la anulas); cualquier otro valor es un estado de pasoDescargar XML y PDF
Sección titulada «Descargar XML y PDF»const { url: xmlUrl } = await factuplan.debitNotes.downloadXml(debitNote.id);const { url: pdfUrl } = await factuplan.debitNotes.downloadPdf(debitNote.id);Listar notas de débito
Sección titulada «Listar notas de débito»const { data: debitNotes, meta } = await factuplan.debitNotes.list({ status: 'COMPLETED', page: 1, limit: 20,})const voided = await factuplan.debitNotes.void(debitNote.id, 'Emitida por error');Tipos de impuesto en reasons
Sección titulada «Tipos de impuesto en reasons»taxType | Descripción |
|---|---|
IVA_RATE | IVA a la tarifa vigente (15%) |
IVA_0 | IVA 0% |
NOT_TAXABLE | No objeto de IVA |
EXEMPT | Exento de IVA |
Cualquier otro valor (
IVA,NO_TAX,ICE,IRBPNR,IVA_EXEMPT) no da error: la API lo emite como IVA 0%.
| Método | Descripción |
|---|---|
debitNotes.create(input) | Emite una nota de débito |
debitNotes.get(id) | Obtiene el detalle |
debitNotes.getStatus(id) | Consulta el estado de procesamiento |
debitNotes.list(params?) | Lista con paginación y filtros |
debitNotes.downloadXml(id) | URL pre-firmada del XML (5 min) |
debitNotes.downloadPdf(id) | URL pre-firmada del PDF RIDE (5 min) |
debitNotes.void(id, reason) | Anula a nivel de sistema |
Firmar XML
Sección titulada «Firmar XML»Si tu sistema ya genera el XML de nota de débito sin firmar, envíalo con
invoices.signAndAuthorize: aunque el método está en invoices, acepta
cualquier tipo de comprobante. Factuplan lo firma con tu certificado y lo
autoriza ante el SRI en la misma petición; la factura original no tiene que
existir en Factuplan.
Con una API key de producción necesitas cupo disponible en tu plan: se comprueba antes de firmar, y solo se descuenta un documento si el SRI lo autoriza. Reenviar el mismo comprobante no lo vuelve a descontar. En el entorno de pruebas no consume cupo.
import { Factuplan } from "factuplan"import { readFileSync } from "fs"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // contribuyente con cuyo certificado se firma})
const xml = readFileSync("./nota-debito-sin-firmar.xml", "utf-8")const result = await factuplan.invoices.signAndAuthorize({ xml })
console.log(result.authorized) // true si el SRI autorizó la nota de débitoconsole.log(result.authorizationNumber) // Número de autorización del SRI (o null)console.log(result.messages) // Mensajes del SRI; los motivos si no autorizóLa respuesta trae los mismos campos que en Firmar XML de
Facturas (authorized, authorizationNumber, authorizationDate,
authorizedXml y messages). Este modo no guarda el comprobante en Factuplan:
no hay id, ni estado que consultar con debitNotes.getStatus, ni PDF.
Retenciones
Sección titulada «Retenciones»Emisión de comprobantes de retención electrónica (codDoc 07) autorizados
por el SRI.
Emitir retención
Sección titulada «Emitir retención»Una retención se emite sobre una compra registrada: la factura que te emitió tu proveedor. Primero registra la compra con su clave de acceso y luego emite la retención indicando esa misma clave.
purchases.registersolo funciona conak_live_*: la clave de acceso se consulta contra el SRI real. Cada compra registrada consume cupo de tu plan.
import { Factuplan } from "factuplan"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // RUC del contribuyente que retiene})
// Clave de acceso (49 dígitos) de la factura que te emitió tu proveedorconst claveCompra = "0109202601179214673900110020010000000011234567815"
// 1. Registrar la compra.// Si ya estaba registrada responde 400 (PU_5504): en ese caso salta este paso.await factuplan.purchases.register(claveCompra)
// 2. Emitir la retención sobre esa compra.// Si la compra no está registrada, la API responde 404 con el código WH_5304.const retencion = await factuplan.withholdings.create({ purchaseAccessKey: claveCompra, codSustento: "01", // Código de sustento del SRI (Tabla 5) taxes: [ { taxType: "RENTA", // RENTA | IVA | ISD taxRateCode: "303", // Código de retención del SRI para ese impuesto taxRate: 10, // Porcentaje de retención (0 a 100) taxBase: 100.0, // Base imponible, mayor que 0 }, ], // fiscalPeriod: "09/2026", // opcional (MM/AAAA); por defecto, el mes de emisión de la compra // devReference: "ret-1001", // opcional: tu propia referencia; hace idempotente la emisión})
console.log(retencion.id) // ID internoconsole.log(retencion.accessKey) // Clave de acceso SRI (49 dígitos)console.log(retencion.status) // 'DRAFT'console.log(retencion.totalWithheld) // Total retenido| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
purchaseAccessKey | string | Sí | Clave de acceso (49 dígitos) de la compra registrada |
codSustento | string | Sí | Código de sustento del SRI (Tabla 5), por ejemplo 01 |
taxes | array | Sí | Al menos una línea con taxType (RENTA, IVA, ISD), taxRateCode, taxRate y taxBase |
fiscalPeriod | string | No | Período fiscal MM/AAAA; por defecto, el mes de emisión de la compra |
additionalInfo | object | No | Pares clave-valor de información adicional |
devReference | string | No | Tu propia referencia; ver Idempotencia con devReference |
El punto de emisión se indica igual que en las facturas (emissionPointId, o
establishment + emissionPoint). Después consulta el estado con
withholdings.getStatus(id) y descarga el XML o el PDF con
withholdings.downloadXml(id) y withholdings.downloadPdf(id).
Firmar XML
Sección titulada «Firmar XML»Si tu sistema ya genera el XML de retención sin firmar, envíalo con
invoices.signAndAuthorize: aunque el método está en invoices, acepta
cualquier tipo de comprobante. Factuplan lo firma con tu certificado y lo
autoriza ante el SRI en la misma petición; en este modo no hace falta registrar
la compra.
Con una API key de producción necesitas cupo disponible en tu plan: se comprueba antes de firmar, y solo se descuenta un documento si el SRI lo autoriza. Reenviar el mismo comprobante no lo vuelve a descontar. En el entorno de pruebas no consume cupo.
import { Factuplan } from "factuplan"import { readFileSync } from "fs"
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!, { ruc: "0950194407001", // contribuyente con cuyo certificado se firma})
const xml = readFileSync("./retencion-sin-firmar.xml", "utf-8")const result = await factuplan.invoices.signAndAuthorize({ xml })
console.log(result.authorized) // true si el SRI autorizó la retenciónconsole.log(result.authorizationNumber) // Número de autorización del SRI (o null)console.log(result.messages) // Mensajes del SRI; los motivos si no autorizóLa respuesta trae los mismos campos que en Firmar XML de
Facturas (authorized, authorizationNumber, authorizationDate,
authorizedXml y messages). Este modo no guarda el comprobante en Factuplan:
no hay id, ni estado que consultar con withholdings.getStatus, ni PDF.
Idempotencia con devReference
Sección titulada «Idempotencia con devReference»devReference es un campo opcional de los cinco métodos de emisión
(invoices.create, creditNotes.create, debitNotes.create,
withholdings.create y purchaseSettlements.create). Es tu propia referencia
del documento —por ejemplo, tu número de pedido— y hace idempotente la
emisión: si repites la petición, no se emite un segundo comprobante.
- Texto libre de 1 a 100 caracteres. Se recortan los espacios de los extremos; el resto se compara tal cual, distinguiendo mayúsculas de minúsculas.
- Es única por contribuyente, entorno y tipo de comprobante.
- No se imprime en el comprobante.
- Va en el cuerpo de la petición: no existe ningún header de idempotencia.
| Petición | Respuesta |
|---|---|
| Primera vez con esa referencia | 201, comprobante nuevo, con duplicate: false |
| Misma referencia y mismo contenido | 201, el comprobante que ya existía, con duplicate: true; no se emite otro |
| Misma referencia y otro contenido | 409 con el código API_10011 |
| La referencia es de un comprobante anulado | 409 con el código API_10012 |
const factura = await factuplan.invoices.create({ customer, items, payments, devReference: "pedido-1001",})
console.log(factura.devReference) // "pedido-1001"console.log(factura.duplicate) // false: se emitió ahora · true: ya existía y no se emitió otro
// ¿No sabes si una emisión llegó a registrarse (por ejemplo, tras un timeout)?// Búscala por su referencia: devuelve cero o un comprobante.const { data } = await factuplan.invoices.list({ devReference: "pedido-1001" })console.log(data[0]?.id, data[0]?.status)Cuando duplicate es true, revisa status: el comprobante que ya existía
puede estar autorizado, en proceso o rechazado. Un comprobante corregido
necesita una referencia nueva. Los dos 409 llegan al SDK como
FactuplanError, y error.details trae los datos del comprobante que ya tiene
esa referencia (receiptId, status, accessKey).
Los comprobantes creados con devReference la devuelven también en list,
get, getStatus, retry y void, y en los eventos de webhook, donde llega
junto con environment (TESTING o PRODUCTION).
Errores
Sección titulada «Errores»El SDK exporta clases tipadas para cada categoría de error. Esto te permite ramificar el manejo según el tipo:
import { FactuplanError, AuthenticationError, RateLimitError } from "factuplan"
try { await factuplan.invoices.create({ customer, items, payments })} catch (error) { if (error instanceof AuthenticationError) { console.error("API key inválida o expirada") } else if (error instanceof RateLimitError) { console.error("Rate limit alcanzado, reintentar") } else if (error instanceof FactuplanError) { console.error(error.code) // ej. "INVOICE_4014" console.error(error.message) console.error(error.statusCode) // ej. 400 console.error(error.details) }}Códigos de error frecuentes
Sección titulada «Códigos de error frecuentes»| Código | HTTP | Descripción |
|---|---|---|
INTEGRATION_8001 | 401 | API key inválida o expirada; el SDK lo lanza como AuthenticationError, con error.code igual a AUTH_ERROR |
GENERAL_0003 | 429 | Límite de velocidad excedido; el SDK lanza RateLimitError para todo 429, con error.code igual a RATE_LIMIT |
INTEGRATION_8004 | 403 | Tu plan no incluye acceso a la API en producción |
SUB_12002 | 403 | Cupo de documentos de tu plan agotado |
GENERAL_0002 | 400 | Datos inválidos o campo no permitido; el detalle viene en error.details.validation |
INVOICE_4003 | 400 | Detalle de factura vacío |
CERT_3008 | 422 | El certificado que intentas subir ya expiró |
CUSTOMER_7004 | 409 | Cliente duplicado |
PRODUCT_6002 | 409 | Código de producto duplicado |
API_10011 | 409 | La devReference ya pertenece a otro documento con contenido distinto |
API_10012 | 409 | La devReference pertenece a un documento anulado |
Rate Limit
Sección titulada «Rate Limit»Cada API key tiene dos límites:
- Velocidad:
100peticiones por segundo por API key. - Cuota mensual: depende del plan contratado (consulta
/precios).
Si superas el límite de velocidad recibes HTTP 429 (GENERAL_0003):
reintenta con espera exponencial. Si se agota el cupo de documentos de tu plan
recibes HTTP 403 (SUB_12002), y si tu API key tiene una cuota mensual
propia y la agotas, HTTP 429 (API_10002): en esos dos casos reintentar no
sirve de nada.
Respuesta al superar el límite
Sección titulada «Respuesta al superar el límite»{ "data": null, "meta": { "requestId": "0f8b6c1e-5d2a-4a7e-9c3b-2f1d6e8a4b70", "timestamp": "2026-01-15T15:04:05.000Z" }, "error": { "code": "GENERAL_0003", "message": "Has superado el límite de peticiones. Intenta de nuevo en unos segundos.", "statusCode": 429, "details": { "message": "Has superado el límite de peticiones. Intenta de nuevo en unos segundos." } }}{ "data": null, "meta": { "requestId": "0f8b6c1e-5d2a-4a7e-9c3b-2f1d6e8a4b70", "timestamp": "2026-01-15T15:04:05.000Z" }, "error": { "code": "API_10002", "message": "Has alcanzado el límite mensual de 500 documentos.", "statusCode": 429, "details": { "message": "Has alcanzado el límite mensual de 500 documentos.", "used": 500, "quota": 500 } }}Manejo recomendado con reintentos
Sección titulada «Manejo recomendado con reintentos»import { RateLimitError } from "factuplan"
// `datos` debe incluir tu propio `devReference` (por ejemplo, tu número de pedido):// si repites la petición con la misma referencia y el mismo contenido, la API// responde con el comprobante que ya existe (`duplicate: true`) y no emite otro.// El SDK no reintenta por su cuenta y corta cada petición a los 30 s.async function emitirConReintento(datos: any, intentos = 3) { for (let i = 0; i < intentos; i++) { try { return await factuplan.invoices.create(datos) } catch (error) { if (error instanceof RateLimitError && i < intentos - 1) { const espera = Math.pow(2, i) * 500 // 500ms, 1s, 2s... await new Promise((r) => setTimeout(r, espera)) } else { throw error } } }}Webhooks
Sección titulada «Webhooks»Antes de procesar un evento webhook, verifica su firma: cada envío lleva el
header X-Factuplan-Signature, calculado con el secreto whsec_… de tu webhook
sobre el cuerpo exacto que recibes. Como segunda comprobación puedes consultar
el comprobante por su ID.
Verificar firma
Sección titulada «Verificar firma»// rawBody: el cuerpo tal como llegó, sin parsear (string o Buffer).// signature: el valor del header X-Factuplan-Signature.function esDeFactuplan( rawBody: string | Buffer, signature: string | string[] | undefined,) { return factuplan.webhooks.verifySignature( rawBody, signature, process.env.FACTUPLAN_WEBHOOK_SECRET!, // el secreto whsec_... de tu webhook )}Devuelve false si la firma no coincide o si el envío tiene más de 5 minutos
de antigüedad. Usa el cuerpo sin parsear: si tu framework ya convirtió el JSON,
volver a serializarlo cambia los bytes y la firma deja de coincidir. El secreto
se muestra una sola vez, al crear el webhook o al rotarlo.
Verificar comprobante
Sección titulada «Verificar comprobante»const receipt = await factuplan.webhooks.verifyReceipt("receipt_id")
// Devuelve null si el comprobante no existe o si la consulta falla// (por ejemplo, si el cliente del SDK no tiene `ruc` configurado).if (receipt) { console.log(receipt.id) console.log(receipt.status) // Mismos estados que invoices.getStatus; COMPLETED al terminar console.log(receipt.accessKey) // Clave de acceso SRI (49 dígitos)}Tip: después de verificar la firma, confirma que el comprobante existe antes de actualizar tu BD o disparar notificaciones a tus usuarios.
Uso & Límites
Sección titulada «Uso & Límites»Consulta el consumo de tu API key en el mes en curso.
const uso = await factuplan.usage()
console.log(uso.currentMonth.documents, "operaciones este mes")console.log("Costo estimado: $" + uso.currentMonth.cost)console.log(uso.monthlyUsed, "/", uso.monthlyQuota ?? "sin cuota propia")Estructura de respuesta
Sección titulada «Estructura de respuesta»| Campo | Tipo | Descripción |
|---|---|---|
currentMonth.documents | number | Operaciones registradas con esta API key en el mes en curso |
currentMonth.cost | number | Costo estimado del mes en USD |
plan | string | Plan de la API key |
monthlyQuota | number | null | Cuota propia de la API key; null si no tiene |
monthlyUsed | number | Operaciones contadas contra esa cuota |
Certificado
Sección titulada «Certificado»Consulta el estado del certificado P12 cargado en tu cuenta.
const cert = await factuplan.certificates.status()
console.log(cert.hasCertificate) // true = hay un certificado cargadoconsole.log(cert.isExpired) // true = el certificado ya vencióconsole.log(cert.daysUntilExpiry) // Días que faltan para el vencimientoconsole.log(cert.expiresAt) // Fecha de vencimiento (ISO 8601)console.log(cert.ruc) // RUC del titularconsole.log(cert.legalName) // Nombre registrado en el certificado| Campo | Tipo | Descripción |
|---|---|---|
hasCertificate | boolean | true si hay un certificado cargado |
isExpired | boolean | true si el certificado ya venció |
daysUntilExpiry | number | null | Días que faltan para el vencimiento |
expiresAt | string | null | Fecha de vencimiento (ISO 8601) |
ruc | string | null | RUC del titular |
legalName | string | null | Nombre registrado en el certificado |
Si
hasCertificateesfalseoisExpiredestrue, las facturas no se firmarán hasta que renueves el certificado en Configuración → Certificado desde tu cuenta.
Subir firma electrónica (P12)
Sección titulada «Subir firma electrónica (P12)»Sube un certificado P12 con su contraseña. Si el RUC del certificado no existe aún como contribuyente en tu cuenta y tu plan lo permite, el contribuyente se crea automáticamente y se vincula a tu API key.
import { readFileSync } from "fs"
const p12 = readFileSync("./mi-firma.p12")const result = await factuplan.certificates.upload(p12, "mi-contraseña")
console.log(result.ruc) // RUC extraído del certificadoconsole.log(result.legalName) // Nombre legalconsole.log(result.expiresAt) // Fecha de vencimientoconsole.log(result.created) // true si se creó un nuevo contribuyente| Campo | Tipo | Descripción |
|---|---|---|
hasCertificate | boolean | true si el certificado se subió correctamente |
isExpired | boolean | true si el certificado ya expiró |
daysUntilExpiry | number | Días restantes hasta el vencimiento |
ruc | string | RUC extraído del certificado |
legalName | string | Nombre legal extraído del certificado |
expiresAt | string | Fecha de vencimiento (ISO 8601) |
created | boolean | true si se creó automáticamente un nuevo contribuyente (solo presente cuando se creó) |
¿Necesitas ayuda integrando el API? Escríbenos a info@factuplan.com.ec o desde el Centro de ayuda.