Pular para o conteúdo principal

Webhooks

Um POST assinado por batalha, quando ela acaba. É a autoridade sobre o resultado, não o feed WebSocket, que é uma visão ao vivo que um espectador pode ter perdido.

A requisição

POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Slotbattle-Sign: 9f2c4a...
{
"battle_id": "btl_CIkE8lctOrFDe1hJlG0T",
"tenant_id": "acme",
"status": "completed",
"seats": [
{
"user_id": "usr_s0",
"seat_ref": "usr_s0",
"is_bot": false,
"ok": true,
"outcome": 3450.0,
"multiplier": 138.0,
"currency": "BRL"
},
{
"user_id": "usr_s1",
"seat_ref": "usr_s1",
"is_bot": true,
"ok": true,
"outcome": 210.5,
"multiplier": 8.42,
"currency": "BRL"
}
]
}
CampoNotas
statuscompleted, failed ou cancelled
reasonPresente em failed e cancelled. Por que terminou assim
seatsAusente em cancelled, porque nada foi gravado
user_id / seat_refA referência do assento, usr_s<índice>. Os dois carregam o mesmo valor
okSe este assento teve sucesso. Uma batalha completed pode conter assento falho
multiplierO multiplicador cru do assento
status: "completed" não significa que todo assento teve sucesso

Significa que a batalha terminou. Cheque ok por assento. Um assento cuja gravação falhou volta com ok: false dentro de uma batalha por outro lado completa, e liquidá-lo como vitória é um bug de dinheiro real.

Verifique a assinatura

X-Slotbattle-Sign é o HMAC-SHA256 em hexadecimal do corpo cru da requisição, com a chave do seu segredo de webhook.

Verifique sobre os bytes que você recebeu, antes de fazer parse. Reserializar o JSON e fazer hash disso não vai bater, porque ordem de chaves e formatação de número mudam.

import { createHmac, timingSafeEqual } from 'node:crypto'

function verify(rawBody, header, secret) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(expected, 'utf8')
const b = Buffer.from(header ?? '', 'utf8')
return a.length === b.length && timingSafeEqual(a, b)
}

Use comparação de tempo constante. === numa assinatura a vaza byte a byte.

$expected = hash_hmac('sha256', $rawBody, $secret);
if (! hash_equals($expected, $request->header('X-Slotbattle-Sign', ''))) {
abort(401);
}

Recuse o que falhar. Uma chamada sem assinatura ou com assinatura errada não é uma chamada do SlotBattle.

A entrega é durável e idempotente

As entregas têm chave battle_id : status : version, então um evento terminal duplicado nunca produz um segundo POST.

  • 3 tentativas, e então a entrega é persistida como falha.
  • Um reconciliador retenta entregas falhas com backoff de 30 segundos.
  • A entrega é por tenant: um casino sem endpoint configurado é pulado, nunca redirecionado para o webhook da plataforma.

O seu endpoint ainda precisa ser idempotente. Timeouts de rede significam que você pode receber uma entrega já processada, já que a garantia é ao menos uma vez e não exatamente uma vez. Use battle_id + status como chave do seu processamento.

Responda rápido

Responda 2xx assim que tiver aceitado o payload de forma durável, e faça a liquidação de forma assíncrona.

Um endpoint lento queima o orçamento de tentativas e empurra a entrega para o reconciliador, o que atrasa o resultado para todo mundo naquela batalha. Escreva numa fila e retorne.

Qualquer coisa fora de 2xx é falha e vai ser retentada.

Configurando o endpoint

O webhook é por casino, na categoria de settings webhook: no console, Configurações → Webhook. Defina a URL e o segredo juntos.

O grupo de credencial é tudo ou nada

Definir a URL e deixar o segredo em branco é recusado na escrita. Meio configurado, ele mandaria os seus resultados assinados com o segredo da plataforma.

O segredo também nunca volta. Ler as configurações responde se ele está definido, não qual é, e enviar o formulário sem ele deixa o armazenado intacto, então apertar Salvar não consegue limpá-lo em silêncio.

Testando sem endpoint público

O endpoint precisa ser alcançável a partir da instância. Durante o desenvolvimento, um túnel como ngrok ou cloudflared apontado para o seu servidor local é a abordagem usual.

O tutorial constrói um receptor que verifica assinaturas e mostra como exercitá-lo.

Por que o socket não substitui

O WebSocket é melhor esforço e não tem replay. Um espectador, ou o seu próprio listener, pode perder o battle.completed por ficar dois segundos desconectado, e nada vai reentregar. O webhook retenta, é idempotente e é assinado.

Use o socket para manter a UI viva, e o webhook para mover dinheiro.

Próximo