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.created— um exame foi criado via API ou dashboard (ainda em status uploading).exam.confirmed— o exame finalizou o upload e está em status ready. Pronto para compartilhar.exam.deleted— o exame foi excluído do dashboard ou via API.exam.updated— algum campo do exame foi atualizado (PATCH /v1/exams/{id}). O payload traz fields e changes com o diff.exam.shared— o exame foi enviado por email a um destinatário (POST /v1/exams/{id}/share). Payload: to, cc, email_id.exam.extra_added— uma tomada extra foi confirmada (multi-tomada para CBCT/ATM). Payload: index, label, file_count, storage_bytes.report.published— um PDF de laudo foi enviado ao exame (via dashboard ou POST /v1/exams/{id}/report).report.signed— o 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.
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 do webhook
Todos os webhooks têm a mesma estrutura raiz: id, type, created_at e data com info específica do 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/..."
}
}Cabeçalhos HTTP
Cada delivery inclui esses headers para você validar a origem e a integridade:
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— o type do evento (ex.: exam.confirmed).X-CBCTHub-Delivery— ID único desta tentativa. Use para idempotência.X-CBCTHub-Signature— assinatura 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.
// 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 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
/api/v1/webhooksCriar subscription. Retorna o secret uma única vez.
/api/v1/webhooksListar todas as subscriptions da conta.
/api/v1/webhooks/{id}Detalhes de uma subscription.
/api/v1/webhooks/{id}Atualizar events, description ou enabled.
/api/v1/webhooks/{id}Excluir subscription. Não reversível.
/api/v1/webhooks/{id}/testEnviar um ping de teste ao endpoint configurado.
/api/v1/webhooks/{id}/deliveriesAudit 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.