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.createdse creó un examen vía API o dashboard (todavía en status uploading).
  • exam.confirmedel examen completó la subida y quedó en status ready. Listo para compartir.
  • exam.deletedel examen fue eliminado del dashboard o vía API.
  • exam.updatedse actualizó algún campo del examen (PATCH /v1/exams/{id}). El payload trae fields y changes con el diff.
  • exam.sharedel examen se envió por email a un destinatario (POST /v1/exams/{id}/share). Payload: to, cc, email_id.
  • exam.extra_addedse confirmó una toma extra (multi-toma para CBCT/ATM). Payload: index, label, file_count, storage_bytes.
  • report.publishedse subió un PDF de informe al examen (vía dashboard o POST /v1/exams/{id}/report).
  • report.signedel 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.

bash
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"
}
El secret aparece solo en la respuesta de creación. Después solo se muestra el prefix (ej: whsec_abc…). Si lo perdés, crea otra subscription y eliminá la vieja.

Payload del webhook

Todos los webhooks tienen la misma estructura raíz: id, type, created_at y data con info específica del evento.

json
{
  "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:

http
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-Eventel type del evento (ej: exam.confirmed).
  • X-CBCTHub-DeliveryID único de este intento. Úsalo para idempotencia.
  • X-CBCTHub-Signaturefirma 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.

javascript
// 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
# 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', 200

Polí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

POST/api/v1/webhooks

Crear subscription. Devuelve el secret una sola vez.

GET/api/v1/webhooks

Listar todas las subscriptions de la cuenta.

GET/api/v1/webhooks/{id}

Detalle de una subscription.

PATCH/api/v1/webhooks/{id}

Actualizar events, description o enabled.

DELETE/api/v1/webhooks/{id}

Eliminar subscription. No reversible.

POST/api/v1/webhooks/{id}/test

Enviar un ping de prueba al endpoint configurado.

GET/api/v1/webhooks/{id}/deliveries

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