Guide 9/12
Webhooks
The clinic pushes events to your HTTPS endpoint as they happen.
Events
campanha.enviada— Campanha enviadacirurgia.fechada— Cirurgia fechadacomunicado.publicado— Comunicado interno publicadoconsentimento.revogado— Consentimento retiradoconsulta.assinada— Consulta assinadademonstracao.pedida— Pedido de demonstração do AnagenSysdocumento.assinado— Documento assinadodocumento.emitido— Documento emitidodocumento_fiscal.emitido— Documento fiscal emitidoepisodio.aberto— Episódio de tratamento abertoepisodio.estado_alterado— Estado do episódio alteradoexecucao.areas_confirmadas— Áreas do dia 0 confirmadasexportacao.concluida— Exportação total prontaformulario.respondido— Formulário respondidoincidente.registado— Incidente de segurança registadolead.convertido— Lead convertido em pacientelead.criado— Lead criadolead.fase_alterada— Lead mudou de fasemarcacao.cancelada— Marcação canceladamarcacao.confirmada— Marcação confirmadamarcacao.criada— Marcação criadamarco.concluido— Marco do acompanhamento concluídomotor.versao_aprovada— Parâmetros do motor aprovadospaciente.criado— Paciente criadopaciente.fundido— Pacientes fundidospagamento.recebido— Pagamento recebidopedido_titular.recebido— Pedido de titular recebidoplano.aprovado— Plano cirúrgico aprovadoportal.consentimento_revogado— Autorização retirada no portalportal.documento_enviado— Documento enviado pelo pacienteportal.fotos_enviadas— Fotografias de acompanhamento enviadasportal.pedido_criado— Pedido criado no portalpre_avaliacao.recebida— Pré-avaliação online recebidaproposta.aceite— Proposta aceiteproposta.enviada— Proposta enviadaproposta.recusada— Proposta recusadareclamacao.recebida— Reclamação, elogio ou sugestão recebidatarefa.concluida— Tarefa concluídatarefa.criada— Tarefa criadawebhook.teste— Entrega de teste
The payload schema of each event is in the reference (section Webhooks). Payloads never contain clinical content.
Delivery
POST /anagensys/eventos HTTP/1.1
Host: crm.exemplo.pt
Content-Type: application/json; charset=UTF-8
User-Agent: AnagenSys-Webhooks/1.0
X-Anagen-Evento: lead.criado
X-Anagen-Evento-Id: evt_6f1c2a9b0d3e4f5a6b7c
X-Anagen-Entrega: 1842
X-Anagen-Tentativa: 1
X-Anagen-Assinatura: t=1761646500,v1=5f2b0e…9c1a
{"evento":"lead.criado","id":"evt_6f1c2a9b0d3e4f5a6b7c","criado_em":"2026-10-28T10:15:00+00:00","clinica_id":1,"dados":{"id":412,"nome":"Rita Matos","origem":"site"}}- Reply with any
2xxwithin 10 seconds; do the heavy work afterwards (queue). - Failures (non-2xx, timeout, network) are retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h, then given up. The clinic sees every delivery and can resend it.
- After many consecutive failures the endpoint is disabled and the clinic is alerted.
- Deliveries may arrive more than once or out of order: de-duplicate by the event
idand usecriado_em. - Only
https://endpoints on public networks; redirects are not followed.
Verify the signature
X-Anagen-Assinatura: t=<epoch>,v1=<hex> where hex = HMAC_SHA256(secret, t + "." + raw_body). Reject if the signature does not match (constant-time comparison) or if t is more than 5 minutes away.
<?php
// Verificação da assinatura (use o corpo CRU, antes de qualquer descodificação).
$segredo = getenv('ANAGEN_WEBHOOK_SECRET');
$corpo = file_get_contents('php://input');
$cab = $_SERVER['HTTP_X_ANAGEN_ASSINATURA'] ?? '';
parse_str(str_replace(',', '&', $cab), $partes);
$t = (int) ($partes['t'] ?? 0);
$v1 = (string) ($partes['v1'] ?? '');
$esperada = hash_hmac('sha256', $t . '.' . $corpo, $segredo);
if (abs(time() - $t) > 300 || ! hash_equals($esperada, $v1)) {
http_response_code(401);
exit;
}
$evento = json_decode($corpo, true);
// Guarde $evento['id'] para ignorar repetições.
http_response_code(204);import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/anagensys/eventos', express.raw({ type: 'application/json' }), (req, res) => {
const cab = req.get('X-Anagen-Assinatura') || '';
const partes = Object.fromEntries(cab.split(',').map((p) => p.split('=')));
const t = Number(partes.t || 0);
const esperada = crypto.createHmac('sha256', process.env.ANAGEN_WEBHOOK_SECRET)
.update(`${t}.${req.body.toString('utf8')}`).digest('hex');
const valida = partes.v1 && esperada.length === partes.v1.length &&
crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(partes.v1));
if (!valida || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString('utf8'));
// Guarde evento.id para ignorar repetições.
res.sendStatus(204);
});import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SEGREDO = os.environ["ANAGEN_WEBHOOK_SECRET"].encode()
@app.post("/anagensys/eventos")
def eventos():
corpo = request.get_data() # corpo cru
partes = dict(p.split("=", 1) for p in request.headers.get("X-Anagen-Assinatura", "").split(",") if "=" in p)
t = int(partes.get("t", 0))
esperada = hmac.new(SEGREDO, f"{t}.".encode() + corpo, hashlib.sha256).hexdigest()
if abs(time.time() - t) > 300 or not hmac.compare_digest(esperada, partes.get("v1", "")):
abort(401)
evento = json.loads(corpo)
# Guarde evento["id"] para ignorar repetições.
return "", 204using System.Security.Cryptography;
using System.Text;
app.MapPost("/anagensys/eventos", async (HttpRequest pedido) =>
{
using var leitor = new StreamReader(pedido.Body, Encoding.UTF8);
var corpo = await leitor.ReadToEndAsync();
var partes = pedido.Headers["X-Anagen-Assinatura"].ToString()
.Split(',').Select(p => p.Split('=', 2)).Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1]);
var t = long.Parse(partes.GetValueOrDefault("t", "0"));
var segredo = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("ANAGEN_WEBHOOK_SECRET")!);
var esperada = Convert.ToHexString(HMACSHA256.HashData(segredo, Encoding.UTF8.GetBytes($"{t}.{corpo}"))).ToLowerInvariant();
var recebida = partes.GetValueOrDefault("v1", "");
var valida = CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(esperada), Encoding.UTF8.GetBytes(recebida));
if (!valida || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return Results.Unauthorized();
// Guarde o «id» do evento para ignorar repetições.
return Results.NoContent();
});The secret (whsec_…) is shown once when the clinic creates the endpoint or rotates it. Store it in a secrets vault.