Developer Hub
Webhooks
Recibe eventos de la clínica en tu sistema externo en cuanto suceden. Cada llamada se firma con HMAC-SHA256 para que verifiques la autenticidad.
Cómo funciona
- Genera un webhook desde el dashboard: Configuración → Webhooks. Marca los eventos que te interesan y guarda el secreto que aparece (solo se muestra una vez).
- Cuando un evento sucede en tu clínica, encolamos un mensaje y hacemos
HTTP POSTa tu URL con el payload firmado. - Verifica la firma comparando el header
X-DentalOS-Signaturecon el HMAC del body usando tu secreto. - Responde con HTTP 2xx en menos de 10 segundos. Si fallas con 5xx, 408, 425 o 429 reintentamos con backoff exponencial (3 intentos). Tras 10 fallas seguidas el webhook se autodeshabilita.
Headers que enviamos
X-DentalOS-Event— el tipo de evento (ej.patient.created).X-DentalOS-Signature—hex(hmac_sha256(secret, body)).X-DentalOS-Timestamp— segundos desde epoch al momento de envío.X-DentalOS-Delivery— UUID único por intento (idempotency key).
Formato del payload
{
"event": "patient.created",
"tenant_id": "f6407f43-b9f9-437b-a565-95135e620453",
"delivered_at": 1730851200,
"data": {
"patient_id": "8d32ee8e-f529-4a78-beae-958ca819c78c",
"first_name": "Webhook",
"last_name": "Trigger Three",
"document_type": "CC",
"created_at": "2026-05-05T23:52:08Z"
}
}El body se serializa con separadores fijos y claves ordenadas alfabéticamente, así reproduces la firma sin ambigüedad.
Verificación HMAC
Tu servidor recibe el body como bytes y compara la firma con tu secreto. Importante: usa una comparación de tiempo constante (hmac.compare_digest o equivalente) para evitar timing attacks.
Python (FastAPI)
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = os.environ["DENTALOS_WEBHOOK_SECRET"]
@app.post("/dentalos-webhook")
async def receive(request: Request):
body = await request.body()
signature = request.headers.get("X-DentalOS-Signature", "")
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise HTTPException(401, "Invalid signature")
event = request.headers["X-DentalOS-Event"]
payload = await request.json()
print(f"Received {event}: {payload}")
return {"ok": True}Node.js (Express)
import express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.DENTALOS_WEBHOOK_SECRET;
app.post(
"/dentalos-webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("X-DentalOS-Signature") || "";
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest("hex");
const valid =
signature.length === expected.length &&
crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
if (!valid) return res.status(401).send("Invalid signature");
const event = req.header("X-DentalOS-Event");
console.log(`Received ${event}: ${req.body.toString()}`);
res.json({ ok: true });
},
);Catálogo de eventos
| Evento | Descripción |
|---|---|
| patient.created | Un nuevo paciente fue registrado. |
| patient.updated | Datos demográficos o clínicos del paciente fueron modificados. |
| appointment.created | Se agendó una cita. |
| appointment.cancelled | Una cita fue cancelada (por paciente o staff). |
| appointment.completed | Una cita marcada como completada (atendida). |
| procedure.completed | Se registró un procedimiento clínico (con código CUPS). |
| payment.received | Se recibió un pago contra una factura. |
| consent.signed | Un consentimiento informado fue firmado por el paciente. |
Sobre confiabilidad
- Reintentamos 3 veces con backoff exponencial (2s, 4s, 8s) cuando recibes 5xx, 408, 425 o 429 — o cuando hay timeout/red.
- Otros 4xx (400, 401, 403, 404…) los tratamos como bug del consumidor y no reintentamos.
- Tras 10 fallas consecutivas el webhook se desactiva automáticamente. El operador lo reactiva desde el dashboard cuando arregle el endpoint.
- Usa el header
X-DentalOS-Deliverycomo idempotency key — recibirás el mismo UUID en cada reintento del mismo intento.