Pular para o conteúdo

Referência da API de carteira

O dinheiro do jogador fica na sua carteira. A plataforma chama o seu servidor a cada movimentação: quatro endpoints POST sob a walletUrl que você nos informa (por exemplo https://wallet.seu-cassino.example/sportsbook).

Endpoints
balance · debit · credit · rollback
Assinatura
HMAC-SHA256, hex
Janela de relógio
±300 s
Timeout
5 s por tentativa

Toda chamada é um POST com corpo JSON e três cabeçalhos:

POST /sportsbook/debit HTTP/1.1
Content-Type: application/json
X-Timestamp: 1760000000
X-Signature: 8bc4fc9b84ee8f997c3f3f51c9e3dfc7e848ddff0ad05908327fca9b4ec70a0e
{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
Campo Tipo
txId string Chave de idempotência, até 200 caracteres. Veja os ids de transação.
playerId string O id do jogador no seu sistema: o externalPlayerId enviado ao criar a sessão.
amount inteiro Unidades menores (centavos). Sempre zero ou positivo; o endpoint dá a direção.
currency string A moeda do tenant, por exemplo USD, BRL.
kind string O tipo da movimentação, veja a tabela abaixo.
betId string A aposta, quando a movimentação pertence a uma.
ref string Contexto: o débito sendo revertido, ou o resultado que gerou um pagamento, por exemplo result:ev_8f2c/1x2@1.
kind Endpoint
balance /balance Consulta de saldo (amount é 0).
bet /debit Um valor apostado.
payout /credit A primeira liquidação de uma aposta: o prêmio, ou 0 para uma aposta perdida.
refund /credit Uma aposta anulada: o valor apostado volta.
cashout /credit O jogador fez cash-out.
resettle /credit ou /debit Uma correção depois de um mercado reliquidado.
rollback /rollback Um débito de aposta que não virou aposta.
adjustment /credit ou /debit Uma correção manual pela equipe da plataforma, auditada.
txId Chamada
balance:<uuid> /balance (novo a cada vez)
bet:<betId> /debit, o valor apostado
rollback:bet:<betId> /rollback desse valor (ref = bet:<betId>)
payout:<betId>:<version> /credit, primeira liquidação (kind payout ou refund)
resettle:<betId>:<version>:<n> /credit ou /debit, a n-ésima liquidação da aposta
cashout:<betId> /credit, cash-out

Trate os formatos como informativos: conte com o txId ser único por movimentação, não com o formato dele.

Responda 2xx com o saldo depois da movimentação:

{ "txId": "bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53", "balance": 49500, "currency": "USD" }

O txId é opcional, mas, se você o enviar, deve ser o da requisição. Para recusar, use um status 4xx e um corpo de erro:

{ "error": { "code": "insufficient_funds", "message": "insufficient funds" } }

Como lemos a sua resposta:

Sua resposta Significa O que fazemos
2xx, JSON válido, sem error Aplicada. Pronto.
4xx, exceto 408 e 429 Recusada, nada se moveu. Uma aposta: é recusada (insufficient_funds diz ao jogador o motivo). Qualquer outra coisa: sem retentativa, marcada para revisão manual.
408, 429, 5xx, timeout, erro de conexão, um 2xx que não conseguimos ler Desconhecido: você pode ou não ter aplicado. Retentada ou revertida, veja Retentativas.
{"txId":"balance:5f0c7a52-3f6e-4f6e-9a54-0b8f8f6e2a11","playerId":"player-42","amount":0,"currency":"USD","kind":"balance"}

Responda {"balance": 50000, "currency": "USD"}. Chamado quando o sportsbook mostra o saldo; nenhum dinheiro se move. Se falhar, o sportsbook não mostra saldo até a próxima chamada bem-sucedida.

{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
  • kind: "bet" é um valor apostado. Se o saldo não bastar, recuse com o código insufficient_funds (402 é o usual; qualquer 4xx exceto 408/429 funciona).
  • Um débito de aposta é enviado uma vez e nunca retentado. Se o resultado for desconhecido, nós o revertemos e recusamos a aposta, para que nenhuma aposta seja aceita com dinheiro que você não confirmou.
  • kind: "resettle" ou "adjustment" é uma correção devida pelo jogador. Aplique mesmo que o saldo fique negativo; veja correções.
{"txId":"payout:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53:1","playerId":"player-42","amount":1050,"currency":"USD","kind":"payout","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","ref":"result:ev_8f2c/1x2@1"}

Créditos são devidos: retentamos até você confirmar (veja abaixo). Aceite amount: 0.

{"txId":"payout:d4e8a6b2-1c3f-4e5a-9b7d-6f0a2c4e8b19:1","playerId":"player-42","amount":0,"currency":"USD","kind":"payout","betId":"d4e8a6b2-1c3f-4e5a-9b7d-6f0a2c4e8b19","ref":"result:ev_8f2c/1x2@1"}

Toda aposta liquidada recebe exatamente um crédito payout:, inclusive quando perde. Use-o para encerrar a aposta nos seus registros (muitos reguladores exigem um resultado explícito por aposta). Responda 2xx com o saldo inalterado.

{"txId":"rollback:bet:f27b9c1e-6d4a-4b8f-83e2-5a1c9d7e0b46","playerId":"player-42","amount":200,"currency":"USD","kind":"rollback","betId":"f27b9c1e-6d4a-4b8f-83e2-5a1c9d7e0b46","ref":"bet:f27b9c1e-6d4a-4b8f-83e2-5a1c9d7e0b46"}

Reverte o débito de aposta cujo txId é o ref, porque a aposta não se concretizou (a sua resposta ao débito se perdeu, ou a aposta falhou depois dele).

  1. Se você aplicou esse débito e ainda não o reverteu: devolva o dinheiro e marque o débito como revertido.
  2. Se você já o reverteu: não faça nada e responda 2xx.
  3. Se você nunca recebeu esse débito (ele estourou o tempo antes de chegar, ou ainda está a caminho): responda 2xx com o saldo atual e guarde o ref. Se o débito chegar depois, recuse com um 4xx (rolled_back). Senão o jogador pagaria por uma aposta que não existe.

amount é o valor apostado original; devolva o que você de fato debitou.

X-Signature = hex( HMAC-SHA256( walletSecret, X-Timestamp + "." + rawBody ) )
  • rawBody é o corpo da requisição exatamente como recebido, byte a byte. Verifique antes de interpretar; nunca reserialize o JSON (a ordem das chaves e os espaços mudariam).
  • X-Timestamp é o horário Unix, em segundos, em que a tentativa foi enviada. Recuse requisições a mais de 300 segundos do seu relógio (proteção contra replay) e mantenha os servidores em NTP.
  • Compare em tempo constante. O hex vem em minúsculas; aceite os dois casos.
  • Uma retentativa traz timestamp e assinatura novos sobre o mesmo corpo.
walletSecret = dev-wallet-secret
X-Timestamp = 1760000000
rawBody = {"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
texto assinado = 1760000000.{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}
X-Signature = 8bc4fc9b84ee8f997c3f3f51c9e3dfc7e848ddff0ad05908327fca9b4ec70a0e

Confira no terminal:

Janela do terminal
printf '%s' '1760000000.{"txId":"bet:8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53","playerId":"player-42","amount":500,"currency":"USD","kind":"bet","betId":"8c1d3f2a-5b7e-4c69-a0d4-2e9f6b1c7a53"}' \
| openssl dgst -sha256 -hmac dev-wallet-secret

Cada exemplo é testado contra requisições assinadas pelo próprio código do cliente de carteira da plataforma: requisições válidas passam; corpo alterado e timestamp de 301 segundos atrás falham.

samples/verify-signature.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';
const MAX_SKEW_SECONDS = 300;
/**
* @param {string} secret your wallet secret
* @param {string} timestamp the X-Timestamp header (unix seconds)
* @param {string} signature the X-Signature header (hex)
* @param {Buffer|string} rawBody the request body exactly as received
* @returns {null | 'bad_timestamp' | 'bad_signature'}
*/
export function verifySignature(secret, timestamp, signature, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {
if (!/^\d+$/.test(timestamp ?? '') || Math.abs(nowSeconds - Number(timestamp)) > MAX_SKEW_SECONDS) return 'bad_timestamp';
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
const given = Buffer.from(signature ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected) ? null : 'bad_signature';
}

Em caso de falha, responda 401 com {"error": {"code": "invalid_signature"}}.

O txId é a chave de idempotência de toda chamada de dinheiro.

  • Guarde cada txId processado com o resultado (status e corpo).
  • Quando um txId chegar de novo, responda o resultado guardado e não mova dinheiro. É isso que torna as retentativas seguras.
  • Se um txId conhecido chegar com outro jogador, endpoint ou valor, recuse (409, duplicate_tx).
  • Aplique a movimentação e guarde o txId na mesma transação do banco.
Timeout por tentativa 5 segundos
Débitos de aposta (kind: bet) Uma tentativa. Resultado desconhecido: revertido, aposta recusada.
Créditos, rollbacks, débitos de correção Até 5 tentativas, com esperas de 200 ms, 400 ms, 800 ms e 1,6 s entre elas (dobrando, no máximo 2 s)
Ainda desconhecido depois disso Fica pendente e é reenviado, com o mesmo txId e corpo, por um job de conciliação a cada minuto, até você responder 2xx ou 4xx
Liquidações Também ficam em fila: uma liquidação que não pôde ser paga é retentada a cada 30 segundos

Assim, um crédito pode chegar minutos ou horas depois da liquidação (após uma indisponibilidade do seu lado), e mais de uma vez. A idempotência torna isso inofensivo.

Um mercado pode ser reliquidado (um resultado corrigido depois da primeira liquidação). A plataforma calcula a diferença de cada aposta afetada e envia uma movimentação resettle com txId resettle:<betId>:<version>:<n>: um crédito se o jogador tem mais a receber, um débito se recebeu a mais.

O jogador pode já ter gastado o prêmio. Aplique o débito de correção mesmo assim e deixe o saldo ficar negativo; recupere-o nas suas condições. Se você o recusar com 4xx, o processamento automático para e a movimentação vai para revisão manual dos dois lados.

Situação Status error.code O jogador vê
Aposta maior que o saldo 402 insufficient_funds “Saldo insuficiente”
Jogador desconhecido 404 player_not_found A aposta é recusada (payment_rejected)
Jogador bloqueado do seu lado 403 player_blocked A aposta é recusada (payment_rejected)
Assinatura inválida ou timestamp velho 401 invalid_signature A aposta é recusada
txId reutilizado com outros parâmetros 409 duplicate_tx A aposta é recusada
Débito já revertido 409 rolled_back A aposta é recusada
Falha temporária 5xx ou timeout qualquer Uma aposta: recusada como temporariamente indisponível. Demais: retentadas

Só insufficient_funds tem significado especial. Qualquer outro código fica registrado no log de chamadas de carteira da plataforma e volta nos detalhes do erro como operatorCode.

  • Mantenha uma linha de ledger por txId: endpoint, tipo, valor, aposta, status, horário.
  • Para cada aposta você verá um débito bet: e depois um rollback (a aposta foi recusada) ou um crédito payout:/cashout: (possivelmente 0), seguido de eventuais movimentações resettle:. Uma aposta só com o débito está aberta.
  • A plataforma registra toda chamada de dinheiro que faz a você (status, tentativas, a sua resposta) e mantém as não resolvidas pendentes até serem resolvidas.
  • Diariamente, compare os seus totais por dia UTC com os Relatórios do back office (GET /api/v1/admin/reports/ggr?from=AAAA-MM-DD&to=AAAA-MM-DD: apostas, prêmios, reembolsos, cash-outs e GGR), e consulte apostas individuais em Apostas (GET /api/v1/admin/bets?player=<id do jogador>).