Overview
Callisto Minis are short multiplayer games: two to four players at a table, each stakes, the game decides, and the pot less the configured rake goes to the winners. You launch your players into a game; the provider calls your wallet for every stake and every payout. The integration is two pieces:
- The operators’ API, which you call: launch a player, list your games, read rounds.
- Your wallet, which we call:
balance,bet,win,refund.
Environments
| Operators’ API | OpenAPI | |
|---|---|---|
| Sandbox | https://sandbox-api.callistominis.com/v1 | /v1/openapi.json |
| Production | https://api.callistominis.com/v1 | /v1/openapi.json |
A sandbox key works in the sandbox only. You move to production with a new key.
Signing
Every call, both ways, carries three headers:
X-Minis-Key: key_id
X-Minis-Timestamp: unix seconds, within 5 minutes of the receiver’s clock
X-Minis-Signature: hex_lowercase(HMAC_SHA256(secret, timestamp + "." + payload)) payload is the raw body exactly as sent. A request without a body signs its request line, for example GET /v1/games or GET /v1/players/p-1/rounds?limit=10. Our calls to your wallet are signed with your key, so you check them with the same secret. Keep the secret on your server only; you can hold two active keys, so you rotate by asking for a second one and dropping the first.
import { createHmac } from 'node:crypto'
const timestamp = Math.floor(Date.now() / 1000).toString()
const body = JSON.stringify(payload) // exactly the bytes you send
const signature = createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex')
headers['X-Minis-Key'] = keyId
headers['X-Minis-Timestamp'] = timestamp
headers['X-Minis-Signature'] = signature Operators’ API
Answers are { "data": ... }.
| Route | What it does |
|---|---|
GET /v1/ping | Who the signature says you are: operator_code, environment. |
GET /v1/games | The games your players can open now: game_id, name, type, min_seats, max_seats, real_money. |
POST /v1/sessions | Open a game for a player; answers the game_url. |
POST /v1/games/{game_id}/demo | A demo with coins and bots, no wallet. Body { "language": "en" } or empty. |
GET /v1/rounds/{round_id} | One round with the seats of your players: session_id, player_alias, stake, rake, payout. Other operators’ players at a shared table are not shown. |
GET /v1/players/{player_id}/rounds | A player’s rounds, newest first, by your own id for the player; offset, limit (50). Answers { items, total }. |
Game ids: snake, sharks, bombs, seabattle.
Launching a player
POST /v1/sessions
{
"game_id": "snake",
"player_id": "p-1",
"player_name": "Ann",
"currency": "EUR",
"token": "<your token for this player>",
"language": "en",
"exit_url": "https://casino.example/lobby",
"cashier_url": "https://casino.example/cashier"
}
{
"data": {
"session_id": "6f1c…",
"game_url": "https://snake.callistominis.com/?session=…",
"expires_at": "2026-10-08T22:00:00Z"
}
}
Open game_url in the player’s frame. The token you pass comes back to your wallet with every call of this session, so you know which player and session a bet belongs to.
Your wallet
The provider calls POST {wallet_url}/{call} with JSON in snake_case, signed with your key. Check the signature and the timestamp before anything else, and refuse a bad one with 401 and an error. Every call carries session_id, token, player_id and currency.
Call Does balanceNothing; answers the balance. betTakes amount. winPays amount, which may be 0. refundGives back the bet bet_transaction_id.
Money calls add:
transaction_idThe provider’s id of this movement, a UUID. The same id twice is the same movement. round_idThe round it belongs to. game_idThe game. amountDecimal, two places, never negative. bet_transaction_idOn win and refund: the bet it closes.
POST {wallet_url}/bet
{
"session_id": "6f1c…",
"token": "<your token>",
"player_id": "p-1",
"currency": "EUR",
"transaction_id": "0b7e…",
"round_id": "snake-…",
"game_id": "snake",
"amount": 5.00
}
Wallet answers
- Done: 2xx with the balance and your id for the movement.
- Refused: 4xx with
error and message. The provider understands insufficient_funds and invalid_token; any other code is a refusal too. - Anything else (5xx, no JSON, no answer within 5 seconds) is unknown: you may or may not have done it.
200 { "balance": 995.00, "transaction_id": "<your id for it>" }
402 { "error": "insufficient_funds", "message": "Not enough money" }
Wallet rules
- Idempotent by
transaction_id. A call you have already done is answered as done, with the same answer, and moves nothing. The provider repeats calls whose answer it did not get. - A refund of a bet you never received succeeds and moves nothing. It happens when your answer to a bet was lost.
- Never refuse a win or a refund short of a broken request. A refused credit goes to manual handling; an unknown one is retried, 5 seconds at first, doubling, at most an hour apart, until it gets through.
- A bet with no answer is given up. After 5 seconds the round does not start; after two minutes the provider sends a
refund for that bet, in case you did take it. - Stakes of a round are all or none. When another player’s bet is refused, yours is refunded.
Money in a round
Every player’s stake is taken before the round starts. When the game ends, each player gets a win, of 0 when they lost; a cancelled round gives each stake back as a refund. The rake is a percentage of the pot configured for your operator, and a payout never exceeds the pot less the rake. In the shared pool your players may win money staked by another operator’s players; who owes whom is settled monthly on your invoice.
Errors
Every refusal of the operators’ API is { error, message, retryable, fields? }. Retry only when retryable is true.
Status errorWhen 401 unauthorizedSignature missing or wrong, a revoked key, a suspended operator; retryable only for the clock. 404 not_foundAn unknown game, player or round, or not yours. 409 currency_not_enabled, game_not_available, … or conflictA business refusal. 422 validation_failed, with fieldsThe request is malformed. 503 unavailableA service behind did not answer; retryable.
Self-test and going live
The integration kit has a self-test that signs like we do and checks your wallet: signature and clock refusals, balance, bet, win, refund, repeats, a refund of a bet you never received, insufficient funds, a foreign token, and that each answer comes within 5 seconds. It moves a few units of the test player’s money and leaves the balance where it found it.
node selftest/wallet-selftest.mjs --wallet-url https://wallet.example/minis \
--key-id <key_id> --secret <secret> \
--player <a test player> --token <a valid token of that player> --currency EUR
- Every self-test line is PASS.
- On the sandbox, launch two players of yours and play a round in two browsers; your wallet sees two bets and two wins, and
GET /v1/rounds/{round_id} agrees. - Stop your wallet during a bet: the round does not start, and the bet is refunded within two minutes.
- We move you to production with a new key.