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

  1. 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).
  2. Cuando un evento sucede en tu clínica, encolamos un mensaje y hacemos HTTP POST a tu URL con el payload firmado.
  3. Verifica la firma comparando el header X-DentalOS-Signature con el HMAC del body usando tu secreto.
  4. 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

EventoDescripción
patient.createdUn nuevo paciente fue registrado.
patient.updatedDatos demográficos o clínicos del paciente fueron modificados.
appointment.createdSe agendó una cita.
appointment.cancelledUna cita fue cancelada (por paciente o staff).
appointment.completedUna cita marcada como completada (atendida).
procedure.completedSe registró un procedimiento clínico (con código CUPS).
payment.receivedSe recibió un pago contra una factura.
consent.signedUn 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-Delivery como idempotency key — recibirás el mismo UUID en cada reintento del mismo intento.
Quiero probar Heka