Webhooks avisam o seu servidor sobre eventos (decisão de KYC, mudança de status de saque, depósito confirmado…) sem que você precise consultar a API repetidamente.
Eventos
| Evento | Quando |
|---|---|
player.status_changed |
a situação da conta do jogador mudou (ex.: suspensão, autoexclusão) |
kyc.status_changed |
a situação do KYC mudou (ex.: em_analise → aprovado) |
deposit.confirmed |
um depósito foi confirmado e creditado |
deposit.failed |
um depósito foi marcado como não concretizado |
withdrawal.status_changed |
a situação pública do saque mudou (under_review, approved, paid, rejected, cancelled, failed) |
webhook.test |
evento de teste, enviado por POST /webhooks/test |
Os esquemas dos payloads estão na seção webhooks da referência OpenAPI. Todo evento tem o mesmo envelope: id, type, createdAt, apiVersion e data.
Receber
- Exponha um endpoint HTTPS público no seu servidor; a URL é configurada pela equipe (não são aceitos
http, IPs privados nemlocalhost). - Responda
2xxem poucos segundos (o limite é de 10 s) e processe o evento de forma assíncrona. Qualquer outra resposta, incluindo redirecionamentos (3xx), conta como falha. - As entregas são at-least-once: podem chegar repetidas. Use o cabeçalho
X-Webhook-Id(ou oiddo evento) como chave de deduplicação. - Falhas são retentadas com espera exponencial (30 s, 1 min, 2 min…, até 6 h), no máximo 8 tentativas. Depois disso o evento fica como falho e a equipe pode reenviá-lo.
Cabeçalhos de cada entrega
| Cabeçalho | Conteúdo |
|---|---|
X-Webhook-Id |
id do evento (igual em todas as tentativas) |
X-Webhook-Timestamp |
epoch em segundos desta tentativa |
X-Webhook-Signature |
hex do HMAC-SHA256 de TIMESTAMP + "." + CORPO_BRUTO com o segredo whsec_… |
Verificar a assinatura
Recalcule o HMAC sobre o corpo bruto (os bytes recebidos, antes de qualquer parse de JSON) e compare em tempo constante.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(rawBody, headers, secret) {
const timestamp = headers["x-webhook-timestamp"];
const signature = headers["x-webhook-signature"];
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}
Rejeite (401) qualquer entrega que falhe na verificação.
Testar
Depois que a equipe configurar a URL e o segredo, chame POST /webhooks/test: um evento webhook.test é enviado à sua URL em alguns segundos. Em sandbox, as rotas de decisão de KYC e saque disparam os eventos reais. O testador da documentação tem um verificador de webhook para conferir uma entrega recebida.
Verificação em outras linguagens e vetor de teste
Os snippets abaixo incluem a verificação do webhook em Node.js, Python, PHP, Go, Java, C# e bash, com o vetor de teste (segredo, timestamp, corpo bruto e assinatura esperada).
sign.mjs
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
/** Assina uma requisição: retorna o valor do cabeçalho X-Signature. */
export function signRequest(secret, method, pathWithQuery, timestamp, body = "") {
const canonical = [
method.toUpperCase(),
pathWithQuery, // exatamente como enviado, com /api/v1 e a query string
timestamp, // epoch em segundos (string)
createHash("sha256").update(body).digest("hex"),
].join("\n");
return createHmac("sha256", secret).update(canonical).digest("hex");
}
/** Verifica um webhook recebido (use o corpo BRUTO, antes de parsear o JSON). */
export function verifyWebhook(secret, timestamp, rawBody, signature) {
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}sign.py
import hashlib
import hmac
def sign_request(secret, method, path_with_query, timestamp, body=""):
"""Assina uma requisição: retorna o valor do cabeçalho X-Signature."""
canonical = "\n".join([
method.upper(),
path_with_query, # exatamente como enviado, com /api/v1 e a query string
timestamp, # epoch em segundos (string)
hashlib.sha256(body.encode()).hexdigest(),
])
return hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
def verify_webhook(secret, timestamp, raw_body, signature):
"""Verifica um webhook recebido (use o corpo BRUTO, antes de parsear o JSON)."""
expected = hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)sign.php
<?php
/** Assina uma requisição: retorna o valor do cabeçalho X-Signature. */
function sign_request(string $secret, string $method, string $pathWithQuery, string $timestamp, string $body = ""): string {
$canonical = implode("\n", [
strtoupper($method),
$pathWithQuery, // exatamente como enviado, com /api/v1 e a query string
$timestamp, // epoch em segundos (string)
hash("sha256", $body),
]);
return hash_hmac("sha256", $canonical, $secret);
}
/** Verifica um webhook recebido (use o corpo BRUTO: file_get_contents("php://input")). */
function verify_webhook(string $secret, string $timestamp, string $rawBody, string $signature): bool {
return hash_equals(hash_hmac("sha256", $timestamp . "." . $rawBody, $secret), $signature);
}sign.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
// signRequest assina uma requisição: retorna o valor do cabeçalho X-Signature.
func signRequest(secret, method, pathWithQuery, timestamp, body string) string {
sum := sha256.Sum256([]byte(body))
canonical := strings.ToUpper(method) + "\n" +
pathWithQuery + "\n" + // exatamente como enviado, com /api/v1 e a query string
timestamp + "\n" + // epoch em segundos (string)
hex.EncodeToString(sum[:])
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
return hex.EncodeToString(mac.Sum(nil))
}
// verifyWebhook verifica um webhook recebido (use o corpo BRUTO).
func verifyWebhook(secret, timestamp, rawBody, signature string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp + "." + rawBody))
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(signature))
}BetSigner.java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class BetSigner {
static String hmacHex(String secret, String message) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return HexFormat.of().formatHex(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
}
/** Assina uma requisição: retorna o valor do cabeçalho X-Signature. */
public static String signRequest(String secret, String method, String pathWithQuery, String timestamp, String body) throws Exception {
String bodyHash = HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-256").digest(body.getBytes(StandardCharsets.UTF_8)));
// pathWithQuery: exatamente como enviado, com /api/v1 e a query string; timestamp: epoch em segundos
return hmacHex(secret, String.join("\n", method.toUpperCase(), pathWithQuery, timestamp, bodyHash));
}
/** Verifica um webhook recebido (use o corpo BRUTO). */
public static boolean verifyWebhook(String secret, String timestamp, String rawBody, String signature) throws Exception {
String expected = hmacHex(secret, timestamp + "." + rawBody);
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8));
}BetSigner.cs
using System;
using System.Security.Cryptography;
using System.Text;
public static class BetSigner
{
static string HmacHex(string secret, string message)
{
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
return Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant();
}
/// <summary>Assina uma requisição: retorna o valor do cabeçalho X-Signature.</summary>
public static string SignRequest(string secret, string method, string pathWithQuery, string timestamp, string body = "")
{
var bodyHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLowerInvariant();
// pathWithQuery: exatamente como enviado, com /api/v1 e a query string; timestamp: epoch em segundos
return HmacHex(secret, string.Join("\n", method.ToUpperInvariant(), pathWithQuery, timestamp, bodyHash));
}
/// <summary>Verifica um webhook recebido (use o corpo BRUTO).</summary>
public static bool VerifyWebhook(string secret, string timestamp, string rawBody, string signature) =>
CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(HmacHex(secret, $"{timestamp}.{rawBody}")),
Encoding.UTF8.GetBytes(signature));sign.sh
#!/usr/bin/env bash
# Assina uma requisição: imprime o valor do cabeçalho X-Signature.
# uso: sign_request SECRET METODO CAMINHO_COM_QUERY TIMESTAMP [CORPO]
sign_request() {
local secret="$1" method="$2" path="$3" ts="$4" body="${5:-}"
local body_hash
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -hex | sed 's/^.* //')
printf '%s\n%s\n%s\n%s' "$method" "$path" "$ts" "$body_hash" \
| openssl dgst -sha256 -hmac "$secret" -hex | sed 's/^.* //'
}
# Assinatura de um webhook: HMAC-SHA256 de "TIMESTAMP.CORPO_BRUTO" com o segredo whsec_...
webhook_signature() {
printf '%s.%s' "$2" "$3" | openssl dgst -sha256 -hmac "$1" -hex | sed 's/^.* //'
}Vetor de teste
Se o seu código gerar exatamente estes valores para as entradas abaixo, a assinatura está correta. O vetor também está em test-vectors.json.
- Segredo do webhook
whsec_vetor_de_teste_nao_use_em_producao- X-Webhook-Timestamp
1700000000- Corpo bruto
{"id":"evt_vetor","type":"webhook.test","createdAt":"2023-11-14T22:13:20.000Z","apiVersion":"1.0.0","data":{"message":"ok"}}- X-Webhook-Signature esperada
a95e51addfc7fd14d048f689f85dfc36b8b8b3f48c8fd8dd98e114c4b6711c14