Como cadastrar um listener no Console, verificar a assinatura HMAC e lidar com retries.
Esta é a página de referência única para integrar webhooks da Cativa: cadastro do listener, lista de eventos, verificação de assinatura, retries e idempotência.
No painel Cativa, abra Console > Webhooks e clique em Adicionar listener. O fluxo pede 3 informações:
URL — o endpoint público do seu app que vai receber o POST (ex: https://meuapp.com/webhooks/cativa).
Eventos — quais nomes de evento você quer escutar (snake_case — ex: user_received_badge, paywall_payment_completed).
Secret — gerado automaticamente. Após criar o listener, abra-o e clique em Reveal secret para copiar o valor (formato whsec_ + 64 caracteres hex). O secret só pode ser visualizado pelo admin do tenant — guarde com segurança no seu cofre de credenciais.
Cada listener tem seu próprio secret. Se você cadastrar dois listeners (ex: um para staging e outro para produção), cada um assina os disparos com a chave dele.
Depois disso, todo evento que bater nas regras do listener é entregue via POST com Content-Type: application/json, X-Cativa-Signature, X-Cativa-Execution-Id e X-Cativa-Automation-Id.
t — timestamp Unix (em segundos) do momento do disparo.
v1 — HMAC-SHA256 (hex) sobre a string "<t>.<rawBody>", usando o secret do listener como chave.
Para validar, faça três passos:
Parse do header em t e v1.
Anti-replay: rejeite o request se |now - t| > 300 segundos (5 minutos é o padrão de mercado). Isso evita ataque de replay com requests antigos capturados.
Recomputar o HMAC e comparar em tempo constante.
Sempre compute o HMAC sobre o body bruto (a string exata recebida no request), nunca sobre o JSON re-serializado. Re-serializar muda espaços e ordem de chaves, invalidando a assinatura.
using System.Globalization;using System.Security.Cryptography;using System.Text;app.MapPost("/webhooks/cativa", async (HttpContext ctx) =>{ var secret = Encoding.UTF8.GetBytes( Environment.GetEnvironmentVariable("CATIVA_WEBHOOK_SECRET")!); // whsec_... using var ms = new MemoryStream(); await ctx.Request.Body.CopyToAsync(ms); var rawBody = ms.ToArray(); var header = ctx.Request.Headers["X-Cativa-Signature"].ToString(); long ts = 0; string? sig = null; foreach (var part in header.Split(',')) { var kv = part.Split('=', 2); if (kv.Length != 2) continue; if (kv[0] == "t") long.TryParse(kv[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out ts); else if (kv[0] == "v1") sig = kv[1]; } if (ts == 0 || sig is null) return Results.BadRequest("bad signature header"); var nowUnix = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(nowUnix - ts) > 300) return Results.BadRequest("stale timestamp"); using var hmac = new HMACSHA256(secret); var data = Encoding.UTF8.GetBytes($"{ts}.").Concat(rawBody).ToArray(); var expected = Convert.ToHexString(hmac.ComputeHash(data)).ToLowerInvariant(); var expectedBytes = Encoding.UTF8.GetBytes(expected); var sigBytes = Encoding.UTF8.GetBytes(sig); if (expectedBytes.Length != sigBytes.Length || !CryptographicOperations.FixedTimeEquals(expectedBytes, sigBytes)) return Results.Unauthorized(); // ... processa o evento usando rawBody return Results.Ok("ok");});
Se seu endpoint não responder com 2xx, a Cativa re-tenta de acordo com a curva:
30s → 5min → 30min → 2h → 6h → 24h
São 6 retries após a tentativa inicial — 7 entregas no total, cobrindo cerca de 33 horas. Cada retry recebe o mesmo X-Cativa-Execution-Id (estável entre retries do mesmo evento) — use-o como chave de idempotência.A Cativa respeita o header Retry-After que você retornar (limitado ao máximo da próxima janela do backoff).
A lógica é: status na faixa 4xx (exceto 408 e 429) significa “o request está errado e re-tentar não vai ajudar” — tipicamente bug do cliente, URL desativada ou auth incorreta. Status 5xx, 408, 429 e erros de transporte significam “tente de novo mais tarde”.
Se a 7ª tentativa também falhar, o disparo é registrado internamente como falho. A v1 ainda não tem painel no Console para inspecionar entregas falhas — monitore o uptime do seu endpoint pelo seu próprio lado e, se desconfiar de eventos perdidos, abra ticket em dev@cativa.digital que o time investiga internamente.
A entrega é at-least-once. Mesmo evento pode chegar mais de uma vez (retries após timeout, perda de conexão na hora de responder, etc.) — você precisa detectar duplicatas no seu lado.A chave canônica é o header X-Cativa-Execution-Id: ele é gerado uma vez por evento e se mantém igual em todos os retries. Salve esse ID na mesma transação da lógica de negócio:
async function processEvent(executionId, payload) { const alreadyProcessed = await db.events.exists(executionId); if (alreadyProcessed) return; // já processado — retorna sucesso await db.transaction(async (tx) => { await applyBusinessLogic(tx, payload); await tx.events.insert({ id: executionId, processedAt: new Date() }); });}
Isso garante que ou tudo aconteceu, ou nada aconteceu — sem chance de processar duas vezes.