Desenvolvedores

Webhooks: avisos em tempo real

Eventos de consulta, sinal, contato e paciente enviados para a sua URL, com assinatura HMAC-SHA256 e novas tentativas. Código para conferir a assinatura.

Como ligar

No painel: Configurações > Conta > Avançado > Integrações > Webhooks, ou pela API (POST /api/v1/webhooks, permissão webhooks:manage). Até 5 por conta, cada um com a URL (https público, porta 443), os eventos e um segredo que aparece uma vez. Enviar teste manda o evento webhook.teste na hora.

Eventos

EventoQuando sai
consulta.criadaToda consulta nova: pela página, pelo painel, pela API, pela lista de espera ou em série (uma por consulta).
consulta.confirmadaQuando a consulta fica confirmada: já ao ser criada confirmada, ao aprovar um pedido ou quando o sinal é pago.
consulta.canceladaCancelada pelo paciente (link ou WhatsApp), pela equipe, em série ou porque o sinal venceu.
consulta.remarcadaMudou de horário (painel, link do paciente ou WhatsApp). Leva o início anterior.
sinal.pagoO sinal da consulta foi pago (Pix ou cartão) ou marcado como pago pela equipe.
contato.novoChegou um pedido de contato pela página (captura do WhatsApp ou Formulário de contato).
paciente.criadoPaciente cadastrado no painel, pela API ou ao agendar pela página. A importação por planilha não dispara.

O aviso

POST com content-type: application/json; charset=utf-8:

Corpo
{
  "id": "e1d2c3b4-a596-4877-8a9b-0c1d2e3f4a5b",
  "evento": "consulta.criada",
  "criado_em": "2026-10-21T15:00:00.000Z",
  "teste": false,
  "conta": { "id": "a0b1c2d3-e4f5-4a6b-8c7d-9e0f1a2b3c4d" },
  "dados": {
    "consulta": {
      "id": "9b2f6c1e-1d2a-4c55-9a51-0d7e3b1c2a10",
      "inicio": "2026-10-28T13:00:00.000Z",
      "fim": "2026-10-28T13:40:00.000Z",
      "status": "confirmado",
      "online": false,
      "tipo_id": "3c5d1f20-8a7b-4c1e-9f00-1a2b3c4d5e6f",
      "local_id": null,
      "origem": null
    },
    "paciente": { "id": "5e8a2b44-7c1d-4f3e-8b9a-2c3d4e5f6a7b", "nome": "Maria Exemplo" }
  }
}
  • consulta.remarcada leva inicio_anterior; sinal.pago leva sinal.valor_centavos e sinal.forma.
  • contato.novo: dados.contato com id, nome, whatsapp e origem (whatsapp ou formulario). A mensagem livre do formulário não vai.
  • paciente.criado: dados.paciente com id, nome, whatsapp, email e origem.
  • Sem dado clínico: nada de prontuário, anotações, motivo do cancelamento, nome do tipo de consulta ou orientações. Para mais, a API com o id.
Cabeçalhos
X-CliLink-Evento: consulta.criada
X-CliLink-Entrega: <id da entrega>
X-CliLink-Assinatura: t=<unix em segundos>,v1=<hex>
User-Agent: CliLink-Webhooks/1

Conferir a assinatura (HMAC-SHA256)

v1 = HMAC-SHA256(segredo, "t.corpo"), em hexadecimal. Confira com o corpo exatamente como chegou (antes de transformar em objeto), em tempo constante, e recuse t com mais de 5 minutos de diferença (proteção contra reenvio).

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

/** true se o aviso veio do CliLink. corpo = texto bruto, ANTES do JSON.parse. */
export function avisoDoCliLink(corpo, cabecalho, segredo) {
  const partes = Object.fromEntries((cabecalho ?? "").split(",").map((p) => p.trim().split("=")));
  const t = Number(partes.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // mais de 5 minutos: recusa
  const esperado = Buffer.from(createHmac("sha256", segredo).update(`${t}.${corpo}`).digest("hex"));
  const recebido = Buffer.from(partes.v1 ?? "");
  return esperado.length === recebido.length && timingSafeEqual(esperado, recebido);
}

// Exemplo com Express: o corpo precisa chegar bruto
app.post("/clilink", express.text({ type: "application/json" }), (req, res) => {
  if (!avisoDoCliLink(req.body, req.get("X-CliLink-Assinatura"), process.env.CLILINK_WHSEC)) return res.sendStatus(401);
  const aviso = JSON.parse(req.body);
  // use aviso.id (ou o cabeçalho X-CliLink-Entrega) para não processar duas vezes
  res.sendStatus(200);
});
Python
import hashlib, hmac, time

def aviso_do_clilink(corpo: bytes, cabecalho: str, segredo: str) -> bool:
    partes = dict(p.strip().split("=", 1) for p in (cabecalho or "").split(",") if "=" in p)
    try:
        t = int(partes.get("t", "0"))
    except ValueError:
        return False
    if not t or abs(time.time() - t) > 300:
        return False
    esperado = hmac.new(segredo.encode(), f"{t}.".encode() + corpo, hashlib.sha256).hexdigest()
    return hmac.compare_digest(esperado, partes.get("v1", ""))

Entrega e novas tentativas

  • Responda 2xx em até 10 segundos. Redirecionamento não é seguido (conta como falha) e o corpo da sua resposta não é lido.
  • Falhou: novas tentativas em 1 min, 5 min, 30 min, 2 h e 6 h (6 no total). Depois, a entrega fica como falhou e dá para reenviar no painel.
  • 20 entregas perdidas seguidas desligam o webhook; ligar de novo zera a conta.
  • O mesmo aviso pode chegar duas vezes (sua resposta demorou e houve nova tentativa): use id ou X-CliLink-Entrega para não processar duas vezes.
  • O endereço é conferido contra rede interna na hora de cada entrega (localhost, 10/8, 192.168/16 e outros não recebem).
Sem servidor próprio? Use o Zapier ou o Make: receitas prontas.

Atualizada em 21/10/2026. Dúvida ou algo faltando? contato@clilink.bio.