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
Requisições
Seção intitulada “Requisições”Toda chamada é um POST com corpo JSON e três cabeçalhos:
POST /sportsbook/debit HTTP/1.1Content-Type: application/jsonX-Timestamp: 1760000000X-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. |
Ids de transação
Seção intitulada “Ids de transação”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.
Respostas
Seção intitulada “Respostas”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. |
Endpoints
Seção intitulada “Endpoints”POST /balance
Seção intitulada “POST /balance”{"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.
POST /debit
Seção intitulada “POST /debit”{"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ódigoinsufficient_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.
POST /credit
Seção intitulada “POST /credit”{"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.
Créditos de valor zero encerram apostas perdidas
Seção intitulada “Créditos de valor zero encerram apostas perdidas”{"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.
POST /rollback
Seção intitulada “POST /rollback”{"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).
- Se você aplicou esse débito e ainda não o reverteu: devolva o dinheiro e marque o débito como revertido.
- Se você já o reverteu: não faça nada e responda
2xx. - Se você nunca recebeu esse débito (ele estourou o tempo antes de chegar,
ou ainda está a caminho): responda
2xxcom o saldo atual e guarde oref. Se o débito chegar depois, recuse com um4xx(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.
Assinatura
Seção intitulada “Assinatura”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.
Exemplo resolvido
Seção intitulada “Exemplo resolvido”walletSecret = dev-wallet-secretX-Timestamp = 1760000000rawBody = {"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 = 8bc4fc9b84ee8f997c3f3f51c9e3dfc7e848ddff0ad05908327fca9b4ec70a0eConfira no 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-secretCódigo de verificação
Seção intitulada “Código de verificação”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.
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';}const MAX_SKEW_SECONDS = 300;
/** * Returns null when valid, else 'bad_timestamp' or 'bad_signature'. * $rawBody is file_get_contents('php://input'), not re-encoded JSON. */function verify_signature(string $secret, string $timestamp, string $signature, string $rawBody, ?int $now = null): ?string{ $now = $now ?? time(); if (!ctype_digit($timestamp) || abs($now - (int) $timestamp) > MAX_SKEW_SECONDS) { return 'bad_timestamp'; } $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret); return hash_equals($expected, strtolower($signature)) ? null : 'bad_signature';}
// In a handler:// $error = verify_signature($secret, $_SERVER['HTTP_X_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_SIGNATURE'] ?? '', file_get_contents('php://input'));import hashlibimport hmacimport time
MAX_SKEW_SECONDS = 300
def verify_signature(secret: str, timestamp: str, signature: str, raw_body: bytes, now: int | None = None) -> str | None: """Returns None when valid, else "bad_timestamp" or "bad_signature".
raw_body is the request body exactly as received (bytes), not re-serialised JSON. """ now = int(time.time()) if now is None else now if not timestamp.isdigit() or abs(now - int(timestamp)) > MAX_SKEW_SECONDS: return "bad_timestamp" expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() return None if hmac.compare_digest(expected.encode(), signature.lower().encode()) else "bad_signature"static final long MAX_SKEW_SECONDS = 300;
/** Returns null when valid, else "bad_timestamp" or "bad_signature". rawBody: the body bytes as received. */static String verifySignature(String secret, String timestamp, String signature, byte[] rawBody, long nowSeconds) throws Exception { if (timestamp == null || !timestamp.matches("\\d{1,18}") || Math.abs(nowSeconds - Long.parseLong(timestamp)) > MAX_SKEW_SECONDS) { return "bad_timestamp"; } Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8)); byte[] expected = mac.doFinal(rawBody); byte[] given; try { given = java.util.HexFormat.of().parseHex(signature == null ? "" : signature); } catch (IllegalArgumentException e) { return "bad_signature"; } return MessageDigest.isEqual(expected, given) ? null : "bad_signature";}const maxSkew = 300 * time.Second
// VerifySignature checks X-Timestamp and X-Signature against the raw body.// It returns "" when valid, else "bad_timestamp" or "bad_signature".func VerifySignature(secret, timestamp, signature string, rawBody []byte, now time.Time) string { ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return "bad_timestamp" } if d := now.Sub(time.Unix(ts, 0)); d > maxSkew || d < -maxSkew { return "bad_timestamp" } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(rawBody) given, err := hex.DecodeString(signature) if err != nil || !hmac.Equal(given, mac.Sum(nil)) { return "bad_signature" } return ""}Em caso de falha, responda 401 com {"error": {"code": "invalid_signature"}}.
Idempotência
Seção intitulada “Idempotência”O txId é a chave de idempotência de toda chamada de dinheiro.
- Guarde cada
txIdprocessado com o resultado (status e corpo). - Quando um
txIdchegar de novo, responda o resultado guardado e não mova dinheiro. É isso que torna as retentativas seguras. - Se um
txIdconhecido chegar com outro jogador, endpoint ou valor, recuse (409,duplicate_tx). - Aplique a movimentação e guarde o
txIdna mesma transação do banco.
Retentativas e timeouts
Seção intitulada “Retentativas e timeouts”| 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.
Correções podem deixar o saldo negativo
Seção intitulada “Correções podem deixar o saldo negativo”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.
Códigos de erro
Seção intitulada “Códigos de erro”| 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.
Conciliação
Seção intitulada “Conciliação”- 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éditopayout:/cashout:(possivelmente 0), seguido de eventuais movimentaçõesresettle:. 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>).