# 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.

Fuente: https://platform.trevol.si/docs/webhooks

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](https://platform.trevol.si/webhooks) 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 evento `webhook.test`.

## Crear un endpoint

`POST /v1/webhook_endpoints` (`createWebhookEndpoint`)

#### cURL

```bash
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"
  ]
}'
```

[Se administra en Webhooks](https://platform.trevol.si/webhooks)

La respuesta trae `secret` (`whsec_…`) solo en esta llamada y al rotarlo.

`POST /v1/webhook_endpoints/{id}/test` (`testWebhookEndpoint`)

#### cURL

```bash
curl -X POST "https://api.trevol.si/v1/webhook_endpoints/we_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b/test" \
  -H "Authorization: Bearer $TREVOL_API_KEY"
```

[Probar en el Playground](https://platform.trevol.si/playground?op=testWebhookEndpoint)

## 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](/docs/webhooks/eventos). 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.

```http
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](https://www.standardwebhooks.com). 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 de `webhook-id.webhook-timestamp.cuerpo`. La clave son los 32 bytes que siguen a `whsec_` (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](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) de tu lenguaje:

```javascript
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 `2xx` en 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 con `POST /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 con `PATCH` `{"status": "enabled"}`.
- `GET /v1/webhook_endpoints/{id}/deliveries` muestra cada entrega y su error (`timeout`, `http_5xx`, `blocked_address`…). `GET /v1/events` guarda los eventos 30 días: sirve para recuperar lo que se perdió mientras tu servidor estaba caído.

`GET /v1/webhook_endpoints/{id}/deliveries` (`listWebhookEndpointDeliveries`)

#### cURL

```bash
curl "https://api.trevol.si/v1/webhook_endpoints/we_3f0c1a2b3f0c1a2b3f0c1a2b3f0c1a2b/deliveries?limit=100" \
  -H "Authorization: Bearer $TREVOL_API_KEY"
```

[Probar en el Playground](https://platform.trevol.si/playground?op=listWebhookEndpointDeliveries)

`POST /v1/events/{id}/resend` (`resendEvent`)

#### cURL

```bash
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"
}'
```

[Probar en el Playground](https://platform.trevol.si/playground?op=resendEvent&ejemplo=reenviar)

## 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` (`rotateWebhookEndpointSecret`)

#### cURL

```bash
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
}'
```

[Se administra en Webhooks](https://platform.trevol.si/webhooks)
