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 operaciones que no requieren contribuyente, como taxpayers.list,
// certificates.upload o queryExternalByAccessKey (esta última exige una API key ak_live_...)
const factuplanSinRuc = 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, notas de débito, retenciones, liquidaciones de compra y cualquier otro comprobante generado en modo TESTING. No uses el entorno de pruebas para documentos que necesites conservar.
Idempotencia
Envía tu propia referencia en el campo opcional devReference (por ejemplo, tu número de pedido) y repetir una petición nunca emitirá un segundo documento. Si tu sistema no recibe la respuesta o recibe un error del servidor (un timeout, un corte de red, un 5xx), el documento pudo quedar guardado: vuelve a enviar la misma petición con la misma referencia y recibirás el documento que ya existe.
Funciona en los documentos que emites por la API: facturas, notas de crédito, notas de débito, retenciones y liquidaciones de compra. Si no envías el campo, todo se comporta como siempre. En el SDK está disponible desde la versión 0.22.0.
import type { CreateInvoiceInput } from 'factuplan';
const peticion: CreateInvoiceInput = {
devReference: 'pedido-1001',
customer: {
identificationType: 'RUC',
identification: '0000000000001',
legalName: 'Cliente S.A.',
email: 'cliente@email.com',
},
items: [
{ code: 'SERV-001', description: 'Servicio de consultoría', quantity: 1, unitPrice: 1.00, taxType: 'IVA_RATE' },
],
};
const factura = await factuplan.invoices.create(peticion);
factura.duplicate; // false: esta llamada emitió el documento
factura.devReference; // 'pedido-1001'
// La misma petición otra vez (por ejemplo, tras un timeout):
const otra = await factuplan.invoices.create(peticion);
otra.duplicate; // true: ya existía, no se emitió ni se cobró nada
otra.id === factura.id // true
otra.status; // el estado actual del documento: revísalo siempreQué responde la API
| Situación | Respuesta |
|---|---|
| Primera vez con esa referencia | 201 · se emite el documento · duplicate: false |
| Misma referencia, mismo contenido | 201 · devuelve el documento que ya existe · duplicate: true |
| Misma referencia, contenido distinto | 409 · API_10011 · con los datos del documento existente |
| Misma referencia y ese documento fue anulado | 409 · API_10012 · usa una referencia distinta |
- Es texto libre de 1 a 100 caracteres. Se compara tal cual: distingue mayúsculas, y los espacios al inicio y al final no cuentan. Debe ser un texto JSON: un número de pedido va entre comillas.
- Es única por contribuyente, ambiente y tipo de documento. Una factura y su nota de crédito pueden llevar la misma referencia.
- La referencia identifica un documento, no un pedido. Para emitir un documento corregido (por ejemplo, después de un rechazo del SRI) o un segundo documento del mismo pedido, usa una referencia distinta, como
pedido-1001-2. - Con
duplicate: truerevisa siempre elstatus: el documento que ya existía puede estar autorizado, en proceso o rechazado. Si está enDRAFT, espera: se procesa solo y puede tardar hasta unos 20 minutos. - No emitas la misma venta con otra referencia mientras no sepas qué pasó con la primera: terminarías con dos documentos. Consulta antes con
?devReference=pedido-1001. - Para volver a procesar un documento que falló usa el endpoint de reintento (facturas, notas de crédito, notas de débito y retenciones), no una petición nueva. Las liquidaciones de compra no tienen reintento por API: emítela de nuevo con una referencia distinta, como
liq-77-2. - Un documento rechazado por el SRI se elimina a los 7 días. Su referencia sigue ocupada y ya no se puede reintentar: emite el documento corregido con una referencia distinta.
- «Mismo contenido» compara la petición tal como la envías: cliente, líneas, pagos e información adicional. El orden de los campos, los campos en
null, las listas u objetos vacíos y opciones comosendEmailno cuentan; todo lo demás sí, así que un reintento debe mandar el mismo body (por ejemplo,discount: 0no es lo mismo que omitirlo). Si recibes un 409, los datos del documento que ya existe vienen en el error. - La referencia vuelve en la respuesta, en el listado, en el detalle, en el estado y en los webhooks (junto con
environment). Puedes buscar por ella con?devReference=pedido-1001. No se imprime en el XML ni en el PDF.
Producción y pruebas no se comportan igual
En producción una referencia usada queda ocupada para siempre. En el entorno de pruebas los documentos se eliminan cada hora y sus referencias quedan libres otra vez, para que puedas repetir tus pruebas.
La idempotencia es este campo del body. Ningún header (como Idempotency-Key) tiene efecto.
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: sin correo no se le envía nada
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'); // no lo borra: lo desactiva (isActive: false) y su identificación sigue ocupadaProductos
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. La factura termina en COMPLETED igual. Debe ser un booleano JSON: un texto como "false" se rechaza con 400. |
| customer.email | string | — | Opcional. Si el comprador no tiene correo, no lo inventes: omite el campo o envíalo vacío ("" o null) y el documento se emite igual, sin enviar correo. Si omites el campo y ese cliente ya tenía un correo guardado, se conserva y se usa; vacío o null lo borra. Un valor que no sea un correo válido sigue respondiendo 400. Si tu versión del SDK aún exige el campo en TypeScript, envía email: '': equivale a null y borra el correo guardado de ese cliente; para conservarlo hay que omitir el campo. |
| customer.customerId | string | — | ID de un cliente que ya creaste. Reemplaza a los datos del cliente: customer: { customerId }. Si no existe o está desactivado, la API responde 404. |
| items[].productId | string | — | ID de un producto de tu catálogo: la línea toma su código, nombre, precio y tipo de impuesto. Solo envías quantity y, si quieres, unitPrice o discount. Si no existe o está desactivado, la API responde 404. |
| devReference | string | — | Tu propia referencia (1 a 100 caracteres). Repetir la petición con la misma referencia no emite otra factura. Ver Idempotencia. |
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, // el mismo cliente del ejemplo anterior
items, // las mismas líneas del ejemplo anterior
});// Si solo tienes un punto de emisión activo, no necesitas enviarlo
const factura = await factuplan.invoices.create({
customer, // el mismo cliente del ejemplo anterior
items, // las mismas líneas del ejemplo anterior
});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: 'COMPLETED',
dateFrom: '2025-01-01T00:00:00-05:00',
dateTo: '2025-12-31T23:59:59-05:00',
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 exacto del documento: DRAFT | PROCESSING | RECEIVED_SRI | AUTHORIZED | COMPLETED | REJECTED | RETURNED | ERROR | VOIDED. Los documentos ya emitidos quedan en COMPLETED |
| dateFrom | string | Desde este instante (ISO 8601). Solo fecha = 00:00 UTC, que en Ecuador son las 19:00 del día anterior; para el día de Ecuador envía "2025-01-01T00:00:00-05:00" |
| dateTo | string | Hasta este instante (ISO 8601). Solo fecha = 00:00 UTC: "2025-12-31" deja fuera lo emitido el 31; para incluirlo envía "2025-12-31T23:59:59-05:00" |
| page | number | Número de página (por defecto: 1) |
| limit | number | Resultados por página (por defecto: 20, máximo: 100) |
| devReference | string | Tu propia referencia. Devuelve el documento que la tiene en el ambiente de tu API key (cero o uno). Ver Idempotencia |
Consultar estado
const estado = await factuplan.invoices.getStatus('factura_id');
// estado.status: COMPLETED (listo) | ERROR | REJECTED | RETURNED | VOIDED
// Cualquier otro valor (DRAFT, GENERATING_XML, SIGNING, SENDING_SRI, RECEIVED_SRI, AUTHORIZED, GENERATING_PDF, SENDING_EMAIL, PROCESSING) es un paso intermedio: sigue consultando
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. La respuesta devuelve el estado AUTHORIZED; cuando su PDF queda listo, el documento pasa a COMPLETED. Eso vale para una factura que el SRI tiene autorizada: si no lo está, se guarda igual con el estado que informa el SRI (RECEIVED_SRI, RETURNED, REJECTED, VOIDED o ERROR), así que revisa siempre status.
La factura debe haber sido emitida por el mismo RUC que envías en x-taxpayer-ruc: si la clave de acceso es de otro RUC, la API responde 400.
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: '0104202601095019440700120010010000000011234567813',
});
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. Si lo guardas, la respuesta devuelve el estado AUTHORIZED; cuando su PDF queda listo, el documento pasa a COMPLETED. Si el SRI no lo tiene autorizado, status trae el estado que informa el SRI (RECEIVED_SRI, RETURNED, REJECTED o ERROR): revísalo antes de darlo por válido.
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!, {
ruc: '0950194407001', // RUC del contribuyente con el que operas
});
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: { customerId: cliente.id }, // un cliente guardado y activo
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 el resultado: COMPLETED es el final correcto; los demás son fallos o anulación
let estado;
do {
await new Promise((r) => setTimeout(r, 3000));
estado = await factuplan.invoices.getStatus(factura.id);
} while (!['COMPLETED', 'ERROR', 'REJECTED', 'RETURNED', 'VOIDED'].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 en la misma llamada.
Es un solo endpoint para todos los tipos de comprobante: POST /v1/developer/sign-and-authorize. Acepta factura, nota de crédito, nota de débito, guía de remisión, retención y liquidación de compra; no hay una ruta por tipo.
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.
Importante: El XML debe enviarse sin firmar y con su declaración <?xml ...?>. Factuplan se encarga de la firma electrónica.
const result = await factuplan.invoices.signAndAuthorize({
xml: xmlString, // XML sin firmar como string
});
console.log(result.authorized); // true | false
console.log(result.authorizationNumber); // número de autorización del SRI
console.log(result.authorizationDate); // fecha de autorización
console.log(result.authorizedXml); // XML autorizado (base64)
console.log(result.messages); // mensajes del SRI
if (!result.authorized) {
// El SRI no lo autorizó: los motivos vienen en result.messages
// y no se descuenta ningún documento de tu plan.
console.error(result.messages);
}Respuesta
La respuesta es la del SRI, tal cual, y llega en la misma llamada. Si el SRI no autoriza el comprobante, authorized viene en false y los motivos están en messages.
| Campo | Tipo | Descripción |
|---|---|---|
| authorized | boolean | Si el SRI autorizó el comprobante |
| authorizationNumber | string | null | Número de autorización del SRI |
| authorizationDate | string | null | Fecha de autorización del SRI |
| authorizedXml | string | null | XML autorizado, en base64 |
| messages | array | Mensajes que devolvió el SRI |
Este endpoint no guarda el comprobante en Factuplan: no devuelve un id, no genera PDF ni envía correo, el comprobante no aparece en tu panel y no hay nada que consultar ni descargar después. Guarda el authorizedXml que recibes.
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. La factura debe existir en Factuplan (emitida aquí o importada antes con invoices.importByAccessKey) y estar autorizada: si no, la API responde 400 CN_5001 y no emite nada.
| 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); // '019432a1-8f2c-...'
console.log(creditNote.accessKey); // '0412202404...'
console.log(creditNote.status); // 'DRAFT'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. Si lo omites se usa el único punto de emisión activo; si tienes más de uno debes indicarlo (o enviar establishment y emissionPoint) |
| payments | array | No | Métodos de pago |
| additionalInfo | object | No | Campos adicionales clave-valor |
| devReference | string | No | Tu propia referencia (1 a 100 caracteres). Repetir la petición con la misma referencia no emite otra nota de crédito. Ver Idempotencia |
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 | "DRAFT" — se autoriza de forma asíncrona |
| total | string | Importe total de la nota de crédito, como texto decimal (ej: "115") |
| devReference | string | Tu referencia. Solo viene si la enviaste |
| duplicate | boolean | Solo viene con devReference: false si esta llamada emitió la nota, true si ya existía |
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: COMPLETED (listo) | ERROR | REJECTED | RETURNED | VOIDED
// Cualquier otro valor (DRAFT, GENERATING_XML, SIGNING, SENDING_SRI, RECEIVED_SRI, AUTHORIZED, GENERATING_PDF, SENDING_EMAIL, PROCESSING) es un paso intermedio: sigue consultando
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: 'COMPLETED',
dateFrom: '2025-01-01T00:00:00-05:00',
dateTo: '2025-12-31T23:59:59-05:00',
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 exacto del documento: DRAFT | PROCESSING | RECEIVED_SRI | AUTHORIZED | COMPLETED | REJECTED | RETURNED | ERROR | VOIDED. Los documentos ya emitidos quedan en COMPLETED |
| dateFrom | string | Desde este instante (ISO 8601). Solo fecha = 00:00 UTC, que en Ecuador son las 19:00 del día anterior; para el día de Ecuador envía "2025-01-01T00:00:00-05:00" |
| dateTo | string | Hasta este instante (ISO 8601). Solo fecha = 00:00 UTC: "2025-12-31" deja fuera lo emitido el 31; para incluirlo envía "2025-12-31T23:59:59-05:00" |
| page | number | Número de página (por defecto: 1) |
| limit | number | Resultados por página (por defecto: 20, máximo: 100) |
| devReference | string | Tu propia referencia: devuelve el documento que la tiene en el ambiente de tu API key (cero o uno) |
Firmar y autorizar XML
Si ya tienes el XML de la nota de crédito (sin firmar), Factuplan lo firma y lo autoriza ante el SRI en la misma llamada. Es el mismo endpoint que para las facturas (POST /v1/developer/sign-and-authorize): el tipo de comprobante se deduce del nodo raíz del XML.
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.
Importante: El XML debe enviarse sin firmar y con su declaración <?xml ...?>.
// Un solo método para todos los tipos de comprobante: vive en invoices
const result = await factuplan.invoices.signAndAuthorize({
xml: xmlString, // XML de la nota de crédito, sin firmar
});
console.log(result.authorized); // true | false
console.log(result.authorizationNumber); // número de autorización del SRI
console.log(result.authorizationDate); // fecha de autorización
console.log(result.authorizedXml); // XML autorizado (base64)
if (!result.authorized) {
console.error(result.messages); // motivos del rechazo del SRI
}Devuelve la respuesta del SRI y no guarda el comprobante: no hay id, PDF ni correo, y nada que consultar o descargar después. Los campos son los mismos que en Firmar y autorizar XML de facturas.
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
Fija el contador de secuencial de notas de crédito del punto de emisión indicado. El valor que envías es el último número usado: la siguiente nota de crédito sale con ese valor más uno (si envías 100, la siguiente será la 000000101).
const result = await factuplan.creditNotes.updateSequential(
'001', // branchCode — código del establecimiento
'001', // emissionCode — código del punto de emisión
100, // creditNoteSequential — último secuencial usado (la siguiente nota será la 101)
);
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 | Valor guardado en el contador. La siguiente nota de crédito sale con este valor + 1 |
| 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_RATE', // IVA_RATE (IVA 15 %) | IVA_0 | NOT_TAXABLE | EXEMPT
},
],
// additionalInfo: { Referencia: 'OC-4521' }, // opcional
});
console.log(debitNote.id); // '019432a1-8f2c-...'
console.log(debitNote.accessKey); // '0512202405...'
console.log(debitNote.status); // 'DRAFT'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_RATE (IVA 15%) | IVA_0 (IVA 0%) | NOT_TAXABLE (no objeto de IVA) | EXEMPT (exento). Usa exactamente estos valores: cualquier otro se acepta sin error y el cargo sale con IVA 0% |
| emissionPointId | string | No | UUID del punto de emisión (si se omite se usa el único punto de emisión activo; con más de uno la API responde 400) |
| additionalInfo | object | No | Campos adicionales clave-valor |
| devReference | string | No | Tu propia referencia (1 a 100 caracteres). Repetir la petición con la misma referencia no emite otra nota de débito. Ver "Idempotencia" |
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');
// Termina en COMPLETED. AUTHORIZED es un estado de paso: autorizada por el SRI, el PDF puede no estar listo.
// Fallos: ERROR | REJECTED | RETURNED. Anulada: VOIDED.
// En curso: DRAFT, PROCESSING, GENERATING_XML, SIGNING, SENDING_SRI, RECEIVED_SRI, GENERATING_PDF, SENDING_EMAIL.
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 lo autoriza ante el SRI en la misma llamada. Es el mismo endpoint que para las facturas (POST /v1/developer/sign-and-authorize): el tipo de comprobante se deduce del nodo raíz del XML.
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.
Importante: El XML debe enviarse sin firmar y con su declaración <?xml ...?>.
// Un solo método para todos los tipos de comprobante: vive en invoices
const result = await factuplan.invoices.signAndAuthorize({
xml: xmlString, // XML de la nota de débito, sin firmar
});
console.log(result.authorized); // true | false
console.log(result.authorizationNumber); // número de autorización del SRI
console.log(result.authorizationDate); // fecha de autorización
console.log(result.authorizedXml); // XML autorizado (base64)
if (!result.authorized) {
console.error(result.messages); // motivos del rechazo del SRI
}Devuelve la respuesta del SRI y no guarda el comprobante: no hay id, PDF ni correo, y nada que consultar o descargar después. Los campos son los mismos que en Firmar y autorizar XML de facturas.
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 contador de secuenciales de notas de débito en el punto de emisión indicado. El valor que envías es el último secuencial usado: la siguiente nota de débito sale con ese valor más uno (si envías 100, la siguiente será la 000000101).
const result = await factuplan.debitNotes.updateSequential(
'001', // branchCode — código del establecimiento
'001', // emissionCode — código del punto de emisión
100, // debitNoteSequential — último secuencial usado (la siguiente será la 101)
);
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 | Valor guardado en el contador. La siguiente nota de débito sale con este valor + 1 |
| updatedAt | string | Fecha de actualización (ISO 8601) |
Retenciones
Emite comprobantes de retención en la fuente (renta, IVA e ISD) sobre una compra registrada — el documento que te emitió tu proveedor — 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.
Cambio importante (SDK 0.18.0): la retención ya no se emite sobre una factura propia, sino sobre la compra que sustenta el gasto — una retención se declara contra el comprobante que recibiste, no contra uno que emitiste. La compra debe existir previamente en Factuplan, identificada por su clave de acceso, y se referencia como purchaseAccessKey. El campo invoiceAccessKey queda eliminado.
Registra antes la compra: hazlo con purchases.register(accessKey) (POST /v1/developer/purchases, ver Registrar una compra) o desde el panel de Factuplan. Registrar una compra solo funciona con API keys de producción y consume cupo de tu plan; con una API key de pruebas solo puedes emitir retenciones sobre compras que ya estén registradas.
| 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({
purchaseAccessKey: '0109202601179214673900110020010000000011234567815', // clave de la COMPRA
codSustento: '01', // Código de sustento (Tabla 5 del SRI)
pagoLocExt: '01', // '01' local | '02' exterior (opcional, default '01')
fiscalPeriod: '09/2026', // Período fiscal MM/AAAA (opcional; por defecto, el mes de la compra)
taxes: [
{
taxType: 'IVA', // 'RENTA' | 'IVA' | 'ISD'
taxRateCode: '9', // Código SRI (tabla 20): '9' = 10% de IVA
taxRate: 10, // Porcentaje de retención
taxBase: 12.23, // Base imponible
},
{
taxType: 'RENTA',
taxRateCode: '303',
taxRate: 10,
taxBase: 101.94,
},
],
additionalInfo: { observacion: 'Servicios profesionales' }, // opcional
});
console.log(withholding.id); // '019432a1-8f2c-...'
console.log(withholding.accessKey); // '0512202407...'
console.log(withholding.status); // 'DRAFT'
console.log(withholding.totalWithheld); // Total retenido
console.log(withholding.purchaseId); // Compra que sustenta la retenciónCuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| purchaseAccessKey | string | Sí | Clave de acceso de 49 dígitos de la compra registrada que sustenta la retención |
| codSustento | string | Sí | Código de sustento del comprobante (tabla 5 del SRI). Ver "Códigos del SRI" |
| taxes | array | Sí | Líneas de retención (mínimo una): taxType, taxRateCode, taxRate, taxBase |
| taxes[].taxType | string | Sí | 'RENTA' | 'IVA' | 'ISD' |
| taxes[].taxRateCode | string | Sí | Código de retención del SRI según el taxType (ver "Códigos del SRI"). En IVA el código no es el porcentaje |
| taxes[].taxRate | number | Sí | Porcentaje a retener. Debe coincidir con el parametrizado por el SRI para ese código |
| taxes[].taxBase | number | Sí | Base imponible; no puede superar el total de la compra |
| pagoLocExt | string | No | Tipo de pago: '01' local (por defecto) o '02' exterior. Con '02' el XML todavía no incluye los datos del pago al exterior |
| fiscalPeriod | string | No | Período fiscal MM/AAAA. Por defecto, el mes de emisión de la compra |
| emissionPointId | string | No | UUID del punto de emisión (auto-resuelto si el contribuyente tiene uno solo activo) |
| additionalInfo | object | No | Campos adicionales clave-valor |
| devReference | string | No | Tu propia referencia (1 a 100 caracteres). Repetir la petición con la misma referencia no emite otra retención. Ver "Idempotencia" |
Respuesta 201
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | ID de la retención |
| accessKey | string | Clave de acceso de 49 dígitos |
| sequential | string | Secuencial (ej: "000000001") |
| status | string | "DRAFT" — se autoriza de forma asíncrona |
| totalWithheld | number | Importe total retenido |
| purchaseId | string | ID de la compra que sustenta la retención |
| devReference | string | Tu propia referencia. Solo viene si la enviaste en la petición |
| duplicate | boolean | Solo viene si enviaste devReference: false si esta llamada emitió la retención; true si ya existía y no se emitió nada nuevo |
Códigos del SRI
Estos son los códigos que acepta la API. Salen de las tablas oficiales del SRI: la de renta, de los porcentajes vigentes desde marzo de 2026 (Resolución NAC-DGERCGC26-00000009), y la de IVA, de la ficha técnica de comprobantes electrónicos.
- Un
taxRateCodeocodSustentoque no esté en estas tablas se rechaza al crear la retención conWH_5308oWH_5307. - La API no valida el porcentaje, pero el SRI sí: si
taxRateno coincide con el parametrizado para el código, la retención queda NO AUTORIZADA con el error 52 ("La tarifa no es igual al parametrizado"). Los códigos de tarifa variable (dividendos, banano, autorretenciones) llevan el porcentaje que corresponda a tu caso. - En IVA el código no es el porcentaje:
"9"es 10% y"1"es 30%. - Todavía no se admiten pagos a no residentes (códigos 500 en adelante).
Retención de IVA (taxType: 'IVA') (8 códigos)
| Código | Concepto | Porcentaje |
|---|---|---|
| 9 | Retención del 10% del IVA | 10% |
| 10 | Retención del 20% del IVA | 20% |
| 1 | Retención del 30% del IVA | 30% |
| 11 | Retención del 50% del IVA | 50% |
| 2 | Retención del 70% del IVA | 70% |
| 3 | Retención del 100% del IVA | 100% |
| 7 | Retención en cero | 0% |
| 8 | No procede retención | 0% |
Retención de renta (taxType: 'RENTA') (80 códigos)
| Código | Concepto | Porcentaje |
|---|---|---|
| 303 | Honorarios profesionales y demás pagos por servicios relacionados con el título profesional | 10% |
| 303A | Servicios profesionales prestados por sociedades residentes | 5% |
| 304 | Servicios predomina el intelecto no relacionados con el título profesional | 10% |
| 304A | Comisiones y demás pagos por servicios predomina intelecto no relacionados con el título profesional | 10% |
| 304B | Pagos a notarios y registradores de la propiedad y mercantil por sus actividades ejercidas como tales | 10% |
| 304C | Pagos a deportistas, entrenadores, árbitros, miembros del cuerpo técnico por sus actividades ejercidas como tales | 10% |
| 304D | Pagos a artistas por sus actividades ejercidas como tales | 10% |
| 304E | Honorarios y demás pagos por servicios de docencia | 10% |
| 307 | Servicios predomina la mano de obra | 3% |
| 308 | Utilización o aprovechamiento de la imagen o renombre (personas naturales, sociedades, "influencers") | 10% |
| 309 | Servicios prestados por medios de comunicación y agencias de publicidad | 3% |
| 310 | Servicio de transporte privado de pasajeros o transporte público o privado de carga | 1% |
| 311 | Pagos a través de liquidación de compra (nivel cultural o rusticidad) | 3% |
| 312 | Transferencia de bienes muebles de naturaleza corporal | 2% |
| 312A | Compras al productor: bienes de origen bioacuático, forestal y los descritos en el art. 27.1 de la LRTI | 1% |
| 312C | Compras al comercializador: bienes de origen bioacuático, forestal y los descritos en el art. 27.1 de la LRTI | 1.75% |
| 314A | Regalías por franquicias según el Código INGENIOS (COESCCI) - pago a personas naturales | 10% |
| 314B | Cánones, derechos de autor, marcas, patentes y similares según el Código INGENIOS (COESCCI) - pago a personas naturales | 10% |
| 314C | Regalías por franquicias según el Código INGENIOS (COESCCI) - pago a sociedades | 10% |
| 314D | Cánones, derechos de autor, marcas, patentes y similares según el Código INGENIOS (COESCCI) - pago a sociedades | 10% |
| 319 | Cuotas de arrendamiento mercantil (prestado por sociedades), inclusive la de opción de compra | 2% |
| 320 | Arrendamiento de bienes inmuebles | 10% |
| 322 | Seguros y reaseguros (primas y cesiones) | 2% |
| 323 | Rendimientos financieros pagados a naturales y sociedades (no a IFIs) | 3% |
| 323A | Rendimientos financieros depósitos cuenta corriente | 3% |
| 323B1 | Rendimientos financieros depósitos cuenta de ahorros sociedades | 3% |
| 323E | Rendimientos financieros depósito a plazo fijo gravados | 3% |
| 323E2 | Rendimientos financieros depósito a plazo fijo exentos | 0% |
| 323F | Rendimientos financieros operaciones de reporto - repos | 3% |
| 323G | Inversiones (captaciones) rendimientos distintos de aquellos pagados a IFIs | 3% |
| 323H | Rendimientos financieros obligaciones | 3% |
| 323I | Rendimientos financieros bonos convertibles en acciones | 3% |
| 323Q | Otros intereses y rendimientos financieros gravados | 3% |
| 323R | Otros intereses y rendimientos financieros exentos | 0% |
| 323S | Pagos y créditos en cuenta efectuados por el BCE y los depósitos centralizados de valores a instituciones del sistema financiero por cuenta de otras personas naturales y sociedades | 3% |
| 323T | Rendimientos financieros originados en la deuda pública ecuatoriana | 0% |
| 323U | Rendimientos financieros de títulos valores de obligaciones de 360 días o más para proyectos públicos en asociación público-privada | 0% |
| 324A | Intereses en operaciones de crédito entre instituciones del sistema financiero y entidades de la economía popular y solidaria | 2% |
| 324B | Inversiones entre instituciones del sistema financiero y entidades de la economía popular y solidaria | 2% |
| 324C | Pagos y créditos en cuenta efectuados por el BCE y los depósitos centralizados de valores a instituciones del sistema financiero por cuenta de otras instituciones del sistema financiero | 2% |
| 325 | Anticipo dividendos | 25% |
| 325A | Préstamos a accionistas, beneficiarios o partícipes residentes o establecidos en el Ecuador | 25% |
| 3250 | Dividendos exentos (por no llegar a franja exenta o beneficio de otras leyes) | 0% |
| 326 | Dividendos distribuidos que correspondan al impuesto a la renta único del art. 27 de la LRTI | variable |
| 327 | Dividendos distribuidos a personas naturales residentes | variable |
| 328 | Dividendos distribuidos a sociedades residentes | 0% |
| 329 | Dividendos distribuidos a fideicomisos residentes | 0% |
| 331 | Dividendos en acciones (capitalización de utilidades) | 0% |
| 332 | Otras compras de bienes y servicios no sujetas a retención (incluye RIMPE - Negocios Populares) | 0% |
| 332B | Compra de bienes inmuebles | 0% |
| 332C | Transporte público de pasajeros | 0% |
| 332D | Pagos en el país por transporte de pasajeros o transporte internacional de carga, a compañías nacionales o extranjeras de aviación o marítimas | 0% |
| 332E | Valores entregados por las cooperativas de transporte a sus socios | 0% |
| 332F | Compraventa de divisas distintas al dólar de los Estados Unidos de América | 0% |
| 332G | Pagos con tarjeta de crédito | 0% |
| 332H | Pago al exterior con tarjeta de crédito reportada por la emisora de tarjeta de crédito, solo recap | 0% |
| 332I | Pago a través de convenio de débito (clientes IFIs) | 0% |
| 333 | Ganancia en la enajenación de derechos representativos de capital u otros derechos de sociedades que se coticen en bolsa de valores del Ecuador | 10% |
| 334 | Contraprestación por la enajenación de derechos representativos de capital u otros derechos de sociedades no cotizados en bolsa de valores del Ecuador | 2% |
| 335 | Loterías, rifas, pronósticos deportivos, apuestas y similares | 15% |
| 336 | Venta de combustibles a comercializadoras (2 por mil) | 0.2% |
| 337 | Venta de combustibles a distribuidores (3 por mil) | 0.3% |
| 338 | Producción y venta local de banano producido o no por el mismo sujeto pasivo | variable |
| 340 | Impuesto único a la exportación de banano | 3% |
| 343 | Otras retenciones aplicables el 1% (incluye RIMPE - Emprendedores) | 1% |
| 343A | Energía eléctrica | 2% |
| 343B | Actividades de construcción de obra material inmueble, urbanización, lotización o actividades similares | 2% |
| 343C | Recepción de botellas plásticas no retornables de PET | 2% |
| 3440 | Otras retenciones aplicables el 3% | 3% |
| 344A | Pago local con tarjeta de crédito/débito reportada por la emisora de tarjeta / entidades del sistema financiero / sistemas auxiliares de pago | 2% |
| 344B | Adquisición de sustancias minerales dentro del territorio nacional | 2% |
| 346 | Otras retenciones aplicables a otros porcentajes | variable |
| 346A | Otras ganancias de capital distintas de enajenación de derechos representativos de capital | variable |
| 346B | Donaciones en dinero - impuesto a las donaciones | variable |
| 346C | Retención a cargo del propio sujeto pasivo por la producción y/o comercialización de minerales y otros bienes | variable |
| 346D | Retención a cargo del propio sujeto pasivo por la comercialización de productos forestales | variable |
| 350 | Otras autorretenciones (incisos 1 y 2 del art. 92.1 del RLRTI) | variable |
| 3480 | Impuesto a la renta único sobre los ingresos de los operadores de pronósticos deportivos | 15% |
| 3481 | Autorretenciones de sociedades grandes contribuyentes | variable |
| 3482 | Comisiones a sociedades nacionales o extranjeras residentes y establecimientos permanentes domiciliados en el país | 5% |
Retención de ISD (taxType: 'ISD') (1 códigos)
| Código | Concepto | Porcentaje |
|---|---|---|
| 4580 | Impuesto a la salida de divisas | 5% |
Sustento del comprobante (codSustento) (11 códigos)
| Código | Concepto |
|---|---|
| 01 | Crédito tributario para declaración de IVA (compras) |
| 02 | Costo o gasto para declaración de impuesto a la renta (compras) |
| 03 | Activo fijo - crédito tributario para declaración de IVA (compras) |
| 04 | Activo fijo - costo o gasto para declaración de impuesto a la renta (compras) |
| 05 | Liquidación de compra de bienes o prestación de servicios en las que el vendedor no tiene sustento tributario |
| 06 | Adquisiciones a proveedores no obligados a emitir comprobantes de venta o no residentes en el Ecuador |
| 07 | Sin derecho a crédito tributario ni a costo o gasto |
| 08 | Reembolso como intermediario |
| 09 | Reembolso por el contribuyente |
| 10 | Impuesto a la salida de divisas |
| 11 | Compensación de deudas tributarias con el sector público |
Qué completa Factuplan en el XML
- Los datos del sujeto retenido (tipo de identificación, razón social, identificación) se toman del proveedor de la compra.
parteRelse envía comoNO. - Los impuestos del documento sustento (
impuestosDocSustento) se agrupan a partir de los ítems de la compra. - Las formas de pago (
pagos) se copian de la compra. Si la compra no trae ninguna, se declara el total con la forma de pago20(otros con utilización del sistema financiero).
Buscar retenciones
const { data: withholdings, meta } = await factuplan.withholdings.list({
status: 'COMPLETED',
dateFrom: '2026-01-01T00:00:00-05:00',
dateTo: '2026-06-30T23:59:59-05:00',
page: 1,
limit: 20,
});Consultar estado
const estado = await factuplan.withholdings.getStatus('WITHHOLDING_ID');
console.log(estado.status); // 'COMPLETED' al terminar ('AUTHORIZED' es un estado de paso)
console.log(estado.authorizationNumber); // Número de autorización SRIDescargar 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 lo autoriza ante el SRI en la misma llamada. Es el mismo endpoint que para las facturas (POST /v1/developer/sign-and-authorize): el tipo de comprobante se deduce del nodo raíz del XML.
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.
Importante: El XML debe enviarse sin firmar y con su declaración <?xml ...?>.
// Un solo método para todos los tipos de comprobante: vive en invoices
const result = await factuplan.invoices.signAndAuthorize({
xml: xmlString, // XML de la retención, sin firmar
});
console.log(result.authorized); // true | false
console.log(result.authorizationNumber); // número de autorización del SRI
console.log(result.authorizationDate); // fecha de autorización
console.log(result.authorizedXml); // XML autorizado (base64)
if (!result.authorized) {
console.error(result.messages); // motivos del rechazo del SRI
}Devuelve la respuesta del SRI y no guarda el comprobante: no hay id, PDF ni correo, y nada que consultar o descargar después. Los campos son los mismos que en Firmar y autorizar XML de facturas.
Anular
const voided = await factuplan.withholdings.void('WITHHOLDING_ID', 'Emitida por error');
console.log(voided.status); // 'VOIDED'La anulación de la retención ante el SRI es responsabilidad del desarrollador. El endpoint /void solo cambia el estado en Factuplan.
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 NUEVAGuías de remisión
La API de desarrolladores no tiene un endpoint para emitir ni para listar guías de remisión (/v1/developer/waybills no existe) y el SDK no incluye un recurso waybills. Puedes emitirlas de dos formas:
- Desde el panel de Factuplan.
- Generando tú el XML
<guiaRemision>y enviándolo aPOST /v1/developer/sign-and-authorizesi está sin firmar (ver Firmar y autorizar XML) o aPOST /v1/developer/authorize-signedsi ya lo firmaste (ver Enviar XML firmado). Estos dos endpoints devuelven la respuesta del SRI y no guardan el comprobante ni generan PDF.
Una guía emitida desde el panel sí queda guardada. Con su id (el receiptId de los webhooks waybill.*) puedes consultar su estado, descargar su XML y su PDF y anularla con los endpoints generales /v1/developer/receipts/{id}/status, /xml, /pdf y /void.
Firma & Contribuyentes
Enviar XML firmado
Si firmas los comprobantes con tu propio certificado, Factuplan puede enviarlos al SRI y devolverte su respuesta. Sirve para cualquier tipo de comprobante: el tipo se deduce del nodo raíz del XML, no hace falta indicarlo.
No confundir con «Firmar XML». Ahí envías el XML sin firmar y Factuplan lo firma. Aquí lo envías ya firmado y Factuplan solo lo transmite al SRI.
Requisitos
- API key de producción (
ak_live_…). Con una key de pruebas el endpoint responde403. - El XML debe venir firmado (con su elemento
<ds:Signature>). Sin firma se rechaza. - El nodo raíz debe ser uno de:
factura,notaCredito,notaDebito,guiaRemision,comprobanteRetencionoliquidacionCompra. - Debe declarar el RUC del proveedor (ver abajo).
- Debes tener cupo disponible en tu plan. Se comprueba antes de enviar al SRI, y solo se descuenta un documento si el SRI autoriza.
- No lleva el header
x-taxpayer-ruc. Ese header sirve para elegir con qué certificado firmar, y aquí el XML ya viene firmado por ti: no necesitas tener ninguna firma cargada en Factuplan. El cupo se descuenta de tu espacio de trabajo.
Este endpoint no genera PDF ni envía correo, y el comprobante no queda registrado en tu panel: Factuplan actúa únicamente como transmisor ante el SRI.
RUC del proveedor
El comprobante debe identificar a Factuplan como proveedor del software. Va dentro de <infoAdicional>, en un <campoAdicional> cuyo atributo nombre es exactamente RUC Proveedor:
<infoAdicional>
<campoAdicional nombre="RUC Proveedor">0993411372001</campoAdicional>
</infoAdicional>El nombre del campo no distingue mayúsculas ni espacios sobrantes (ruc proveedor también vale), pero el valor debe coincidir exactamente con el RUC de arriba. Si falta el campo o el RUC es otro, el comprobante se rechaza antes de llegar al SRI y no consume cupo.
Enviar al SRI
La respuesta es la del SRI, tal cual: si autorizó, con qué número y en qué fecha, el XML autorizado en base64 y los mensajes que haya devuelto.
const result = await factuplan.documents.authorizeSigned({
xml: signedXmlString, // comprobante YA firmado, como string
});
console.log(result.authorized); // true | false
console.log(result.authorizationNumber); // número de autorización del SRI
console.log(result.authorizationDate); // fecha de autorización
console.log(result.authorizedXml); // XML autorizado (base64)
console.log(result.messages); // mensajes del SRI
if (!result.authorized) {
// El SRI no lo autorizó: los motivos vienen en result.messages
// y no se descuenta ningún documento de tu plan.
console.error(result.messages);
}Errores propios de este endpoint
| Código | Qué pasó |
|---|---|
| INTEGRATION_8005 | Usaste una API key de pruebas. Este endpoint solo acepta ak_live_…. |
| API_10004 | El contenido enviado no es un XML válido. |
| API_10007 | El XML no lleva firma electrónica. Este endpoint no firma. |
| API_10008 | El nodo raíz no es un comprobante que el SRI reconozca. |
| API_10009 | Falta el campoAdicional con nombre RUC Proveedor. |
| API_10010 | El RUC Proveedor declarado no es el de Factuplan. El error te dice el esperado y el recibido. |
Manejo de errores
import { FactuplanError, AuthenticationError } from 'factuplan';
try {
await factuplan.invoices.create(datos); // datos: el body de la factura
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('API key inválida');
} else if (error instanceof FactuplanError) {
console.error(error.code); // p. ej. "GENERAL_0002"
console.error(error.message); // Descripción
console.error(error.statusCode); // p. ej. 400
console.error(error.details); // Detalles adicionales
}
}Códigos de error
| Código | HTTP | Descripción |
|---|---|---|
| INTEGRATION_8001 | 401 | API key inválida, inactiva o expirada (en el SDK: AuthenticationError, con code AUTH_ERROR) |
| GENERAL_0003 | 429 | Límite de 100 peticiones por segundo excedido (en el SDK: RateLimitError, con code RATE_LIMIT) |
| INTEGRATION_8004 | 403 | Tu plan no incluye el acceso a la API en producción, o tu suscripción no está activa |
| API_10002 | 429 | Cuota mensual propia de la API key agotada, solo si la key tiene una asignada (en el SDK también llega como RateLimitError; el mensaje lo indica y esperar no lo resuelve) |
| SUB_12002 | 403 | Cupo de documentos del plan agotado |
| SUB_12008 | 403 | Cupo del plan y excedente agotados |
| GENERAL_0002 | 400 | Datos de la petición inválidos; el mensaje indica qué corregir |
| INVOICE_4003 | 400 | Detalle de factura vacío |
| CERT_3008 | 422 | Certificado expirado |
| 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: usa una referencia distinta |
| WH_5304 | 404 | No se encontró una compra registrada con esa clave de acceso |
| WH_5305 | 400 | La compra todavía no está registrada correctamente y no puede sustentar una retención |
| WH_5306 | 409 | Esa compra ya tiene una retención emitida |
| WH_5307 | 400 | El código de sustento no existe en la tabla del SRI |
| WH_5308 | 400 | El código de retención no corresponde al impuesto seleccionado |
Rate Limit
Cada API key tiene un límite de velocidad para garantizar un servicio estable para todos los usuarios. Además, cada documento que emites cuenta contra el cupo de documentos de tu plan, que comparten todas las API keys y el panel de tu espacio de trabajo.
100
peticiones por segundo por API key
Según plan
documentos emitidos por mes
Al superar el límite de velocidad recibirás un HTTP 429 (GENERAL_0003): espera y reintenta con espera exponencial. Si se agota el cupo de documentos de tu plan, la respuesta es un HTTP 403 (SUB_12002, o SUB_12008 si también agotaste el excedente): reintentar no sirve, renueva o amplía tu plan.
Respuesta al superar el límite
{
"data": null,
"meta": { "requestId": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f", "timestamp": "2026-10-08T15: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": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f", "timestamp": "2026-10-08T15:04:05.000Z" },
"error": {
"code": "SUB_12002",
"message": "Has alcanzado el límite de documentos de tu plan (500/500)",
"statusCode": 403,
"details": { "documentType": "INVOICE", "limit": 500, "used": 500, "reserved": 0 }
}
}Manejo recomendado con reintentos
import { RateLimitError } from 'factuplan';
// Un 429 se rechaza antes de procesar la petición: reintentarla no emite nada dos veces.
// Para reintentar también tras un timeout o un 5xx, envía siempre devReference en datos.
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.
Eventos
Cada evento se llama {comprobante}.{resultado}. El resultado es authorized (el SRI autorizó el comprobante), rejected (el SRI lo rechazó) o error (no pudimos completar su emisión). Al crear un webhook eliges a cuáles se suscribe, y puedes cambiarlos después desde Developer → Webhooks sin perder el secreto ni el historial.
| Comprobante | Eventos |
|---|---|
| Factura | invoice.authorized · invoice.rejected · invoice.error |
| Nota de crédito | credit_note.authorized · credit_note.rejected · credit_note.error |
| Nota de débito | debit_note.authorized · debit_note.rejected · debit_note.error |
| Guía de remisión | waybill.authorized · waybill.rejected · waybill.error |
| Liquidación de compra | purchase_settlement.authorized · purchase_settlement.rejected · purchase_settlement.error |
| Comprobante de retención | withholding.authorized · withholding.rejected · withholding.error |
| Cualquier comprobante (genéricos) | receipt.authorized · receipt.rejected · receipt.error |
Los eventos receipt.* son genéricos: se disparan con cualquier tipo de comprobante, además del evento propio de ese tipo. Un webhook suscrito a receipt.authorized y a invoice.authorized recibe dos notificaciones por la misma factura, cada una con un id de evento distinto. Suscríbete a los genéricos o a los de cada tipo, no a ambos.
receipt.email_bounced avisa que el correo de un comprobante autorizado no llegó a tu cliente: la dirección no existe, el destinatario lo marcó como spam antes o ya estaba bloqueada. Llega solo si lo eliges y no cambia el comprobante, que sigue autorizado. Trae los mismos datos de los demás eventos y además email.to, email.reason (bounce o suppressed) y, cuando el proveedor de correo los da, email.bounceType, email.bounceSubType y email.message. Úsalo para pedirle a tu cliente un correo correcto: mientras no lo cambies, no le enviaremos más correos a esa dirección.
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 Developer → 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 7 veces más con espera creciente (30s, 1m, 2m, 4m, 8m, 16m y 32m: cerca de una hora en total). - Tras 10 entregas fallidas consecutivas (una entrega cuenta como fallida cuando agota sus reintentos) desactivamos el webhook y avisamos por correo al propietario del workspace. Puedes reactivarlo desde Developer → Webhooks; al hacerlo, el contador de fallos vuelve a cero.
- Mientras el webhook está desactivado por fallos no enviamos nada. Durante los 7 días siguientes a la desactivación, cada evento queda registrado en su historial como entrega omitida; pasado ese plazo los eventos ya no se registran, así que reactívalo antes para no perderlos.
- El historial de entregas del panel muestra las 50 más recientes de cada webhook. Con el filtro Solo sin entregar ves únicamente las que no llegaron (fallidas, omitidas o todavía en curso), también las que ya no caben entre las más recientes: cada entrega que reenvías con éxito sale de esa lista y aparecen las siguientes.
- Para reenviar una entrega el webhook debe estar activo y la entrega no puede estar todavía en su ciclo de reintentos: mientras dure aparece como Reintentando y la reintentamos nosotros. Tampoco se reenvía un evento
errororejectedde un comprobante que después fue autorizado, porque quedó obsoleto. El reenvío es la misma entrega: conserva eliddel evento. - Todos los reintentos y reenvíos 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');
// null: no se encontró o la consulta falló (el SDK no distingue los dos casos)
if (receipt) {
console.log(receipt.id);
console.log(receipt.status); // p. ej. COMPLETED (emisión terminada), AUTHORIZED (autorizado, PDF en camino), REJECTED, ERROR
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 tu API key en el mes en curso.
Consultar uso
const uso = await factuplan.usage();
console.log('Operaciones este mes:', uso.currentMonth.documents);
console.log('Costo estimado: $' + uso.currentMonth.cost);
console.log('Cuota propia de la API key:', uso.monthlyQuota ?? 'sin cuota');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 una asignada |
| monthlyUsed | number | Operaciones contadas contra esa cuota |
Certificado
Consulta el estado del certificado electrónico P12 configurado en tu cuenta.
Estado del certificado
const cert = await factuplan.certificates.status();
console.log(cert.hasCertificate); // true = hay una firma cargada
console.log(cert.isExpired); // true = la firma ya venció
console.log(cert.daysUntilExpiry); // Días que faltan para el vencimiento
console.log(cert.expiresAt); // Fecha de vencimiento (ISO 8601)
console.log(cert.ruc); // RUC que consta en el certificado
console.log(cert.legalName); // Nombre del titularRespuesta
| Campo | Tipo | Descripción |
|---|---|---|
| hasCertificate | boolean | true si el contribuyente tiene una firma cargada |
| isExpired | boolean | true si la firma ya venció |
| daysUntilExpiry | number | null | Días que faltan para el vencimiento |
| ruc | string | null | RUC que consta en el certificado |
| legalName | string | null | Nombre del titular que consta en el certificado |
| issuedAt | string | null | Fecha de emisión (ISO 8601) |
| expiresAt | string | null | Fecha de vencimiento (ISO 8601) |
| issuer | string | null | Entidad certificadora |
Si isExpired es true, 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 |
| devReference | string | Tu referencia; solo si el documento se emitió con una |
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 | Vuelve a enviar el comprobante al SRI. En REJECTED y RETURNED regenera el XML con una clave de acceso nueva; en ERROR reenvía el XML ya firmado si existe, o lo genera de nuevo |
| RECEIVED_SRI, o REJECTED porque el SRI ya tenía registrada esa clave de acceso | authorize | Vuelve a consultar la autorización en el SRI, con la misma clave de acceso |
| AUTHORIZED sin PDF, o ERROR ya autorizado por el SRI y sin PDF | Regenera únicamente el RIDE (PDF) | |
| PROCESSING, SENDING_SRI, VOIDED, COMPLETED, 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 action es reprocess y el comprobante estaba en REJECTED o RETURNED |
| message | string | Mensaje de confirmación |
| devReference | string | Tu referencia; solo si el documento se emitió con una |
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.
Un comprobante se descuenta de tu plan una sola vez, cuando el SRI lo autoriza: reintentarlo nunca lo cobra dos veces. Si todavía no se ha descontado, el reintento necesita que tengas cupo disponible.
Secuencial de facturas
Fija el contador de facturas de un punto de emisión específico. 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 factura será 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 000000101
);
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 | Valor guardado en el contador. La siguiente factura sale con este valor + 1 |
| 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. Si tu API key no tenía ningún contribuyente vinculado (operaba con todos), desde ese momento queda restringida a este contribuyente: vincula los demás con POST /developer/taxpayers/link.
import { readFileSync } from 'fs';
const p12 = readFileSync('./mi-firma.p12');
// Si el RUC del certificado todavía no existe en tu workspace, usa un cliente creado sin la opción ruc
// (o con el RUC de un contribuyente que ya exista): el header x-taxpayer-ruc de un RUC desconocido se rechaza con 400.
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, la contraseña es incorrecta o la entidad certificadora del certificado no está soportada. |
| 422 | El certificado está vencido. |
Compras
Las facturas que tus proveedores te emitieron. Registras una compra a partir de su clave de acceso: Factuplan descarga del SRI el comprobante ya autorizado, crea al proveedor y a los productos que falten, y genera el PDF. No firma nada, no envía nada al SRI y no manda correos.
No confundas compras con liquidaciones de compra. Una liquidación (SRI tipo 03) es un comprobante que emites tú cuando tu proveedor no puede darte factura. Una compra es lo contrario: un comprobante que otro contribuyente emitió a tu nombre y que tú solo registras.
Registrar una compra solo funciona con API keys de producción (ak_live_...). La clave de acceso se consulta contra el SRI real, así que en el entorno de pruebas no hay comprobantes que registrar: devuelve 403 con el código INTEGRATION_8005. El resto de endpoints de esta sección (listar, obtener, descargar, eliminar) funcionan en ambos entornos.
Registrar una compra consume cupo mensual de tu plan. Reintentar el PDF y eliminar una compra no consumen cupo, y eliminar tampoco lo devuelve.
Todos los endpoints de esta sección requieren la cabecera x-taxpayer-ruc con el RUC del contribuyente que hizo la compra.
Registrar una compra
Registra la factura que te emitió tu proveedor a partir de su clave de acceso de 49 dígitos. Por ahora solo se admiten facturas (tipo 01), y solo con una API key de producción.
const compra = await factuplan.purchases.register(
'0509202601099234567800120020030000001231234567818',
);
console.log(compra.id);
console.log(compra.supplierId); // proveedor creado o reutilizado; su nombre llega en purchases.get(compra.id)
console.log(compra.total);
console.log(compra.status); // AUTHORIZED mientras se genera el PDFCuerpo de la petición
| Campo | Tipo | Descripción |
|---|---|---|
| accessKey | string | Clave de acceso de 49 dígitos de la factura de compra |
Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | ID de la compra en Factuplan |
| status | string | AUTHORIZED (PDF en proceso), COMPLETED (PDF listo) o ERROR |
| accessKey | string | Clave de acceso de la factura |
| sriDocType | string | Tipo de documento del SRI. Por ahora siempre 01 |
| supplierId | string | ID del proveedor, creado automáticamente si no existía |
| supplier | object | No llega en esta respuesta. En el listado trae legalName e identification; en el detalle, además, los datos de contacto |
| issueDate | string | Fecha de emisión de la factura (ISO 8601) |
| authorizationNumber | string | null | Número de autorización del SRI |
| subtotal / taxAmount / total | string | Importes, como string para no perder decimales |
| items | array | No llega en esta respuesta: solo en el detalle (GET /developer/purchases/{id}) |
| withholdings | array | No llega en esta respuesta: solo en el detalle, con la retención emitida sobre la compra si ya existe |
Errores específicos
| Código | HTTP | Cuándo ocurre |
|---|---|---|
| INTEGRATION_8005 | 403 | Estás usando una API key de pruebas (ak_test_...). Este endpoint requiere una de producción. |
| GENERAL_0002 | 400 | La clave de acceso falta o no tiene 49 dígitos numéricos. |
| PU_5502 | 400 | La clave no corresponde a una factura (tipo 01). Aún no se admiten otros tipos. |
| PU_5503 | 400 | Esa clave es de un comprobante que emitiste tú, no de una compra. |
| PU_5504 | 400 | Esa compra ya está registrada (incluso si la eliminaste antes). |
| PU_5505 | 400 | No se pudo descargar el comprobante del SRI. Verifica la clave de acceso. |
| PU_5506 | 400 | El comprobante no está autorizado en el SRI. |
| PU_5508 | 400 | La factura fue emitida a otro RUC, no al del contribuyente indicado. |
| SUB_12002 | 403 | Se agotó el cupo de documentos de tu plan (las compras registradas cuentan en el mismo cupo que los comprobantes que emites). |
Buscar compras
Lista las compras registradas del contribuyente, con paginación. Las fechas se interpretan en días de Ecuador y son inclusivas.
const { data, meta } = await factuplan.purchases.list({
status: 'COMPLETED',
dateFrom: '2026-09-01',
dateTo: '2026-09-30',
page: 1,
limit: 20,
});
console.log(meta.total, meta.totalPages);
for (const compra of data) {
console.log(compra.sequential, compra.supplier?.legalName, compra.total);
}Parámetros de consulta
| Campo | Tipo | Descripción |
|---|---|---|
| status | string | AUTHORIZED, COMPLETED o ERROR |
| supplierId | string | Filtrar por proveedor (ID del contacto) |
| dateFrom | string | Desde (YYYY-MM-DD, día de Ecuador, inclusive) |
| dateTo | string | Hasta (YYYY-MM-DD, día de Ecuador, inclusive) |
| search | string | Busca por razón social del proveedor, secuencial o clave de acceso |
| page | number | Número de página. Por defecto 1 |
| limit | number | Elementos por página. Por defecto 20, máximo 100 |
Obtener una compra
Devuelve el detalle completo: proveedor, ítems con sus impuestos, totales y la retención emitida sobre esa compra, si ya existe.
const compra = await factuplan.purchases.get('019432a1-8f2c-7c3d-9a41-2b6e5d0f1a77');
console.log(compra.items?.length);
console.log(compra.withholdings); // [] si todavía no le emitiste retenciónDescargar XML / PDF
A diferencia de los comprobantes que emites tú, las descargas de compras llegan en base64, no como una URL pre-firmada. El XML es el autorizado por el SRI, tal cual lo emitió tu proveedor.
El PDF se genera de forma asíncrona. Si pides el PDF mientras todavía se está generando, o si quedó en error, recibes 404 con el código PU_5510. Espera unos segundos o usa el reintento si la compra está en estado ERROR.
import { writeFileSync } from 'fs';
const pdf = await factuplan.purchases.getPdf(compra.id);
writeFileSync(pdf.filename, Buffer.from(pdf.base64, 'base64'));
const xml = await factuplan.purchases.getXml(compra.id);
writeFileSync(xml.filename, Buffer.from(xml.base64, 'base64'));Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| base64 | string | Contenido del archivo codificado en base64 |
| filename | string | Nombre sugerido, p. ej. COMPRA-001-001-000000123.pdf |
| contentType | string | application/pdf o application/xml |
Reintentar el PDF
Vuelve a encolar la generación del PDF de una compra que quedó en estado ERROR. Usa el XML que ya está guardado, así que no descarga nada del SRI otra vez y no consume cupo. Si el PDF ya está generado o todavía está en proceso, devuelve 400 con el código PU_5511.
const result = await factuplan.purchases.retryPdf(compra.id);
console.log(result.success); // true
console.log(result.message); // "Estamos regenerando el PDF de la compra."Eliminar una compra
Elimina el registro de la compra en Factuplan. Es un borrado lógico y no afecta en nada al comprobante en el SRI, que sigue autorizado.
Eliminar no devuelve el cupo que consumiste al registrarla, y la clave de acceso queda reservada: volver a registrar esa misma compra devuelve 400 con el código PU_5504.
const result = await factuplan.purchases.delete(compra.id);
console.log(result.success); // trueContribuyentes vinculados
Las tres peticiones siguientes controlan con qué contribuyentes puede operar tu API key. Solo cambian el vínculo entre la API key y el contribuyente: nunca crean ni eliminan un contribuyente, ni tocan su firma electrónica, sus comprobantes o su configuración.
Una API key sin ningún contribuyente vinculado no está restringida: puede operar con todos los contribuyentes de tu workspace. Por eso, vincular el primer contribuyente la restringe únicamente a él, y no puedes desvincular el último.
Este endpoint devuelve los contribuyentes con los que esta API key puede operar. Si unrestricted es true, la llave no tiene restricciones y taxpayers llega vacío.
const { unrestricted, taxpayers } = await factuplan.taxpayers.list();
if (unrestricted) {
console.log('Esta API key puede operar con TODOS los contribuyentes');
} else {
for (const t of taxpayers) {
console.log(t.ruc, t.legalName, t.hasCertificate);
}
}Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| unrestricted | boolean | true si la API key no tiene contribuyentes vinculados y puede operar con todos |
| taxpayers | array | Contribuyentes vinculados a la API key |
| taxpayers[].ruc | string | RUC del contribuyente |
| taxpayers[].legalName | string | Razón social del contribuyente |
| taxpayers[].hasCertificate | boolean | true si tiene una firma electrónica cargada |
| taxpayers[].certificateExpiresAt | string | null | Vencimiento de la firma (ISO 8601), null si no tiene |
Vincular contribuyente
Añade un contribuyente que ya existe en tu workspace a la lista de contribuyentes con los que esta API key puede operar. Es idempotente: vincular dos veces el mismo RUC devuelve alreadyLinked: true y no cambia nada.
Nunca crea un contribuyente ni consume cupo de contribuyentes de tu plan: si el RUC no existe en tu workspace devuelve 404. Para crear un contribuyente nuevo usa POST /developer/certificate.
// Si tu API key ya está restringida, usa un cliente creado sin la opción ruc (o con el RUC de un
// contribuyente ya vinculado): el header x-taxpayer-ruc de un RUC aún no vinculado se rechaza con 403.
const result = await factuplan.taxpayers.link('0950194407001');
console.log(result.linked); // true
console.log(result.alreadyLinked); // false si acaba de vincularse
console.log(result.restrictedNow); // true si la llave pasó de "todos" a solo este
console.log(result.linkedCount); // contribuyentes vinculados en totalCuerpo de la petición
| Campo | Tipo | Descripción |
|---|---|---|
| ruc | string | RUC de 13 dígitos del contribuyente a vincular. Debe existir en tu workspace. |
Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| ruc | string | RUC del contribuyente vinculado |
| legalName | string | Razón social del contribuyente |
| linked | boolean | Siempre true: confirma que el contribuyente quedó vinculado |
| alreadyLinked | boolean | true si ya estaba vinculado y la petición no cambió nada |
| restrictedNow | boolean | true si este vínculo restringió una API key que antes operaba con todos |
| linkedCount | number | Contribuyentes vinculados tras la operación |
Errores específicos
| HTTP | Cuándo ocurre |
|---|---|
| 400 | El RUC no tiene 13 dígitos numéricos. |
| 404 | El RUC no existe como contribuyente en tu workspace. |
Desvincular contribuyente
Quita un contribuyente de la lista de contribuyentes con los que esta API key puede operar. Es idempotente: desvincular un RUC que no estaba vinculado devuelve alreadyUnlinked: true.
No elimina el contribuyente. Su firma electrónica, sus comprobantes y su configuración quedan intactos, y sigue disponible desde el panel y desde otras API keys. Tampoco puedes desvincular el último contribuyente: devuelve 409 con el código INTEGRATION_8008, porque dejar la lista vacía habilitaría la llave para operar con todos los contribuyentes del workspace.
// Usa un cliente creado sin la opción ruc (o con el RUC de un contribuyente que siga vinculado):
// el header x-taxpayer-ruc de un RUC que no está vinculado se rechaza con 403.
const result = await factuplan.taxpayers.unlink('0950194407001');
console.log(result.unlinked); // true
console.log(result.alreadyUnlinked); // false si acaba de desvincularse
console.log(result.linkedCount); // contribuyentes que siguen vinculados
// El contribuyente NO fue eliminado: sigue en tu workspace.Cuerpo de la petición
| Campo | Tipo | Descripción |
|---|---|---|
| ruc | string | RUC de 13 dígitos del contribuyente a desvincular |
Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| ruc | string | RUC del contribuyente desvinculado |
| unlinked | boolean | Siempre true: confirma que ya no está vinculado |
| alreadyUnlinked | boolean | true si no estaba vinculado y la petición no cambió nada |
| linkedCount | number | Contribuyentes que siguen vinculados tras la operación |
Errores específicos
| HTTP | Cuándo ocurre |
|---|---|
| 400 | El RUC no tiene 13 dígitos numéricos. |
| 404 | El RUC no existe como contribuyente en tu workspace. |
| 409 | Es el único contribuyente vinculado (INTEGRATION_8008). Vincula otro antes de desvincular este. |