npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@firmo-ecf/sdk

v0.1.2

Published

SDK oficial de Firmo para emitir e-CF (DGII, República Dominicana)

Readme

@firmo-ecf/sdk

SDK oficial de Firmo en TypeScript para emitir y consultar comprobantes fiscales electrónicos (e-CF) de la DGII (República Dominicana).

  • Sin dependencias: usa el fetch nativo. Funciona en Node ≥ 18, navegadores, Deno, Bun y entornos edge.
  • Tipado completo de la API v1.
  • Idempotency-Key automática en cada POST, reintentos con backoff en 429/5xx (respeta Retry-After).
  • Errores tipados en español.
  • Verificación de firmas de webhooks con crypto.subtle.

Instalación

npm install @firmo-ecf/sdk
# o
pnpm add @firmo-ecf/sdk

Inicio rápido

import { Firmo } from '@firmo-ecf/sdk';

const firmo = new Firmo({ apiKey: process.env.FIRMO_API_KEY! });

const ecf = await firmo.ecf.emitir(
  {
    tipo: 31, // Factura de Crédito Fiscal
    comprador: { rnc: '131880681', razonSocial: 'Cliente SRL', correo: '[email protected]' },
    items: [
      { descripcion: 'Consultoría', cantidad: 10, precioUnitario: 2500, indicadorBienServicio: 2 },
      { descripcion: 'Libro', cantidad: 1, precioUnitario: 800, indicadorFacturacion: 4 }, // exento
    ],
  },
  { esperar: true }, // ?wait=true: espera hasta 20 s el resultado de la DGII
);

console.log(ecf.encf, ecf.estado, ecf.montoTotal); // E310000000001 aceptado 30300

Las claves sk_test_… operan en sandbox (simulador de la DGII si la empresa no ha subido certificado). Las sk_live_… operan en producción.

Opciones del cliente

new Firmo({
  apiKey: 'sk_test_…',
  baseUrl: 'https://api.firmo.do', // por defecto
  empresaId: '…', // header Firmo-Company (integradores con varias empresas)
  timeoutMs: 60_000, // por intento
  maxRetries: 2, // reintentos en 429/5xx/red
  fetch: customFetch, // opcional
});

El SDK envía en cada petición la cabecera Firmo-Channel: sdk, para que Firmo registre en el campo canal del e-CF que se emitió con el SDK. Es información declarada por el cliente (no se verifica). FiltrosEcf.canal filtra firmo.ecf.listar() por canal.

Para actuar sobre otra empresa sin crear un cliente desde cero: firmo.conEmpresa(empresaId).

Recursos

| Recurso | Métodos | |---|---| | firmo.ecf | emitir(input, { esperar?, idempotencyKey? }), obtener(idOEncf), listar(filtros), listarTodos(filtros), xml(idOEncf), qr(idOEncf) (ArrayBuffer PNG), pdf(idOEncf) (ArrayBuffer; HTML imprimible en v1), reenviar(idOEncf) (solo error o pendiente sin avanzar), consultarEstado(idOEncf) (consulta real a la DGII), corregir(idOEncf, input) (rechazados), intentos(idOEncf), reenviarPendientes(), enviarCorreo(idOEncf, correo) | | firmo.recibidos | listar(filtros), listarTodos(filtros), obtener(id), aprobar(id, motivo?), rechazar(id, motivo) | | firmo.anulaciones | crear({ rangos }) | | firmo.empresa | obtener(), actualizar(cambios), subirCertificado(archivo, password) | | firmo.secuencias | listar(), crear({ tipo, desde, hasta, venceEn }) | | firmo.empresas | listar(), crear(input) (integradores) | | firmo.webhooks | listar(), crear({ url, eventos }), eliminar(id) | | firmo.consultas | estado({ encf, rncEmisor, rncComprador?, codigoSeguridad? }) | | firmo.certificacion | progreso() | | Firmo.cuentas | crear(input, { baseUrl? }) (estático, sin API key) | | Firmo.webhooks | verificarFirma(body, header, secreto, toleranciaSeg = 300), construirEvento(...) (estáticos) |

Paginación

const pagina = await firmo.ecf.listar({ estado: 'rechazado', desde: '2026-10-01', limite: 50 });
// GET /v1/ecf?status=rechazado&from=2026-10-01&limit=50
// pagina.datos, pagina.siguiente (cursor)

for await (const ecf of firmo.ecf.listarTodos({ tipo: 32, desde: '2026-10-01' })) {
  console.log(ecf.encf, ecf.montoTotal);
}

Descargas

import { writeFile } from 'node:fs/promises';

const xml = await firmo.ecf.xml('E310000000001');
await writeFile('E310000000001.png', Buffer.from(await firmo.ecf.qr('E310000000001')));
const html = new TextDecoder().decode(await firmo.ecf.pdf('E310000000001'));

Certificado digital

import { readFile } from 'node:fs/promises';
await firmo.empresa.subirCertificado(await readFile('certificado.p12'), process.env.P12_PASSWORD!);

Registro de sandbox

const cuenta = await Firmo.cuentas.crear({ nombre: 'Ana Pérez', correo: '[email protected]', empresa: 'Ana SRL' });
// cuenta.apiKey → sk_test_… (solo se muestra una vez)

Totales antes de emitir

calcularTotales replica la lógica de la DGII de forma local (sin red): agrupa por indicador de facturación (1 = 18%, 2 = 16%, 3 = 0%, 4 = exento, 0 = no facturable) y redondea el ITBIS a 2 decimales.

import { calcularTotales } from '@firmo-ecf/sdk';

const t = calcularTotales([
  { cantidad: 2, precioUnitario: 1500 },
  { cantidad: 1, precioUnitario: 300, indicadorFacturacion: 4 },
]);
// t.montoGravadoTotal 3000, t.totalItbis 540, t.montoExento 300, t.montoTotal 3840

Errores

Todas las fallas lanzan FirmoError (status, codigo, mensaje, detalles) o una subclase:

| Clase | Cuándo | |---|---| | FirmoValidacionError | 400 validacion (ver detalles) | | FirmoAuthError | 401 no_autorizado, 403 prohibido | | FirmoLimiteError | 429 limite_excedido (retryAfterSeg) | | FirmoDgiiNoDisponibleError | 502 dgii_no_disponible: el e-CF queda en cola y Firmo lo reintenta | | FirmoError | resto (no_encontrado, secuencia_agotada, certificado_faltante, conexion, timeout…) |

import { FirmoError, FirmoValidacionError } from '@firmo-ecf/sdk';

try {
  await firmo.ecf.emitir(input);
} catch (e) {
  if (e instanceof FirmoValidacionError) console.error(e.mensaje, e.detalles);
  else if (e instanceof FirmoError && e.codigo === 'secuencia_agotada') {
    // registra un nuevo rango con firmo.secuencias.crear(...)
  } else throw e;
}

Idempotencia y reintentos

Cada POST lleva una Idempotency-Key (crypto.randomUUID()) que se reutiliza en los reintentos, así que un reintento nunca duplica un e-CF. Pasa tu propia clave para que sea estable entre procesos:

await firmo.ecf.emitir(input, { idempotencyKey: `pedido-${pedido.id}` });

Se reintentan 429, 5xx y errores de red (por defecto 2 veces) con backoff exponencial y jitter, respetando Retry-After. Los PATCH no se reintentan.

Webhooks

Firmo envía POST con { id, evento, creadoEn, datos } y el header Firmo-Signature: t=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secreto, t + "." + body). Verifica siempre con el cuerpo crudo:

// Express
import express from 'express';
import { Firmo } from '@firmo-ecf/sdk';

app.post('/webhooks/firmo', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await Firmo.webhooks.verificarFirma(req.body, req.get('Firmo-Signature'), process.env.FIRMO_WEBHOOK_SECRET!);
  if (!ok) return res.status(400).send('firma inválida');
  const evento = JSON.parse(req.body.toString('utf8'));
  if (evento.evento === 'ecf.aceptado') {
    /* … */
  }
  res.sendStatus(200);
});
// Next.js / Hono / Cloudflare Workers (Request estándar)
export async function POST(req: Request) {
  const body = await req.text();
  try {
    const evento = await Firmo.webhooks.construirEvento(body, req.headers.get('Firmo-Signature'), process.env.FIRMO_WEBHOOK_SECRET!);
    // evento.evento, evento.datos
    return new Response('ok');
  } catch {
    return new Response('firma inválida', { status: 400 });
  }
}

La tolerancia por defecto es de 300 s entre t y la hora actual (protege contra repetición); pasa otro valor como cuarto argumento (0 la desactiva).

Eventos: ecf.aceptado, ecf.aceptado_condicional, ecf.rechazado, ecf.error, ecf.reintentado, recibido.nuevo, certificado.por_vencer, secuencia.por_agotarse.

Licencia

MIT