FactuplanSDK
Postman

Factuplan SDK

Librería oficial de JavaScript y TypeScript para crear facturas electrónicas autorizadas por el SRI de Ecuador. Zero dependencies.

Node.js 18+TypeScriptESM & CJSSin dependencias

Instalación

npm
npm install factuplan
pnpm
pnpm add factuplan
yarn
yarn add factuplan

Autenticació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.

Inicializar el cliente
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.

Emitir con tu referencia
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 siempre

Qué responde la API

SituaciónRespuesta
Primera vez con esa referencia201 · se emite el documento · duplicate: false
Misma referencia, mismo contenido201 · devuelve el documento que ya existe · duplicate: true
Misma referencia, contenido distinto409 · API_10011 · con los datos del documento existente
Misma referencia y ese documento fue anulado409 · 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: true revisa siempre el status: el documento que ya existía puede estar autorizado, en proceso o rechazado. Si está en DRAFT, 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 como sendEmail no cuentan; todo lo demás sí, así que un reintento debe mandar el mismo body (por ejemplo, discount: 0 no 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 ocupada

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 SDKDescripció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

CampoTipoDefaultDescripción
sendEmailbooleantrueSi 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.emailstring—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.customerIdstring—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[].productIdstring—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.
devReferencestring—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 secuencial

Punto 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.

Por códigos SRI
// 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
});
Auto-detección
// 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:

taxTypeDescripción
IVA_RATEAplica una tarifa de IVA específica (usar campo tax).
IVA_0Aplica tarifa 0% (Código SRI 0).
NOT_TAXABLENo aplica IVA (Código SRI 6).
EXEMPTExento 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.

taxCódigo SRIDescripción
00IVA 0%
55IVA 5%
88IVA 8%
122IVA 12%
143IVA 14%
154IVA 15% (default)
Ejemplo con IVA 5%
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ódigoDescripción
01Sin utilización del sistema financiero
15Compensación de deudas
16Tarjeta de débito
17Dinero electrónico
18Tarjeta prepago
19Tarjeta de crédito
20Otros con utilización del sistema financiero
21Endoso de títulos
Unidad de tiempo (timeUnit)Descripción
diasDías
mesesMeses
aniosAñ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ámetroTipoDescripción
rucstringFiltrar por RUC o cédula del cliente
statusstringEstado exacto del documento: DRAFT | PROCESSING | RECEIVED_SRI | AUTHORIZED | COMPLETED | REJECTED | RETURNED | ERROR | VOIDED. Los documentos ya emitidos quedan en COMPLETED
dateFromstringDesde 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"
dateTostringHasta 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"
pagenumberNúmero de página (por defecto: 1)
limitnumberResultados por página (por defecto: 20, máximo: 100)
devReferencestringTu 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.

Solo API key de producción (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);
POST/developer/receipts/query

Consultar 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.

Solo API key de producción (ak_live_...). Esta función consulta el SRI en tiempo real y no está disponible con claves de prueba (ak_test_...).

Parámetros

CampoTipoRequeridoDescripción
accessKeystringSíClave de acceso de 49 dígitos del SRI
savebooleanNo (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.
Nota sobre el contador: El crédito se descuenta siempre que el comprobante esté autorizado en el SRI, independientemente del valor de 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 base64

Ejemplo completo

ejemplo-factura.ts
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.

CampoTipoDescripción
authorizedbooleanSi el SRI autorizó el comprobante
authorizationNumberstring | nullNúmero de autorización del SRI
authorizationDatestring | nullFecha de autorización del SRI
authorizedXmlstring | nullXML autorizado, en base64
messagesarrayMensajes 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 SDKDescripció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

CampoTipoReq.Descripción
invoiceAccessKeystringSíClave de acceso de 49 dígitos de la factura original
reasonstringSíMotivo de la nota de crédito
itemsarraySíÍtems a acreditar (misma estructura que en facturas)
emissionPointIdstringNoUUID 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)
paymentsarrayNoMétodos de pago
additionalInfoobjectNoCampos adicionales clave-valor
devReferencestringNoTu 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

CampoTipoDescripción
idstringID de la nota de crédito
accessKeystringClave de acceso de 49 dígitos
sequentialstringSecuencial (ej: "000000001")
statusstring"DRAFT" — se autoriza de forma asíncrona
totalstringImporte total de la nota de crédito, como texto decimal (ej: "115")
devReferencestringTu referencia. Solo viene si la enviaste
duplicatebooleanSolo 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ámetroTipoDescripción
rucstringFiltrar por RUC o cédula del cliente
statusstringEstado exacto del documento: DRAFT | PROCESSING | RECEIVED_SRI | AUTHORIZED | COMPLETED | REJECTED | RETURNED | ERROR | VOIDED. Los documentos ya emitidos quedan en COMPLETED
dateFromstringDesde 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"
dateTostringHasta 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"
pagenumberNúmero de página (por defecto: 1)
limitnumberResultados por página (por defecto: 20, máximo: 100)
devReferencestringTu 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ón

La 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 NUEVA

Secuencial

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'
CampoTipoDescripción
branchCodestringCódigo del establecimiento
emissionCodestringCódigo del punto de emisión
creditNoteSequentialnumberValor guardado en el contador. La siguiente nota de crédito sale con este valor + 1
updatedAtstringFecha 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 SDKDescripció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

CampoTipoReq.Descripción
invoiceAccessKeystringSíClave de acceso de 49 dígitos de la factura original
reasonsarraySíMotivos / cargos (al menos uno requerido)
reasons[].descriptionstringSíDescripción del cargo o ajuste
reasons[].amountnumberSíImporte del cargo (mínimo 0.01)
reasons[].taxTypestringSí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%
emissionPointIdstringNoUUID 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)
additionalInfoobjectNoCampos adicionales clave-valor
devReferencestringNoTu 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 NUEVA

Secuencial

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'
CampoTipoDescripción
branchCodestringCódigo del establecimiento
emissionCodestringCódigo del punto de emisión
debitNoteSequentialnumberValor guardado en el contador. La siguiente nota de débito sale con este valor + 1
updatedAtstringFecha 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 SDKDescripció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ón

Cuerpo de la petición

CampoTipoReq.Descripción
purchaseAccessKeystringSíClave de acceso de 49 dígitos de la compra registrada que sustenta la retención
codSustentostringSíCódigo de sustento del comprobante (tabla 5 del SRI). Ver "Códigos del SRI"
taxesarraySíLíneas de retención (mínimo una): taxType, taxRateCode, taxRate, taxBase
taxes[].taxTypestringSí'RENTA' | 'IVA' | 'ISD'
taxes[].taxRateCodestringSí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[].taxRatenumberSíPorcentaje a retener. Debe coincidir con el parametrizado por el SRI para ese código
taxes[].taxBasenumberSíBase imponible; no puede superar el total de la compra
pagoLocExtstringNoTipo de pago: '01' local (por defecto) o '02' exterior. Con '02' el XML todavía no incluye los datos del pago al exterior
fiscalPeriodstringNoPeríodo fiscal MM/AAAA. Por defecto, el mes de emisión de la compra
emissionPointIdstringNoUUID del punto de emisión (auto-resuelto si el contribuyente tiene uno solo activo)
additionalInfoobjectNoCampos adicionales clave-valor
devReferencestringNoTu propia referencia (1 a 100 caracteres). Repetir la petición con la misma referencia no emite otra retención. Ver "Idempotencia"

Respuesta 201

CampoTipoDescripción
idstringID de la retención
accessKeystringClave de acceso de 49 dígitos
sequentialstringSecuencial (ej: "000000001")
statusstring"DRAFT" — se autoriza de forma asíncrona
totalWithheldnumberImporte total retenido
purchaseIdstringID de la compra que sustenta la retención
devReferencestringTu propia referencia. Solo viene si la enviaste en la petición
duplicatebooleanSolo 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 taxRateCode o codSustento que no esté en estas tablas se rechaza al crear la retención con WH_5308 o WH_5307.
  • La API no valida el porcentaje, pero el SRI sí: si taxRate no 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ódigoConceptoPorcentaje
9Retención del 10% del IVA10%
10Retención del 20% del IVA20%
1Retención del 30% del IVA30%
11Retención del 50% del IVA50%
2Retención del 70% del IVA70%
3Retención del 100% del IVA100%
7Retención en cero0%
8No procede retención0%
Retención de renta (taxType: 'RENTA') (80 códigos)
CódigoConceptoPorcentaje
303Honorarios profesionales y demás pagos por servicios relacionados con el título profesional10%
303AServicios profesionales prestados por sociedades residentes5%
304Servicios predomina el intelecto no relacionados con el título profesional10%
304AComisiones y demás pagos por servicios predomina intelecto no relacionados con el título profesional10%
304BPagos a notarios y registradores de la propiedad y mercantil por sus actividades ejercidas como tales10%
304CPagos a deportistas, entrenadores, árbitros, miembros del cuerpo técnico por sus actividades ejercidas como tales10%
304DPagos a artistas por sus actividades ejercidas como tales10%
304EHonorarios y demás pagos por servicios de docencia10%
307Servicios predomina la mano de obra3%
308Utilización o aprovechamiento de la imagen o renombre (personas naturales, sociedades, "influencers")10%
309Servicios prestados por medios de comunicación y agencias de publicidad3%
310Servicio de transporte privado de pasajeros o transporte público o privado de carga1%
311Pagos a través de liquidación de compra (nivel cultural o rusticidad)3%
312Transferencia de bienes muebles de naturaleza corporal2%
312ACompras al productor: bienes de origen bioacuático, forestal y los descritos en el art. 27.1 de la LRTI1%
312CCompras al comercializador: bienes de origen bioacuático, forestal y los descritos en el art. 27.1 de la LRTI1.75%
314ARegalías por franquicias según el Código INGENIOS (COESCCI) - pago a personas naturales10%
314BCánones, derechos de autor, marcas, patentes y similares según el Código INGENIOS (COESCCI) - pago a personas naturales10%
314CRegalías por franquicias según el Código INGENIOS (COESCCI) - pago a sociedades10%
314DCánones, derechos de autor, marcas, patentes y similares según el Código INGENIOS (COESCCI) - pago a sociedades10%
319Cuotas de arrendamiento mercantil (prestado por sociedades), inclusive la de opción de compra2%
320Arrendamiento de bienes inmuebles10%
322Seguros y reaseguros (primas y cesiones)2%
323Rendimientos financieros pagados a naturales y sociedades (no a IFIs)3%
323ARendimientos financieros depósitos cuenta corriente3%
323B1Rendimientos financieros depósitos cuenta de ahorros sociedades3%
323ERendimientos financieros depósito a plazo fijo gravados3%
323E2Rendimientos financieros depósito a plazo fijo exentos0%
323FRendimientos financieros operaciones de reporto - repos3%
323GInversiones (captaciones) rendimientos distintos de aquellos pagados a IFIs3%
323HRendimientos financieros obligaciones3%
323IRendimientos financieros bonos convertibles en acciones3%
323QOtros intereses y rendimientos financieros gravados3%
323ROtros intereses y rendimientos financieros exentos0%
323SPagos 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 sociedades3%
323TRendimientos financieros originados en la deuda pública ecuatoriana0%
323URendimientos financieros de títulos valores de obligaciones de 360 días o más para proyectos públicos en asociación público-privada0%
324AIntereses en operaciones de crédito entre instituciones del sistema financiero y entidades de la economía popular y solidaria2%
324BInversiones entre instituciones del sistema financiero y entidades de la economía popular y solidaria2%
324CPagos 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 financiero2%
325Anticipo dividendos25%
325APréstamos a accionistas, beneficiarios o partícipes residentes o establecidos en el Ecuador25%
3250Dividendos exentos (por no llegar a franja exenta o beneficio de otras leyes)0%
326Dividendos distribuidos que correspondan al impuesto a la renta único del art. 27 de la LRTIvariable
327Dividendos distribuidos a personas naturales residentesvariable
328Dividendos distribuidos a sociedades residentes0%
329Dividendos distribuidos a fideicomisos residentes0%
331Dividendos en acciones (capitalización de utilidades)0%
332Otras compras de bienes y servicios no sujetas a retención (incluye RIMPE - Negocios Populares)0%
332BCompra de bienes inmuebles0%
332CTransporte público de pasajeros0%
332DPagos en el país por transporte de pasajeros o transporte internacional de carga, a compañías nacionales o extranjeras de aviación o marítimas0%
332EValores entregados por las cooperativas de transporte a sus socios0%
332FCompraventa de divisas distintas al dólar de los Estados Unidos de América0%
332GPagos con tarjeta de crédito0%
332HPago al exterior con tarjeta de crédito reportada por la emisora de tarjeta de crédito, solo recap0%
332IPago a través de convenio de débito (clientes IFIs)0%
333Ganancia en la enajenación de derechos representativos de capital u otros derechos de sociedades que se coticen en bolsa de valores del Ecuador10%
334Contraprestación por la enajenación de derechos representativos de capital u otros derechos de sociedades no cotizados en bolsa de valores del Ecuador2%
335Loterías, rifas, pronósticos deportivos, apuestas y similares15%
336Venta de combustibles a comercializadoras (2 por mil)0.2%
337Venta de combustibles a distribuidores (3 por mil)0.3%
338Producción y venta local de banano producido o no por el mismo sujeto pasivovariable
340Impuesto único a la exportación de banano3%
343Otras retenciones aplicables el 1% (incluye RIMPE - Emprendedores)1%
343AEnergía eléctrica2%
343BActividades de construcción de obra material inmueble, urbanización, lotización o actividades similares2%
343CRecepción de botellas plásticas no retornables de PET2%
3440Otras retenciones aplicables el 3%3%
344APago local con tarjeta de crédito/débito reportada por la emisora de tarjeta / entidades del sistema financiero / sistemas auxiliares de pago2%
344BAdquisición de sustancias minerales dentro del territorio nacional2%
346Otras retenciones aplicables a otros porcentajesvariable
346AOtras ganancias de capital distintas de enajenación de derechos representativos de capitalvariable
346BDonaciones en dinero - impuesto a las donacionesvariable
346CRetención a cargo del propio sujeto pasivo por la producción y/o comercialización de minerales y otros bienesvariable
346DRetención a cargo del propio sujeto pasivo por la comercialización de productos forestalesvariable
350Otras autorretenciones (incisos 1 y 2 del art. 92.1 del RLRTI)variable
3480Impuesto a la renta único sobre los ingresos de los operadores de pronósticos deportivos15%
3481Autorretenciones de sociedades grandes contribuyentesvariable
3482Comisiones a sociedades nacionales o extranjeras residentes y establecimientos permanentes domiciliados en el país5%
Retención de ISD (taxType: 'ISD') (1 códigos)
CódigoConceptoPorcentaje
4580Impuesto a la salida de divisas5%
Sustento del comprobante (codSustento) (11 códigos)
CódigoConcepto
01Crédito tributario para declaración de IVA (compras)
02Costo o gasto para declaración de impuesto a la renta (compras)
03Activo fijo - crédito tributario para declaración de IVA (compras)
04Activo fijo - costo o gasto para declaración de impuesto a la renta (compras)
05Liquidación de compra de bienes o prestación de servicios en las que el vendedor no tiene sustento tributario
06Adquisiciones a proveedores no obligados a emitir comprobantes de venta o no residentes en el Ecuador
07Sin derecho a crédito tributario ni a costo o gasto
08Reembolso como intermediario
09Reembolso por el contribuyente
10Impuesto a la salida de divisas
11Compensació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. parteRel se envía como NO.
  • 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 pago 20 (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 SRI

Descargar 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 NUEVA

Guí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 a POST /v1/developer/sign-and-authorize si está sin firmar (ver Firmar y autorizar XML) o a POST /v1/developer/authorize-signed si 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 responde 403.
  • 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, comprobanteRetencion o liquidacionCompra.
  • 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ódigoQué pasó
INTEGRATION_8005Usaste una API key de pruebas. Este endpoint solo acepta ak_live_….
API_10004El contenido enviado no es un XML válido.
API_10007El XML no lleva firma electrónica. Este endpoint no firma.
API_10008El nodo raíz no es un comprobante que el SRI reconozca.
API_10009Falta el campoAdicional con nombre RUC Proveedor.
API_10010El 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ódigoHTTPDescripción
INTEGRATION_8001401API key inválida, inactiva o expirada (en el SDK: AuthenticationError, con code AUTH_ERROR)
GENERAL_0003429Límite de 100 peticiones por segundo excedido (en el SDK: RateLimitError, con code RATE_LIMIT)
INTEGRATION_8004403Tu plan no incluye el acceso a la API en producción, o tu suscripción no está activa
API_10002429Cuota 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_12002403Cupo de documentos del plan agotado
SUB_12008403Cupo del plan y excedente agotados
GENERAL_0002400Datos de la petición inválidos; el mensaje indica qué corregir
INVOICE_4003400Detalle de factura vacío
CERT_3008422Certificado expirado
CUSTOMER_7004409Cliente duplicado
PRODUCT_6002409Código de producto duplicado
API_10011409La devReference ya pertenece a otro documento con contenido distinto
API_10012409La devReference pertenece a un documento anulado: usa una referencia distinta
WH_5304404No se encontró una compra registrada con esa clave de acceso
WH_5305400La compra todavía no está registrada correctamente y no puede sustentar una retención
WH_5306409Esa compra ya tiene una retención emitida
WH_5307400El código de sustento no existe en la tabla del SRI
WH_5308400El 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.

Límite de velocidad

100

peticiones por segundo por API key

Cupo de documentos

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

HTTP 429 — Límite de velocidad
{
  "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." }
  }
}
HTTP 403 — Cupo agotado
{
  "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.

ComprobanteEventos
Facturainvoice.authorized · invoice.rejected · invoice.error
Nota de créditocredit_note.authorized · credit_note.rejected · credit_note.error
Nota de débitodebit_note.authorized · debit_note.rejected · debit_note.error
Guía de remisiónwaybill.authorized · waybill.rejected · waybill.error
Liquidación de comprapurchase_settlement.authorized · purchase_settlement.rejected · purchase_settlement.error
Comprobante de retenciónwithholding.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 error o rejected de un comprobante que después fue autorizado, porque quedó obsoleto. El reenvío es la misma entrega: conserva el id del evento.
  • Todos los reintentos y reenvíos de un mismo evento comparten el id del evento y la cabecera X-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

CampoTipoDescripción
currentMonth.documentsnumberOperaciones registradas con esta API key en el mes en curso
currentMonth.costnumberCosto estimado del mes en USD
planstringPlan de la API key
monthlyQuotanumber | nullCuota propia de la API key; null si no tiene una asignada
monthlyUsednumberOperaciones 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 titular

Respuesta

CampoTipoDescripción
hasCertificatebooleantrue si el contribuyente tiene una firma cargada
isExpiredbooleantrue si la firma ya venció
daysUntilExpirynumber | nullDías que faltan para el vencimiento
rucstring | nullRUC que consta en el certificado
legalNamestring | nullNombre del titular que consta en el certificado
issuedAtstring | nullFecha de emisión (ISO 8601)
expiresAtstring | nullFecha de vencimiento (ISO 8601)
issuerstring | nullEntidad 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ón

Respuesta

CampoTipoDescripción
idstringID del comprobante anulado
statusstring"VOIDED" (estado anulado)
accessKeystringClave de acceso del comprobante
voidReasonstringMotivo de la anulación
messagestringMensaje de confirmación
devReferencestringTu 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 comprobanteactionQué hace
ERROR, REJECTED, RETURNEDreprocessVuelve 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 accesoauthorizeVuelve 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 PDFpdfRegenera ú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

CampoTipoDescripción
idstringID del comprobante reintentado
typestringINVOICE | CREDIT_NOTE | DEBIT_NOTE | WITHHOLDING
actionstringPaso que se volvió a ejecutar: reprocess | authorize | pdf
statusstringEstado del comprobante tras encolar el reintento
accessKeystring | nullClave de acceso vigente. Cambia si action es reprocess y el comprobante estaba en REJECTED o RETURNED
messagestringMensaje de confirmación
devReferencestringTu 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 8601

Respuesta

CampoTipoDescripción
branchCodestringCódigo del establecimiento
emissionCodestringCódigo del punto de emisión
invoiceSequentialnumberValor guardado en el contador. La siguiente factura sale con este valor + 1
updatedAtstringFecha 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 contribuyente

Respuesta

CampoTipoDescripción
hasCertificatebooleantrue si el certificado se subió correctamente
isExpiredbooleantrue si el certificado ya expiró
rucstringRUC extraído del certificado
legalNamestringNombre legal extraído del certificado
expiresAtstringFecha de vencimiento (ISO 8601)
createdboolean?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 nueva

Campos del formulario

CampoTipoDescripción
filearchivoNuevo archivo P12 (.p12 o .pfx)
passwordstringContraseña del certificado
rucstringRUC de 13 dígitos del contribuyente cuya firma se reemplaza

Respuesta

CampoTipoDescripción
hasCertificatebooleantrue si la nueva firma quedó cargada y operativa
isExpiredbooleantrue si el certificado ya expiró
daysUntilExpirynumberDías restantes hasta el vencimiento
rucstringRUC del titular del certificado
legalNamestringNombre legal extraído del certificado
issuedAtstringFecha de emisión (ISO 8601)
expiresAtstringVencimiento de la firma nueva (ISO 8601)
issuerstringEntidad certificadora emisora
updatedbooleanSiempre true: confirma que la firma fue reemplazada
previousExpiresAtstring | nullVencimiento de la firma anterior, null si no tenía

Errores específicos

HTTPCuándo ocurre
404El RUC no existe como contribuyente en tu workspace. Usa POST /developer/certificate para cargarlo por primera vez.
403Tu API key está restringida a otros contribuyentes y no puede operar con ese RUC.
400El 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.
422El 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 PDF

Cuerpo de la petición

CampoTipoDescripción
accessKeystringClave de acceso de 49 dígitos de la factura de compra

Respuesta

CampoTipoDescripción
idstringID de la compra en Factuplan
statusstringAUTHORIZED (PDF en proceso), COMPLETED (PDF listo) o ERROR
accessKeystringClave de acceso de la factura
sriDocTypestringTipo de documento del SRI. Por ahora siempre 01
supplierIdstringID del proveedor, creado automáticamente si no existía
supplierobjectNo llega en esta respuesta. En el listado trae legalName e identification; en el detalle, además, los datos de contacto
issueDatestringFecha de emisión de la factura (ISO 8601)
authorizationNumberstring | nullNúmero de autorización del SRI
subtotal / taxAmount / totalstringImportes, como string para no perder decimales
itemsarrayNo llega en esta respuesta: solo en el detalle (GET /developer/purchases/{id})
withholdingsarrayNo llega en esta respuesta: solo en el detalle, con la retención emitida sobre la compra si ya existe

Errores específicos

CódigoHTTPCuándo ocurre
INTEGRATION_8005403Estás usando una API key de pruebas (ak_test_...). Este endpoint requiere una de producción.
GENERAL_0002400La clave de acceso falta o no tiene 49 dígitos numéricos.
PU_5502400La clave no corresponde a una factura (tipo 01). Aún no se admiten otros tipos.
PU_5503400Esa clave es de un comprobante que emitiste tú, no de una compra.
PU_5504400Esa compra ya está registrada (incluso si la eliminaste antes).
PU_5505400No se pudo descargar el comprobante del SRI. Verifica la clave de acceso.
PU_5506400El comprobante no está autorizado en el SRI.
PU_5508400La factura fue emitida a otro RUC, no al del contribuyente indicado.
SUB_12002403Se 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

CampoTipoDescripción
statusstringAUTHORIZED, COMPLETED o ERROR
supplierIdstringFiltrar por proveedor (ID del contacto)
dateFromstringDesde (YYYY-MM-DD, día de Ecuador, inclusive)
dateTostringHasta (YYYY-MM-DD, día de Ecuador, inclusive)
searchstringBusca por razón social del proveedor, secuencial o clave de acceso
pagenumberNúmero de página. Por defecto 1
limitnumberElementos 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ón

Descargar 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

CampoTipoDescripción
base64stringContenido del archivo codificado en base64
filenamestringNombre sugerido, p. ej. COMPRA-001-001-000000123.pdf
contentTypestringapplication/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); // true

Contribuyentes 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

CampoTipoDescripción
unrestrictedbooleantrue si la API key no tiene contribuyentes vinculados y puede operar con todos
taxpayersarrayContribuyentes vinculados a la API key
taxpayers[].rucstringRUC del contribuyente
taxpayers[].legalNamestringRazón social del contribuyente
taxpayers[].hasCertificatebooleantrue si tiene una firma electrónica cargada
taxpayers[].certificateExpiresAtstring | nullVencimiento 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 total

Cuerpo de la petición

CampoTipoDescripción
rucstringRUC de 13 dígitos del contribuyente a vincular. Debe existir en tu workspace.

Respuesta

CampoTipoDescripción
rucstringRUC del contribuyente vinculado
legalNamestringRazón social del contribuyente
linkedbooleanSiempre true: confirma que el contribuyente quedó vinculado
alreadyLinkedbooleantrue si ya estaba vinculado y la petición no cambió nada
restrictedNowbooleantrue si este vínculo restringió una API key que antes operaba con todos
linkedCountnumberContribuyentes vinculados tras la operación

Errores específicos

HTTPCuándo ocurre
400El RUC no tiene 13 dígitos numéricos.
404El 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

CampoTipoDescripción
rucstringRUC de 13 dígitos del contribuyente a desvincular

Respuesta

CampoTipoDescripción
rucstringRUC del contribuyente desvinculado
unlinkedbooleanSiempre true: confirma que ya no está vinculado
alreadyUnlinkedbooleantrue si no estaba vinculado y la petición no cambió nada
linkedCountnumberContribuyentes que siguen vinculados tras la operación

Errores específicos

HTTPCuándo ocurre
400El RUC no tiene 13 dígitos numéricos.
404El RUC no existe como contribuyente en tu workspace.
409Es el único contribuyente vinculado (INTEGRATION_8008). Vincula otro antes de desvincular este.