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
| Evento | Quando sai |
|---|---|
consulta.criada | Toda consulta nova: pela página, pelo painel, pela API, pela lista de espera ou em série (uma por consulta). |
consulta.confirmada | Quando a consulta fica confirmada: já ao ser criada confirmada, ao aprovar um pedido ou quando o sinal é pago. |
consulta.cancelada | Cancelada pelo paciente (link ou WhatsApp), pela equipe, em série ou porque o sinal venceu. |
consulta.remarcada | Mudou de horário (painel, link do paciente ou WhatsApp). Leva o início anterior. |
sinal.pago | O sinal da consulta foi pago (Pix ou cartão) ou marcado como pago pela equipe. |
contato.novo | Chegou um pedido de contato pela página (captura do WhatsApp ou Formulário de contato). |
paciente.criado | Paciente 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:
{
"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.remarcadalevainicio_anterior;sinal.pagolevasinal.valor_centavosesinal.forma.contato.novo:dados.contatocom id, nome, whatsapp e origem (whatsapp ou formulario). A mensagem livre do formulário não vai.paciente.criado:dados.pacientecom 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.
X-CliLink-Evento: consulta.criada
X-CliLink-Entrega: <id da entrega>
X-CliLink-Assinatura: t=<unix em segundos>,v1=<hex>
User-Agent: CliLink-Webhooks/1Conferir 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).
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);
});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
idouX-CliLink-Entregapara 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).
Atualizada em 21/10/2026. Dúvida ou algo faltando? contato@clilink.bio.