Factuplan SDK
Librería oficial de JavaScript y TypeScript para crear facturas electrónicas autorizadas por el SRI de Ecuador. Zero dependencies.
Instalación
npm install factuplanpnpm add factuplanyarn add factuplanAutenticación
Crea una API key desde Developer → Crear API key en tu cuenta de Factuplan. Los API keys son a nivel de workspace — debes enviar el header x-taxpayer-ruc en cada petición para identificar el contribuyente a operar.
import { Factuplan } from 'factuplan';
// Inicializar con RUC del contribuyente (requerido para la mayoría de operaciones)
const factuplan = new Factuplan('ak_test_tu_api_key_aqui', {
ruc: '0950194407001', // RUC del contribuyente a operar
});
// Sin RUC (solo para queryExternalByAccessKey u operaciones que no requieren contribuyente)
const factuplan = new Factuplan('ak_test_tu_api_key_aqui');Nunca expongas tu API key en el frontend. Úsala solo desde tu servidor backend.
Entorno de pruebas (ak_test_...)
Los documentos emitidos con una API key de prueba se eliminan automáticamente cada hora. Esto incluye facturas, notas de crédito, guías de remisión y cualquier otro comprobante generado en modo TESTING. No uses el entorno de pruebas para documentos que necesites conservar.
Clientes
Crear
const cliente = await factuplan.customers.create({
identificationType: 'RUC', // RUC | CEDULA | PASSPORT | FINAL_CONSUMER | EXTERIOR
identification: '0000000000001', // RUC=13 dígitos | CEDULA=10 dígitos
legalName: 'Empresa Demo S.A.',
email: 'contacto@empresa.com', // opcional
address: 'Guayaquil, Ecuador', // opcional
phone: '+593000000000', // opcional — ej. +593 EC, +1 US, +44 UK
});Listar
const { data: clientes, meta } = await factuplan.customers.list({
page: 1,
limit: 20,
search: 'empresa',
});
console.log(meta.total, 'clientes encontrados');Obtener / Actualizar / Eliminar
const cliente = await factuplan.customers.get('id');
await factuplan.customers.update('id', { email: 'nuevo@email.com' });
await factuplan.customers.delete('id');Productos
Crear
const producto = await factuplan.products.create({
code: 'SERV-001',
auxiliaryCode: 'SERV-001', // opcional — por defecto toma el mismo valor que code
name: 'Servicio de consultoría',
unitPrice: 150.00,
type: 'SERVICE', // PRODUCT | SERVICE
taxType: 'IVA_RATE', // IVA_0 | IVA_RATE | NOT_TAXABLE | EXEMPT
description: 'Hora de consultoría técnica',
});Listar / Actualizar / Eliminar
const { data: productos } = await factuplan.products.list({ search: 'consultoría' });
await factuplan.products.update('id', { unitPrice: 175.00 });
await factuplan.products.delete('id');Facturas
| Método SDK | Descripción |
|---|---|
| invoices.create(input) | Crear factura electrónica completa |
| invoices.list(params) | Buscar y listar facturas con filtros |
| invoices.get(id) | Obtener detalle del comprobante |
| invoices.getStatus(id) | Consultar estado de procesamiento |
| invoices.downloadXml(id) | Descargar XML autorizado (URL pre-firmada) |
| invoices.downloadPdf(id) | Descargar PDF RIDE (URL pre-firmada) |
| invoices.importByAccessKey(input) | Importar factura existente desde el SRI |
| invoices.queryExternalByAccessKey(input) | Consultar comprobante externo por clave |
| invoices.void(id, reason) | Anular comprobante |
| invoices.retry(id) | Reintentar una factura que no llegó a autorizarse |
| invoices.updateSequential(branch, point, seq) | Cambiar secuencial del punto de emisión |
Crear factura
Crea una factura electrónica completa. El sistema genera el XML, lo firma, lo envía al SRI, genera el PDF y envía el email al cliente. Usa sendEmail: false para omitir el correo.
Parámetros destacados
| Campo | Tipo | Default | Descripción |
|---|---|---|---|
| sendEmail | boolean | true | Si false, el PDF se genera pero no se envía correo al cliente. |
const factura = await factuplan.invoices.create({
// Punto de emisión: elige UNA de estas 3 opciones
// Opción 1: por UUID
emissionPointId: 'ep_id',
// Opción 2: por códigos SRI
// establishment: '001',
// emissionPoint: '001',
// Opción 3: omitir todo (auto-detecta si solo hay un punto de emisión activo)
customer: {
identificationType: 'RUC',
identification: '0000000000001', // RUC=13 dígitos | CEDULA=10 dígitos
legalName: 'Cliente S.A.',
email: 'cliente@email.com',
saveToContacts: true,
},
items: [
{
code: 'SERV-001',
auxiliaryCode: 'SERV-001', // opcional — por defecto toma el mismo valor que code
description: 'Servicio de consultoría',
quantity: 1,
unitPrice: 1.00,
discount: 0,
taxType: 'IVA_RATE', // IVA_0 | IVA_RATE | NOT_TAXABLE | EXEMPT
tax: 15, // Tarifa IVA: 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: omite el envío de correo al cliente (default: true)
});
console.log(factura.id); // ID del comprobante
console.log(factura.accessKey); // Clave de acceso (49 dígitos)
console.log(factura.sequential); // Número secuencialPunto de emisión
El punto de emisión se puede resolver de tres formas. Si no se envía ningún parámetro, el sistema auto-detecta el único punto de emisión activo del contribuyente.
// Usar códigos SRI del establecimiento y punto de emisión
const factura = await factuplan.invoices.create({
establishment: '001',
emissionPoint: '001',
customer: { ... },
items: [ ... ],
});// Si solo tienes un punto de emisión activo, no necesitas enviarlo
const factura = await factuplan.invoices.create({
customer: { ... },
items: [ ... ],
});Tipos de Impuesto (taxType)
Define el tipo de impuesto aplicable a cada ítem. Los valores posibles son:
| taxType | Descripción |
|---|---|
| IVA_RATE | Aplica una tarifa de IVA específica (usar campo tax). |
| IVA_0 | Aplica tarifa 0% (Código SRI 0). |
| NOT_TAXABLE | No aplica IVA (Código SRI 6). |
| EXEMPT | Exento de IVA (Código SRI 7). |
Tarifas de IVA
El campo tax permite especificar la tarifa de IVA cuando usas taxType: 'IVA_RATE'. Si no se envía, se aplica 15% por defecto.
| 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) |
items: [
{
code: 'PROD-001',
description: 'Producto con IVA reducido',
quantity: 1,
unitPrice: 100.00,
taxType: 'IVA_RATE',
tax: 5, // IVA 5% en lugar del 15% por defecto
},
]Formas de pago
Usa el arreglo payments para especificar una o varias formas de pago. Cada objeto debe incluir el method (código SRI), amount (monto total), y opcionalmente term y timeUnit. Si no se envía el arreglo, se aplica '01' (Sin utilización del sistema financiero) por defecto.
Importante
La suma de los campos amount en todas las formas de pago enviadas debe ser exactamente igual al total de la factura (incluyendo impuestos). Si los montos no coinciden, la petición será rechazada con un error 400 Bad Request.
| 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 |
| Unidad de tiempo (timeUnit) | Descripción |
|---|---|
| dias | Días |
| meses | Meses |
| anios | Años |
Redondeo y Precisión
Factuplan utiliza reglas de redondeo automáticas para cumplir con los estándares del SRI manteniendo la mayor precisión en los cálculos.
Precios Unitarios
Puedes enviar el campo unitPrice con hasta 4 decimales. Esto es ideal para productos con costos unitarios bajos donde la precisión es crítica.
Totales e Impuestos
Todos los totales finales (subtotal, IVA, total factura) se redondean automáticamente a 2 decimales usando el estándar del SRI.
Escenarios de Redondeo
1. Multiplicación de precisión:
Cantidad: 1000 × Precio: 0.1234 = Subtotal: 123.40
2. Redondeo de IVA (15%):
Base Imponible: 10.05 × 0.15 = 1.5075 → IVA: 1.51
Total a pagar (Base + IVA): 11.56
Buscar facturas
Lista las facturas emitidas con paginación. Filtra por RUC del cliente, estado y rango de fechas.
const result = await factuplan.invoices.list({
status: 'AUTHORIZED',
dateFrom: '2025-01-01',
dateTo: '2025-12-31',
page: 1,
limit: 20,
});
result.data.forEach((inv) => {
console.log(inv.sequential, inv.total, inv.customer.legalName);
});
// result.meta: { total, page, limit, totalPages }| Parámetro | Tipo | Descripción |
|---|---|---|
| ruc | string | Filtrar por RUC o cédula del cliente |
| status | string | Estado: DRAFT | PENDING | PROCESSING | AUTHORIZED | REJECTED | VOIDED |
| dateFrom | string | Fecha de inicio en formato ISO 8601 (ej. "2025-01-01") |
| dateTo | string | Fecha de fin en formato ISO 8601 (ej. "2025-12-31") |
| page | number | Número de página (por defecto: 1) |
| limit | number | Resultados por página (por defecto: 20, máximo: 100) |
Consultar estado
const estado = await factuplan.invoices.getStatus('factura_id');
// estado.status: PROCESSING | AUTHORIZED | COMPLETED | ERROR | REJECTED
if (estado.status === 'COMPLETED') {
console.log(estado.authorizationNumber);
}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
Importa una factura ya autorizada por el SRI usando su clave de acceso de 49 dígitos. Útil para sincronizar comprobantes emitidos fuera de Factuplan.
ak_live_...). Esta función consulta el SRI en tiempo real y no está disponible con claves de prueba (ak_test_...).const factura = await factuplan.invoices.importByAccessKey({
accessKey: '0104202601099337815000110010010000000011234567890',
});
console.log(factura.id);
console.log(factura.sequential);/developer/receipts/queryConsultar comprobante externo por clave de acceso
Consulta un comprobante ya autorizado por el SRI emitido desde cualquier sistema de facturación. El contribuyente emisor se crea automáticamente en tu workspace (con firma vacía) si no existe. Soporta Facturas, Notas de Crédito y Guías de Remisión.
ak_live_...). Esta función consulta el SRI en tiempo real y no está disponible con claves de prueba (ak_test_...).Parámetros
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| accessKey | string | Sí | Clave de acceso de 49 dígitos del SRI |
| save | boolean | No (default: true) | Si false, no guarda registros en la base de datos ni genera PDF. El contador sí se descuenta si el comprobante está autorizado. |
save. Si el comprobante no está autorizado, no se descuenta ningún crédito.// Consultar y guardar (comportamiento por defecto)
const comprobante = await factuplan.invoices.queryExternalByAccessKey({
accessKey: '0104202601019999999900110010010000000011234567813',
});
console.log(comprobante.id); // ID del documento guardado
console.log(comprobante.status); // 'AUTHORIZED'
console.log(comprobante.xmlBase64); // XML en base64
// Solo verificar sin guardar (save=false)
const verificado = await factuplan.invoices.queryExternalByAccessKey({
accessKey: '0104202601019999999900110010010000000011234567813',
save: false,
});
console.log(verificado.id); // null (no guardado)
console.log(verificado.xmlBase64); // XML autorizado en base64Ejemplo completo
import { Factuplan } from 'factuplan';
const factuplan = new Factuplan(process.env.FACTUPLAN_API_KEY!);
async function crearFactura() {
// 1. Buscar o crear cliente
const { data: clientes } = await factuplan.customers.list({
search: '0993378150001',
});
let cliente = clientes[0] ?? await factuplan.customers.create({
identificationType: 'RUC',
identification: '0993378150001',
legalName: 'Mi Cliente S.A.',
email: 'cliente@email.com',
});
// 2. Crear factura (sin emissionPointId → auto-detecta)
const factura = await factuplan.invoices.create({
customer: {
identificationType: cliente.identificationType,
identification: cliente.identification,
legalName: cliente.legalName,
email: cliente.email,
},
items: [
{
code: 'PROD-001',
description: 'Laptop Dell XPS 15',
quantity: 1,
unitPrice: 1500.00,
taxType: 'IVA_RATE',
tax: 15,
},
],
payments: [
{
method: '19',
amount: 1725.00, // Total con IVA
term: 6,
timeUnit: 'meses',
},
],
});
// 3. Esperar autorización
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 y autorizar XML
Si ya generas tu propio XML de factura (sin firmar), puedes enviarlo a Factuplan para que lo firme con tu certificado electrónico y lo autorice ante el SRI.
Importante: El XML debe enviarse sin firmar. Factuplan se encarga de la firma electrónica.
const result = await factuplan.invoices.signAndAuthorize({
xml: xmlString, // XML sin firmar como string
});
console.log(result.id); // ID del comprobante
console.log(result.accessKey); // Clave de acceso SRI
console.log(result.status); // Estado inicial
// Esperar autorización
let estado;
do {
await new Promise(r => setTimeout(r, 3000));
estado = await factuplan.invoices.getStatus(result.id);
} while (!['COMPLETED', 'ERROR', 'REJECTED'].includes(estado.status));Notas de crédito
Corrige total o parcialmente una factura ya autorizada. Se identifica la factura original por su clave de acceso de 49 dígitos. Si no existe en el sistema, se importa automáticamente desde el SRI.
| Método SDK | Descripción |
|---|---|
| creditNotes.create(input) | Emitir nota de crédito |
| creditNotes.list(params) | Buscar y listar notas de crédito con filtros |
| creditNotes.get(id) | Obtener detalle del comprobante |
| creditNotes.getStatus(id) | Consultar estado de procesamiento |
| creditNotes.downloadXml(id) | Descargar XML autorizado (URL pre-firmada) |
| creditNotes.downloadPdf(id) | Descargar PDF RIDE (URL pre-firmada) |
| creditNotes.void(id, reason) | Anular nota de crédito |
| creditNotes.retry(id) | Reintentar una nota de crédito que no llegó a autorizarse |
Emitir nota de crédito
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',
},
],
});
console.log(creditNote.id); // 'clx1234...'
console.log(creditNote.accessKey); // '0412202404...'
console.log(creditNote.status); // 'PROCESSING'Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| invoiceAccessKey | string | Sí | Clave de acceso de 49 dígitos de la factura original |
| reason | string | Sí | Motivo de la nota de crédito |
| items | array | Sí | Ítems a acreditar (misma estructura que en facturas) |
| emissionPointId | string | No | UUID del punto de emisión (hereda el de la factura si se omite) |
| payments | array | No | Métodos de pago |
| additionalInfo | object | No | Campos adicionales clave-valor |
Respuesta 201
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | ID de la nota de crédito |
| accessKey | string | Clave de acceso de 49 dígitos |
| sequential | string | Secuencial (ej: "000000001") |
| status | string | "PROCESSING" — se autoriza de forma asíncrona |
| total | number | Importe total de la nota de crédito |
Consultar detalle
const creditNote = await factuplan.creditNotes.get('CREDIT_NOTE_ID');
console.log(creditNote.id);
console.log(creditNote.accessKey);
console.log(creditNote.status);
console.log(creditNote.total);Consultar estado
const estado = await factuplan.creditNotes.getStatus('CREDIT_NOTE_ID');
// estado.status: PROCESSING | AUTHORIZED | COMPLETED | ERROR | REJECTED
if (estado.status === 'COMPLETED') {
console.log(estado.authorizationNumber);
}Descargar XML y PDF
const { url: xmlUrl } = await factuplan.creditNotes.downloadXml('CREDIT_NOTE_ID');
const { url: pdfUrl } = await factuplan.creditNotes.downloadPdf('CREDIT_NOTE_ID');Buscar notas de crédito
Lista las notas de crédito emitidas con paginación. Filtra por RUC del cliente, estado y rango de fechas.
const result = await factuplan.creditNotes.list({
status: 'AUTHORIZED',
dateFrom: '2025-01-01',
dateTo: '2025-12-31',
page: 1,
limit: 20,
});
result.data.forEach((nc) => {
console.log(nc.sequential, nc.total, nc.customer.legalName);
});
// result.meta: { total, page, limit, totalPages }| Parámetro | Tipo | Descripción |
|---|---|---|
| ruc | string | Filtrar por RUC o cédula del cliente |
| status | string | Estado: DRAFT | PENDING | PROCESSING | AUTHORIZED | REJECTED | VOIDED |
| dateFrom | string | Fecha de inicio en formato ISO 8601 (ej. "2025-01-01") |
| dateTo | string | Fecha de fin en formato ISO 8601 (ej. "2025-12-31") |
| page | number | Número de página (por defecto: 1) |
| limit | number | Resultados por página (por defecto: 20, máximo: 100) |
Firmar y autorizar XML
Si ya tienes el XML de la nota de crédito (sin firmar), Factuplan lo firma y autoriza ante el SRI. La factura vinculada se resuelve automáticamente desde el XML.
Importante: El XML debe enviarse sin firmar.
const result = await factuplan.creditNotes.signAndAuthorize({
xml: xmlString,
});
console.log(result.id);
console.log(result.accessKey);
console.log(result.status);Anular nota de crédito
const result = await factuplan.creditNotes.void(
'CREDIT_NOTE_ID',
'Error en los datos del cliente',
);
console.log(result.status); // "VOIDED"
console.log(result.voidReason); // Motivo ingresado
console.log(result.message); // ConfirmaciónLa anulación de la nota de crédito ante el SRI es responsabilidad del desarrollador. El endpoint /void solo cambia el estado en Factuplan.
Reintentar nota de crédito
Vuelve a lanzar el flujo de emisión de una nota de crédito que no llegó a autorizarse. El paso que se reintenta se deduce del estado y llega en action. Los estados admitidos y las advertencias están en Reintentar comprobante.
const result = await factuplan.creditNotes.retry('CREDIT_NOTE_ID');
console.log(result.action); // "reprocess" | "authorize" | "pdf"
console.log(result.accessKey); // Puede ser una clave NUEVASecuencial
Establece el próximo número secuencial para notas de crédito en el punto de emisión indicado. El nuevo valor entra en efecto en el siguiente comprobante emitido.
const result = await factuplan.creditNotes.updateSequential(
'001', // branchCode — código del establecimiento
'001', // emissionCode — código del punto de emisión
100, // creditNoteSequential — nuevo valor del secuencial
);
console.log(result.creditNoteSequential); // 100
console.log(result.updatedAt); // '2026-06-08T12:00:00.000Z'| Campo | Tipo | Descripción |
|---|---|---|
| branchCode | string | Código del establecimiento |
| emissionCode | string | Código del punto de emisión |
| creditNoteSequential | number | Nuevo valor del secuencial aplicado |
| updatedAt | string | Fecha de actualización (ISO 8601) |
Notas de débito
Registra cargos adicionales sobre una factura ya autorizada: ajustes de precio, mora, flete no facturado, etc. Se identifica la factura original por su clave de acceso de 49 dígitos.
| Método SDK | Descripción |
|---|---|
| debitNotes.create(input) | Emitir nota de débito |
| debitNotes.list(params) | Buscar y listar notas de débito con filtros |
| debitNotes.get(id) | Obtener detalle del comprobante |
| debitNotes.getStatus(id) | Consultar estado de procesamiento |
| debitNotes.downloadXml(id) | Descargar XML autorizado (URL pre-firmada) |
| debitNotes.downloadPdf(id) | Descargar PDF RIDE (URL pre-firmada) |
| debitNotes.void(id, reason) | Anular nota de débito |
| debitNotes.retry(id) | Reintentar una nota de débito que no llegó a autorizarse |
Emitir nota de débito
const debitNote = await factuplan.debitNotes.create({
invoiceAccessKey: '2312202301179214289100110010020000001231234567811',
reasons: [
{
description: 'Ajuste por flete no facturado',
amount: 25.00,
taxType: 'IVA', // IVA | IVA_0 | NO_TAX | ICE | IRBPNR | IVA_EXEMPT
},
],
// additionalInfo: { Referencia: 'OC-4521' }, // opcional
});
console.log(debitNote.id); // 'clx1234...'
console.log(debitNote.accessKey); // '0512202404...'
console.log(debitNote.status); // 'PROCESSING'Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| invoiceAccessKey | string | Sí | Clave de acceso de 49 dígitos de la factura original |
| reasons | array | Sí | Motivos / cargos (al menos uno requerido) |
| reasons[].description | string | Sí | Descripción del cargo o ajuste |
| reasons[].amount | number | Sí | Importe del cargo (mínimo 0.01) |
| reasons[].taxType | string | Sí | Tipo de impuesto: IVA | IVA_0 | NO_TAX | ICE | IRBPNR | IVA_EXEMPT |
| emissionPointId | string | No | UUID del punto de emisión (auto-detecta si se omite) |
| additionalInfo | object | No | Campos adicionales clave-valor |
Buscar notas de débito
const { data: debitNotes, meta } = await factuplan.debitNotes.list({
status: 'COMPLETED',
page: 1,
limit: 20,
});Consultar estado
const estado = await factuplan.debitNotes.getStatus('DEBIT_NOTE_ID');
// estado.status: PROCESSING | AUTHORIZED | COMPLETED | ERROR | REJECTED
if (estado.status === 'COMPLETED') {
console.log(estado.authorizationNumber);
}Descargar XML y PDF
const { url: xmlUrl } = await factuplan.debitNotes.downloadXml('DEBIT_NOTE_ID');
const { url: pdfUrl } = await factuplan.debitNotes.downloadPdf('DEBIT_NOTE_ID');Firmar y autorizar XML
Si ya tienes el XML de la nota de débito (sin firmar), Factuplan lo firma y autoriza ante el SRI. La factura vinculada se resuelve automáticamente desde el XML.
Importante: El XML debe enviarse sin firmar.
const result = await factuplan.debitNotes.signAndAuthorize({
xml: xmlString,
});
console.log(result.id);
console.log(result.accessKey);
console.log(result.status);Anular
const voided = await factuplan.debitNotes.void('DEBIT_NOTE_ID', 'Emitida por error');
console.log(voided.status); // 'VOIDED'La anulación de la nota de débito ante el SRI es responsabilidad del desarrollador. El endpoint /void solo cambia el estado en Factuplan.
Reintentar nota de débito
Vuelve a lanzar el flujo de emisión de una nota de débito que no llegó a autorizarse. El paso que se reintenta se deduce del estado y llega en action. Los estados admitidos y las advertencias están en Reintentar comprobante.
const result = await factuplan.debitNotes.retry('DEBIT_NOTE_ID');
console.log(result.action); // "reprocess" | "authorize" | "pdf"
console.log(result.accessKey); // Puede ser una clave NUEVASecuencial
Establece el próximo número secuencial para notas de débito en el punto de emisión indicado. El nuevo valor entra en efecto en el siguiente comprobante emitido.
const result = await factuplan.debitNotes.updateSequential(
'001', // branchCode — código del establecimiento
'001', // emissionCode — código del punto de emisión
100, // debitNoteSequential — nuevo valor del secuencial
);
console.log(result.debitNoteSequential); // 100
console.log(result.updatedAt); // '2026-06-08T12:00:00.000Z'| Campo | Tipo | Descripción |
|---|---|---|
| branchCode | string | Código del establecimiento |
| emissionCode | string | Código del punto de emisión |
| debitNoteSequential | number | Nuevo valor del secuencial aplicado |
| updatedAt | string | Fecha de actualización (ISO 8601) |
Retenciones
Emite comprobantes de retención en la fuente (renta, IVA e ISD) vinculados a una factura ya autorizada, identificada por su clave de acceso de 49 dígitos. El sistema reserva el secuencial, genera el XML, lo firma y lo autoriza ante el SRI de forma asíncrona.
| Método SDK | Descripción |
|---|---|
| withholdings.create(input) | Emitir comprobante de retención |
| withholdings.list(params) | Buscar y listar retenciones con filtros |
| withholdings.get(id) | Obtener detalle del comprobante |
| withholdings.getStatus(id) | Consultar estado de procesamiento |
| withholdings.downloadXml(id) | Descargar XML autorizado (URL pre-firmada) |
| withholdings.downloadPdf(id) | Descargar PDF RIDE (URL pre-firmada) |
| withholdings.void(id, reason) | Anular retención |
| withholdings.retry(id) | Reintentar una retención que no llegó a autorizarse |
Emitir retención
const withholding = await factuplan.withholdings.create({
invoiceAccessKey: '2312202301179214289100110010020000001231234567811',
taxes: [
{
taxType: 'RENTA', // 'RENTA' | 'IVA' | 'ISD'
taxRateCode: '303', // Código SRI (Tabla 25)
taxRate: 10, // Porcentaje de retención
taxBase: 1000.00, // Base imponible
fiscalPeriod: '06/2026', // Período fiscal MM/YYYY (opcional)
},
{
taxType: 'IVA',
taxRateCode: '725',
taxRate: 30,
taxBase: 150.00,
},
],
additionalInfo: { observacion: 'Servicios profesionales' }, // opcional
});
console.log(withholding.id); // 'clx1234...'
console.log(withholding.accessKey); // '0512202404...'
console.log(withholding.status); // 'PROCESSING'Buscar retenciones
const { data: withholdings, meta } = await factuplan.withholdings.list({
status: 'AUTHORIZED',
dateFrom: '2026-01-01',
dateTo: '2026-06-30',
page: 1,
limit: 20,
});Consultar estado
const estado = await factuplan.withholdings.getStatus('WITHHOLDING_ID');
console.log(estado.status); // 'AUTHORIZED'
console.log(estado.authorizationNumber); // Número de autorización SRI
console.log(estado.authorizationDate); // Fecha de autorizaciónDescargar XML / PDF
const { url: xmlUrl } = await factuplan.withholdings.downloadXml('WITHHOLDING_ID');
const { url: pdfUrl } = await factuplan.withholdings.downloadPdf('WITHHOLDING_ID');Firmar y autorizar XML
Si ya tienes el XML de la retención (sin firmar), Factuplan lo firma y autoriza ante el SRI. La factura vinculada se resuelve automáticamente desde el XML.
Importante: El XML debe enviarse sin firmar.
const result = await factuplan.withholdings.signAndAuthorize({
xml: xmlString,
});
console.log(result.id);
console.log(result.accessKey);
console.log(result.status);Anular
const voided = await factuplan.withholdings.void('WITHHOLDING_ID', 'Emitida por error');
console.log(voided.status); // 'VOIDED'Reintentar retención
Vuelve a lanzar el flujo de emisión de una retención que no llegó a autorizarse. El paso que se reintenta se deduce del estado y llega en action. Los estados admitidos y las advertencias están en Reintentar comprobante.
const result = await factuplan.withholdings.retry('WITHHOLDING_ID');
console.log(result.action); // "reprocess" | "authorize" | "pdf"
console.log(result.accessKey); // Puede ser una clave NUEVALa anulación de la retención ante el SRI es responsabilidad del desarrollador. El endpoint /void solo cambia el estado en Factuplan.
Guías de remisión
Emite guías de remisión electrónicas para documentar el traslado de mercadería. El sistema genera el XML, lo firma con tu certificado y lo autoriza ante el SRI de forma asíncrona.
| Método SDK | Descripción |
|---|---|
| waybills.create(input) | Emitir guía de remisión |
| waybills.list(params) | Buscar y listar guías con filtros |
| waybills.get(id) | Obtener detalle de la guía |
| waybills.getStatus(id) | Consultar estado de procesamiento |
| waybills.downloadXml(id) | Descargar XML autorizado (URL pre-firmada) |
| waybills.downloadPdf(id) | Descargar PDF RIDE (URL pre-firmada) |
Emitir guía de remisión
const guia = await factuplan.waybills.create({
emissionPointId: 'EMISSION_POINT_ID',
customer: {
identificationType: 'RUC',
identification: '1790012345001',
legalName: 'Empresa Destino S.A.',
email: 'destino@empresa.com',
},
transporterRuc: '1790012345001',
transporterName: 'Transportes del Norte S.A.',
vehiclePlate: 'ABC-1234',
departureAddress: 'Av. 10 de Agosto N23-45, Quito',
transportStartDate: '2026-06-10',
transportEndDate: '2026-06-11',
items: [
{
code: 'PROD-001',
description: 'Caja de mercadería',
quantity: 10,
},
],
destinations: [
{
receiverIdentification: '0950194407001',
receiverName: 'Cliente Final',
address: 'Av. del Ejército 123, Guayaquil',
transferReason: 'Venta de mercadería',
},
],
});
console.log(guia.id); // 'clx1234...'
console.log(guia.accessKey); // '0606202604...'
console.log(guia.status); // 'PROCESSING'| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| emissionPointId | string | Sí | ID del punto de emisión |
| customer | object | Sí | Destinatario (ID existente o datos inline) |
| transporterRuc | string | Sí | RUC del transportista |
| transporterName | string | Sí | Nombre del transportista |
| vehiclePlate | string | Sí | Placa del vehículo (ej. ABC-1234) |
| departureAddress | string | Sí | Dirección de inicio del transporte |
| transportStartDate | string | Sí | Fecha inicio transporte (ISO 8601) |
| transportEndDate | string | Sí | Fecha fin transporte (ISO 8601) |
| items | array | Sí | Mercadería transportada |
| destinations | array | No | Destinos del traslado |
| additionalInfo | object | No | Información adicional clave-valor |
Buscar guías
const { data: guias, meta } = await factuplan.waybills.list({
status: 'AUTHORIZED',
dateFrom: '2026-01-01',
dateTo: '2026-06-30',
page: 1,
limit: 20,
});Consultar estado
const estado = await factuplan.waybills.getStatus('WAYBILL_ID');
console.log(estado.status); // 'AUTHORIZED'
console.log(estado.authorizationNumber); // Número de autorización SRI
console.log(estado.authorizationDate); // Fecha de autorizaciónDescargar XML / PDF
const { url: xmlUrl } = await factuplan.waybills.downloadXml('WAYBILL_ID');
const { url: pdfUrl } = await factuplan.waybills.downloadPdf('WAYBILL_ID');Anular
const voided = await factuplan.waybills.void('WAYBILL_ID', 'Emitida por error');
console.log(voided.status); // 'VOIDED'La anulación de la guía ante el SRI es responsabilidad del desarrollador. El endpoint /void solo cambia el estado en Factuplan.
Firma & Certificados
Manejo de errores
import { FactuplanError, AuthenticationError } from 'factuplan';
try {
await factuplan.invoices.create({ ... });
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('API key inválida');
} else if (error instanceof FactuplanError) {
console.error(error.code); // "INVOICE_4002"
console.error(error.message); // Descripción
console.error(error.statusCode); // 422
console.error(error.details); // Detalles adicionales
}
}Códigos de error
| Código | HTTP | Descripción |
|---|---|---|
| AUTH_ERROR | 401 | API key inválida o expirada |
| RATE_LIMIT | 429 | Límite de solicitudes excedido |
| API_10001 | 403 | Plan no compatible con la operación |
| API_10002 | 429 | Cuota mensual excedida |
| INVOICE_4002 | 422 | Datos del cliente inválidos |
| INVOICE_4003 | 422 | Detalle de factura vacío |
| CERT_3008 | 422 | Certificado expirado |
| CUSTOMER_7004 | 409 | Cliente duplicado |
| PRODUCT_6002 | 409 | Código de producto duplicado |
Rate Limit
Cada API key tiene un límite de velocidad y una cuota mensual para garantizar un servicio estable para todos los usuarios.
100
peticiones por segundo por API key
Según plan
documentos emitidos por mes
Al superar cualquiera de los dos límites recibirás un HTTP 429. Implementa reintentos con espera exponencial para manejarlos correctamente.
Respuesta al superar el límite
// HTTP 429 — Rate limit de velocidad (100 req/s)
{
"success": false,
"error": {
"code": "GENERAL_0003",
"message": "Has superado el límite de peticiones. Intenta de nuevo en unos segundos."
}
}
// HTTP 429 — Cuota mensual agotada
{
"success": false,
"error": {
"code": "API_10002",
"message": "Has alcanzado el límite mensual de 500 documentos.",
"details": { "used": 500, "quota": 500 }
}
}Manejo recomendado con reintentos
import { RateLimitError } from 'factuplan';
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...
console.log(`Rate limit — reintentando en ${espera}ms`);
await new Promise(r => setTimeout(r, espera));
} else {
throw error;
}
}
}
}Webhooks
Cuando un comprobante cambia de estado te enviamos un POST a la URL que configures. Cada entrega va firmada: verifica siempre la firma antes de confiar en el contenido.
Verificar la firma
Cada entrega incluye la cabecera X-Factuplan-Signature con este formato:
X-Factuplan-Signature: t=1756288800,v1=5f3c9a...
X-Factuplan-Event: invoice.authorized
X-Factuplan-Delivery: 0198f2c1-...
X-Factuplan-Webhook-Id: 0198f2b0-...t es el timestamp Unix del envío y v1 es el HMAC-SHA256, en hexadecimal, de la cadena {t}.{cuerpo_crudo}. La clave es el secreto whsec_… que se muestra una sola vez al crear el webhook.
import express from 'express';
import { verifyWebhookSignature } from 'factuplan';
const app = express();
// Conserva el cuerpo crudo: es sobre esos bytes que se calcula la firma.
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf } }));
app.post('/webhooks/factuplan', (req, res) => {
const valido = verifyWebhookSignature(
(req as any).rawBody,
req.headers['x-factuplan-signature'],
process.env.FACTUPLAN_WEBHOOK_SECRET!,
);
if (!valido) return res.status(400).send('firma invalida');
// Responde rapido y procesa despues.
res.sendStatus(200);
procesarEvento(req.body);
});Firma sobre el cuerpo crudo. Si tu framework ya parseó el JSON y lo vuelves a serializar, los bytes cambian y la firma no coincide.
Valida la ventana de tiempo. El timestamp va dentro del HMAC; rechazar entregas con más de 5 minutos es lo que evita que alguien reenvíe una entrega antigua.
Compara en tiempo constante (timingSafeEqual), no con ===.
Puedes rotar el secreto cuando quieras desde Desarrollador → Webhooks. El anterior deja de validar de inmediato y el nuevo solo se muestra al rotar, así que ten listo el despliegue de tu endpoint antes de hacerlo.
Entregas y reintentos
- Consideramos entregada una notificación solo si respondes
2xx. Si no, reintentamos hasta 5 veces con espera creciente (aprox. 5s, 25s, 2m, 10m, 52m). - Tras 10 entregas fallidas consecutivas desactivamos el webhook. Puedes reactivarlo desde el panel.
- Todos los reintentos de un mismo evento comparten el
iddel evento y la cabeceraX-Factuplan-Delivery. Guárdalos y descarta duplicados: un reintento puede llegar después de que el original ya se procesó. - Responde rápido (idealmente menos de 10 segundos) y procesa de forma asíncrona. El tiempo de espera de nuestra parte es de 10 segundos.
- El ambiente de pruebas dispara exactamente los mismos eventos, con el mismo formato y la misma firma que producción.
Verificar comprobante
const receipt = await factuplan.webhooks.verifyReceipt('receipt_id');
console.log(receipt.id);
console.log(receipt.status); // PROCESSING | AUTHORIZED | COMPLETED | ERROR | REJECTED
console.log(receipt.accessKey); // Clave de acceso SRI (49 dígitos)Tip: Llama a este endpoint al recibir un evento webhook para confirmar que el comprobante existe antes de actualizar tu base de datos.
Uso & Límites
Consulta el consumo de API del mes en curso y el límite de tu plan.
Consultar uso
const uso = await factuplan.usage();
console.log(uso.requestsUsed, '/', uso.requestsLimit);
console.log('Costo estimado: $' + uso.estimatedCost);Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| requestsUsed | number | Solicitudes usadas este mes |
| requestsLimit | number | Límite del plan actual |
| estimatedCost | number | Costo estimado en USD |
| periodStart | string | Inicio del período (ISO 8601) |
| periodEnd | string | Fin del período (ISO 8601) |
Certificado
Consulta el estado del certificado electrónico P12 configurado en tu cuenta.
Estado del certificado
const cert = await factuplan.certificateStatus();
console.log(cert.valid); // true = vigente
console.log(cert.expiresAt); // Fecha de vencimiento (ISO 8601)
console.log(cert.taxpayerId); // RUC del titular
console.log(cert.commonName); // Nombre en el certificadoRespuesta
| Campo | Tipo | Descripción |
|---|---|---|
| valid | boolean | true si el certificado está vigente |
| expiresAt | string | Fecha de vencimiento (ISO 8601) |
| taxpayerId | string | RUC del titular del certificado |
| commonName | string | Nombre registrado en el certificado |
Si valid es false, las facturas no podrán firmarse hasta que renueves el certificado en Configuración → Certificado.
Anular comprobante
Anula un comprobante a nivel del sistema. El estado cambia a VOIDED. Solo se pueden anular comprobantes en estado AUTHORIZED o COMPLETED.
const result = await factuplan.invoices.void(
'ID_DEL_COMPROBANTE',
'Error en los datos del cliente',
);
console.log(result.status); // "VOIDED"
console.log(result.voidReason); // Motivo ingresado
console.log(result.message); // ConfirmaciónRespuesta
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | ID del comprobante anulado |
| status | string | "VOIDED" (estado anulado) |
| accessKey | string | Clave de acceso del comprobante |
| voidReason | string | Motivo de la anulación |
| message | string | Mensaje de confirmación |
La anulación ante el SRI es responsabilidad del desarrollador. Este endpoint solo cambia el estado del comprobante en Factuplan.
Reintentar comprobante
Vuelve a lanzar el flujo de emisión de un comprobante que no llegó a autorizarse. No necesitas saber en qué paso se quedó: el sistema lo deduce del estado del comprobante y te lo informa en el campo action.
Factuplan ya reintenta solo los fallos transitorios del SRI (5 intentos por cola, más un barrido automático cada 5 minutos). Este endpoint es para cuando esos reintentos automáticos se agotaron y quieres volver a intentarlo desde tu integración.
| Estado del comprobante | action | Qué hace |
|---|---|---|
| ERROR, REJECTED, RETURNED | reprocess | Regenera el XML, lo firma y lo reenvía al SRI |
| RECEIVED_SRI | authorize | Vuelve a consultar la autorización en el SRI |
| AUTHORIZED sin PDF | Regenera únicamente el RIDE (PDF) | |
| PROCESSING, SENDING_SRI, VOIDED, AUTHORIZED con PDF | — | Devuelve 400: no hay nada que reintentar |
const result = await factuplan.invoices.retry('ID_DEL_COMPROBANTE');
console.log(result.action); // "reprocess" | "authorize" | "pdf"
console.log(result.status); // Estado tras encolar el reintento
console.log(result.accessKey); // Clave de acceso vigente (puede ser NUEVA)
// Mismo método en el resto de comprobantes:
// factuplan.creditNotes.retry(id)
// factuplan.debitNotes.retry(id)
// factuplan.withholdings.retry(id)Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | ID del comprobante reintentado |
| type | string | INVOICE | CREDIT_NOTE | DEBIT_NOTE | WITHHOLDING |
| action | string | Paso que se volvió a ejecutar: reprocess | authorize | pdf |
| status | string | Estado del comprobante tras encolar el reintento |
| accessKey | string | null | Clave de acceso vigente. Cambia si el comprobante estaba en REJECTED o RETURNED |
| message | string | Mensaje de confirmación |
En REJECTED y RETURNED el SRI rechazó el contenido del XML, así que el comprobante se regenera con una clave de acceso nueva y con los mismos datos. Si la devolución fue por datos inválidos, el reintento volverá a fallar: corrige el origen y emite un comprobante nuevo. Guarda siempre el accessKey que devuelve la respuesta.
El reintento no consume cuota mensual — solo se cobra la emisión original.
Secuencial de facturas
Establece el próximo número secuencial para facturas en un punto de emisión específico. El nuevo valor entra en efecto en el siguiente comprobante emitido.
const result = await factuplan.invoices.updateSequential(
'001', // branchCode — código del establecimiento
'001', // emissionCode — código del punto de emisión
100, // nuevo secuencial
);
console.log(result.invoiceSequential); // 100
console.log(result.updatedAt); // ISO 8601Respuesta
| 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) |
El cambio afecta únicamente el secuencial de facturas en el punto de emisión indicado. Para actualizar el secuencial de notas de crédito o notas de débito usa sus respectivos endpoints.
Subir firma electrónica (P12)
Sube un certificado P12. 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 certificado
console.log(result.legalName); // Nombre legal
console.log(result.expiresAt); // Fecha de vencimiento
console.log(result.created); // true si se creó un nuevo contribuyenteRespuesta
| Campo | Tipo | Descripción |
|---|---|---|
| hasCertificate | boolean | true si el certificado se subió correctamente |
| isExpired | boolean | true si el certificado ya expiró |
| 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 |
Actualizar firma electrónica (P12)
Reemplaza la firma electrónica de un contribuyente que ya existe en tu workspace, indicando su RUC. Es la vía para renovar una firma vencida o por vencer sin tocar el resto de la configuración del contribuyente. El RUC del certificado debe coincidir con el RUC enviado.
A diferencia de POST /developer/certificate, este endpoint nunca crea un contribuyente ni consume cupo de contribuyentes de tu plan: si el RUC no existe en tu workspace devuelve 404.
import { readFileSync } from 'fs';
const p12 = readFileSync('./mi-firma-renovada.p12');
const result = await factuplan.certificates.update(
p12,
'mi-contraseña',
'0950194407001', // RUC del contribuyente a actualizar
);
console.log(result.updated); // true
console.log(result.previousExpiresAt); // vencimiento de la firma anterior
console.log(result.expiresAt); // vencimiento de la firma nuevaCampos del formulario
| Campo | Tipo | Descripción |
|---|---|---|
| file | archivo | Nuevo archivo P12 (.p12 o .pfx) |
| password | string | Contraseña del certificado |
| ruc | string | RUC de 13 dígitos del contribuyente cuya firma se reemplaza |
Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| hasCertificate | boolean | true si la nueva firma quedó cargada y operativa |
| isExpired | boolean | true si el certificado ya expiró |
| daysUntilExpiry | number | Días restantes hasta el vencimiento |
| ruc | string | RUC del titular del certificado |
| legalName | string | Nombre legal extraído del certificado |
| issuedAt | string | Fecha de emisión (ISO 8601) |
| expiresAt | string | Vencimiento de la firma nueva (ISO 8601) |
| issuer | string | Entidad certificadora emisora |
| updated | boolean | Siempre true: confirma que la firma fue reemplazada |
| previousExpiresAt | string | null | Vencimiento de la firma anterior, null si no tenía |
Errores específicos
| HTTP | Cuándo ocurre |
|---|---|
| 404 | El RUC no existe como contribuyente en tu workspace. Usa POST /developer/certificate para cargarlo por primera vez. |
| 403 | Tu API key está restringida a otros contribuyentes y no puede operar con ese RUC. |
| 400 | El RUC del certificado no coincide con el ruc enviado, el P12 es inválido o la contraseña es incorrecta. |
| 422 | El certificado está vencido o su entidad certificadora no está autorizada por el SRI. |