Webhooks
Eventos en tiempo real con HMAC + retries
Los webhooks te permiten recibir notificaciones HTTP automáticas cuando ocurren eventos en CBCTHub. En lugar de hacer polling cada minuto, CBCTHub te envía un POST a tu URL apenas pasa algo: un examen confirmado, un informe firmado, una eliminación. Ideal para sincronizar con tu PMS, disparar workflows, o notificar a otros sistemas.
Eventos disponibles
Cuando creas una subscription eliges a qué eventos suscribirte. Estos son los tipos disponibles hoy:
exam.created— se creó un examen vía API o dashboard (todavía en status uploading).exam.confirmed— el examen completó la subida y quedó en status ready. Listo para compartir.exam.deleted— el examen fue eliminado del dashboard o vía API.exam.updated— se actualizó algún campo del examen (PATCH /v1/exams/{id}). El payload trae fields y changes con el diff.exam.shared— el examen se envió por email a un destinatario (POST /v1/exams/{id}/share). Payload: to, cc, email_id.exam.extra_added— se confirmó una toma extra (multi-toma para CBCT/ATM). Payload: index, label, file_count, storage_bytes.report.published— se subió un PDF de informe al examen (vía dashboard o POST /v1/exams/{id}/report).report.signed— el informe se firmó electrónicamente y ya tiene URL pública de verificación.
Crear una subscription
Llama POST /api/v1/webhooks con tu URL pública (debe ser HTTPS) y los eventos que quieres recibir. La respuesta incluye un secret — guárdalo en tu sistema, lo vas a necesitar para verificar la firma. El secret se muestra UNA SOLA VEZ; si lo pierdes hay que crear otra subscription.
curl -X POST https://cbcthub.com/api/v1/webhooks \
-H "Authorization: Bearer $CBCTHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://miclinica.com/webhooks/cbcthub",
"events": ["exam.confirmed", "report.signed"],
"description": "Sync con PMS interno"
}'
→ {
"id": "wh_a1b2c3d4e5",
"url": "https://miclinica.com/webhooks/cbcthub",
"events": ["exam.confirmed", "report.signed"],
"description": "Sync con PMS interno",
"enabled": true,
"secret": "whsec_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6",
"created_at": "2026-06-23T14:32:18Z"
}Payload del webhook
Todos los webhooks tienen la misma estructura raíz: id, type, created_at y data con info específica del evento.
{
"id": "evt_9z8y7x6w5v",
"type": "exam.confirmed",
"created_at": "2026-06-23T14:33:01Z",
"data": {
"exam_id": "exm_abc123",
"exam_type": "cbct",
"patient_name": "Juan Pérez",
"patient_id": "12345678-9",
"status": "ready",
"share_url": "https://cbcthub.com/share/...",
"viewer_url": "https://cbcthub.com/viewer/..."
}
}Headers HTTP
Cada delivery incluye estos headers para que valides el origen y la integridad:
POST /webhooks/cbcthub HTTP/1.1
Host: miclinica.com
Content-Type: application/json
User-Agent: CBCTHub-Webhooks/1.0
X-CBCTHub-Event: exam.confirmed
X-CBCTHub-Delivery: dlv_unique-per-attempt
X-CBCTHub-Signature: sha256=base64-encoded-hmac
<json body>X-CBCTHub-Event— el type del evento (ej: exam.confirmed).X-CBCTHub-Delivery— ID único de este intento. Úsalo para idempotencia.X-CBCTHub-Signature— firma HMAC-SHA-256 del body crudo, en base64, con prefijo sha256=.
Verificar la firma HMAC
Calcula HMAC-SHA-256 del body crudo (raw bytes, no del JSON parseado) usando tu secret y compara en tiempo constante con la firma del header. Si no coincide, descarta el request — alguien intentó falsificarlo.
// Node.js — Express ejemplo
const crypto = require('crypto');
const express = require('express');
const app = express();
// Captura el body crudo (NO uses express.json() acá: pierde los bytes originales).
app.post('/webhooks/cbcthub',
express.raw({ type: 'application/json' }),
(req, res) => {
const signatureHeader = req.headers['x-cbcthub-signature'] || '';
const signature = signatureHeader.replace('sha256=', '');
const rawBody = req.body; // Buffer
const computed = crypto
.createHmac('sha256', process.env.CBCTHUB_WEBHOOK_SECRET)
.update(rawBody)
.digest('base64');
const sigBuf = Buffer.from(signature);
const computedBuf = Buffer.from(computed);
if (sigBuf.length !== computedBuf.length ||
!crypto.timingSafeEqual(sigBuf, computedBuf)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(rawBody.toString('utf8'));
console.log('Evento recibido:', event.type, event.data);
// Responde rápido y procesa en background
res.status(200).send('ok');
}
);# Python — Flask ejemplo
import os, hmac, hashlib, base64
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['CBCTHUB_WEBHOOK_SECRET'].encode('utf-8')
@app.route('/webhooks/cbcthub', methods=['POST'])
def cbcthub_webhook():
raw = request.get_data() # bytes
header = request.headers.get('X-CBCTHub-Signature', '')
signature = header.replace('sha256=', '')
digest = hmac.new(SECRET, raw, hashlib.sha256).digest()
computed = base64.b64encode(digest).decode('utf-8')
if not hmac.compare_digest(signature, computed):
abort(401)
event = request.get_json()
print('Evento:', event['type'], event['data'])
return 'ok', 200Política de reintentos
Si tu endpoint no responde 2xx en 10 segundos, reintentamos con backoff exponencial: 30s, 2min, 10min, 1h, 6h, 24h. Máximo 6 intentos por evento. Después de 20 fallos consecutivos en cualquier evento, la subscription se deshabilita automáticamente (enabled=false) para no quemar tu endpoint.
ℹ Cronograma de retries
- Intento 1: inmediato
- Intento 2: +30 segundos
- Intento 3: +2 minutos
- Intento 4: +10 minutos
- Intento 5: +1 hora
- Intento 6: +6 horas
- Último: +24 horas (abandonado)
Endpoints
/api/v1/webhooksCrear subscription. Devuelve el secret una sola vez.
/api/v1/webhooksListar todas las subscriptions de la cuenta.
/api/v1/webhooks/{id}Detalle de una subscription.
/api/v1/webhooks/{id}Actualizar events, description o enabled.
/api/v1/webhooks/{id}Eliminar subscription. No reversible.
/api/v1/webhooks/{id}/testEnviar un ping de prueba al endpoint configurado.
/api/v1/webhooks/{id}/deliveriesAudit log de las últimas 50 entregas.
Buenas prácticas
- Verifica la firma HMAC SIEMPRE. Sin verificación cualquiera puede enviarte un POST forjado a tu endpoint público.
- Implementa idempotencia con X-CBCTHub-Delivery. Si recibes el mismo delivery_id dos veces (por un retry tras un timeout tuyo), procésalo solo una vez.
- Responde 2xx rápido (< 10s) y procesa en background. Si tu lógica de negocio tarda, devuelve 200 OK y encola un job interno.
- Usa GET /v1/webhooks/{id}/deliveries para auditar las últimas 50 entregas: status code, response body, retry count. Útil para debug.
- En staging usa POST /v1/webhooks/{id}/test para enviarte un ping de prueba sin esperar un evento real.