Un webhook es una URL de tu sistema a la que Firmo hace POST cuando pasa algo: un e-CF aceptado, uno rechazado, una factura recibida de un suplidor. Así no tienes que consultar a cada rato.
Eventos#
| Evento | Cuándo se envía |
|---|---|
ecf.aceptado | La DGII aceptó un e-CF. |
ecf.aceptado_condicional | La DGII lo aceptó con observaciones. |
ecf.rechazado | La DGII lo rechazó. |
ecf.error | Error técnico: no llegó a la DGII (se reintenta). |
recibido.nuevo | Llegó un e-CF de un suplidor. |
certificado.por_vencer | Tu certificado digital está por vencer. |
secuencia.por_agotarse | Un rango de e-NCF está por agotarse. |
Crear un webhook#
POST/v1/webhooks
curl -X POST https://api.firmo.do/v1/webhooks \
-H "Authorization: Bearer $FIRMO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tusistema.do/webhooks/firmo",
"eventos": ["ecf.aceptado", "ecf.rechazado", "ecf.error"]
}'{
"id": "4b3a2918-0f7e-4d6c-b5a4-938271605f4e",
"url": "https://tusistema.do/webhooks/firmo",
"eventos": ["ecf.aceptado", "ecf.rechazado", "ecf.error"],
"secreto": "…",
"activo": true,
"creadoEn": "2026-10-06T14:20:00.000Z"
}secreto solo se muestra al crear el webhook. Guárdalo: lo necesitas para verificar la firma.También puedes listarlos con GET /v1/webhooks y eliminarlos con DELETE /v1/webhooks/{id}.
Formato de entrega#
Firmo hace POST con un JSON { id, evento, creadoEn, datos }, donde datos trae el recurso afectado.
{
"id": "evt_5c1f0a2b",
"evento": "ecf.aceptado",
"creadoEn": "2026-10-06T14:14:04.120Z",
"datos": {
"id": "0f8c2a9e-6b1d-4c2a-9a51-3e7d1b2c4f60",
"encf": "E310000000001",
"estado": "aceptado",
"montoTotal": 11800
}
}Responde con un código 2xx lo antes posible. Si tu servidor falla o no responde, Firmo reintenta hasta 8 veces con espera exponencial. Puede llegar el mismo evento más de una vez: usa id para no procesarlo dos veces.
Verificar la firma#
Cada entrega trae el header:
Firmo-Signature: t=1791296044,v1=5f2b9c…t: momento del envío en segundos Unix.v1: HMAC-SHA256 del textot + "." + cuerpo, con tu secreto como llave, en hexadecimal.
Calcula el mismo HMAC con el cuerpo crudo y compáralo en tiempo constante. Ejemplo en Node con Express:
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRETO = process.env.FIRMO_WEBHOOK_SECRET; // se muestra una sola vez al crear el webhook
const TOLERANCIA_SEG = 300; // rechaza entregas de más de 5 minutos
function verificarFirma(cuerpoCrudo, header) {
if (!header) return false;
const partes = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(partes.t);
const v1 = partes.v1;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCIA_SEG) return false;
const esperado = crypto
.createHmac('sha256', SECRETO)
.update(`${t}.${cuerpoCrudo}`)
.digest('hex');
const a = Buffer.from(esperado);
const b = Buffer.from(v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Importante: verifica sobre el cuerpo CRUDO, antes de parsear el JSON.
app.post('/webhooks/firmo', express.raw({ type: 'application/json' }), (req, res) => {
const cuerpo = req.body.toString('utf8');
if (!verificarFirma(cuerpo, req.get('Firmo-Signature'))) {
return res.status(400).send('Firma inválida');
}
const evento = JSON.parse(cuerpo);
// Usa evento.id para no procesar dos veces la misma entrega.
switch (evento.evento) {
case 'ecf.aceptado':
// marca la factura como aceptada en tu sistema
break;
case 'ecf.rechazado':
// revisa evento.datos y corrige
break;
}
res.sendStatus(200);
});
app.listen(3000);