Implementar corretamente a verificação HMAC para autenticar que cada notificação vem da Whalemate.
Lidar com repetições, duplicados e timeouts sem erros em produção.
Ter snippets prontos para copiar nas linguagens mais comuns.
Servidor acessível publicamente por HTTPS com certificado TLS válido (não autoassinado).
Capacidade de responder em menos de 10 segundos — se o processamento demorar mais, enfileire o trabalho e responda 200 OK imediatamente.
URL com esquema https:// — a Whalemate rejeita http:// e domínios privados (localhost, 192.168.x.x, etc.).
Configure o webhook em
Configurações → Integrações → Webhooks → Configurar(ver documentação funcional). O segredo HMAC é exibido apenas uma vez ao salvar — copie-o antes de continuar com a verificação de assinatura.
Todos os eventos compartilham a mesma estrutura base. O body é enviado como JSON em UTF-8.
{
"event": "campaign.clicked",
"campaign_id": 142,
"campaign_type": "phishing",
"employee_email": "[email protected]",
"employee_id": 1204,
"occurred_at": "2026-04-24T08:14:55Z"
}
Campo
Tipo
Anulável
Descrição
event
string
Não
Nome do evento
campaign_id
integer
Não
ID interno da campanha
campaign_type
string
Não
Tipo de campanha. Valor atual: "phishing"
employee_email
string
Sim
Email do funcionário. Ver nota abaixo.
employee_id
integer
Sim
ID interno do funcionário. Ver nota abaixo.
occurred_at
string
Não
Timestamp ISO 8601 UTC
employee_emaileemployee_idpodem sernullapenas no eventocampaign.openedquando o pixel de rastreamento é ativado por um scanner de segurança ou proxy. Em todos os demais eventos, estão sempre presentes.Os eventos são entregues na ordem em que ocorrem, mas a Whalemate não garante que cheguem ao seu servidor na mesma ordem. Use o campo
occurred_atpara ordená-los cronologicamente no seu sistema.
Header
Exemplo
Descrição
Content-Type
application/json
Sempre application/json; charset=utf-8
User-Agent
WhalemateWebhooks/1.0
Identificador do cliente HTTP
X-Whalemate-Delivery-Id
"4521"
ID único desta entrega
X-Whalemate-Timestamp
"1714986000"
Unix timestamp UTC do momento do envio
X-Whalemate-Signature
sha256=abc123...
Assinatura HMAC-SHA256
Você pode usar
User-Agent: WhalemateWebhooks/1.0como filtro rápido, mas não o use como mecanismo de segurança. Sempre verifique a assinatura HMAC.
Cada webhook inclui uma assinatura HMAC-SHA256 no header X-Whalemate-Signature. Você deve verificá-la antes de processar qualquer payload.
Algoritmo:
firma = "sha256=" + HMAC_SHA256(key=secret, message=timestamp + "." + raw_body)
secret: o segredo gerado ao ativar o webhook (disponível apenas uma vez).
timestamp: valor do header X-Whalemate-Timestamp.
raw_body: o corpo JSON exatamente como chega, sem parsear, como string ou bytes em UTF-8.
Passos:
Leia o body como string bruta antes de qualquer parsing JSON.
Construa a mensagem: timestamp + "." + raw_body.
Calcule o HMAC-SHA256 da mensagem usando o segredo como chave.
Compare o resultado (com prefixo sha256=) com X-Whalemate-Signature usando comparação de tempo constante.
Verifique que |now() - timestamp| < 300 segundos (5 minutos) para prevenir replay attacks.
Se você parsear o JSON e re-serializá-lo, a ordem das chaves ou o espaçamento pode mudar e a assinatura não vai coincidir. Sempre trabalhe com os bytes originais do body.
Cenário
Resposta recomendada
Evento recebido e válido
200 OK imediato, processe de forma assíncrona
Assinatura inválida
401 Unauthorized
Timestamp expirado
400 Bad Request
Erro interno
500 — a Whalemate tentará novamente
Timeout (> 10s)
A Whalemate marca como falha e tenta novamente
O corpo da resposta não importa. A Whalemate o registra, mas não o processa.
A Whalemate pode entregar o mesmo evento mais de uma vez em circunstâncias excepcionais de rede. Seu sistema deve ser idempotente.
Estratégia recomendada:
Use X-Whalemate-Delivery-Id como chave de idempotência.
Antes de processar, verifique se você já processou esse Delivery ID.
Se já existir, responda 200 OK sem processar novamente.
delivery_id = request.headers.get("X-Whalemate-Delivery-Id")
if already_processed(delivery_id):
return Response(status=200) # duplicado ignorado
mark_as_processed(delivery_id)
process_event(request.json)
function verifyWebhookSignature(string $secret, string $timestamp, string $body, string $signature): bool {
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
return hash_equals($expected, $signature);
}
$timestamp = $_SERVER['HTTP_X_WHALEMATE_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WHALEMATE_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(400); exit('Timestamp too old');
}
if (!verifyWebhookSignature(getenv('WHALEMATE_WEBHOOK_SECRET'), $timestamp, $body, $signature)) {
http_response_code(401); exit('Invalid signature');
}
http_response_code(200);
$event = json_decode($body, true);
// procesar $event...
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
def verify_signature(secret, timestamp, raw_body, signature):
message = f"{timestamp}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/webhook")
def webhook():
secret = os.environ["WHALEMATE_WEBHOOK_SECRET"]
timestamp = request.headers.get("X-Whalemate-Timestamp", "")
signature = request.headers.get(