Ir al contenido

Webhooks

Los webhooks avisan a tu sistema cuando cambia el estado de un documento, sin que tengas que consultar. Son indispensables en modo async y cuando la API responde 202.

Ventana de terminal
curl https://api.sudte.com/v1/webhook-endpoints \
-H "Authorization: Bearer $SUDTE_API_KEY" \
-H "Idempotency-Key: webhook-erp-1" \
-H "Content-Type: application/json" \
-d '{ "url": "https://erp.example.com/sudte/webhook", "events": ["document.processed", "document.rejected"] }'

La respuesta incluye el secreto de firma (whsec_…) una sola vez: guárdalo. La URL debe ser https y pública. Los endpoints son por ambiente: los registrados con una key sk_test_ reciben solo eventos de pruebas. También puedes crearlos desde el panel, en Desarrolladores → Webhooks, y enviar un evento de prueba con POST /v1/webhook-endpoints/{id}/test.

Evento Cuándo
document.processed El MH selló el documento.
document.rejected El MH rechazó el documento. Incluye su código y observaciones.
document.contingency El MH no respondió tras los reintentos; el documento pasó a contingencia.
document.voided Se selló el evento de invalidación del documento.
document.delivered Se envió el correo con el JSON y el PDF al receptor.
fiscal.report.ready · fiscal.report.failed Un reporte fiscal terminó de generarse o falló.
fiscal.period.closed · fiscal.period.drift_detected Se cerró un periodo fiscal, o cambiaron datos de un periodo cerrado.

Cada entrega es un POST con cuerpo JSON:

{
"id": "evt_01JA2P9T3K5M7N9Q1R3S5T7V9W",
"object": "event",
"type": "document.processed",
"environment": "01",
"created_at": "2026-10-12T16:15:04Z",
"data": {
"id": "doc_01JA2M7R5N3P8Q4S6T0V2W4X6Y",
"object": "document",
"type": "03",
"status": "processed",
"environment": "01",
"emitter_id": "emi_01J9Z8K2QF4T6W3X5Y7A9B1C2D",
"external_id": "factura-2026-00831",
"generation_code": "5D0C2B1A-7E3F-4A9B-8C6D-1E2F3A4B5C6D",
"control_number": "DTE-03-M001P001-000000000000057",
"reception_stamp": "20264F1A8C3E9B2D7A6C5E4F3B2A1D9C8E7F6A5B",
"processed_at": "2026-10-12T10:15:03-06:00",
"total": "168.00",
"mh_response": { "code": "001", "message": "RECIBIDO", "observations": [] }
}
}

Cabeceras de cada entrega:

Cabecera Contenido
Sudte-Signature t=<unix>,v1=<firma>. Ver verificación.
Sudte-Event-Id Identificador del evento (igual a id). Úsalo para no procesar dos veces el mismo evento.
Sudte-Delivery-Attempt Número de intento de entrega.
User-Agent sudte-webhooks/1.0

La firma es HMAC-SHA256(secreto, t + "." + cuerpo) en hexadecimal, donde cuerpo son los bytes exactos recibidos (no lo vuelvas a serializar). Rechaza los eventos cuya marca de tiempo t tenga más de 5 minutos de diferencia con tu reloj.

import crypto from 'node:crypto';
export function verificarFirma(cuerpo, cabecera, secreto, tolerancia = 300) {
const partes = Object.fromEntries(cabecera.split(',').map((p) => p.trim().split('=')));
const t = Number(partes.t);
if (!t || !partes.v1 || Math.abs(Date.now() / 1000 - t) > tolerancia) return false;
const esperada = crypto.createHmac('sha256', secreto).update(`${t}.${cuerpo}`).digest('hex');
if (esperada.length !== partes.v1.length) return false;
return crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(partes.v1));
}
// Express: usa el cuerpo crudo
app.post('/sudte/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verificarFirma(req.body.toString('utf8'), req.get('Sudte-Signature'), process.env.SUDTE_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const evento = JSON.parse(req.body);
res.sendStatus(200); // responde rápido y procesa en segundo plano
});
  • Responde con un código 2xx en menos de 15 segundos. Si respondes otro código o no respondes, sudte reintenta con espera exponencial durante 24 horas.
  • Las entregas son al menos una vez: puede llegar el mismo evento más de una vez. Guarda el Sudte-Event-Id y descarta los repetidos.
  • El orden no está garantizado. Si necesitas el estado más reciente, consulta GET /v1/documents/{id}.
  • Procesa en segundo plano: guarda el evento, responde 200 y luego actualiza tu sistema.