# API de Integração v1 > Jumbo · jogo de crash Este arquivo é a documentação inteira, em Markdown, gerada do mesmo texto que a página em https://docs.aircrash.net. O contrato legível por máquina, com todo campo e todo código de erro, está em https://docs.aircrash.net/openapi.yaml e foi conferido contra o servidor que roda. Valores monetários são inteiros, em centavos. Erro de negócio chega em HTTP 200 com `status:"error"`. Se você for ler uma seção só, leia a das armadilhas. ## As duas direções | Integração | Sentido | O que carrega | | --- | --- | --- | | Catálogo e lançamento | operador → provedor | quais jogos existem, e abrir uma partida para um jogador | | Carteira | provedor → operador | resolver a sessão, saldo, débito, crédito, estorno | ## As oito que quebram integração Todas contrariam uma convenção comum, e é por isso que estão juntas e antes das rotas: são os pontos em que implementar *o que costuma ser* produz um cliente que compila, passa no teste feliz e erra em produção. | o palpite | o contrato | | --- | --- | | Recusa vem em 4xx | Erro de **negócio** vem em **HTTP 200**, com `status:"error"` e `error_code`. Ramifique em `status`, nunca no status HTTP. Um cliente que teste `response.ok` lê uma recusa como sucesso. | | Um código de erro desconhecido é final | A regra padrão é **retentar**, até 20 vezes. São finais só sete: `INSUFFICIENT_FUNDS`, `TOKEN_EXPIRED`, `TOKEN_INVALID`, `PLAYER_NOT_FOUND`, `BET_NOT_FOUND`, `CURRENCY_MISMATCH` e `PLAYER_BLOCKED`. Inventar um nome para recusar em definitivo produz 20 tentativas idênticas contra a sua carteira. | | Assinar o JSON | Assine **os bytes exatos do corpo**. Desserializar e serializar de novo antes do HMAC muda espaçamento e ordem de chaves, e a assinatura deixa de bater com um conteúdo idêntico. | | `request_id` é a chave de idempotência | A chave é o `transaction_id`. O `request_id` nomeia a **tentativa** e muda a cada uma, junto com o `timestamp`. Deduplicar por `request_id` não deduplica nada; comparar o corpo inteiro rejeita toda retentativa. | | `/balance` sempre traz `player_id` | A **primeira** chamada de cada sessão vai só com o `token`: é ela que descobre o jogador. Exigir `player_id` ali recusa a entrada de todo mundo. | | Valor monetário é decimal | É **inteiro, em centavos**. `R$ 3,56` é `356`. O bundle do jogo já mandou `114.99999999999999` para uma aposta de R$ 1,15 quando o campo era decimal. | | As duas direções seguem a mesma convenção | Não seguem. A regra do 200 vale para a **carteira**; as duas rotas do **jogo** respondem `4xx`, porque ali não há decisão de negócio, só pedido inválido. Os dois conjuntos de `error_code` são disjuntos. | | `freebet_id` só vem quando é brinde | O campo existe **sempre**, valendo `null` na aposta comum. Ramifique em `freebet_id !== null`, nunca na presença da chave. | > **Se você vai implementar com ajuda de IA** Dê ao modelo o [`openapi.yaml`](https://docs.aircrash.net/openapi.yaml) e o [texto integral em Markdown](https://docs.aircrash.net/llms-full.pt.txt), não esta página: o HTML leva folha de estilo e os três idiomas juntos. Depois peça que ele confira o cliente contra esta tabela, linha por linha. ## Autenticação: um esquema só, nas duas direções ``` X-Signature: hex(HMAC-SHA256(corpo_da_requisição, segredo)) Authorization: Bearer ``` Assine **os bytes exatos do corpo**, e confira sobre esses mesmos bytes. Decodificar o JSON e serializá-lo de novo antes do HMAC muda o espaçamento e a ordem das chaves, e a assinatura deixa de bater com um conteúdo idêntico. É o erro mais comum, nos dois lados. O `timestamp` vai **dentro do corpo assinado**, em milissegundos, sem cabeçalho próprio. Fora de uma janela de **5 minutos** o pedido é recusado, nos dois sentidos: é o que impede reenviar um pedido capturado. Todo pedido leva `operator` e `timestamp`; os que saem daqui levam também `request_id`, `currency` e `game_id`. O `request_id` nomeia a **tentativa**, não o movimento: duas tentativas do mesmo `transaction_id` têm ids diferentes, e é assim que você acha uma delas no seu log. O `Bearer` vale só na direção que sai daqui. Nas duas rotas que expomos não há chave nossa, e a assinatura autentica sozinha: por isso o jogo **recusa subir** em modo operador sem segredo. Sem segredo configurado, o `X-Signature` não é enviado e a janela não se aplica. > **Falha de autenticação** Operador desconhecido, operador desabilitado, sem segredo, assinatura errada, pedido velho: todos devolvem o mesmo `401 INVALID_SIGNATURE`. Distinguir contaria a quem está tentando adivinhar qual palpite chegou mais perto. ## O que nós expomos (operador → provedor) Duas rotas, sobre `https://jumbo.aircrash.net`, que é também a base das URLs de lançamento que o `/v1/launch` devolve. Uma instalação em outro endereço publica o dela. ### POST /v1/games todos os jogos habilitados para o operador `->` ```json {"operator":"casa-do-norte", "timestamp":1755680000000, "currency":"BRL"} ``` `<-` ```json { "status": "ok", "operator": "casa-do-norte", "currencies": ["BRL", "USD"], "games": [ { "game_id": "jumbo-crash", "name": "Jumbo", "studio": "jumbo", "category": "CRASH", "demo": false, "mobile": true, "desktop": true, "freebet": true, "currency": "BRL", "min_bet": 100, "max_bet": 50000, "max_win": 5000000 } ] } ``` `freebet` diz se o jogo aceita rodadas grátis concedidas pelo operador. #### Os três limites Só aparecem quando você envia `currency`, valem para aquela moeda, e são **centavos inteiros**. | Campo | Significado | | --- | --- | | min_bet | menor aposta aceita | | max_bet | maior aposta aceita | | max_win | teto de pagamento; prêmio maior é pago no teto | > **Limite ausente** Um campo de limite ausente quer dizer que o limite é **desconhecido**, nunca que não há limite. É por isso que ele é omitido em vez de sair zerado: um zero seria lido como aposta mínima de zero, que é justamente o que o servidor recusa aceitar. `currencies` é o conjunto de moedas habilitadas para você. É daí que sai em que moedas você pode conceder freebet: uma concessão numa moeda que a mesa não aceita é uma promessa que nunca se cumpre. ### POST /v1/launch abre uma partida para um jogador Devolve a URL para carregar em iframe ou redirecionar. `->` ```json { "operator": "casa-do-norte", "timestamp": 1755680000000, "game_id": "jumbo-crash", "player_id": "618004", "token": "e7c1f0a9d3b84e2f95a6", "currency": "BRL", "language": "pt", "demo": false, "mobile": true, "lobby_url": "https://seu-site/lobby", "deposit_url": "https://seu-site/deposito" } ``` | Campo | Obrigatório | Notas | | --- | --- | --- | | game_id | sim | tem de ser o jogo desta instalação | | token | salvo em demo | vai em toda chamada de carteira | | player_id | salvo em demo | o identificador do jogador no seu sistema | | currency | recomendado | a moeda da sessão | | language | não | pt, en ou es | | player_name | não | apelido ou nome; é ofuscado ao exibir | | theme | não | o tema visual da mesa | | demo, mobile | não | demo não precisa de token nem de jogador | | lobby_url, deposit_url | não | guardados na sessão; a mesa ainda não os exibe | `<-` ```json {"status":"ok", "url":"https://jumbo.aircrash.net/?sid=v3Nq8Lm2&lang=pt&cur=BRL"} ``` > **O token não vai na URL** A URL carrega um **ticket** nosso. O seu `token` e o seu `player_id` não estão nela: nós os pegamos servidor a servidor e trocamos por esse ticket, que é aleatório, de **uso único** e vale **15 minutos**. É a diferença entre uma credencial ao portador no histórico do navegador, no cabeçalho Referer e no log de qualquer proxy no caminho, e nenhuma dessas coisas. Erros: `MISSING_GAME_ID`, `MISSING_TOKEN`, `MISSING_PLAYER_ID`, `INVALID_TOKEN` (400), `GAME_NOT_FOUND` (404), `DEMO_NOT_AVAILABLE` (400). ## Moedas e idiomas Todo dinheiro é **centavo inteiro**. `1500` quer dizer 15,00 na moeda da sessão. Um campo monetário em unidades da moeda, ou em ponto flutuante, é inválido. As moedas habilitadas para um operador voltam em `currencies` no `POST /v1/games`. Não há lista fixa aqui de propósito: a lista é a configuração da sua instalação, e publicá-la em dois lugares é garantir que um dia divirjam. ### Idiomas `pt`, `en` e `es`. Qualquer outro valor, e a ausência do campo, caem no idioma padrão da instalação, e não em `en`. A divergência com a referência é deliberada: cair num idioma que a maioria da base não lê, por causa de um campo esquecido, é pior do que cair no idioma da casa. ## O que o operador expõe (provedor → operador) Quatro rotas. ``` POST {wallet_url}/balance token → jogador, saldo, moeda POST {wallet_url}/bet débito POST {wallet_url}/win crédito POST {wallet_url}/rollback estorno ``` ### Formato da resposta `<-` ```json {"status":"ok", "balance":98500, "currency":"BRL", "player_id":"618004", "transaction_id":"w-77120"} ``` `<-` ```json {"status":"error", "error_code":"INSUFFICIENT_FUNDS", "error_message":"saldo insuficiente"} ``` `balance` é centavo inteiro, **depois** da operação, e é obrigatório no sucesso. `currency` é a moeda da conta. Mande em toda resposta. `transaction_id` na resposta é o **seu** número para o movimento, não o nosso. Ele existe para a conciliação: é o que se cita ao abrir um chamado sobre um movimento específico. ### Campos por rota `-> /bet` ```json { "operator": "casa-do-norte", "timestamp": 1755680000000, "request_id": "req-9f1464ae0da0664c", "game_id": "jumbo-crash", "currency": "BRL", "token": "e7c1f0a9d3b84e2f95a6", "player_id": "618004", "transaction_id": "b-4471902", "round_id": "88213", "amount": 1500 } ``` | Rota | Campos | | --- | --- | | /balance | player_id, token | | /bet | player_id, token, amount, transaction_id, round_id | | /win | player_id, token, amount, transaction_id, bet_id, round_id, type | | /rollback | player_id, token, amount, transaction_id, bet_id, round_id | Mais `operator`, `timestamp`, `request_id` e `currency` em todas, e `game_id` quando o jogo é conhecido. O ciclo de freebet acrescenta `freebet_id` ao `/bet` e ao `/win`; o estorno acrescenta `reason`, que é diagnóstico e não muda nada do lado de lá. > **O `type` do /win** Nomeia a natureza do crédito, sem obrigar você a cruzar com o débito. `win` é prêmio de aposta paga com dinheiro do jogador, `freebet` é prêmio de rodada custeada por você, e `zero_win` é o evento terminal de uma rodada perdida. Sem o rótulo, um crédito de valor zero seria indistinguível de um prêmio que por acaso deu zero. > **O `amount` do /rollback** Vai junto, e é **sempre** o valor do movimento referenciado em `bet_id`, nunca um número escolhido na hora. Confira que ele bate com o débito que você tem: um estorno que devolva mais do que foi tirado é uma porta aberta, e quem a fecha é essa conferência do seu lado. ### Códigos de erro | Código | Significado | Retentamos? | | --- | --- | --- | | INSUFFICIENT_FUNDS | saldo insuficiente | não | | TOKEN_EXPIRED | sessão expirada | não | | TOKEN_INVALID | sessão que nunca existiu | não | | PLAYER_NOT_FOUND | jogador desconhecido | não | | BET_NOT_FOUND | a aposta referenciada não existe | não | | CURRENCY_MISMATCH | moeda errada para esta conta | não | | PLAYER_BLOCKED | jogador impedido de apostar | não | | INTERNAL_ERROR | falha do seu lado | sim |