Pular para o conteúdo principal
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.

Como cadastrar

No painel Cativa, abra Console > Webhooks e clique em Adicionar listener. O fluxo pede 3 informações:
  1. URL — o endpoint público do seu app que vai receber o POST (ex: https://meuapp.com/webhooks/cativa).
  2. Eventos — quais nomes de evento você quer escutar (snake_case — ex: user_received_badge, paywall_payment_completed).
  3. 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.

Eventos disponíveis

EventoDescrição
user_createdNovo usuário cadastrado no tenant.
user_joined_groupUsuário entrou num grupo (manual, via badge ou via convite).
user_received_badgeBadge atribuído a um usuário.
post_createdPost publicado num grupo.
paywall_payment_completedPagamento do paywall confirmado.
comment_created Comentário criado num post.
course_completed Usuário completou um curso inteiro.
lesson_completed Usuário completou uma aula.
user_received_private_message Mensagem privada recebida.
Use exatamente esses nomes em snake_case ao cadastrar o listener — é como o Console faz o match.

Verificando a assinatura HMAC

Cada disparo chega com o header:
X-Cativa-Signature: t=1715177521,v1=8c1d4e2a3b5f4d8a9c6e7f0b1a2d3e4f8a9c6e7f0b1a2d3e4f8a9c6e7f0b1a2d
  • 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:
  1. Parse do header em t e v1.
  2. 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.
  3. 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.

Node.js (Express)

import express from 'express';
import crypto from 'crypto';

const app = express();
const SECRET = process.env.CATIVA_WEBHOOK_SECRET; // whsec_...

app.post(
  '/webhooks/cativa',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const header = req.header('X-Cativa-Signature') ?? '';
    const parts = Object.fromEntries(
      header.split(',').map(p => p.split('='))
    );
    const ts = Number(parts.t);
    const sig = parts.v1;

    if (!ts || !sig) return res.status(400).send('bad signature header');
    if (Math.abs(Date.now() / 1000 - ts) > 300) {
      return res.status(400).send('stale timestamp');
    }

    const expected = crypto
      .createHmac('sha256', SECRET)
      .update(`${ts}.${req.body.toString('utf8')}`)
      .digest('hex');

    const ok = crypto.timingSafeEqual(
      Buffer.from(expected, 'hex'),
      Buffer.from(sig, 'hex')
    );
    if (!ok) return res.status(401).send('invalid signature');

    const event = JSON.parse(req.body.toString('utf8'));
    // ... processa o evento
    res.status(200).send('ok');
  }
);

Python (Flask)

import hmac
import hashlib
import os
import time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['CATIVA_WEBHOOK_SECRET'].encode()  # whsec_...

@app.post('/webhooks/cativa')
def receive():
    header = request.headers.get('X-Cativa-Signature', '')
    parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p)
    try:
        ts = int(parts['t'])
        sig = parts['v1']
    except (KeyError, ValueError):
        abort(400, 'bad signature header')

    if abs(time.time() - ts) > 300:
        abort(400, 'stale timestamp')

    raw_body = request.get_data()  # bytes brutos, não usar request.json
    expected = hmac.new(
        SECRET,
        f"{ts}.".encode() + raw_body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, sig):
        abort(401, 'invalid signature')

    payload = request.get_json()
    # ... processa o evento
    return ('ok', 200)

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

var secret = []byte(os.Getenv("CATIVA_WEBHOOK_SECRET")) // whsec_...

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "read body", http.StatusBadRequest)
        return
    }

    var ts int64
    var sig string
    for _, part := range strings.Split(r.Header.Get("X-Cativa-Signature"), ",") {
        kv := strings.SplitN(part, "=", 2)
        if len(kv) != 2 {
            continue
        }
        switch kv[0] {
        case "t":
            ts, _ = strconv.ParseInt(kv[1], 10, 64)
        case "v1":
            sig = kv[1]
        }
    }
    if ts == 0 || sig == "" {
        http.Error(w, "bad signature header", http.StatusBadRequest)
        return
    }

    if abs(time.Now().Unix()-ts) > 300 {
        http.Error(w, "stale timestamp", http.StatusBadRequest)
        return
    }

    mac := hmac.New(sha256.New, secret)
    mac.Write([]byte(strconv.FormatInt(ts, 10) + "."))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(sig)) {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    // ... processa o evento usando body
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("ok"))
}

func abs(x int64) int64 {
    if x < 0 {
        return -x
    }
    return x
}

C# (ASP.NET Core)

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");
});

Retries e falhas permanentes

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).

Tabela de comportamento por status

CódigoComportamento
2xxSucesso — sem retry
408 Request TimeoutRetry
429 Too Many RequestsRetry (respeita Retry-After)
5xx (500, 502, 503, 504, …)Retry
Erros de rede / timeouts TCPRetry
400 Bad RequestFalha permanente — sem retry
401 UnauthorizedFalha permanente — sem retry
403 ForbiddenFalha permanente — sem retry
404 Not FoundFalha permanente — sem retry
410 GoneFalha permanente — sem retry
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”.

Quando todas as tentativas falham

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.

Idempotência

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.

Eventos relacionados

Webhooks (visão geral)

Por que webhooks, garantias de entrega e o formato dos payloads.

user_received_badge

Página de referência completa de um evento — payload, headers e receiver de exemplo.