Skip to content
Anagen
PT EN
Request assessment

Guide 9/12

Webhooks

The clinic pushes events to your HTTPS endpoint as they happen.

Events

  • campanha.enviada — Campanha enviada
  • cirurgia.fechada — Cirurgia fechada
  • comunicado.publicado — Comunicado interno publicado
  • consentimento.revogado — Consentimento retirado
  • consulta.assinada — Consulta assinada
  • demonstracao.pedida — Pedido de demonstração do AnagenSys
  • documento.assinado — Documento assinado
  • documento.emitido — Documento emitido
  • documento_fiscal.emitido — Documento fiscal emitido
  • episodio.aberto — Episódio de tratamento aberto
  • episodio.estado_alterado — Estado do episódio alterado
  • execucao.areas_confirmadas — Áreas do dia 0 confirmadas
  • exportacao.concluida — Exportação total pronta
  • formulario.respondido — Formulário respondido
  • incidente.registado — Incidente de segurança registado
  • lead.convertido — Lead convertido em paciente
  • lead.criado — Lead criado
  • lead.fase_alterada — Lead mudou de fase
  • marcacao.cancelada — Marcação cancelada
  • marcacao.confirmada — Marcação confirmada
  • marcacao.criada — Marcação criada
  • marco.concluido — Marco do acompanhamento concluído
  • motor.versao_aprovada — Parâmetros do motor aprovados
  • paciente.criado — Paciente criado
  • paciente.fundido — Pacientes fundidos
  • pagamento.recebido — Pagamento recebido
  • pedido_titular.recebido — Pedido de titular recebido
  • plano.aprovado — Plano cirúrgico aprovado
  • portal.consentimento_revogado — Autorização retirada no portal
  • portal.documento_enviado — Documento enviado pelo paciente
  • portal.fotos_enviadas — Fotografias de acompanhamento enviadas
  • portal.pedido_criado — Pedido criado no portal
  • pre_avaliacao.recebida — Pré-avaliação online recebida
  • proposta.aceite — Proposta aceite
  • proposta.enviada — Proposta enviada
  • proposta.recusada — Proposta recusada
  • reclamacao.recebida — Reclamação, elogio ou sugestão recebida
  • tarefa.concluida — Tarefa concluída
  • tarefa.criada — Tarefa criada
  • webhook.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 2xx within 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 id and use criado_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 "", 204
using 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.