@betalyx/facturacion-ec
v1.1.8
Published
Facturación electrónica SRI Ecuador — XAdES-BES SHA-256, validadores, catálogos.
Maintainers
Readme
@betalyx/facturacion-ec
Facturación electrónica del SRI Ecuador en TypeScript: emisión firmada (XAdES-BES SHA-256) de los 5 comprobantes, RIDE (PDF + QR) y helper de anulación.
Para agentes de IA (léelo primero)
Este paquete emite comprobantes electrónicos del SRI Ecuador (facturas, notas de crédito/débito, retenciones, guías de remisión).
- Núcleo (emisión XML, firma, envío al SRI):
import { crearCliente } from '@betalyx/facturacion-ec' - RIDE (PDF + QR):
import { generarRideFactura } from '@betalyx/facturacion-ec/ride'— submódulo separado; requiere lasoptionalDependenciespdfkityqrcode. - Patrón de resultado: todas las funciones de emisión devuelven
Promise<ResultadoEmision>. Siempre revisaresultado.exito. NO usestry/catchpara el flujo normal de éxito/fallo;try/catches solo para errores de programación (bugs).
Ver Reglas que un agente debe respetar para los gotchas críticos antes de integrar.
Instalación
pnpm add @betalyx/facturacion-ec
npm i @betalyx/facturacion-ec
yarn add @betalyx/facturacion-ecEl RIDE (PDF) requiere las dependencias opcionales
pdfkityqrcode. Con npm/pnpm se instalan por defecto; si usas--no-optionaltendrás que instalarlas a mano cuando uses el submódulo/ride.
Uso rápido (Factura)
import { crearCliente, Ambiente, TipoEmision, TipoIdentificacion } from '@betalyx/facturacion-ec';
import { readFileSync } from 'node:fs';
const cliente = crearCliente({
emisor: {
ruc: '0924383631001',
razonSocial: 'MI EMPRESA S.A.',
direccionMatriz: 'Guayaquil',
direccionEstablecimiento: 'Guayaquil',
codigoEstablecimiento: '001',
puntoEmision: '001',
obligadoContabilidad: true,
ambiente: Ambiente.PRUEBAS,
tipoEmision: TipoEmision.NORMAL,
},
certificado: {
buffer: readFileSync('./cert.p12'),
clave: 'mi-clave',
},
});
// emitirFactura SIEMPRE devuelve ResultadoEmision — nunca lanza en el flujo normal
const resultado = await cliente.emitirFactura(
{
fechaEmision: new Date(),
tipoIdentificacionComprador: TipoIdentificacion.CEDULA,
identificacionComprador: '0926687856',
razonSocialComprador: 'JUAN PEREZ',
totalSinImpuestos: 100,
totalDescuento: 0,
totalConImpuestos: [{ codigo: '2', codigoPorcentaje: '4', baseImponible: 100, valor: 15 }],
importeTotal: 115,
moneda: 'DOLAR',
pagos: [{ formaPago: '01', total: 115 }],
detalles: [/* ... */],
},
'000000001',
);
// Patrón uniforme: revisar resultado.exito
if (resultado.exito) {
console.log('AUTORIZADO:', resultado.numeroAutorizacion);
} else {
console.log('Estado:', resultado.estado);
console.log('TraceId (para buscar en logs):', resultado.traceId);
console.log('Para el usuario:', resultado.mensajeAmigable);
// resultado.diagnostico[0] = {
// codigoSri: 'LOCAL_ErrorCertificadoExpirado' o '35',
// categoria: 'CERTIFICADO' | 'TOTALES' | ...,
// recuperable: true | false,
// accionSugerida: 'Renovar el certificado...',
// }
console.log('Diagnóstico:', resultado.diagnostico);
}Nota de Crédito
Cuándo usarlo: para acreditar (reversar, total o parcialmente) una factura ya autorizada. La NC referencia la factura original por número y fecha. No es posible emitir una NC a consumidor final (el SRI la rechaza; la librería la valida localmente).
import { crearCliente, TipoIdentificacion } from '@betalyx/facturacion-ec';
// `cliente` es la instancia de crearCliente({...}) de la sección "Uso rápido".
const resultado = await cliente.emitirNotaCredito(
{
fechaEmision: new Date(),
tipoIdentificacionComprador: TipoIdentificacion.CEDULA,
identificacionComprador: '0926687856',
razonSocialComprador: 'JUAN PEREZ',
codDocModificado: '01', // 01 = factura
numDocModificado: '001-001-000000123', // factura que se acredita
fechaEmisionDocSustento: new Date('2026-05-01'),
totalSinImpuestos: 100,
totalConImpuestos: [{ codigo: '2', codigoPorcentaje: '4', baseImponible: 100, valor: 15 }],
valorModificacion: 115,
moneda: 'DOLAR',
motivo: 'Devolución parcial',
detalles: [/* ... mismos DetalleFactura ... */],
},
'000000001',
);
// Mismo patrón que factura: revisar resultado.exitoNotas:
- La NC a consumidor final se rechaza localmente (
ErrorIdentificacionInvalida) antes de llegar al SRI. codDocModificado: '01'para créditar una factura; ver catálogoCATALOGOS.documentosSustentopara otros tipos.
Nota de Débito
Cuándo usarlo: para cobrar recargos (intereses, comisiones) sobre una factura ya emitida. Los motivos reemplazan a los detalles de línea. Usa calcularTotalesNotaDebito para calcular los totales automáticamente.
import { calcularTotalesNotaDebito, TipoIdentificacion } from '@betalyx/facturacion-ec';
// `cliente` es la instancia de crearCliente({...}) de la sección "Uso rápido".
const motivos = [{ razon: 'Interés por mora', valor: 1 }];
const totales = calcularTotalesNotaDebito(motivos, { codigoPorcentaje: '4', tarifa: 15 });
const resultado = await cliente.emitirNotaDebito(
{
fechaEmision: new Date(),
tipoIdentificacionComprador: TipoIdentificacion.CEDULA,
identificacionComprador: '0926687856',
razonSocialComprador: 'JUAN PEREZ',
codDocModificado: '01', // 01 = factura
numDocModificado: '001-001-000000123', // factura que origina el débito
fechaEmisionDocSustento: new Date('2026-05-01'),
totalSinImpuestos: totales.totalSinImpuestos,
impuestos: totales.impuestos,
valorTotal: totales.valorTotal,
motivos,
},
'000000001',
);
// Mismo patrón que factura: revisar resultado.exitoNotas:
- Para varias tarifas de IVA arma
impuestos[]manualmente y omite el helper. calcularTotalesNotaDebitoasume una sola tarifa de IVA para todos los motivos.
Guía de Remisión
Cuándo usarlo: para documentar el transporte de mercadería. No tiene montos ni impuestos; su estructura es emisor → transportista → destinatarios → detalles. La clave de acceso se deriva de fechaIniTransporte (este comprobante no tiene fechaEmision propia).
import { TipoIdentificacion } from '@betalyx/facturacion-ec';
// `cliente` es la instancia de crearCliente({...}) de la sección "Uso rápido".
const resultado = await cliente.emitirGuiaRemision(
{
dirPartida: 'Av. Principal 123, Guayaquil',
razonSocialTransportista: 'TRANSPORTE RAPIDO S.A.',
tipoIdentificacionTransportista: TipoIdentificacion.RUC,
rucTransportista: '0924383631001',
placa: 'ABC-1234',
fechaIniTransporte: new Date(),
fechaFinTransporte: new Date(),
destinatarios: [
{
identificacionDestinatario: '0926687856',
razonSocialDestinatario: 'JUAN PEREZ',
dirDestinatario: 'Calle Secundaria 456, Quito',
motivoTraslado: 'Traslado de mercadería',
detalles: [
{ descripcion: 'PRODUCTO A', cantidad: 10 },
{ descripcion: 'PRODUCTO B', cantidad: 5 },
],
},
],
},
'000000001',
);
// Mismo patrón que factura: revisar resultado.exitoNotas:
- La clave de acceso usa
fechaIniTransporteen lugar defechaEmision. - Soporta múltiples destinatarios en el mismo comprobante.
Comprobante de Retención
Cuándo usarlo: para documentar la retención de impuestos (Renta e IVA) al pagar a un proveedor. Usa calcularValorRetenido para calcular el monto retenido en cada línea.
import { calcularValorRetenido, TipoIdentificacion } from '@betalyx/facturacion-ec';
// `cliente` es la instancia de crearCliente({...}) de la sección "Uso rápido".
const fechaEmision = new Date();
const periodoFiscal = `${String(fechaEmision.getMonth() + 1).padStart(2, '0')}/${fechaEmision.getFullYear()}`;
const resultado = await cliente.emitirRetencion(
{
fechaEmision,
tipoIdentificacionSujetoRetenido: TipoIdentificacion.RUC,
identificacionSujetoRetenido: '0924383631001',
razonSocialSujetoRetenido: 'PROVEEDOR S.A.',
periodoFiscal, // 'mm/aaaa'
docsSustento: [
{
codSustento: '01', // Tabla 4 ATS
codDocSustento: '01', // 01 = factura
numDocSustento: '001001000000123', // 15 dígitos sin guiones
fechaEmisionDocSustento: new Date('2026-05-01'),
pagoLocExt: '01', // 01 = pago local
totalSinImpuestos: 100,
importeTotal: 115,
impuestos: [
{
codImpuestoDocSustento: '2', // 2 = IVA
codigoPorcentaje: '4', // 4 = 15%
baseImponible: 100,
tarifa: 15,
valorImpuesto: 15,
},
],
retenciones: [
{
codigo: '1', // 1 = Renta
codigoRetencion: '312', // Tabla 19 IR
baseImponible: 100,
porcentajeRetener: 1.75,
valorRetenido: calcularValorRetenido(100, 1.75), // → 1.75
},
],
pagos: [{ formaPago: '01', total: 115 }],
},
],
},
'000000001',
);
// Mismo patrón que factura: revisar resultado.exitoNotas:
- Alcance MVP: retención local sobre documento sustento. Sin reembolsos, dividendos ni pago al exterior.
periodoFiscaltiene formato'mm/aaaa'(p.ej.'06/2026').
RIDE (PDF + QR)
Los 5 comprobantes tienen generador de RIDE. Todos devuelven Promise<Uint8Array> con los bytes del PDF.
Importante: el RIDE se importa desde @betalyx/facturacion-ec/ride, no desde la raíz.
import {
generarRideFactura,
generarRideNotaCredito,
generarRideNotaDebito,
generarRideRetencion,
generarRideGuiaRemision,
} from '@betalyx/facturacion-ec/ride';
import { writeFileSync } from 'node:fs';
// Factura
const pdfFac = await generarRideFactura({
emisor, // ConfiguracionEmisor
factura, // DatosFactura
claveAcceso: resultado.claveAcceso, // 49 dígitos (siempre presente; = numeroAutorizacion cuando hay autorización online)
marca: {
colorPrimario: '#1a4f8b',
colorSecundario: '#eef2f7',
textoPie: 'Gracias por su compra — MI EMPRESA',
datosContacto: { telefono: '04-2000000', email: '[email protected]', web: 'miempresa.ec' },
// logo?: Uint8Array // PNG/JPG opcional
// fuente?: Uint8Array // TTF/OTF — por defecto usa Inter (SIL OFL)
},
});
writeFileSync('factura.pdf', pdfFac);
// Nota de Crédito
const pdfNc = await generarRideNotaCredito({ emisor, notaCredito, claveAcceso, marca });
writeFileSync('nota-credito.pdf', pdfNc);
// Nota de Débito
const pdfNd = await generarRideNotaDebito({ emisor, notaDebito, claveAcceso, marca });
writeFileSync('nota-debito.pdf', pdfNd);
// Comprobante de Retención
const pdfRet = await generarRideRetencion({ emisor, retencion, claveAcceso, marca });
writeFileSync('retencion.pdf', pdfRet);
// Guía de Remisión
const pdfGr = await generarRideGuiaRemision({ emisor, guiaRemision, claveAcceso, marca });
writeFileSync('guia-remision.pdf', pdfGr);Tipografía: el RIDE embebe Inter Regular e Inter Bold por defecto (SIL OFL 1.1 — ver
LICENSE-fontsen el paquete). Para usar tu propia tipografía TTF/OTF, pasamarca.fuente.Dependencias:
generarRideXrequierepdfkityqrcode, declaradas comooptionalDependencies. El núcleo de emisión XML no las carga si no importas/ride.
Anulación de comprobantes
La anulación ante el SRI es un trámite por el portal (SRI en línea), no una API. prepararAnulacion no anula nada: calcula las reglas y arma una guía del trámite a partir de la clave de acceso.
import { prepararAnulacion } from '@betalyx/facturacion-ec';
const r = prepararAnulacion({
claveAcceso: '...49 dígitos...',
esConsumidorFinal: false, // declarado por ti; la clave no lo indica
// feriados: [new Date(2026, 7, 10)], // opcional; si no se pasan, solo se ajusta por fin de semana
});
if (!r.anulablePorPortal) {
console.log(r.motivo); // p.ej. factura a consumidor final
} else if (!r.dentroDePlazo) {
console.log(r.motivo); // fuera de plazo → usar nota de crédito
} else {
console.log(r.tipoComprobante, 'anulable hasta', r.fechaLimite);
console.log(r.requiereAceptacionReceptor ? 'Requiere aceptación del receptor (5 días hábiles)' : 'No requiere aceptación');
}
console.log(r.instrucciones); // mensaje guía del trámiteNotas:
- Plazo: hasta el día 7 del mes siguiente a la emisión (ajustado a día hábil).
- NC/ND/retenciones requieren aceptación del receptor (5 días hábiles).
- Si no se pasan
feriados, solo se ajusta por fin de semana; el campoinstruccioneslo advierte. - Para feriados nacionales (Ecuador), pásalos explícitamente como
Date[].
Reglas que un agente debe respetar
Lista de gotchas críticos para integración correcta:
No usar try/catch para el flujo normal.
emitirFactura,emitirNotaCredito,emitirNotaDebito,emitirRetencionyemitirGuiaRemisionSIEMPRE devuelvenPromise<ResultadoEmision>— nunca lanzan en el flujo normal de éxito/fallo del SRI. Controla el resultado conresultado.exito. Reservatry/catchpara errores de programación (bug que impide que la función corra).Consumidor final: usa identificación
9999999999999+ razón socialCONSUMIDOR FINAL. La librería normaliza automáticamente en facturas. La Nota de Crédito a consumidor final se rechaza localmente (ErrorIdentificacionInvalida) antes de llegar al SRI — nunca la intentes emitir.La clave de acceso tiene 49 dígitos. En modo offline (sin enviar al SRI),
resultado.claveAcceso(49 dígitos) actúa como número de autorización. Pasa estos 49 dígitos agenerarRideXcomoclaveAcceso.La anulación NO es por API.
prepararAnulacionsolo guía el trámite; no anula nada en el SRI. La anulación efectiva se hace por el portal SRI en línea.El RIDE se importa de
/ride, no de la raíz.import { generarRideFactura } from '@betalyx/facturacion-ec/ride'— si importas desde la raíz obtendrás un error de resolución de módulo.Para diagnosticar fallos: usa
resultado.estado(estado del SRI),resultado.diagnostico[]({ codigoSri, categoria, recuperable, accionSugerida }),resultado.mensajeAmigable(texto para mostrar al usuario), yresultado.traceId(para correlacionar con logs).
Referencia de API pública
| Símbolo | Entry point | Firma resumida | Devuelve |
|---|---|---|---|
| crearCliente | . | crearCliente(opts: OpcionesClienteBetalyx) | ClienteBetalyx |
| cliente.emitirFactura | . | (factura: DatosFactura, secuencial: string) | Promise<ResultadoEmision> |
| cliente.emitirNotaCredito | . | (nc: DatosNotaCredito, secuencial: string) | Promise<ResultadoEmision> |
| cliente.emitirNotaDebito | . | (nd: DatosNotaDebito, secuencial: string) | Promise<ResultadoEmision> |
| cliente.emitirRetencion | . | (ret: DatosRetencion, secuencial: string) | Promise<ResultadoEmision> |
| cliente.emitirGuiaRemision | . | (guia: DatosGuiaRemision, secuencial: string) | Promise<ResultadoEmision> |
| calcularTotalesNotaDebito | . | (motivos: MotivoNotaDebito[], iva: { codigoPorcentaje: string; tarifa: number }) | TotalesNotaDebito |
| calcularValorRetenido | . | (baseImponible: number, porcentajeRetener: number) | number |
| generarClaveAcceso | . | (params: ParamsClaveAcceso) | string |
| parsearClaveAcceso | . | (clave: string) | ClaveAccesoParseada |
| validarClaveAcceso | . | (clave: string) | boolean |
| prepararAnulacion | . | (opts: OpcionesAnulacion) | ResultadoAnulacion |
| validarCedula | . | (cedula: string) | ResultadoValidacion |
| validarRuc | . | (ruc: string) | ResultadoValidacion |
| validarIdentificacion | . | (identificacion: string, tipo: TipoIdentificacion) | ResultadoValidacion |
| obtenerTarifaIva | . | (codigo: string) | number \| null \| undefined |
| CATALOGOS | . | { tiposImpuesto, codigosPorcentajeIva, formasPago, tiposIdentificacion, documentosSustento } | objeto de solo lectura |
| generarRideFactura | ./ride | (opts: OpcionesRideFactura) | Promise<Uint8Array> |
| generarRideNotaCredito | ./ride | (opts: OpcionesRideNotaCredito) | Promise<Uint8Array> |
| generarRideNotaDebito | ./ride | (opts: OpcionesRideNotaDebito) | Promise<Uint8Array> |
| generarRideRetencion | ./ride | (opts: OpcionesRideRetencion) | Promise<Uint8Array> |
| generarRideGuiaRemision | ./ride | (opts: OpcionesRideGuiaRemision) | Promise<Uint8Array> |
| generarQrPng | ./ride | (contenido: string) | Promise<Buffer> |
Los tipos de datos (
DatosFactura,ConfiguracionEmisor,ResultadoEmision, etc.) se exportan del entry point.y se documentan en los.d.tsdel paquete.
Comprobantes soportados
| Tipo | Código | Estado | |---|---|---| | Factura | 01 | ✅ | | Nota de Crédito | 04 | ✅ | | Nota de Débito | 05 | ✅ | | Guía de Remisión | 06 | ✅ | | Retención | 07 | ✅ |
Errores
import {
ErrorSri,
ErrorCertificadoInvalido,
ErrorCertificadoExpirado,
ErrorClaveAccesoInvalida,
ErrorFirma,
ErrorRedSri,
ErrorTimeoutSri,
ErrorTotalesInconsistentes,
ErrorIdentificacionInvalida, // NC/ND a consumidor final; ver sección "Nota de Crédito" y Gotcha #2
ErrorGuiaRemisionInvalida, // guía con datos incoherentes; ver sección "Guía de Remisión"
} from '@betalyx/facturacion-ec';Estos errores solo se lanzan por bugs del programador (configuración incorrecta, tipo incorrecto). El flujo normal de éxito/fallo del SRI se comunica a través de ResultadoEmision.
Estabilidad y versionado
Desde v1.0.0, este paquete sigue Semantic Versioning.
- La API de la tabla de referencia es estable desde 1.0.0.
- Cambios incompatibles en esa API dispararán una versión mayor (2.0.0+).
- Todo lo no documentado como público (internos de firma, SOAP, c14n, validadores de coherencia, helpers de XML) es implementación interna y puede cambiar entre versiones menores sin previo aviso.
Licencia
MIT
