Documentación · Webhooks · API v1 · versión 2026-10-22
Webhooks
Avisos firmados (Standard Webhooks) cuando un documento se valida, se rechaza, queda en error o se entrega; cómo crearlos, verificarlos, reintentos y rotación del secreto.
Un webhook es una dirección de tu servidor a la que Trevol le avisa cuando un documento se valida, se rechaza, queda en error o se entrega. Evita que tengas que preguntar, y es la forma de enterarte del resultado de un 202 processing.
- Cada webhook es de una sola empresa y un solo ambiente, como las llaves. Hasta 5 por empresa y ambiente.
- Créalo en la consola o por API (llave de escritura). El secreto (
whsec_…) se muestra una sola vez: guárdalo en tu servidor. - La URL debe ser
https://con un nombre de dominio (no una IP), sin usuario ni clave y con puerto 443 o 1024–65535. Trevol resuelve el nombre en cada envío y nunca manda a direcciones privadas o locales. - Prueba el endpoint con
POST /v1/webhook_endpoints/{id}/test: llega un eventowebhook.test.
POST /v1/webhook_endpoints
curl -X POST "https://api.trevol.si/v1/webhook_endpoints" \
-H "Authorization: Bearer $TREVOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-servidor.co/webhooks/trevol",
"description": "Producción: facturación",
"events": [
"invoice.accepted",
"invoice.rejected",
"credit_note.accepted"
]
}'La respuesta trae secret (whsec_…) solo en esta llamada y al rotarlo.
POST /v1/webhook_endpoints/{id}/test
curl -X POST "https://api.trevol.si/v1/webhook_endpoints/we_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b/test" \
-H "Authorization: Bearer $TREVOL_API_KEY"Los eventos
Hay 50 tipos (con webhook.test): los de la factura y las notas, el documento soporte, el documento equivalente (con sus notas de ajuste), la nómina y los eventos RADIAN, y los de la cuenta. Están todos en Tipos de evento. Una lista explícita admite hasta 60. Si suscribes ["*"] recibes todos los que existan.
Webhooks creados antes
Un webhook creado antes de que existieran los de documento soporte, equivalente, nómina y RADIAN con ["*"] sigue recibiendo solo los 22 de entonces (su catalog_version es 1) hasta que guardes de nuevo su lista de eventos —con PATCH y events, o desde la consola—; los nuevos nacen con catalog_version: 2. Así ningún receptor existente recibe tipos que no conoce. balance.low, numbering.low y numbering.expiring solo existen en producción (llaves tvl_live_…).
Qué llega
El evento lleva un resumen del documento (identificador, estado, número, CUFE o CUDE, total, códigos de la DIAN y entrega), nunca los datos del comprador ni las líneas, y en ….error un texto fijo por código. Pide el documento completo con GET /v1/invoices/{id}. El webhook puede llegar antes que la respuesta HTTP del POST: usa data.id, que es el mismo.
POST /trevol HTTP/1.1
Content-Type: application/json
User-Agent: Trevol-Webhooks/1.0 (+https://platform.trevol.si/docs/webhooks)
webhook-id: evt_8c1e0f0a7d3b4a6f9e2d1c0b5a4f3e2d
webhook-timestamp: 1761163445
webhook-signature: v1,K5oZfzN0ZGxPpoGxC2c0vL1o0U3q6Qh2oA0h1c8l8bE=
{"id":"evt_8c1e0f0a7d3b4a6f9e2d1c0b5a4f3e2d","object":"event","type":"invoice.accepted","timestamp":"2026-10-22T20:04:05Z",
"environment":"test","api_version":"2026-10-22",
"data":{"object":"invoice","id":"inv_08cd6774e31e401c9fa0fbf92b3fbdc0","status":"accepted","environment":"test",
"number":"SETP994307051","prefix":"SETP","external_id":"VENTA-2026-000123","origin":"api","cufe":"52cede538652…",
"issued_at":"2026-10-22T15:04:05-05:00","total":"178500.00","invoice":null,
"dian":{"status_code":"00","rules":[]},"delivery":{"pdf":false,"zip":false,"email":{"status":"pending"}}}}Verifica la firma
Seguimos Standard Webhooks. Cada entrega trae:
webhook-id: el mismo en cada reintento y en un reenvío manual. Úsalo para no procesar dos veces el mismo evento: la verificación no evita repeticiones dentro de los 5 minutos ni los reintentos de entrega.webhook-timestamp: segundos Unix del intento.webhook-signature:v1,y el HMAC-SHA256 en base64 dewebhook-id.webhook-timestamp.cuerpo. La clave son los 32 bytes que siguen awhsec_(en base64). Durante la gracia de una rotación llegan dos firmas separadas por un espacio: basta con que una coincida.
Calcúlala sobre el cuerpo tal como llegó (los bytes exactos, nunca el JSON ya decodificado), compárala en tiempo constante y rechaza los avisos con más de 5 minutos de diferencia.
Hasta que los SDK se publiquen, verifica con este código (Node.js, sin dependencias) o con una biblioteca abierta de Standard Webhooks de tu lenguaje:
import { createHmac, timingSafeEqual } from 'node:crypto';
// cuerpo: el texto crudo; cabeceras: un objeto con los nombres en minúsculas (Express: req.headers).
export function verificar(cuerpo, cabeceras, secreto) {
const id = cabeceras['webhook-id'];
const marca = cabeceras['webhook-timestamp'];
const firmas = cabeceras['webhook-signature'];
if (!id || !/^\d+$/.test(marca ?? '') || !firmas) return false;
if (Math.abs(Date.now() / 1000 - Number(marca)) > 300) return false; // más de 5 minutos
const clave = Buffer.from(secreto.replace(/^whsec_/, ''), 'base64');
const esperada = createHmac('sha256', clave).update(`${id}.${marca}.${cuerpo}`).digest();
return firmas.split(' ').some((f) => {
const [version, valor] = f.split(',');
if (version !== 'v1' || !valor) return false;
const recibida = Buffer.from(valor, 'base64');
return recibida.length === esperada.length && timingSafeEqual(recibida, esperada);
});
}Deduplica por webhook-id: esta verificación no evita repeticiones dentro de los 5 minutos ni los reintentos de entrega. Nunca compares las firmas con ==: usa una comparación en tiempo constante.
Entrega y reintentos
- Cualquier
2xxen 10 segundos cuenta como entregado. Un 3xx (no seguimos redirecciones), un 4xx, un 5xx, un tiempo agotado o un error de TLS cuentan como fallo. No guardamos la respuesta de tu servidor. - Reintentos con espera creciente: 1 min, 5 min, 30 min, 2 h, 5 h, 10 h y 14 h (8 intentos en unas 32 horas). Después la entrega queda
failed; reenvíala conPOST /v1/events/{id}/resend(3 intentos). - Si el endpoint falla sin parar durante 5 días, Trevol lo desactiva (
status: disabled) y le avisa por correo al titular. Se reactiva conPATCH{"status": "enabled"}. GET /v1/webhook_endpoints/{id}/deliveriesmuestra cada entrega y su error (timeout,http_5xx,blocked_address…).GET /v1/eventsguarda los eventos 30 días: sirve para recuperar lo que se perdió mientras tu servidor estaba caído.
GET /v1/webhook_endpoints/{id}/deliveries
curl "https://api.trevol.si/v1/webhook_endpoints/we_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b/deliveries?limit=100" \
-H "Authorization: Bearer $TREVOL_API_KEY"POST /v1/events/{id}/resend
curl -X POST "https://api.trevol.si/v1/events/evt_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b/resend" \
-H "Authorization: Bearer $TREVOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"endpoint_id": "we_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b"
}'Rotar el secreto
Si pierdes el secreto, rótalo: el anterior sigue firmando hasta 72 horas para que no pierdas avisos (durante ese tiempo llegan dos firmas en webhook-signature).
POST /v1/webhook_endpoints/{id}/rotate_secret
curl -X POST "https://api.trevol.si/v1/webhook_endpoints/we_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b/rotate_secret" \
-H "Authorization: Bearer $TREVOL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"grace_period_hours": 24
}'