API seamless · Integração de operadores

Descarregar contrato

# ReelSparkz — API v1 seamless para operadores

Base pública: `https://bet77-arcade-mz.fly.dev/api/v1`
Modelo: **seamless / carteira única**. O saldo pertence ao operador. A ReelSparkz consulta e movimenta essa carteira por callbacks entre servidores. Não existem transferências do jogador para um “Saldo Jogos”.

Esta é a API da ReelSparkz para múltiplos operadores. Cada operador integra os seus próprios jogadores, domínio, callbacks e credenciais.

## 1. Credenciais e configuração

No painel do operador → Integração API:

- Criar uma chave sandbox para testar. O responsável do operador ou o Admin pode emitir chaves live. A primeira chave live ativa o operador, salvo restrição definida pelo Admin. O Admin mantém o poder de bloquear o operador e revogar chaves.
- Configurar o URL base HTTPS dos callbacks para cada ambiente, por exemplo `https://wallet.operador.example/novaarcade`.
- Copiar o `signingSecret` devolvido uma única vez para o servidor do operador. Este segredo HMAC é diferente da chave API. É guardado cifrado no servidor ReelSparkz.
- Autorizar o domínio do iframe e habilitar os produtos pretendidos.

Também é possível configurar por `POST /wallet/config`, com permissão `wallet:configure`:

```json
{"callbackUrl":"https://wallet.operador.example/novaarcade"}
```

Resposta: `configured`, `environment`, `callbackUrl`, `signingSecret`. `GET /wallet/config` devolve o estado e URL, nunca o segredo. Configurações são independentes por operador e ambiente. Nesta versão a configuração é de criação única: uma substituição devolve 409 e exige reconciliação dos pedidos/sessões existentes. Não substituir nem perder o segredo enquanto houver operações pendentes.

URLs HTTPS públicas na porta 443, sem query, fragmento ou credenciais. O serviço resolve e fixa um endereço IPv4 público por pedido, rejeita destinos privados e não segue redirecionamentos. Implementar os endpoints antes do primeiro lançamento. Configurar o URL não executa uma movimentação nem comprova a disponibilidade do servidor do operador.

Chaves anteriores podem continuar a consultar/lançar conforme as suas permissões. Para configurar callbacks por API, uma chave necessita de `wallet:configure`; se não tiver, usar o painel ou emitir outra chave.

## 2. Pedidos do operador à ReelSparkz

Enviar `X-API-Key: <chave>` ou `Authorization: Bearer <chave>`. A chave fica exclusivamente no backend do operador. Todos os corpos são JSON. Não enviar chaves no iframe, frontend, aplicação móvel ou query string.

| Método | Caminho | Permissão | Resultado |
|---|---|---|---|
| GET | /games | catalog:read | Catálogo habilitado, suporte seamless e configuração de carteira |
| GET | /currencies | catalog:read | Moedas suportadas |
| GET | /wallet/config | wallet:read | Estado da configuração deste operador/ambiente |
| POST | /wallet/config | wallet:configure | Configurar callbacks e obter segredo HMAC uma única vez |
| POST | /players | players:write | Registar jogador e moeda; não carrega nem consulta saldo |
| GET | /players/:playerId | players:read | Perfil e saldo consultado no operador |
| GET | /players/:playerId/balance | wallet:read | Saldo atual consultado no operador |
| GET | /players/:playerId/history | history:read | Apostas, resultados e estado de liquidação |
| GET | /players/:playerId/transactions | history:read | Callbacks de débito, prémio e reembolso, incluindo pendentes |
| POST | /games/launch | games:launch | URL de jogo com ticket de uso único, válido 60 segundos |

`/platform/wallet/deposit`, `/platform/wallet/withdraw` e `/platform/wallet/refund` da versão de transferência estão desativados (409). Depósitos e levantamentos são tratados pelo próprio operador, fora da ReelSparkz. Dados de carteiras antigas são preservados. Um jogador com saldo antigo transferido não pode iniciar uma sessão seamless antes da reconciliação desse saldo.

Sucesso: HTTP 200 `{ "success": true, "data": { ... } }`.
Erro: `{ "success": false, "error": { "code": 503, "message": "..." } }`.

400 pedido inválido; 401 chave inválida/revogada; 403 permissão ou domínio; 404 recurso inexistente; 409 conflito/configuração incompatível; 429 limite; 503 callback não configurado, carteira indisponível ou adaptador inexistente. Limite técnico: 300 pedidos por minuto/IP.

## 3. Jogador e lançamento

`POST /players`:

```json
{"playerId":"cliente_42","username":"Jogador","currency":"MZN"}
```

A conta identifica o jogador no operador. O mesmo ID em dois operadores ou ambientes representa contas distintas. A moeda não pode ser alterada. Repetir a criação com a mesma moeda devolve a conta existente. Moedas: MZN, BRL, EUR, USD, com duas casas decimais. Montantes são inteiros em unidades mínimas: `1000 = 10.00`. Não há conversão cambial.

`POST /games/launch`:

```json
{"playerId":"cliente_42","game":"race","parentOrigin":"https://casino.operador.example","currency":"MZN"}
```

Antes de emitir o ticket, a ReelSparkz valida operador, produto, domínio, moeda e callback de saldo. Falha de carteira impede o lançamento; nunca substitui o saldo por dinheiro fictício.

Resposta inclui `launchUrl`, `expiresIn: 60`, `walletModel: seamless`, `environment: live|sandbox`, `mode: seamless|seamless-sandbox`, `currency`. Abrir `launchUrl` no iframe do domínio autorizado. A sessão do jogo expira em uma hora. Bloquear operador, domínio ou produto revoga o acesso; a liquidação de apostas já aceites continua para não perder prémios.

## 4. Callbacks ReelSparkz → operador

Todos são HTTP POST, com JSON e os headers:

- `X-Wallet-Timestamp`: Unix em milissegundos.
- `X-Wallet-Signature`: HMAC-SHA256 hexadecimal.

Assinatura sobre **timestamp + ponto + bytes UTF-8 exatos do corpo recebido**, usando `signingSecret`:

```js
createHmac('sha256', signingSecret).update(timestamp + '.' + rawBody).digest('hex')
```

Comparar em tempo constante e rejeitar timestamps fora de uma janela de 5 minutos. Validar `operatorId`, `environment`, `sandbox`, jogador e moeda. Toda a mensagem, incluindo ambiente, montante e fim de ronda, está assinada. Não usar a chave API como segredo HMAC.

| Endpoint no operador | Função |
|---|---|
| POST {callbackUrl}/balance | Consultar saldo, sem alterar a carteira |
| POST {callbackUrl}/debit | Debitar aposta antes de a aceitar |
| POST {callbackUrl}/credit | Creditar prémio; receber e aceitar também valor zero |
| POST {callbackUrl}/rollback | Devolver um débito confirmado tarde demais para aceitar a aposta |

Saldo:

```json
{"transactionId":"consulta-unica","playerId":"cliente_42","currency":"MZN","operatorId":"id-do-operador","environment":"live","sandbox":false,"type":"balance","roundEnd":false}
```

Movimento (exemplo de débito):

```json
{
  "transactionId":"bet-uuid:debit",
  "betId":"bet-uuid",
  "roundId":"bet-uuid",
  "gameRoundId":123,
  "game":"race",
  "playerId":"cliente_42",
  "amountMinor":1000,
  "currency":"MZN",
  "parentOrigin":"https://casino.operador.example",
  "referenceTransactionId":"bet-uuid:debit",
  "operatorId":"id-do-operador",
  "environment":"live",
  "sandbox":false,
  "type":"bet",
  "roundEnd":false
}
```

Os montantes são **não negativos**. `debit` subtrai; `credit` e `rollback` adicionam. `type` é `bet`, `win` ou `refund`. `roundId` identifica a ronda financeira de uma aposta; `gameRoundId` identifica a ronda coletiva do motor. Duas apostas numa corrida têm rondas financeiras diferentes. O crédito ou reembolso final traz `roundEnd: true`.

Resposta HTTP 200 obrigatória em todos os callbacks:

```json
{"ok":true,"transactionId":"bet-uuid:debit","balanceMinor":99000,"currency":"MZN"}
```

Devolver a mesma `transactionId` recebida, moeda exata e saldo inteiro não negativo. Saldo insuficiente em debit: HTTP 402:

```json
{"ok":false,"code":"INSUFFICIENT_FUNDS","transactionId":"bet-uuid:debit","balanceMinor":500,"currency":"MZN"}
```

Não devolver sucesso antes de gravar a operação de forma durável.

## 5. Idempotência e falhas

O operador deve, numa única transação de base de dados:

1. Consultar o movimento por `(operatorId, environment, transactionId)`.
2. Se já existir, validar que o conteúdo financeiro é igual e devolver a resposta original, sem movimentar saldo novamente.
3. Bloquear/atualizar a carteira correta, validar moeda e saldo, aplicar o movimento e guardar a resposta juntamente com o débito/crédito.
4. Em rollback, validar `referenceTransactionId`; não reembolsar mais do que uma vez o mesmo débito. Registar também callbacks de crédito zero para fechar a ronda.

Um timeout é um resultado desconhecido: a operação pode já ter sido aplicada no operador. A ReelSparkz mantém a operação pendente e repete a mesma `transactionId`, com timestamp/assinatura novos. Há fila persistente e recuo exponencial até 60 segundos, sem descartar automaticamente prémios pendentes. O processamento faz lotes de até 12 operações.

Timeout de comunicação: 5 segundos. Erro HTTP, corpo inválido, moeda divergente ou saldo inválido deixam a operação pendente. Uma recusa 402/INSUFFICIENT_FUNDS encerra a aposta como recusada. Se o débito for confirmado após o prazo da aposta, a ReelSparkz envia rollback. Prémios só ficam liquidados após confirmação do callback credit.

## 6. Sandbox e histórico

Sandbox usa configuração e segredo próprios e envia `environment: sandbox`, `sandbox: true`. O operador deve usar exclusivamente a sua carteira de teste para esses callbacks. Não aplicar chamadas sandbox ao saldo real. Não há fallback local nem carregamento automático na ReelSparkz.

Histórico e transações: `?limit=50&offset=0`, máximo 200, por jogador/operador/ambiente. Datas em milissegundos Unix. Histórico inclui aposta, prémio, moeda, estado e `roundEnd`. Transações incluem ID estável, tipo, montante, tentativas e estado `pending|done`. O saldo atual deve ser consultado em `/platform/balance`, não deduzido de páginas incompletas do histórico.

## Imagens dos jogos

`GET /api/v1/games` devolve `imageUrl` em cada jogo do catálogo habilitado. É um URL absoluto HTTPS em produção, público e sem chave ou sessão, utilizável diretamente em `<img src="...">` noutra aplicação. As imagens também permitem leitura cross-origin (CORS).

Exemplo de campos de um jogo:

```json
{
  "id": "race",
  "name": "Corrida dos Chapas",
  "imageUrl": "https://bet77-arcade-mz.fly.dev/game-images/race.webp",
  "walletModel": "seamless",
  "launchSupported": true,
  "environment": "live"
}
```

A resposta real inclui um parâmetro de versão `?v=...`, alterado quando o ficheiro muda. Usa o URL devolvido, sem construir o caminho a partir do nome do jogo. Os formatos podem ser WebP, PNG, JPEG ou SVG, com proporções diferentes. Ter imagem não indica disponibilidade para apostar: verifica sempre `launchSupported`. Sportsbook usa uma ilustração genérica e continua sem adaptador de lançamento.

## 7. Jogos disponíveis e limites desta versão

Adaptadores seamless: **Combustível (motor de duração fixa), Corrida dos Chapas, Banca da Sorte, Raspa Moçambique, Valeu Boi e Fortune Tiger independente**, sujeitos à habilitação por operador e compatibilidade da configuração RTP.

Sportsbook não tem adaptador seamless nesta versão. O catálogo identifica suporte e rejeita lançamentos sem adaptador. Habilitar um produto não cria automaticamente o adaptador financeiro.

Tiger independente: português, moedas MZN/BRL/EUR/USD, RTP alvo de 94% (calculado: 93,99999932%, incluindo a ronda gratuita), versão matemática `tiger-independent-94-v1`. Não é o motor oficial da PGSoft. O ID de catálogo `fortune-tiger-demo` mantém-se por compatibilidade; o modo financeiro é definido pela chave e pela sessão, não pelo nome do ID. Cada giro é uma ronda financeira; a continuação gratuita envia débito de valor zero, que o operador deve aceitar sem retirar saldo. O callback credit, incluindo valor zero, encerra essa ronda. Máximo por giro: 1.500×; máximo do ciclo pago mais gratuito: 2.500×.

Raspa: MZN, RTP teórico de 95%, apostas de 1.000 a 50.000 unidades menores, em múltiplos de 100. Débito e prémio (incluindo prémio zero) são liquidados na compra; raspar apenas revela a carta e não volta a creditar.

Valeu Boi: RTP configurado de 94%, com arredondamento das cotações e pagamentos que pode reduzir o retorno efetivo; apostas de 100 a 10.000 unidades menores. A corrida começa após confirmação do débito e dura 12 segundos. A liquidação continua mesmo se o jogador fechar a página.

Estes adaptadores guardam o resultado antes de contactar a carteira, sem o revelar antes da confirmação financeira. A mesma chave de idempotência repete a jogada original. Um débito confirmado após 15 segundos é reembolsado; débitos ou créditos com resposta incerta são repetidos com o mesmo transactionId. Uma ronda pendente bloqueia novas jogadas nesse jogo para o jogador.

É possível entregar este contrato a qualquer operador. Para operar, cada integrador precisa implementar os quatro callbacks, configurar o URL/segredo e completar testes de integração com a sua própria carteira. Publicar a API não significa que uma carteira externa já esteja ligada.

## Iframe móvel: tamanho e navegação do operador

Os jogos detetam automaticamente quando estão num iframe e usam um layout compacto. O operador deve atribuir ao iframe a **altura visível disponível**, descontando o cabeçalho e a navegação fixa inferior. Uma altura fixa de 800px dentro de um ecrã mais pequeno deixa os controlos atrás da navegação do site.

O módulo público abaixo calcula a altura disponível e acompanha mudanças de orientação e do teclado. Executar **na página do operador** depois de inserir o iframe; não dentro do jogo. Usar o `launchUrl` devolvido pelo backend, sem expor a chave API.

```js
import { fitNovaArcadeFrame } from 'https://bet77-arcade-mz.fly.dev/iframe-resize.js';
const cleanup = fitNovaArcadeFrame(document.querySelector('#nova-game'), {
  bottomElement: document.querySelector('#mobile-navigation')
});
// Ao desmontar a página: cleanup();
```

Substituir os seletores pelos elementos reais da aplicação. Se a página tiver muito conteúdo acima do iframe, usar um ecrã dedicado de jogo com um botão de voltar compacto. O módulo não remove nem altera a navegação do operador.

### Atualizar o saldo do site durante o jogo

O iframe emite `postMessage({type: 'novaarcade:wallet-changed', version: 1})` quando o saldo apresentado muda. O evento não contém valores financeiros nem credenciais. O site deve voltar a consultar o seu backend e atualizar o saldo visível. Validar sempre a origem e `event.source`; o helper faz estas duas verificações:

```js
import { onNovaArcadeWalletChange } from 'https://bet77-arcade-mz.fly.dev/iframe-resize.js';
const unsubscribe = onNovaArcadeWalletChange(document.querySelector('#nova-game'), async () => {
  await refreshMyAccountBalance(); // Função da aplicação do operador.
});
// Ao sair do jogo: unsubscribe();
```

Os jogos usam ecrã fixo, sem scroll na área principal. Regras e histórico abrem em painéis sobrepostos; esses painéis podem rolar quando o texto não cabe no ecrã.