> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brelio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recibe webhooks

> Suscribe una URL a los eventos de Brelio, verifica la firma de cada entrega en Node.js o Python y gestiona los reintentos.

En lugar de consultar cada poco, tu servidor recibe un `POST` firmado cuando pasa algo en la clínica. Brelio firma las entregas con el esquema [Standard Webhooks](https://www.standardwebhooks.com), así que cualquier librería de Standard Webhooks las verifica.

> [!NOTE]
>
> En esta versión el único evento al que puedes suscribirte es `webhookEndpoint.tested`, la entrega de prueba. Los eventos de citas y pacientes son los siguientes; `GET /v1/webhook-event-types` siempre lista los disponibles.

## 1. Lista los tipos de evento

```bash theme={null}
curl https://api.brelio.app/v1/webhook-event-types \
  -H "Authorization: Bearer $BRELIO_API_KEY"
```

```json Respuesta theme={null}
{
  "data": [
    {
      "type": "webhookEndpoint.tested",
      "version": 1,
      "description": "A test delivery was requested for a webhook endpoint.",
      "module": "webhooks",
      "privacy": "operational"
    }
  ]
}
```

`privacy` es `operational` o `personal` (sobre pacientes). Los eventos clínicos nunca se entregan por webhook. Los eventos `personal` solo se entregan mientras el [área de datos](https://docs.brelio.com/es/concepts/data-areas.md) `patients` de la clínica está activada; mientras está desactivada, los campos de pacientes de otros eventos llegan como `null`.

## 2. Crea el webhook

Tu clave necesita `integrations.keys.manage`. La URL debe ser `https` en un nombre de host público (sin direcciones IP ni `localhost`).

```bash theme={null}
curl -X POST https://api.brelio.app/v1/webhooks \
  -H "Authorization: Bearer $BRELIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://hooks.example.com/brelio",
    "description": "CRM de la clínica",
    "eventTypes": ["webhookEndpoint.tested"]
  }'
```

```json 201 Created theme={null}
{
  "id": "e91750cd-0f3b-4acb-800a-1c7323e66bd7",
  "url": "https://hooks.example.com/brelio",
  "description": "CRM de la clínica",
  "eventTypes": ["webhookEndpoint.tested"],
  "status": "enabled",
  "secretHint": "whsec_…6OQ=",
  "secretCreatedAt": "2026-11-09T18:01:30.790Z",
  "previousSecretExpiresAt": null,
  "failingSince": null,
  "disablesAt": null,
  "disabledAt": null,
  "createdBy": "api_key_85C593155A6442B58528D38FC59B0CE9",
  "version": 1,
  "createdAt": "2026-11-09T18:01:30.790Z",
  "updatedAt": "2026-11-09T18:01:30.790Z",
  "secret": "whsec_iSiK6+Zxx2+8QtctJLM+19HZZ+hc9hlmlXi67gh66OQ="
}
```

Guarda `secret` ahora, en tu gestor de secretos: solo se muestra aquí y al rotarlo. Los webhooks se crean solo por REST, nunca por MCP, así que el secreto nunca llega a un modelo de IA.

## 3. Verifica cada entrega

Cada entrega es un `POST` con un cuerpo JSON y tres cabeceras:

```http theme={null}
POST /brelio HTTP/1.1
Content-Type: application/json
User-Agent: Brelio-Webhooks/1
webhook-id: 5c2f8b7e-1d3a-4e6f-8a9b-0c1d2e3f4a5b
webhook-timestamp: 1794247290
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{"id":"0d8b3c1e-…","type":"webhookEndpoint.tested","version":1,"timestamp":"2026-11-09T18:01:30.851Z","data":{"endpointId":"e91750cd-0f3b-4acb-800a-1c7323e66bd7"}}
```

La firma es `base64(HMAC-SHA256(clave, "{webhook-id}.{webhook-timestamp}.{cuerpo sin procesar}"))`, donde `clave` es la parte de tu secreto después de `whsec_`, decodificada de base64. Para verificarla:

1. Calcula la firma sobre los bytes **sin procesar** del cuerpo, antes de interpretar el JSON.
2. Compárala en tiempo constante con cada entrada `v1,` de `webhook-signature` (separadas por espacios: durante una rotación del secreto hay dos).
3. Rechaza marcas de tiempo a más de 5 minutos de tu reloj, para evitar reenvíos.
4. Responde `2xx` rápido; haz el trabajo después.

```javascript Node.js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'

const SECRET = process.env.BRELIO_WEBHOOK_SECRET // whsec_…
const TOLERANCE_SECONDS = 5 * 60

/** Standard Webhooks: HMAC-SHA256 of `${id}.${timestamp}.${body}` with the base64 key after `whsec_`. */
export function verifyBrelioWebhook(secret, headers, rawBody) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signatures = headers['webhook-signature']
  if (!id || !timestamp || !signatures) return false
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest()
  // During a secret rotation Brelio signs with both secrets: `v1,<sig> v1,<sig>`.
  return signatures.split(' ').some((candidate) => {
    const [version, signature] = candidate.split(',')
    if (version !== 'v1' || !signature) return false
    const received = Buffer.from(signature, 'base64')
    return received.length === expected.length && timingSafeEqual(received, expected)
  })
}

const seen = new Set() // use your database in production

createServer((req, res) => {
  let rawBody = ''
  req.on('data', (chunk) => (rawBody += chunk))
  req.on('end', () => {
    if (!verifyBrelioWebhook(SECRET, req.headers, rawBody)) {
      res.writeHead(400).end('invalid signature')
      return
    }
    const id = req.headers['webhook-id']
    if (!seen.has(id)) {
      seen.add(id)
      const event = JSON.parse(rawBody)
      console.log('received', event.type, event.id)
    }
    res.writeHead(204).end()
  })
}).listen(3000)
```

```python Python (Flask) theme={null}
import base64, hashlib, hmac, json, os, time
from flask import Flask, request

SECRET = os.environ["BRELIO_WEBHOOK_SECRET"]  # whsec_…
TOLERANCE_SECONDS = 5 * 60


def verify_brelio_webhook(secret: str, headers, raw_body: bytes) -> bool:
    """Standard Webhooks: HMAC-SHA256 of f"{id}.{timestamp}.{body}" with the base64 key after whsec_."""
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not msg_id or not timestamp or not signatures or not timestamp.isdigit():
        return False
    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    # During a secret rotation Brelio signs with both secrets: "v1,<sig> v1,<sig>".
    return any(
        candidate.startswith("v1,") and hmac.compare_digest(candidate[3:], expected)
        for candidate in signatures.split(" ")
    )


app = Flask(__name__)
seen: set[str] = set()  # use your database in production


@app.post("/brelio")
def brelio_webhook():
    raw_body = request.get_data()  # the exact bytes, before any JSON parsing
    if not verify_brelio_webhook(SECRET, request.headers, raw_body):
        return "invalid signature", 400
    msg_id = request.headers["webhook-id"]
    if msg_id not in seen:
        seen.add(msg_id)
        event = json.loads(raw_body)
        print("received", event["type"], event["id"])
    return "", 204
```

O usa las librerías oficiales de Standard Webhooks, que hacen las mismas comprobaciones:

```javascript Node.js (standardwebhooks) theme={null}
import { createServer } from 'node:http'
import { Webhook } from 'standardwebhooks'

const webhook = new Webhook(process.env.BRELIO_WEBHOOK_SECRET.replace(/^whsec_/, ''))

createServer((req, res) => {
  let rawBody = ''
  req.on('data', (chunk) => (rawBody += chunk))
  req.on('end', () => {
    try {
      const event = webhook.verify(rawBody, req.headers)
      console.log('received', event.type, event.id)
      res.writeHead(204).end()
    } catch {
      res.writeHead(400).end('invalid signature')
    }
  })
}).listen(3000)
```

```python Python (standardwebhooks) theme={null}
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["BRELIO_WEBHOOK_SECRET"])
app = Flask(__name__)

@app.post("/brelio")
def brelio_webhook():
    try:
        event = wh.verify(request.get_data(), dict(request.headers))
    except Exception:
        return "invalid signature", 400
    print("pylib ok", event["type"])
    return "", 204
```

Instálalas con `npm install standardwebhooks` o `pip install standardwebhooks`.

## 4. Envía una prueba

```bash theme={null}
curl -X POST https://api.brelio.app/v1/webhooks/$WEBHOOK_ID/test \
  -H "Authorization: Bearer $BRELIO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
```

```json Respuesta theme={null}
{ "id": "e91750cd-0f3b-4acb-800a-1c7323e66bd7", "eventType": "webhookEndpoint.tested" }
```

La prueba se entrega a este webhook, esté suscrito a lo que esté. Como mucho 5 pruebas por minuto por webhook (`429 WEBHOOK_TEST_RATE_LIMITED`).

## El cuerpo de la entrega

| Campo | Significado |
| - | - |
| `id` | El id del evento. El mismo evento entregado a dos webhooks tiene el mismo `id`. |
| `type` | El tipo de evento, por ejemplo `webhookEndpoint.tested`. |
| `version` | La versión de la forma de `data`. Un cambio incompatible sale como versión nueva. |
| `timestamp` | Cuándo ocurrió el evento, en ISO 8601. |
| `data` | El contenido del evento. |

## Reintentos y duplicados

* Una entrega tiene éxito cuando tu servidor responde `2xx` en menos de 15 segundos. No se siguen redirecciones.
* Una entrega fallida se reintenta a los 5 s, 5 min, 30 min, 2 h, 5 h, 10 h y después cada 12 h, durante 3 días.
* Si no llega nada en 3 días, el webhook se **desactiva** y se avisa a los propietarios de la clínica. `failingSince` y `disablesAt` del webhook te avisan antes.
* `webhook-id` es el id de la entrega y es el mismo en cada reintento y en un reenvío manual. Guarda los ids que ya procesaste y descarta los repetidos.
* Las entregas pueden llegar desordenadas. Usa `timestamp`, o lee el registro desde la API, cuando el orden importe.

## Diagnostica un webhook

```bash theme={null}
curl https://api.brelio.app/v1/webhooks/$WEBHOOK_ID \
  -H "Authorization: Bearer $BRELIO_API_KEY"
```

`recentDeliveries` muestra de cada entrega su `status` (`pending`, `succeeded`, `failed`, `skipped`), `attempts`, `lastStatusCode`, `lastError` y `nextAttemptAt`. Cuando hayas arreglado tu servidor, reenvía una entrega ya en lugar de esperar:

```bash theme={null}
curl -X POST https://api.brelio.app/v1/webhooks/$WEBHOOK_ID/redeliver \
  -H "Authorization: Bearer $BRELIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"deliveryId": "5c2f8b7e-1d3a-4e6f-8a9b-0c1d2e3f4a5b"}'
```

## Rota el secreto

```bash theme={null}
curl -X POST https://api.brelio.app/v1/webhooks/$WEBHOOK_ID/rotate-secret \
  -H "Authorization: Bearer $BRELIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"expectedVersion": 1}'
```

La respuesta trae el nuevo `secret`. Durante 24 horas (`previousSecretExpiresAt`) las entregas se firman con los dos secretos, para que puedas desplegar el nuevo sin perder nada. Los receptores de arriba ya aceptan cualquiera de las dos firmas.

## Elimínalo

```bash theme={null}
curl -X POST https://api.brelio.app/v1/webhooks/$WEBHOOK_ID/delete \
  -H "Authorization: Bearer $BRELIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"expectedVersion": 2}'
```

```json Respuesta theme={null}
{ "id": "e91750cd-0f3b-4acb-800a-1c7323e66bd7", "deleted": true }
```

Eliminarlo borra el webhook y su registro de entregas; no se envía nada más.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.