Saltar para o conteúdo
Anagen
PT EN
Pedir avaliação

Guia 9/12

Webhooks

A clínica envia os eventos para o seu endereço HTTPS à medida que acontecem.

Eventos

  • 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

O esquema do conteúdo de cada evento está na referência (secção Webhooks). O conteúdo nunca leva dados clínicos.

Entrega

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"}}
  • Responda com qualquer 2xx em menos de 10 segundos; faça o trabalho pesado depois (fila).
  • Falhas (não-2xx, tempo esgotado, rede) repetem-se ao fim de 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h e depois desiste-se. A clínica vê cada entrega e pode reenviá-la.
  • Depois de muitas falhas seguidas o destino é desativado e a clínica é avisada.
  • As entregas podem chegar mais de uma vez ou fora de ordem: ignore repetições pelo id do evento e use criado_em.
  • Só destinos https:// em redes públicas; não se seguem redireções.

Verificar a assinatura

X-Anagen-Assinatura: t=<epoch>,v1=<hex> em que hex = HMAC_SHA256(segredo, t + "." + corpo_cru). Recuse se a assinatura não coincidir (comparação em tempo constante) ou se t estiver a mais de 5 minutos.

<?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();
});

O segredo (whsec_…) é mostrado uma única vez, quando a clínica cria o destino ou o roda. Guarde-o num cofre de segredos.