Saltar al contenido
firmo
Docs / Webhooks

Webhooks

Eventos, formato de entrega, reintentos y verificación de la firma Firmo-Signature.

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#

EventoCuándo se envía
ecf.aceptadoLa DGII aceptó un e-CF.
ecf.aceptado_condicionalLa DGII lo aceptó con observaciones.
ecf.rechazadoLa DGII lo rechazó.
ecf.errorError técnico: no llegó a la DGII (se reintenta).
recibido.nuevoLlegó un e-CF de un suplidor.
certificado.por_vencerTu certificado digital está por vencer.
secuencia.por_agotarseUn rango de e-NCF está por agotarse.

Crear un webhook#

POST/v1/webhooks

bash
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"]
  }'
Respuesta (Webhook)
{
  "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"
}
El 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.

Cuerpo (datos abreviado)
{
  "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:

http
Firmo-Signature: t=1791296044,v1=5f2b9c…
  • t: momento del envío en segundos Unix.
  • v1: HMAC-SHA256 del texto t + "." + 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:

verificar-webhook.js
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);