Integrar
Webhooks
Avisos do clube para o seu sistema: eventos, assinatura e retentativas.
Cadastre um endpoint em Integrações › Webhooks (um para teste, outro para produção). O Studio mostra o segredo whsec_… uma vez e já manda um webhook.test para conferir a URL. Marque os eventos que você usa. Sem nenhum marcado, chegam os de prêmio e entrega (reward.*), referral.qualified e message.requested; os avisos de participante (participant.created, level.up, checkin.completed…) só chegam se estiverem marcados pelo nome.
| Evento | Quando |
|---|---|
reward.delivery_requested | O seu sistema precisa creditar um prêmio (bônus, giros, saldo, cashback). Veja Entregas. |
reward.granted | Qualquer prêmio concedido (informativo). |
reward.delivered / reward.delivery_failed | Uma entrega foi confirmada / marcada como falha. |
participant.created | Participante novo no clube. |
level.up / tier.changed | Subiu de nível / mudou de elo. |
checkin.completed | Fez o check-in do dia. |
mission.completed | Completou uma missão. |
wheel.spun | Girou a roleta. |
code.redeemed | Ativou um código promocional. |
store.purchased | Trocou moedas por um prêmio na loja. |
cashback.credited | Cashback do dia pago. |
referral.qualified | Indicação qualificada. |
message.requested | CRM no modo webhook: o clube pede e você envia o e-mail/SMS. |
Formato
POST /seu/endpoint
Content-Type: application/json
X-Clube-Event: level.up
X-Clube-Delivery: whd_…
X-Clube-Signature: t=1727712000,v1=5f0c…
{
"id": "evt_…",
"type": "level.up",
"created_at": "2026-09-30T18:00:00.000Z",
"club_id": "club_…",
"env": "live",
"data": { "participant": { "id": "part_…", "external_id": "user_123" }, "level": 4, "from": 3 }
}data sempre traz participant.external_id (o seu ID) nos eventos de participante. Use o id do envelope para não processar o mesmo aviso duas vezes.
Verificar a assinatura
v1 = HMAC-SHA256 com o segredo do endpoint sobre t + "." + corpo cru. Recuse se não bater ou se t tiver mais de 5 minutos. Use o corpo cru, antes de converter o JSON.
import crypto from "node:crypto";
export function webhookValido(corpoCru, header, segredo) {
const p = Object.fromEntries(header.split(",").map((x) => x.split("=")));
const esperado = crypto.createHmac("sha256", segredo).update(`${p.t}.${corpoCru}`).digest("hex");
const recente = Math.abs(Date.now() / 1000 - Number(p.t)) < 300;
return recente && p.v1?.length === esperado.length &&
crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(p.v1));
}$corpo = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_CLUBE_SIGNATURE'] ?? ''), $p);
$esperado = hash_hmac('sha256', ($p['t'] ?? '') . '.' . $corpo, getenv('ICLUBING_WEBHOOK_SECRET'));
if (abs(time() - (int)($p['t'] ?? 0)) > 300 || !hash_equals($esperado, $p['v1'] ?? '')) {
http_response_code(401); exit;
}
$evento = json_decode($corpo, true);import hmac, hashlib, time
def webhook_valido(corpo_cru: bytes, header: str, segredo: str) -> bool:
p = dict(x.split("=", 1) for x in header.split(","))
esperado = hmac.new(segredo.encode(), p["t"].encode() + b"." + corpo_cru, hashlib.sha256).hexdigest()
return abs(time.time() - int(p["t"])) < 300 and hmac.compare_digest(esperado, p.get("v1", ""))Respostas e retentativas
- Responda 2xx em até 10 s. Processamento demorado: responda 200 e trabalhe numa fila.
- Sem 2xx, tentamos de novo em 1 min, 5 min, 30 min, 2 h, 12 h e mais três vezes a cada 24 h. Depois disso o envio fica como falhou no Studio, com Reenviar.
- Eventos que você não usa: responda 200 e ignore.
- Integrações › Webhooks mostra cada envio com status, tentativas e o erro da última.