Webhooks

Eventos em tempo real com HMAC + retries

Os webhooks permitem receber notificações HTTP automáticas quando ocorrem eventos no CBCTHub. Em vez de fazer polling a cada minuto, o CBCTHub envia um POST à sua URL no momento em que algo acontece: um exame confirmado, um laudo assinado, uma exclusão. Ideal para sincronizar com seu PMS, disparar workflows ou notificar outros sistemas.

Eventos disponíveis

Ao criar uma subscription você escolhe a quais eventos se inscrever. Estes são os tipos disponíveis hoje:

  • exam.createdum exame foi criado via API ou dashboard (ainda em status uploading).
  • exam.confirmedo exame finalizou o upload e está em status ready. Pronto para compartilhar.
  • exam.deletedo exame foi excluído do dashboard ou via API.
  • exam.updatedalgum campo do exame foi atualizado (PATCH /v1/exams/{id}). O payload traz fields e changes com o diff.
  • exam.sharedo exame foi enviado por email a um destinatário (POST /v1/exams/{id}/share). Payload: to, cc, email_id.
  • exam.extra_addeduma tomada extra foi confirmada (multi-tomada para CBCT/ATM). Payload: index, label, file_count, storage_bytes.
  • report.publishedum PDF de laudo foi enviado ao exame (via dashboard ou POST /v1/exams/{id}/report).
  • report.signedo laudo foi assinado eletronicamente e já tem URL pública de verificação.

Criar uma subscription

Chame POST /api/v1/webhooks com sua URL pública (deve ser HTTPS) e os eventos que deseja. A resposta inclui um secret — guarde-o, você vai precisar para verificar a assinatura. O secret é mostrado UMA ÚNICA VEZ; se você perder, precisa criar outra 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"
}
O secret só aparece na resposta de criação. Depois só se vê o prefix (ex.: whsec_abc…). Se perder, crie outra subscription e exclua a antiga.

Payload do webhook

Todos os webhooks têm a mesma estrutura raiz: id, type, created_at e data com info específica do 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/..."
  }
}

Cabeçalhos HTTP

Cada delivery inclui esses headers para você validar a origem e a integridade:

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-Evento type do evento (ex.: exam.confirmed).
  • X-CBCTHub-DeliveryID único desta tentativa. Use para idempotência.
  • X-CBCTHub-Signatureassinatura HMAC-SHA-256 do body cru, em base64, com prefixo sha256=.

Verificar a assinatura HMAC

Calcule HMAC-SHA-256 do body cru (raw bytes, não do JSON parseado) usando seu secret e compare em tempo constante com a assinatura do header. Se não baterem, descarte o request — alguém tentou falsificar.

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 retentativas

Se o endpoint não retornar 2xx em 10 segundos, reintentamos com backoff exponencial: 30s, 2min, 10min, 1h, 6h, 24h. Máximo 6 tentativas por evento. Após 20 falhas consecutivas em qualquer evento, a subscription é desabilitada automaticamente (enabled=false) para não sobrecarregar seu endpoint.

Cronograma de retries

  • Tentativa 1: imediata
  • Tentativa 2: +30 segundos
  • Tentativa 3: +2 minutos
  • Tentativa 4: +10 minutos
  • Tentativa 5: +1 hora
  • Tentativa 6: +6 horas
  • Última: +24 horas (depois abandonado)

Endpoints

POST/api/v1/webhooks

Criar subscription. Retorna o secret uma única vez.

GET/api/v1/webhooks

Listar todas as subscriptions da conta.

GET/api/v1/webhooks/{id}

Detalhes de uma subscription.

PATCH/api/v1/webhooks/{id}

Atualizar events, description ou enabled.

DELETE/api/v1/webhooks/{id}

Excluir subscription. Não reversível.

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

Enviar um ping de teste ao endpoint configurado.

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

Audit log das últimas 50 entregas.

Boas práticas

  • SEMPRE verifique a assinatura HMAC. Sem verificação qualquer um pode enviar um POST forjado ao seu endpoint público.
  • Implemente idempotência com X-CBCTHub-Delivery. Se receber o mesmo delivery_id duas vezes (por um retry após timeout seu), processe apenas uma vez.
  • Responda 2xx rápido (< 10s) e processe em background. Se sua lógica de negócio demora, retorne 200 OK e enfileire um job interno.
  • Use GET /v1/webhooks/{id}/deliveries para auditar as últimas 50 entregas: status code, response body, retry count. Útil para debug.
  • Em staging use POST /v1/webhooks/{id}/test para enviar um ping de teste sem esperar um evento real.