# 19flip Integration Docs
> How a casino integrates with the 19flip game provider.
19flip is a B2B game provider. Casinos (operators) integrate once with the 19flip API and get access to all games. 19flip launches game sessions and moves money through the casino's wallet callbacks.
# Start here
# 19flip integration docs
19flip is a B2B game provider. A casino (in these docs: an **operator**) integrates once with the 19flip API and gets access to every 19flip game. These pages describe that integration completely: how to launch a game, the wallet callbacks you implement, and the rules that keep money correct. They are written for casino developers and for LLM agents.
## How it works
The casino calls 19flip to **launch a game** for a player and gets back a `launch_url` to open in the browser. 19flip never holds player money. Whenever a game needs to read a balance or move money, 19flip calls the casino's **wallet callbacks**: five `POST` endpoints under the casino's `callback_url` (`/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback`, `/win/rollback`). Every callback is signed with the casino's secret key and carries an `idempotency_key` that the casino must use to deduplicate.
```
flowchart LR
Casino[Casino backend] -- "POST /api/operator/v1/launch-game
Api-Key" --> Provider[19flip API]
Player[Player browser] -- "opens launch_url" --> Game[19flip game]
Provider -- "POST {callback_url}/balance, /bet/debit, ...
X-Signature" --> Casino
```
The diagram in words: the casino backend calls the 19flip API to launch a game; the player's browser opens the returned `launch_url`; for every balance read and money movement 19flip calls the casino's wallet callbacks.
The 19flip API is `https://games.19flip.com` (since 2026-10-26). The former address `https://19flip.site` keeps working: it redirects API calls with `308`, which keeps the method, the body and the headers, and pages with `301`; switch your configuration to the new address all the same. Your API key, secret key and the settings of your account are issued by the 19flip team.
## Read in this order
1. [Quickstart](https://docs.19flip.com/operator-integration/quickstart/index.md) – the whole integration on one page.
1. [Authentication](https://docs.19flip.com/operator-integration/authentication/index.md) – `Api-Key`, IP allowlist, key rotation, and how to verify `X-Signature` on callbacks.
1. [Launch game](https://docs.19flip.com/operator-integration/launch-game/index.md) – `POST /api/operator/v1/launch-game`.
1. [Wallet callbacks](https://docs.19flip.com/operator-integration/wallet-callbacks/index.md) – the five endpoints you implement, with exact JSON.
1. [Idempotency & retries](https://docs.19flip.com/operator-integration/idempotency-and-retries/index.md) – the rules that keep money correct.
1. [Errors](https://docs.19flip.com/operator-integration/errors/index.md) – what we return to you and what you should return to us.
1. [Testing](https://docs.19flip.com/operator-integration/testing/index.md) – self-test checklist and the connection test.
1. [Game specifics](https://docs.19flip.com/operator-integration/game-specifics/index.md) – what differs between games.
1. [Live banners](https://docs.19flip.com/operator-integration/live-banners/index.md) – optional lobby widget with the current draw.
## For LLM agents
Machine-readable indexes are published at the site root: `/llms.txt` (page list with descriptions) and `/llms-full.txt` (all pages concatenated). Prefer them over crawling HTML.
# Operator integration
# Authentication
How your calls to 19flip are authenticated, and how you verify that a wallet callback really comes from 19flip. There are two keys: `api_key` authenticates both directions, `secret_key` signs every callback we send you.
## Your keys
| Key | Used by you | Used by 19flip |
| ------------ | --------------------------------------------- | ----------------------------------------------------------------------------- |
| `api_key` | Sent in the `Api-Key` header on `launch-game` | Sent back to you in `Authorization: Bearer` and `X-API-Key` on every callback |
| `secret_key` | Used to verify `X-Signature` on callbacks | Used to compute `X-Signature` on callbacks |
Both are issued by 19flip. Keep them server-side. There is no separate "provider key": the key in our callbacks is **your own** `api_key`.
## Calls from you to 19flip
### `Api-Key` header
Every request to the operator API must carry your key:
```
POST /api/operator/v1/launch-game HTTP/1.1
Host: {PROVIDER_HOST}
Content-Type: application/json
Api-Key: YOUR_API_KEY
```
The header name is `Api-Key` (HTTP header names are case-insensitive). `Authorization: Bearer` is **not** accepted on the operator API. You do not need to sign requests to the operator API. An `X-Signature` header on `launch-game` is ignored.
The checks run in this order:
| Check | Failure | HTTP | `code` | `detail` |
| --------------------------------------------------------------------------------- | ---------------------- | ---- | -------------------- | ------------------------------------------------- |
| Header present | missing | 401 | `AUTH_ERROR` | `Api key not provided` |
| Key matches your current key, or your previous key during a rotation grace period | unknown or expired key | 401 | `AUTH_ERROR` | `Operator not found` |
| Caller IP is in your IP allowlist (if you have one) | IP not listed | 403 | `IP_NOT_ALLOWED` | `IP 203.0.113.7 is not allowed for this operator` |
| Your account is `ACTIVE` | account suspended | 403 | `OPERATOR_SUSPENDED` | `Operator is suspended` |
### IP allowlist
- The allowlist is a list of IP addresses or CIDR ranges (for example `203.0.113.0/24`, `2001:db8::/32`). A single IP is stored as a `/32` (or `/128`).
- An **empty** allowlist allows any IP.
- The caller IP is the address our edge proxy sees for your connection. If you call us through your own proxy or NAT, list its public egress address.
- The allowlist applies to your calls **to** 19flip. It does not affect callbacks.
### Key rotation
19flip can rotate your `api_key`, your `secret_key`, or both.
| What is rotated | Your calls to us (`Api-Key`) | Our callbacks to you |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `api_key` | The **previous** key keeps working for a grace period (default **24 hours**; set per rotation between 0 and 720 hours; 0 means it stops immediately). After that it is rejected with `401 Operator not found`. | Carry the **new** key in `Authorization` / `X-API-Key` **immediately**. |
| `secret_key` | Not affected. | Are signed with the **new** secret **immediately**. There is no grace period. |
To rotate without failed callbacks, for the duration of the switch accept **both** the old and the new `api_key` on callbacks and verify `X-Signature` against **both** the old and the new `secret_key`. Agree the rotation time with 19flip.
## Callbacks from 19flip to you
Every wallet callback is an HTTP `POST` with these headers:
| Header | Value |
| --------------- | ------------------------------------------------------------------ |
| `Content-Type` | `application/json` |
| `Authorization` | `Bearer ` |
| `X-API-Key` | `` |
| `X-Signature` | `hex(sha256(raw_body + secret_key))`, lowercase hex, 64 characters |
Check the key in either `Authorization` or `X-API-Key` (they carry the same value), **and** check the signature. The signature is what proves the body was not changed.
### Signature algorithm
```
X-Signature = lowercase_hex( SHA-256( raw_body_bytes || utf8(secret_key) ) )
```
- `raw_body_bytes` are the exact bytes of the HTTP request body as received.
- The secret is appended **after** the body. There is no separator, no HMAC, no timestamp.
- The body is always JSON and always non-empty on callbacks, so `X-Signature` is always present.
Verify against the raw bytes
Hash the body exactly as it arrived. Do **not** parse the JSON and serialize it again before hashing: key order, spaces and number formatting would change and the signature would never match. Our bodies are serialized with a space after `:` and `,` (for example `{"player_id": "player-123", "currency": "USD"}`) and non-ASCII characters escaped as `\uXXXX`. You do not need to rely on this – just hash what you received.
### Test vector
Use this to check your implementation.
- `secret_key`: `my-secret-key`
- Raw body (75 bytes, no trailing newline):
```
{"player_id": "player-123", "currency": "USD", "session_id": "healthcheck"}
```
- Expected `X-Signature`:
```
db1f31056abe973a2928255330023db0d9af60b293f79fdef524df8296f7e32a
```
A second vector, a bet debit body with the same secret:
```
{"player_id": "player-123", "amount": 1.5, "currency": "USD", "round_id": 48213, "session_id": "3f6c2a9e-8d41-4b7a-9c55-1e2f0a7b9d10", "idempotency_key": "bb-tkt-7-48213-9921", "game_id": "bingo-bongo", "transaction_id": null}
```
```
f14b64c92d180da3c164bcda1a38a0f9cc02d49b7c2113675c428d6f5a8826e6
```
### Verification in Python
```
import hashlib
import hmac
import json
from fastapi import FastAPI, HTTPException, Request
API_KEY = "YOUR_API_KEY"
SECRET_KEYS = ["YOUR_SECRET_KEY"] # add the old secret here during a rotation
app = FastAPI()
def signature_ok(raw_body: bytes, received: str) -> bool:
for secret in SECRET_KEYS:
expected = hashlib.sha256(raw_body + secret.encode("utf-8")).hexdigest()
if hmac.compare_digest(expected, received.lower()):
return True
return False
def api_key_ok(request: Request) -> bool:
auth = request.headers.get("authorization", "")
bearer = auth[7:] if auth.startswith("Bearer ") else None
key = bearer or request.headers.get("x-api-key")
return key is not None and hmac.compare_digest(key, API_KEY)
async def verified_json(request: Request) -> dict:
raw = await request.body() # exact bytes, before any parsing
if not signature_ok(raw, request.headers.get("x-signature", "")):
raise HTTPException(status_code=401, detail="Invalid signature")
if not api_key_ok(request):
raise HTTPException(status_code=401, detail="Invalid API key")
return json.loads(raw)
@app.post("/api/providers/19flip/balance")
async def balance(request: Request):
body = await verified_json(request)
...
```
```
import hashlib
import hmac
def verify(raw_body: bytes, x_signature: str, secret_key: str) -> bool:
expected = hashlib.sha256(raw_body + secret_key.encode("utf-8")).hexdigest()
return hmac.compare_digest(expected, x_signature.lower())
raw = b'{"player_id": "player-123", "currency": "USD", "session_id": "healthcheck"}'
assert verify(raw, "db1f31056abe973a2928255330023db0d9af60b293f79fdef524df8296f7e32a", "my-secret-key")
```
### Verification in Node.js
```
const crypto = require("crypto");
const express = require("express");
const API_KEY = process.env.NINETEENFLIP_API_KEY;
const SECRET_KEYS = [process.env.NINETEENFLIP_SECRET_KEY]; // add the old secret during a rotation
const app = express();
// Keep the raw bytes: use express.raw, NOT express.json, on these routes.
const rawJson = express.raw({ type: "application/json", limit: "1mb" });
function signatureOk(rawBody, received) {
if (typeof received !== "string" || received.length !== 64) return false;
const got = Buffer.from(received.toLowerCase(), "utf8");
return SECRET_KEYS.some((secret) => {
const expected = crypto
.createHash("sha256")
.update(Buffer.concat([rawBody, Buffer.from(secret, "utf8")]))
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected, "utf8"), got);
});
}
function apiKeyOk(req) {
const auth = req.get("authorization") || "";
const key = auth.startsWith("Bearer ") ? auth.slice(7) : req.get("x-api-key");
return typeof key === "string" && key === API_KEY;
}
function verified(req, res, next) {
if (!signatureOk(req.body, req.get("x-signature"))) {
return res.status(401).json({ error: "Invalid signature" });
}
if (!apiKeyOk(req)) {
return res.status(401).json({ error: "Invalid API key" });
}
req.payload = JSON.parse(req.body.toString("utf8"));
next();
}
app.post("/api/providers/19flip/balance", rawJson, verified, async (req, res) => {
const { player_id, currency, session_id } = req.payload;
// ...
});
```
```
const crypto = require("crypto");
function verify(rawBody /* Buffer */, xSignature, secretKey) {
const expected = crypto
.createHash("sha256")
.update(Buffer.concat([rawBody, Buffer.from(secretKey, "utf8")]))
.digest("hex");
const got = Buffer.from(String(xSignature).toLowerCase(), "utf8");
const exp = Buffer.from(expected, "utf8");
return got.length === exp.length && crypto.timingSafeEqual(got, exp);
}
const raw = Buffer.from('{"player_id": "player-123", "currency": "USD", "session_id": "healthcheck"}', "utf8");
console.log(verify(raw, "db1f31056abe973a2928255330023db0d9af60b293f79fdef524df8296f7e32a", "my-secret-key")); // true
```
### Common mistakes
| Mistake | Symptom |
| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Hashing `JSON.stringify(req.body)` / `json.dumps(parsed)` instead of the raw bytes | Every signature fails |
| A body-parsing middleware consumed the stream before your handler | Raw body is empty or re-encoded |
| Hashing `secret + body` instead of `body + secret` | Every signature fails |
| Using HMAC-SHA256 instead of plain SHA-256 | Every signature fails |
| Comparing with `==` | Works, but leaks timing; use a constant-time compare |
| Rejecting callbacks right after an `api_key` or `secret_key` rotation | Callbacks fail until you switch keys; accept both during the switch |
### What to return when authentication fails
Answer `401` with any JSON body. We treat 4xx as a definitive refusal: the call is **not retried** and the operation is recorded as failed on our side. Make sure a key or signature problem is visible in your monitoring – it stops all money movement for your players.
# Errors
Two directions, two sets of rules: the errors 19flip returns to you on `launch-game`, and the HTTP statuses your wallet should return to 19flip on callbacks. Getting the second part right decides whether a player sees "insufficient funds" or "technical error", and whether money operations are retried.
## Errors returned by 19flip
### Response format
Every error from the operator API has the same JSON body:
```
{
"success": false,
"code": "GAME_UNAVAILABLE",
"traceback": "Xk3P9qLm2Z",
"detail": "Game block-boost is under maintenance",
"errors": null
}
```
| Field | Type | Description |
| ----------- | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `success` | boolean | Always `false`. |
| `code` | string | Stable machine-readable code (table below). Branch on this and on the HTTP status. |
| `traceback` | string | 10-character id of this error in our logs. Quote it when contacting 19flip support. |
| `detail` | string or null | Human-readable message. Wording may change; do not parse it. |
| `errors` | array or null | Only for request validation errors: `[{"loc": ["body", ""], "msg": ""}]`. Otherwise `null`. |
### Codes
| HTTP | `code` | Returned on `launch-game` when | Retry? |
| ---- | ------------------------ | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| 400 | `REQUEST_ERROR` | Invalid body; unknown `game_id`; currency not supported or not allowed; RTP not allowed | No – fix the request |
| 401 | `AUTH_ERROR` | `Api-Key` missing or unknown | No – check the key |
| 403 | `IP_NOT_ALLOWED` | Caller IP not in your allowlist | No – ask 19flip to update the allowlist |
| 403 | `OPERATOR_SUSPENDED` | Your account is suspended | No – contact 19flip |
| 403 | `GAME_UNAVAILABLE` | Game disabled, under maintenance, not configured, or switched off for your account | Later (maintenance) |
| 403 | `PLAYER_BLOCKED` | The player is blocked by 19flip (`LIVE` only) | No |
| 502 | `EXTERNAL_SERVICE_ERROR` | The game refused or could not open the session; `detail` starts with `Game API error:` | Yes, if the game was unreachable; no, if the game refused (e.g. `DEMO` in Bingo Bongo) |
| 500 | `UNKNOWN_ERROR` | Unexpected failure. Also returned for a wrong path or HTTP method (`detail` `Not Found` / `Method Not Allowed`) | Yes, with backoff |
Other codes exist in the platform (`INSUFFICIENT_FUNDS` 402, `BET_LIMIT_EXCEEDED` 400, `NOT_FOUND` 404, `FORBIDDEN` 403, `CONFLICT` 409, `TOO_MANY_REQUESTS` 429). They are used between 19flip and its games and are not returned by `launch-game`.
The [live banner](https://docs.19flip.com/operator-integration/live-banners/index.md) endpoints use the same envelope. `GET /api/operator/v1/banners` can return `AUTH_ERROR`, `IP_NOT_ALLOWED` and `OPERATOR_SUSPENDED` as above. Both state endpoints return `404 NOT_FOUND` for an unknown key, an unknown or disabled banner, or a currency you cannot use, and `400 REQUEST_ERROR` for a `currency` that is not three letters. When the game is down, the state endpoints do not fail. They return `200` with `"status": "unavailable"`.
The [round history](https://docs.19flip.com/operator-integration/round-history/index.md) endpoints use the same envelope and the same `AUTH_ERROR`, `IP_NOT_ALLOWED` and `OPERATOR_SUSPENDED` codes. `GET /api/operator/v1/rounds/{game_round_id}` returns `404 NOT_FOUND` for a round that does not exist or has not finished; a missing `game_id` or a bad parameter returns `400 REQUEST_ERROR`.
The full list of `launch-game` conditions with example `detail` values is in [Launch game](https://docs.19flip.com/operator-integration/launch-game/#checks-and-errors).
## What your wallet should return
Your answer to a wallet callback is interpreted by its **HTTP status** first:
| Situation | Your HTTP status | Body | What 19flip does |
| ---------------------------------------------------------------------------------------------- | ---------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------- |
| Operation applied, or already applied under this `idempotency_key` | `200` | Transaction or balance response | Success |
| Player cannot afford the bet (or a win rollback, if you refuse negative balances) | **`402`** | `{"error": "insufficient balance"}` | Final. The game is told `INSUFFICIENT_FUNDS` and can offer the player a top-up. Not retried. |
| Bad signature or API key | `401` | `{"error": "Invalid signature"}` | Final. Recorded as failed. Not retried. |
| Unknown player | `404` | `{"error": "player not found"}` | Final. Not retried. |
| Any other refusal (validation, player restricted, currency mismatch) | `400`, `403`, `409` or `422` | `{"error": ""}` | Final. Not retried. |
| You could not process the request and **did not** apply it (database down, dependency timeout) | `500` / `503` | anything | Retried (up to 3 attempts in total), then outcome unknown |
| You are not sure whether you applied it | `500` / `503` | anything | Same as above. Deduplication makes the retry safe. |
Rules:
- **Use `402` for insufficient funds.** It is the only signal that reliably reaches the game as "insufficient funds".
- **Never answer `200` with `"status": "FAILED"` to decline.** 19flip passes a `2xx` to the game as a completed call, and some games treat any `2xx` as money moved. Declines must be `4xx`.
- **Never answer `4xx` if you might have applied the operation.** `4xx` means "definitely not applied" and is not retried.
- **Never answer `5xx` for a request you have refused on purpose.** It will be retried three times and then escalated as an unknown outcome.
### How 19flip reads a 4xx body
19flip takes the error text from the `message` field of your JSON body, else from `error`, else the whole body. Any `4xx` is treated as **insufficient funds** when either:
- the status is `402`, or
- that text contains `insufficient` or `balance` (case-insensitive).
Keep the word `balance` out of other errors
A `404` with `{"error": "Player balance not found"}` or a `400` with `{"message": "Invalid balance currency"}` is classified as insufficient funds, and the player is told to top up. Word other refusals without `balance` or `insufficient`.
All other `4xx` answers reach the game as a request error with the text `Provider error (): `.
### Timeouts and 5xx
`5xx`, timeouts (10 s) and connection errors are retried: 3 attempts in total with 1 s and 2 s pauses. If all attempts fail, the game gets `EXTERNAL_SERVICE_ERROR` and 19flip records the operation as **outcome unknown**. See [Idempotency & retries](https://docs.19flip.com/operator-integration/idempotency-and-retries/index.md) for how such operations are settled.
## What the player sees
| Wallet answer | Typical game behaviour |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `402` on `/bet/debit` | "Not enough funds" message; no bet placed |
| Other `4xx` on `/bet/debit` | Generic error; no bet placed |
| `5xx` / timeout on `/bet/debit` after all retries | Generic error; the bet's outcome is unknown until it is repeated, rolled back or resolved |
| `5xx` / timeout on `/win/credit` after all retries | Outcome unknown. Bingo Bongo keeps the win as owed and repeats it later with the same key; Block Boost and Mister Twister report an error in the round. The operation appears in 19flip's queue for manual resolution. |
| `4xx` on `/win/credit` | Treated as a **final refusal**. 19flip stores it under the `idempotency_key` and answers any repeat of that key itself, without calling you again. The player does not receive the win until it is settled manually (Bingo Bongo escalates such payouts to its staff). Never answer `4xx` to a valid win. |
# Game specifics
The integration contract is the same for every game, but games use it differently: which modes they support, which callbacks they send and when, and what arrives after the player has left. Read this before enabling a game for your players.
## Summary
| | Block Boost | Mister Twister | Bingo Bongo | Magic Jump | Flip Jet | Ну, подними! | Book of Dawn | Dacha Dogs | Dacha Fishing | Dacha Pickles |
| -------------------------- | ------------------------------------------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `game_id` | `block-boost` | `mister-twister` | `bingo-bongo` | `magic-double` | `flip-jet` | `egg-rush` | `book-of-dawn` | `dacha-dogs` | `dacha-fishing` | `dacha-pickles` |
| Type | Single player, skill + luck | Single player | Multiplayer bingo with a shared prize pool, jackpot and live host | Single player, instant: one bet = one round, pure chance | Multiplayer crash game: one shared round for all players, up to 2 bets per player per round | Single player crash game: the player starts their own round, optional Boost Start at ×2 / ×5 | Single player slot 5×3, 10 lines: one spin = one round, the whole free-spins bonus included; optional ante and bonus buy | Single player slot 5×4, 40 lines: one spin = one round, the whole «Грядки» free-spins bonus included; optional bonus buy | Single player slot 5×3, 10 lines: one spin = one round, the whole «Садок» free-spins bonus included; optional bonus buy | Single player slot 5×4, 40 lines with Hold & Win: one spin = one round, the whole «Закатка» bonus included; per-bet jars kept between sessions; optional bonus buy |
| `DEMO` mode | Yes | Yes | **No** – `launch-game` fails with `502 EXTERNAL_SERVICE_ERROR` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `rtp` | Whole number; currently `97`, `98` or `99` | Whole number; currently `97` | Not used (ignored) | Whole number `95`, `96`, `97` (default) or `98` | Only `97` (default); any other value fails the launch | Only `97` (default); any other value fails the launch | Only `97` (default); any other value fails the launch | Only `97` (default); any other value fails the launch | Only `97` (default); any other value fails the launch | Only `97` (default); any other value fails the launch |
| Bet amounts | Bet ladder from your limits | Bet ladder from your limits | Card price set per (your account, currency) by 19flip; ladder ignored | Bet ladder from your limits (**required**) | Any amount between the smallest and largest step of your ladder (**required**) | Bet ladder from your limits (**required**) | Bet ladder from your limits (**required**) | Bet ladder from your limits (**required**) | Bet ladder from your limits (**required**) | Bet ladder from your limits (**required**) |
| Max win limit | Not applied (the game caps a round at its own maximum multiplier) | Not applied (same) | Not applied | **Applied**: multipliers with `bet × multiplier > max_win` are refused | **Applied**: the bet is cashed out automatically when its win reaches `max_win` | **Applied**: the round is cashed out automatically when its win reaches `max_win` | **Applied**: a round is cut at `max_win` (and at 5000 × bet) | **Applied**: a round is cut at `max_win` (and at 5000 × bet) | **Applied**: a round is cut at `max_win` (and at 5000 × bet) | **Applied**: a round is cut at `max_win` (and at 10 000 × bet) |
| `lang` | Used in `launch_url` | Ignored | Used in `launch_url` (≤ 8 characters) | Used in `launch_url` | Used in `launch_url` | Used in `launch_url` | Used in `launch_url` | Used in `launch_url` | Used in `launch_url` | Used in `launch_url` |
| Launch token | Valid 24 h | Valid 24 h | **Single-use, valid 5 minutes** | Valid 24 h | Valid 24 h | Valid 24 h | Valid 24 h | Valid 24 h | Valid 24 h | Valid 24 h |
| Callbacks used | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback`, `/win/rollback` | same as Block Boost | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance` (when the game opens, then about every 5 s while the game tab is visible), `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) | `/balance`, `/bet/debit`, `/win/credit`, `/bet/rollback` (never `/win/rollback`) |
| `game_id` in callbacks | `null` | `null` | `"bingo-bongo"` | `"magic-double"` (rollbacks carry no `game_id`) | `"flip-jet"` (rollbacks carry no `game_id`) | `"egg-rush"` (rollbacks carry no `game_id`) | `"book-of-dawn"` (rollbacks carry no `game_id`) | `"dacha-dogs"` (rollbacks carry no `game_id`) | `"dacha-fishing"` (rollbacks carry no `game_id`) | `"dacha-pickles"` (rollbacks carry no `game_id`) |
| `idempotency_key` format | 64-char lowercase hex | 64-char lowercase hex | `bb----` | 64-char lowercase hex | `flipjet::DEBIT`, `flipjet::CREDIT`, `flipjet::ROLLBACK` | 64-char lowercase hex | 64-char lowercase hex | 64-char lowercase hex | 64-char lowercase hex | 64-char lowercase hex |
| Wins after the player left | No | No | **Yes**, routinely | Only when a credit failed and is retried (see below) | **Yes**: auto cashouts are paid even if the player closed the game | **Yes**: auto cashouts are paid even if the player closed the game | Only when a credit failed and is retried | Only when a credit failed and is retried | Only when a credit failed and is retried | Only when a credit failed and is retried |
Keys sent by 19flip staff when resolving an operation manually look like `manual-rb-` in any game.
## Block Boost
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, send `demo_balance`; without it the demo starts at `0` and the player cannot bet. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be a whole number accepted by the game (currently `97`, `98` or `99`). A value such as `96.5` makes the launch fail with `502`.
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours.
### Money flow (LIVE)
1. The player chooses a bet from the ladder and starts a round. No money moves yet.
1. Every paid action in the round (requesting a new set of shapes) is one `/bet/debit` of the round's bet amount. The game calls `/balance` right before each debit to check affordability. One round therefore has **one or more** debits, all with the same `round_id`.
1. The round ends either when the player takes the reward or when the board locks (no shape of the current set fits). In both cases the game calls `/balance` and then one `/win/credit` with the round's total winnings, if they are greater than `0`; a round with zero winnings ends without a credit.
1. Rollbacks happen only if the game fails internally right after your successful debit or credit. It then sends `/bet/rollback` or `/win/rollback` with your `transaction_id` as `original_transaction_id` and a `reason` such as `Rollback due to error after bet_debit: …`.
`game_id` and `transaction_id` are `null` in its debits and credits.
## Mister Twister
Mister Twister uses the same launch and money flow as Block Boost (several debits per round, one credit at the end, rollbacks only after an internal failure, `game_id` `null`, 64-character hex keys). Differences:
- `rtp`: currently only `97`.
- `lang` is not added to the launch URL (`…?token=`).
## Magic Jump
Changed in 2.21.0
Magic Double is renamed to **Magic Jump**. Only the display name changed: the `game_id` stays `magic-double`, nothing changes in your integration.
Changed in 2.17.0
The game is live in production (2026-10-08). It is switched on per casino by 19flip: ask your account manager if it is not in your catalogue yet.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be a whole number accepted by the game: `95`, `96`, `97` or `98` (default `97`). The game returns the same RTP for every multiplier, so the player's choice changes only the variance.
- A bet ladder for the currency is **required**: without one the launch fails.
- `max_win` from your limits is applied per round: the player cannot pick a multiplier whose `bet × multiplier` exceeds it. The largest possible win is `100 × bet`.
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into `ru` and `en`; any other `lang` shows English.
### Money flow (LIVE)
1. The player picks a bet from the ladder and a multiplier (x1 … x100) and plays. The debit is sent when the player presses STOP, not when the frame starts. Each round is exactly **one** `/bet/debit` of the bet amount, with its own `round_id`. A debit you decline (`402` or another `4xx`) ends the round with no further calls.
1. After your successful debit the game draws the outcome. On a win it sends one `/win/credit` of `bet × multiplier` with the same `round_id`; a lost round has no credit.
1. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, it sends `/bet/rollback` for that round with the debit's `idempotency_key` as `original_transaction_id` (19flip resolves it to your transaction) and reason `Debit outcome unknown: …`. The rollback is retried with the same key until it gets a definite answer, never sooner than a minute after the previous attempt, so a rollback can arrive a few minutes after the debit. Apply it whenever you have applied the debit, even if the debit reached you late.
1. If your credit fails, the game does not roll anything back: it repeats `/win/credit` with the same `idempotency_key` until it succeeds (at the next player action or in the background, never sooner than a minute after the previous attempt). Credits can therefore arrive after the player has left.
`game_id` is `"magic-double"` in debits and credits; `transaction_id` is `null`.
## Flip Jet
Added in 2.26.0
The game is live in production (2026-10-11). It is switched on per casino by 19flip: ask your account manager if it is not in your catalogue yet.
Flip Jet is a crash game. All players play the same round: bets are accepted during an 8-second countdown, then the plane takes off and the multiplier grows from 1.00x until the plane flies away at a random moment. A bet that is cashed out before that pays `bet × multiplier`. A bet that is not cashed out is lost. The crash point of every round is fixed before bets open and can be verified by the player after the round (provably fair). The RTP is 97 % for any cashout strategy.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks, and other players do not see demo bets.
- `rtp` must be `97` or omitted. Any other value fails the launch. All casinos play in the same room, so every player of the room shares one RTP.
- A bet ladder for the currency is **required**: without one the launch fails. The player can bet any amount (to 0.01) between the smallest and the largest step of the ladder. The ladder steps are only quick-pick amounts in the game.
- `max_win` from your limits is applied per bet. When `bet × multiplier` reaches `max_win`, the game cashes the bet out automatically at that point. A bet whose win would exceed `max_win` even at 1.01x is refused. Without `max_win`, the largest win is `10 000 × bet`.
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into `ru` and `en`.
### Shared round and what players see
- **One room for everyone.** Players of all casinos and all currencies play the same rounds. The "All bets" list shows other players' bets in their own currency, under a masked nickname: the first two and last two characters of your `player_id` (`an***73`). Never use e-mails, phone numbers or other personal data as `player_id`.
- Totals and the daily top are counted only in the viewer's currency.
- **Visual players (optional, off by default).** On request 19flip can turn on, for your casino only, a range of simulated players (for example 15–20). They add to the "playing" counter, and 40–80 % of them bet in each round: amounts from your ladder in the player's currency, some cash out before the crash, the rest lose. They never call your wallet, are not in reports or the daily top, and players of other casinos do not see them. A change applies when the player next opens the game.
- A player can have up to **two** bets in one round, each with its own optional auto cashout.
### Money flow (LIVE)
Every bet is its own round in the callbacks: a separate `round_id` per bet, so the two bets of one player in the same game round never share a `round_id`. The shared game round is in `game_round_id`, which every debit, credit and bet rollback carries (since 2.34.0).
1. **Bet.** When the player places a bet during the countdown, the game sends one `/bet/debit` of the bet amount. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, the bet is refused and the game sends `/bet/rollback` with reason `EXTERNAL_SERVICE_ERROR`.
1. **Cancel.** A bet can be cancelled until takeoff: `/bet/rollback` of the full amount, reason `CANCELLED`.
1. **Late debit.** If your debit is confirmed only after takeoff, the bet does not take part in the round and is refunded: `/bet/rollback`, reason `LATE_DEBIT`. Answer debits quickly. A debit that takes more than a few seconds near the end of the countdown misses the round.
1. **Win.** On a cashout (manual, auto, `max_win` or the 10 000x cap) the game sends one `/win/credit` of `bet × multiplier`, rounded down to the currency's precision, with the bet's `round_id` and the cash-out `multiplier` (e.g. `"2.35"`). If your credit fails, the game repeats it with the same `idempotency_key` until it succeeds, about every 30 seconds. A win is never rolled back. Credits can arrive after the player has left: auto cashouts happen on the server.
1. **Loss.** No money callback. The round keeps only its debit; the loss is reported in `/round/finish` (`result` `lost`).
1. **Interrupted round.** If a round is interrupted by a failure on our side, every bet still in flight is refunded: `/bet/rollback`, reason `ROUND_VOIDED`. Bets already cashed out stay won and are paid.
1. **Round end.** Right after the crash, one [`/round/finish`](https://docs.19flip.com/operator-integration/wallet-callbacks/#post-roundfinish) per round in which your players had `LIVE` bets: `game_round_id`, `crash_point`, `started_at`, `finished_at` and each of your bets with `result` `won` / `lost` / `refunded`. An interrupted round comes with `status` `interrupted` and `crash_point` `null`. Missed callbacks can be fetched from the [round history](https://docs.19flip.com/operator-integration/round-history/index.md).
While a `LIVE` game is open in a visible tab, Flip Jet re-reads the balance with `/balance` about every 5 seconds and right after the player returns to the tab, so a deposit made while the game is open shows up without restarting it. Expect this steady `/balance` traffic per open game.
`original_transaction_id` in a rollback is the debit's `idempotency_key` (`flipjet::DEBIT`). 19flip resolves it to your transaction. Rollbacks are retried with the same key until they get a definite answer. Apply a rollback whenever you have applied the debit, even if the debit reached you late.
`game_id` is `"flip-jet"` in debits and credits; `transaction_id` is `null`. Debits, credits and bet rollbacks end with `game_round_id`; credits also with `multiplier`.
### Crash point and round-based promotions
The crash point of a round is never sent before the round ends. After it ends it is in `/round/finish` and in the [round history](https://docs.19flip.com/operator-integration/round-history/index.md), and the player sees it in the game. To run a promotion on the crash point (for example a jackpot when a round crashes above 19.19x), take the round from `/round/finish` and the qualifying bets from its `bets`, or match your `/bet/debit` callbacks by `game_round_id`.
### Golden Flight (bonus buy)
Added in 2.73.0
The wallet and round-history contract is in place. The feature is not yet live: the in-game art is being finalised and it has not been released to production. It is **off** for every casino and is switched on only on your request.
Golden Flight lets a player buy a personal flight for **20×, 50× or 100× the bet**, on one of three routes. The flight never crashes: its landing multiplier is decided and paid at the moment of purchase. On the way the shuttle catches bonuses and bombs that move the count up and down, starting from the guaranteed minimum; they only show the path to the landing decided at purchase. There is no cash-out button. The RTP is **97 %** for every route and price, and the odds table is shown to the player before the purchase.
| Route | Character | Guaranteed minimum at 20× / 50× / 100× | Largest landing |
| --------------- | ------------------------------------ | -------------------------------------- | --------------- |
| Northern Lights | calm: high minimum | 9.75x / 24.55x / 49.73x | 1 000 × bet |
| Solar Wind | like a crash round without the crash | 2.04x / 5.73x / 12.64x | 10 000 × bet |
| Event Horizon | extreme | 1.61x / 4.39x / 9.44x | 100 000 × bet |
The landing is in multiples of the **base bet**, and since 2.86.0 the game says so everywhere: the purchase window shows the range in money and the formula «payout = bet × result», and the result card leads with the payout and whether it covered the price. The show lasts 20 s; the money is debited and credited at purchase, before it starts. Most purchases return less than the price: only about 24 % (Northern Lights), 10–13 % (Solar Wind) and 8–9 % (Event Horizon) land at or above it.
**Money flow.** One purchase is one round in the callbacks, with its own `round_id`. It never shares a `round_id` with a bet.
1. `/bet/debit` of the **price** (`base bet × 20|50|100`), with `round_type` `"golden_flight"`.
1. Right away, `/win/credit` of `base bet × landing`, with `multiplier` = landing and `round_type` `"golden_flight"`. The credit is always sent: a purchase always wins something, at least the minimum. If your credit fails, it is retried with the same `idempotency_key` until it succeeds.
1. If the debit outcome is unknown, `/bet/rollback`, reason `EXTERNAL_SERVICE_ERROR`, as for bets.
1. One `/round/finish` with `type` `"golden_flight"`, `route`, `price_mult` and a single `won` bet. `crash_point` is the landing.
`game_round_id` of a purchase is unique and never equals that of a shared round.
**Limits.** Your `min_bet` / `max_bet` apply to the **base bet**, not to the price: a purchase at 100× may debit up to `100 × max_bet`. `max_win` is respected: when the largest possible landing would exceed `max_win`, the game lowers that purchase's cap and raises its guaranteed minimum, so the RTP stays 97 %. A price is not offered when `max_win / bet` is below 2.5× the price.
**Exposure.** A single Event Horizon purchase can pay up to 100 000 × the base bet (10× the regular Flip Jet cap), capped by your `max_win`. Set `max_win` with this in mind before you ask to switch the feature on.
**Regulation.** Bonus buy is banned or restricted in a number of jurisdictions, at least the UK (UKGC) and Germany (GlüStV 2021); check yours. The feature is off by default and is switched on per casino under your licence. An optional pause between purchases (in seconds) can be set as well. Every purchase needs an explicit confirmation from the player; there is no auto-buy and no repeat-in-one-tap.
### Bonus wagering
The smallest auto cashout is 1.01x: such a bet wins about 96 % of the time and loses 3 % of the stake on average, with almost no variance. If your bonus terms count Flip Jet bets toward wagering, consider excluding the game or counting it at a reduced rate.
### Manual cashout and network delay
A manual cashout counts at the moment the request reaches the game server, not when the player taps. On a slow connection the player can see a multiplier below the crash and still lose. Auto cashout does not depend on the connection. The game's rules tell the player this.
## Ну, подними!
Added in 2.38.0
«Ну, подними!» (`egg-rush`) is switched on per casino by 19flip: ask your account manager if it is not in your catalogue yet.
«Ну, подними!» is a single-player crash game styled as a Soviet LCD handheld: the player presses START, the multiplier grows from ×1.00 and the player cashes out before an egg breaks. Every player has their own round, so there is no shared round, countdown or bet feed. The RTP is 97 % for any cash-out strategy.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be `97` or omitted. Any other value fails the launch.
- A bet ladder for the currency is **required**: without one the launch fails. The player bets one of the ladder steps.
- `max_win` from your limits is applied per round. When `bet × multiplier` reaches `max_win`, the game cashes the round out automatically at that point. A bet that would exceed `max_win` even at ×1.01 is refused. Without `max_win`, the largest win is `1000 × bet` (the ×1000 cap).
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into `ru` and `en`; any other `lang` shows English.
### Boost Start
The player can pay more to start the round at ×2 or ×5 instead of ×1.00. A boosted round is guaranteed to reach its starting level; after that it behaves like any other round.
- **Price.** `bet × level / 0.97`, rounded up to the cent: ×2 costs `2.0619 × bet` (bet 10 → 20.62), ×5 costs `5.1546 × bet` (bet 10 → 51.55). The win is still `bet × multiplier`: the bet, not the price, is multiplied.
- **Limited by your ladder.** The debit of a boosted round is the price, and 19flip checks every debit against your `max_bet`. So a boost is offered only while its price does not exceed the **largest step of your ladder**. With a ladder up to 100, ×2 is offered for bets up to 48.50 and ×5 for bets up to 19.40; for higher ladder steps the boost cards are disabled in the game.
- With `max_win`, a boost is refused when `bet × level` already reaches `max_win`.
- Boost is not available in auto play.
### Money flow (LIVE)
Every round has its own `round_id`. There is no `game_round_id` and no `/round/finish` for this game.
1. **Start.** When the player presses START (or BOOST START), the game sends one `/bet/debit` of the bet, or of the boost price for a boosted round. A debit you decline (`402` or another `4xx`) ends the round with no further calls. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, it sends `/bet/rollback` with the debit's `idempotency_key` as `original_transaction_id` and reason `Debit outcome unknown: …`. The rollback is retried with the same key until it gets a definite answer, never sooner than a minute after the previous attempt.
1. **Win.** On a cashout (manual, auto, `max_win` or the ×1000 cap) the game sends one `/win/credit` of `bet × multiplier`, rounded down to the cent, with the cash-out `multiplier` (e.g. `"3.47"`). If your credit fails, the game repeats it with the same `idempotency_key` until it succeeds, never sooner than a minute after the previous attempt. A win is never rolled back. Credits can arrive after the player has left: auto cashouts happen on the server.
1. **Loss.** No money callback. The round keeps only its debit.
1. **Interrupted round.** If the game service restarts while a round is in flight, the round is cancelled and the full debited amount is returned: `/bet/rollback`, reason `server_restart`. A round whose auto cashout, cap or `max_win` would have come before the crash is paid instead.
`/balance` is called when the game opens, after a declined debit or an interrupted round, and while a payout is being retried; the game does not poll it.
`game_id` is `"egg-rush"` in debits and credits; `transaction_id` is `null`. Rollbacks carry no `game_id`.
### Bonus wagering
A boosted round cashed out right after the start wins in about 99 % of cases and returns about 97 % of the boost price, with almost no variance. The smallest manual or auto cashout of a normal round (×1.01) behaves the same way. If your bonus terms count «Ну, подними!» bets toward wagering, consider excluding the game or counting it at a reduced rate.
### Manual cashout and network delay
A manual cashout counts at the moment the request reaches the game server, not when the player taps. On a slow connection the player can see a multiplier below the crash and still lose. Auto cashout does not depend on the connection. The game's rules tell the player this.
## Book of Dawn
Added in 2.50.0
«Book of Dawn» (`book-of-dawn`) is not in production yet. 19flip will switch it on per casino: ask your account manager.
«Book of Dawn» is a classic "book" slot: 5 reels × 3 rows, 10 fixed lines, the book is both wild and scatter. Three or more books give 10 free spins with an expanding symbol; free spins can be retriggered. The RTP is 97 %, and 96.99 % with the ante.
**One spin is one round.** The game plays out the whole round, including every free spin, when the player presses spin or buys the bonus. The round therefore has one debit and at most one credit, however long the bonus animation takes on screen. A reload in the middle of free spins moves no money: the win is already credited, and the game only replays the animation.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be `97` or omitted. Any other value fails the launch with `400 REQUEST_ERROR` «RTP … is not allowed for this game».
- A bet ladder for the currency is **required**: without one the launch fails. The player bets one of the ladder steps.
- `max_win` from your limits is applied per round (see [Max win](#book-of-dawn-max-win)).
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into `ru` and `en`; Russian is shown for `ru` and other CIS languages, English otherwise.
### Ante and bonus buy
Both change only the **price** of the round, that is, the debit amount. The bet that the paytable multiplies stays the same.
| Option | Price | Return | Available when |
| ------------------------------------------------------- | ------------ | ------------ | --------------------------------------------------------------------------- |
| Ante «Двойная книга» (books land more often) | `1.25 × bet` | 96.99 % | `1.25 × bet` is a whole number of minor units and does not exceed `max_bet` |
| Buy «Книга» (bonus with 1 expanding symbol) | `64 × bet` | about 97.2 % | the price does not exceed `max_bet` |
| Buy «Золотая страница» (bonus with 2 expanding symbols) | `108 × bet` | about 97.0 % | the price does not exceed `max_bet` |
- **Limited by `max_bet`.** 19flip checks every debit against your `max_bet`, so the game offers an option only while its price is within it. With `max_bet` 100, «Книга» is available up to a bet of 1.56 and «Золотая страница» up to 0.92. Unavailable options are disabled in the game.
- **Ante on uneven bets.** With 2 decimals the ante is unavailable on bets not divisible by 0.04, for example 0.10, 0.30 or 0.50. Ladders made of multiples of four minor units avoid this.
- A bonus buy returns less than its price about two times out of three: the average comes from rare large bonuses. The rules show this to the player.
- Bonus buy can be switched off only for all casinos at once. If bonus buy is not allowed in your market, tell your account manager before enabling the game.
### «Карты Босса»
After any paid spin (not a bonus buy) there is a 1 in 1320 chance of a «Карты Босса» round: the player opens cards until three of one suit, which gives a fixed prize of 10×, 25×, 100× or 1000× the bet. These are **not progressive jackpots**: there is no shared pool and nothing is collected from bets. The prize is part of the same round, so it is included in that round's single credit and in the 97 %.
### Max win
A round wins at most **5000 × bet**, and at most `max_win` if you set one. Reaching the cap ends the round at once, free spins included, and the credit is exactly the cap. Big rounds are rare but carry a real share of the RTP, so a `max_win` below `5000 × max_bet` lowers the RTP on high bets. We recommend `max_win ≥ 5000 × max_bet`.
### Money flow (LIVE)
Every round has its own `round_id`. There is no `game_round_id` and no `/round/finish` for this game.
1. **Debit.** On spin or bonus buy the game sends one `/bet/debit` of the round price: the bet, `1.25 × bet` with ante, or the buy price. A debit you decline (`402` or another `4xx`) ends the round with no further calls. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, it sends `/bet/rollback` with the debit's `idempotency_key` as `original_transaction_id`, and retries the rollback with the same key until it gets a definite answer, never sooner than a minute after the previous attempt.
1. **Win.** If the round won anything (base spin, free spins and «Карты Босса» together), the game sends one `/win/credit` of the whole win with `multiplier` = win / bet (e.g. `"57.30"`). If your credit fails, the game repeats it with the same `idempotency_key` until it succeeds, never sooner than a minute after the previous attempt. A win is never rolled back.
1. **Loss.** No money callback. The round keeps only its debit.
The credit is at most 5000 × the debit. Because the price of a bonus buy is 64× or 108× the bet, its credit is at most about 78× or 46× its debit.
`/balance` is called when the game opens and when the player's balance has to be re-read (after a reconnect or while a payout is being retried); the game does not poll it.
`game_id` is `"book-of-dawn"` in debits and credits; `transaction_id` is `null`. Rollbacks carry no `game_id`.
### Bonus wagering
Bonus buy turns a large stake into one high-variance round with about 97 % return. If your bonus terms count «Book of Dawn» bets toward wagering, consider excluding bonus buys or counting them at a reduced rate.
## Dacha Dogs
Added in 2.65.0
«Dacha Dogs» (`dacha-dogs`) is not available to casinos yet. 19flip will switch it on per casino: ask your account manager.
«Dacha Dogs» is a slot with 5 reels × 4 rows and 40 fixed lines. Paw symbols are wilds with a multiplier (×2, ×3, ×5 or ×10). Three dog houses give 10 free spins of the «Грядки» bonus, where paw multipliers add up under their reels until the bonus ends; free spins can be retriggered (+5). The RTP is 97 %.
**One spin is one round.** The game plays out the whole round, including every free spin, when the player presses spin or buys the bonus. The round therefore has one debit and at most one credit, however long the bonus animation takes on screen. A reload in the middle of free spins moves no money: the win is already credited, and the game only replays the animation.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be `97` or omitted. Any other value fails the launch with `400 REQUEST_ERROR` «RTP … is not allowed for this game».
- A bet ladder for the currency is **required**: without one the launch fails. The player bets one of the ladder steps; the line bet is the bet / 40.
- `max_win` from your limits is applied per round (see [Max win](#dacha-dogs-max-win)).
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into Russian (`ru`), Ukrainian (`uk` or `ua`) and English (any other `lang`).
### Bonus buy
The bonus buy costs **100 × bet** and starts the «Грядки» bonus at once; its return is 97.01 %. Like any debit, its price is checked against your `max_bet`, so the game offers the buy only while `100 × bet ≤ max_bet`: with `max_bet` 100, up to a bet of 1.00. Otherwise the buy is disabled in the game. A bonus buy returns less than its price in most cases: the average comes from rare large bonuses, and the rules show this to the player. Bonus buy can be switched off only for all casinos at once; if it is not allowed in your market, tell your account manager before enabling the game.
### Small bets
Wins are rounded **down** to the minor unit once per round (the line bet is the bet / 40). On the smallest bets this costs a little RTP: about −0.7 percentage points at 0.10, −0.3 at 0.30, −0.14 at 0.50, at most −0.06 from 1.00. Bets that are multiples of 0.40 lose nothing.
### Max win
A round wins at most **5000 × bet**, and at most `max_win` if you set one. Reaching the cap ends the round at once, free spins included, and the credit is exactly the cap. We recommend `max_win ≥ 5000 × max_bet`: a lower limit lowers the RTP on high bets.
### Money flow (LIVE)
Every round has its own `round_id`. There is no `game_round_id` and no `/round/finish` for this game.
1. **Debit.** On spin or bonus buy the game sends one `/bet/debit` of the round price: the bet or `100 × bet`. A debit you decline (`402` or another `4xx`) ends the round with no further calls. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, it sends `/bet/rollback` with the debit's `idempotency_key` as `original_transaction_id`, and retries the rollback with the same key until it gets a definite answer, never sooner than a minute after the previous attempt.
1. **Win.** If the round won anything (base spin and free spins together), the game sends one `/win/credit` of the whole win with `multiplier` = win / bet (e.g. `"57.30"`). If your credit fails, the game repeats it with the same `idempotency_key` until it succeeds, never sooner than a minute after the previous attempt. A win is never rolled back.
1. **Loss.** No money callback. The round keeps only its debit.
The credit is at most 5000 × the debit; for a bonus buy (price 100 × bet) that is at most 50× its debit.
`/balance` is called when the game opens and when the player's balance has to be re-read (after a reconnect or while a payout is being retried); the game does not poll it.
`game_id` is `"dacha-dogs"` in debits and credits; `transaction_id` is `null`. Rollbacks carry no `game_id`.
### Bonus wagering
Bonus buy turns a large stake into one high-variance round with about 97 % return. If your bonus terms count «Dacha Dogs» bets toward wagering, consider excluding bonus buys or counting them at a reduced rate.
## Dacha Fishing
Added in 2.76.0
«Dacha Fishing» (`dacha-fishing`) is not available to casinos yet. 19flip will switch it on per casino: ask your account manager.
«Dacha Fishing» is a slot with 5 reels × 3 rows and 10 fixed lines. Fish symbols carry a value of 2× to 50× the bet; the fisher is a wild. Three or more floats give 10, 15 or 20 free spins of the «Садок» bonus: every fisher on the screen collects the values of all fish, every 4 fishers add 10 spins and raise the collect multiplier ×1 → ×2 → ×3 → ×10, a cat drops fish when a fisher lands alone, and a golden catfish is worth up to 500×. Free spins can be retriggered (+10). The RTP is 97 %.
**One spin is one round.** The game plays out the whole round, including every free spin, when the player presses spin or buys the bonus. The round therefore has one debit and at most one credit, however long the bonus animation takes on screen. A reload in the middle of free spins moves no money: the win is already credited, and the game only replays the animation.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be `97` or omitted. Any other value fails the launch with `400 REQUEST_ERROR` «RTP … is not allowed for this game».
- A bet ladder for the currency is **required**: without one the launch fails. The player bets one of the ladder steps; the line bet is the bet / 10.
- `max_win` from your limits is applied per round (see [Max win](#dacha-fishing-max-win)).
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into Russian (`ru`), Ukrainian (`uk` or `ua`) and English (any other `lang`).
### Bonus buy
The bonus buy costs **100 × bet** and starts 10 free spins of the «Садок» bonus at once; its return is about 97 %. Bought bonuses use their own reels, so their average differs from a bonus won in the base game; the rules tell the player this. Like any debit, the price is checked against your `max_bet`, so the game offers the buy only while `100 × bet ≤ max_bet`: with `max_bet` 100, up to a bet of 1.00. Otherwise the buy is disabled in the game. A bonus buy returns less than its price in most cases: the average comes from rare large bonuses. Bonus buy can be switched off only for all casinos at once; if it is not allowed in your market, tell your account manager before enabling the game.
### Small bets
Wins are rounded **down** to the minor unit once per round (the line bet is the bet / 10). Bets that are multiples of 0.10 lose nothing. Other bets lose a little RTP: about −0.83 percentage points at 0.05, −0.28 at 0.15, −0.17 at 0.25, −0.06 at 0.75.
### Max win
A round wins at most **5000 × bet**, and at most `max_win` if you set one. Reaching the cap ends the round at once, free spins included, and the credit is exactly the cap. We recommend `max_win ≥ 5000 × max_bet`: a lower limit lowers the RTP on high bets.
### Money flow (LIVE)
The same as [Dacha Dogs](#dacha-dogs): every round has its own `round_id`, there is no `game_round_id` and no `/round/finish`.
1. **Debit.** On spin or bonus buy the game sends one `/bet/debit` of the round price: the bet or `100 × bet`. A debit you decline (`402` or another `4xx`) ends the round with no further calls. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, it sends `/bet/rollback` with the debit's `idempotency_key` as `original_transaction_id`, and retries the rollback with the same key until it gets a definite answer, never sooner than a minute after the previous attempt.
1. **Win.** If the round won anything (base spin and free spins together), the game sends one `/win/credit` of the whole win with `multiplier` = win / bet (e.g. `"57.30"`). If your credit fails, the game repeats it with the same `idempotency_key` until it succeeds, never sooner than a minute after the previous attempt. A win is never rolled back.
1. **Loss.** No money callback. The round keeps only its debit.
The credit is at most 5000 × the debit; for a bonus buy (price 100 × bet) that is at most 50× its debit.
`/balance` is called when the game opens and when the player's balance has to be re-read (after a reconnect or while a payout is being retried); the game does not poll it.
`game_id` is `"dacha-fishing"` in debits and credits; `transaction_id` is `null`. Rollbacks carry no `game_id`.
### Bonus wagering
Bonus buy turns a large stake into one high-variance round with about 97 % return. If your bonus terms count «Dacha Fishing» bets toward wagering, consider excluding bonus buys or counting them at a reduced rate.
## Dacha Pickles
Added in 2.88.0
«Dacha Pickles» (`dacha-pickles`) is not available to casinos yet. 19flip will switch it on per casino: ask your account manager.
«Dacha Pickles» is a slot with 5 reels × 4 rows and 40 fixed lines and a Hold & Win bonus, «Закатка». Coin lids carry a value of 0.5× to 50× the bet or a MINI 20× / MINOR 50× / MAJOR 250× jackpot; 6 or more coins, 3 scatters or a full jar start the bonus. In the bonus, coins stick, empty cells respin, special lids collect, boost or copy coin values, and filling the whole field pays GRAND 2000×. The RTP is 97 % over prolonged play at one bet (see [Jars](#dacha-pickles-jars)).
**One spin is one round.** The game plays out the whole round, including the whole bonus, when the player presses spin or buys the bonus. The round therefore has one debit and at most one credit, however long the bonus animation takes on screen. A reload in the middle of the bonus moves no money: the win is already credited, and the game only replays the animation.
### Launch
- `mode` `LIVE` or `DEMO`. For `DEMO`, `demo_balance` sets the starting balance; without it the demo starts at `1000`. `DEMO` sessions make **no** wallet callbacks.
- `rtp` must be `97` or omitted. Any other value fails the launch with `400 REQUEST_ERROR` «RTP … is not allowed for this game».
- A bet ladder for the currency is **required**: without one the launch fails. The player bets one of the ladder steps; the line bet is the bet / 40.
- `max_win` from your limits is applied per round (see [Max win](#dacha-pickles-max-win)).
- The launch URL is `…?token=&lang=`; the token is valid for 24 hours. The game is translated into Russian (`ru`), Ukrainian (`uk` or `ua`) and English (any other `lang`).
### Jars
Three jars above the reels fill with coloured lids; a full jar (10 lids) starts the bonus with its own effect. Jars are the only thing the game keeps between rounds:
- they are stored **per your casino, player, currency, mode and bet**: every bet of the ladder has its own three jars, and `LIVE` and `DEMO` never mix;
- they **do not expire**: they survive closing the game, a new session and a new day. A bonus buy plays on temporary jars and does not touch the saved ones;
- if you remove a bet from your ladder, its jars stay stored but the player cannot reach them until the bet returns.
Part of the return is paid when a jar fills, so the 97 % is reached over prolonged play at one bet. A player who starts from empty jars and plays a short session gets less: about **91 %** for 100–1 000 spins, about 94 % for 3 000 spins; about 6 percentage points of the RTP come from the jars. The rules (i button) show these numbers to the player. Your reports will therefore show an RTP a little below 97 % while players have unfinished jars, more so if they often change bets.
### Bonus buy
Two buys, both with a return of about 97 %:
| Option | Price | What it starts |
| ----------------- | ------------- | -------------------------------------------------------------------------- |
| «Закатка» | **80 × bet** | the bonus with 6 coins |
| «Бабушкин погреб» | **250 × bet** | the bonus with 6 coins, one of them a Grandpa lid, and a random jar effect |
Bought bonuses use their own coin tables, so their average differs from a bonus won in the base game; the rules tell the player this. Like any debit, the price is checked against your `max_bet`, so the game offers an option only while `price × bet ≤ max_bet`: with `max_bet` 100, «Закатка» up to a bet of 1.25 and «Бабушкин погреб» up to 0.40. Otherwise that option is disabled in the game (a forced request gets `BUY_UNAVAILABLE`). A bonus buy returns less than its price in most cases: the average comes from rare large bonuses. Bonus buy can be switched off only for all casinos at once; if it is not allowed in your market, tell your account manager before enabling the game.
### Small bets
Wins are rounded **down** to the minor unit once per round (the line bet is the bet / 40). Bets that are multiples of 0.40 lose nothing. Other bets lose a little RTP: about −1.14 percentage points at 0.10, −0.28 at 0.20, −0.49 at 0.25, −0.23 at 0.50, −0.06 at 1.00. The rules state the RTP for bets from 1.00.
### Max win
A round wins at most **10 000 × bet**, and at most `max_win` if you set one. Reaching the cap ends the round at once, the bonus included, and the credit is exactly the cap. We recommend `max_win ≥ 10 000 × max_bet`: a lower limit lowers the RTP on high bets; below 2000 × bet it also cuts every GRAND.
### Money flow (LIVE)
The same as [Dacha Fishing](#dacha-fishing): every round has its own `round_id`, there is no `game_round_id` and no `/round/finish`.
1. **Debit.** On spin or bonus buy the game sends one `/bet/debit` of the round price: the bet, `80 × bet` or `250 × bet`. A debit you decline (`402` or another `4xx`) ends the round with no further calls, and the jars do not change. If your debit times out or returns `5xx`, the game repeats it **once with the same `idempotency_key`**. If the outcome is still unknown, it sends `/bet/rollback` with the debit's `idempotency_key` as `original_transaction_id`, and retries the rollback with the same key until it gets a definite answer, never sooner than a minute after the previous attempt.
1. **Win.** If the round won anything (base spin and bonus together), the game sends one `/win/credit` of the whole win with `multiplier` = win / bet (e.g. `"57.30"`). If your credit fails, the game repeats it with the same `idempotency_key` until it succeeds, never sooner than a minute after the previous attempt. A win is never rolled back.
1. **Loss.** No money callback. The round keeps only its debit.
The credit is at most 10 000 × the debit; for a bonus buy that is at most 125× («Закатка») or 40× («Бабушкин погреб») its debit.
`/balance` is called when the game opens and when the player's balance has to be re-read (after a reconnect or while a payout is being retried); the game does not poll it.
`game_id` is `"dacha-pickles"` in debits and credits; `transaction_id` is `null`. Rollbacks carry no `game_id`.
### Bonus wagering
Bonus buy turns a large stake into one high-variance round with about 97 % return. If your bonus terms count «Dacha Pickles» bets toward wagering, consider excluding bonus buys or counting them at a reduced rate.
## Bingo Bongo
Bingo Bongo is a shared-table game: many players buy cards for the same scheduled round, and the prize pool (bank) is formed from their purchases. This changes several assumptions that hold for single-player games.
### Launch
- **No demo.** `mode` `DEMO` is refused by the game and `launch-game` returns `502 EXTERNAL_SERVICE_ERROR` (`detail` begins with `Game API error:`). Launch Bingo Bongo in `LIVE` only.
- **One table per currency.** Your players are grouped by your account **and** currency: USD players and EUR players play at different tables, with separate banks and jackpots. The first launch in a new currency creates that table automatically with default card prices and no schedule or host until 19flip configures it. Tell 19flip in advance which currencies you will use.
- `rtp`, `demo_balance` and the bet ladder are ignored; card prices come from the table's configuration.
- `player_id` must be at most 128 characters. Other players see a nickname; unless the player picks one in the game, it is `player_` followed by the **first 8 characters of your `player_id`**. Never use e-mails, phone numbers or other personal data as `player_id`.
- `lang` must be at most 8 characters.
- The token in `launch_url` is **single-use and expires after 5 minutes**. Open the URL immediately; launch again if the player comes back later.
### Live banner
Bingo Bongo has a [live banner](https://docs.19flip.com/operator-integration/live-banners/index.md) for your lobby. Its data comes from the table of your account in the banner's currency, so show players the banner in the currency they will play in:
- Before a draw, it announces the next scheduled draw with a countdown (`scheduled`). Times are shown in the visitor's own time zone.
- While tickets are on sale (`selling`), it shows the draw number, the pot, players, cards and the jackpot.
- During the draw (`drawing`), it shows the last ball and how many of the 90 balls are out. A ball appears on the banner only after it is shown to players in the game.
- It hides when the table has no running round and no scheduled draw.
The draw number counts the rounds of your table (all rounds, including cancelled ones), so it differs between currencies.
### Money flow
| What happens in the game | Callback | `idempotency_key` |
| ---------------------------------------- | ------------------------------------------- | -------------------------------- |
| Lobby opens / balance refresh | `/balance` | – |
| Card purchase | `/bet/debit` | `bb-tkt---` |
| Boost purchase | `/bet/debit` | `bb-tkt---` |
| Tip to the host (paid by the player) | `/bet/debit` | `bb-tkt---` |
| Bank prize | `/win/credit` | `bb-bank---` |
| Jackpot, minor tier | `/win/credit` | `bb-jpn---` |
| Jackpot, major tier | `/win/credit` | `bb-jpx---` |
| Insurance payout | `/win/credit` | `bb-ins---` |
| Tip received by the host | `/win/credit` to the **host's** `player_id` | `bb-tip---` |
| Round cancelled: refund of each purchase | `/bet/rollback`, `reason` `round cancelled` | `bb-rfnd---` |
| – | `/win/rollback` | never sent |
The numbers inside the keys are internal ids; treat the keys as opaque strings. The purpose (card, boost, prize, jackpot, insurance, tip) is only visible in the key prefix – the callback type is just "debit" or "credit".
`game_id` is `"bingo-bongo"`; `transaction_id` is `null`.
### Wins arrive asynchronously
- Prizes, jackpot tiers, insurance and tips are paid by a background process **after the draw**, typically seconds to minutes later and possibly much later if your wallet was unavailable.
- The player may have closed the game long before. The win is still valid and must be credited.
- Unconfirmed payouts are **retried with growing delays, using the same `idempotency_key`**, until your wallet confirms them or they are escalated to 19flip staff. Deduplicate and answer each repeat with the stored result.
- The `session_id` on a late win may be a **different** session of the same player than the one in which the cards were bought (the game uses the player's latest session when the original one is not at hand). Always credit by `player_id` and `currency`; use `session_id` only for reporting.
- A round can produce several credits for the same player (bank prize, jackpot tier, insurance), each with its own key.
### Refunds of cancelled rounds
If a round is cancelled, every purchase is refunded with `/bet/rollback` for the purchase amount. `original_transaction_id` is your `transaction_id` from the purchase's debit if 19flip received it; if the purchase debit timed out, it is the purchase's `idempotency_key` (`bb-tkt-…`). Look up both. If you never applied the purchase, answer `200` with an unchanged balance (see [Wallet callbacks](https://docs.19flip.com/operator-integration/wallet-callbacks/#post-betrollback)).
### Declines
Answer `402` when the player cannot afford a purchase: Bingo Bongo then tells the player "not enough funds" (see also how 19flip classifies other answers in [Errors](https://docs.19flip.com/operator-integration/errors/#how-19flip-reads-a-4xx-body)). Other refusals are shown as a generic error. A refused **win** is not retried automatically by 19flip and ends up in manual handling – never refuse a valid win.
# Idempotency & retries
The same money operation can reach your wallet more than once. This page explains when and why, what timeouts and retries 19flip uses, and exactly how your wallet must deduplicate. **Rule: one `idempotency_key` = at most one money movement, and every repeat gets the same answer.**
## Why you will see repeats
| Source | When | Same `idempotency_key`? | Same body? |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| 19flip transport retry | Your endpoint answered `5xx`, timed out, or the connection failed | yes | yes, byte for byte |
| Game repeat | A game did not get a definite answer and asks again to learn the outcome (for example Bingo Bongo retries unpaid wins with growing delays until you confirm, possibly for hours) | yes | yes |
| Manual retry by 19flip staff | An operation with an unknown outcome is re-sent from the 19flip admin panel (wins and rollbacks only) | yes | same fields; `round_id` may be a string of digits, `game_id` may now be filled |
| Manual bet rollback by 19flip staff | A bet with an unknown outcome is reversed | **new** key `manual-rb-`; `original_transaction_id` points at the bet | – |
A transport retry can arrive **while your first attempt is still running**: when we time out after 10 seconds we do not know whether you finished, and your server keeps processing the first request. Your deduplication must be safe under concurrency, not only sequentially.
## Timeouts and retries (19flip → your wallet)
| Parameter | Value |
| ---------------------------------------- | ----------------------------------------------------------------------------------- |
| Timeout per attempt | **10 s** (whole request: connect, send, receive) |
| Attempts | **3** in total (the first try + up to 2 retries) |
| Pause before retry | 1 s before the 2nd attempt, 2 s before the 3rd (`1 s × 2^n`) |
| Retried on | HTTP `5xx`, timeout, connection error (DNS, refused, reset, TLS) |
| Not retried | HTTP `4xx` (a definitive answer), and any other non-`2xx` status such as a redirect |
| Worst case until the game gets an answer | about 33 s (10 + 1 + 10 + 2 + 10) |
| Connection test | 5 s timeout, 1 attempt |
These are the current defaults for all casinos. 19flip can tune them; any change will be announced to you in advance.
`/round/finish` (Flip Jet, moves no money) uses the same 3 attempts. If they all fail with `5xx`, a timeout or a connection error, 19flip tries again later: after 1, 5, 15 and 60 minutes, then after 3, 6 and 12 hours (8 deliveries in total), each with the same body and `idempotency_key`. A `4xx` answer ends the retries. Rounds whose callback was lost can be fetched from the [round history](https://docs.19flip.com/operator-integration/round-history/index.md).
After the last failed attempt the game receives an error and the operation is recorded on our side as **outcome unknown**. It is then settled in one of these ways, all of which rely on your deduplication:
1. The game repeats the same operation with the same key (and you return the stored answer, or apply it if you never saw it).
1. The game or 19flip staff rolls the bet back with `/bet/rollback`, referencing it by its `idempotency_key` (because we never received your `transaction_id`).
1. 19flip staff resolve it manually with you. For that, you must be able to look up an operation by `idempotency_key`.
## What your wallet must do
1. **Deduplicate on `idempotency_key` alone**, across all players, sessions, games and endpoints. Put a UNIQUE constraint on it. Do not combine it with `player_id` or `round_id`.
1. **Store the answer** you gave (HTTP status and body) together with the key, in the same database transaction as the balance change.
1. **Return the stored answer for a repeat**: same HTTP status, same `transaction_id`, same `balance_before`, same `balance_after`, same `status`. Do not return the *current* balance, and do not move money again.
1. **Repeat declines too.** If you answered `402` to a key, answer `402` to its repeats. Do not re-evaluate the balance under the same key.
1. **Serialize concurrent requests with the same key.** The second request must wait for the first and then return its stored answer, or fail with `5xx` so that it is retried. It must never apply the operation a second time.
1. **If a repeat has a different body** (different amount or player) under a known key, do not apply it. Return the stored answer and alert your team: it indicates a bug, not a legitimate operation.
1. **Keep keys for a long time.** Wins can arrive hours after the round and manual resolution can happen days later. Keep keys at least as long as you keep transaction records, and never less than 30 days.
1. **Rollbacks are at most once per original.** A second rollback of the same original (under a different key) returns the first rollback's result and moves no money.
## Reference implementation (debit)
Python with PostgreSQL (asyncpg). The same pattern applies to `/win/credit` and the rollbacks.
```
import json
import uuid
from decimal import Decimal
# CREATE TABLE wallet_ops (
# idempotency_key text PRIMARY KEY,
# kind text NOT NULL, -- BET_DEBIT, WIN_CREDIT, ...
# player_id text NOT NULL,
# amount numeric(18,2) NOT NULL,
# http_status int NOT NULL,
# response jsonb NOT NULL,
# transaction_id text UNIQUE, -- your id, returned to 19flip
# created_at timestamptz NOT NULL DEFAULT now()
# );
async def bet_debit(conn, body: dict) -> tuple[int, dict]:
key = body["idempotency_key"]
async with conn.transaction():
# Serialize concurrent requests with the same key.
await conn.execute("SELECT pg_advisory_xact_lock(hashtext($1))", key)
stored = await conn.fetchrow(
"SELECT http_status, response FROM wallet_ops WHERE idempotency_key = $1", key)
if stored:
return stored["http_status"], json.loads(stored["response"]) # identical answer, no money moved
player = await conn.fetchrow(
"SELECT balance FROM players WHERE id = $1 FOR UPDATE", body["player_id"])
if player is None:
return 404, {"error": "player not found"}
amount = Decimal(str(body["amount"]))
before = player["balance"]
if before < amount:
status, response = 402, {"error": "insufficient balance", "balance": float(before)}
else:
after = before - amount
await conn.execute("UPDATE players SET balance = $1 WHERE id = $2", after, body["player_id"])
tx_id = str(uuid.uuid4())
status, response = 200, {
"transaction_id": tx_id,
"balance_before": float(before),
"balance_after": float(after),
"status": "SUCCESS",
}
await conn.execute(
"INSERT INTO wallet_ops (idempotency_key, kind, player_id, amount, http_status, response, transaction_id)"
" VALUES ($1, 'BET_DEBIT', $2, $3, $4, $5, $6)",
key, body["player_id"], amount, status, json.dumps(response), response.get("transaction_id"))
return status, response
```
Notes:
- The `404` for an unknown player is not stored, so a repeat is evaluated again. Store it too if you prefer.
- Money is `Decimal`/`numeric`, never float arithmetic. Convert to a JSON number only in the response.
## What 19flip does on its side
19flip deduplicates too, which reduces but does not remove repeats to your wallet:
- An operation whose outcome is known (you answered `2xx`, or a definitive `4xx`) is stored under its key. When the game repeats it, 19flip returns the stored result **without calling you**.
- An operation whose outcome is unknown (`5xx`, timeout, connection error after all attempts) is **sent to you again** when the game repeats it with the same key.
- Transport retries happen inside a single call and are not deduplicated on our side – that is your job.
- 19flip treats `idempotency_key` as unique across all games, so you can too.
# Launch game
`POST /api/operator/v1/launch-game` opens a new game session for one player and returns the URL to show them. This is the one endpoint every casino must call. Call it every time a player opens a game, including after a click on a [live banner](https://docs.19flip.com/operator-integration/live-banners/index.md).
## Request
```
POST /api/operator/v1/launch-game HTTP/1.1
Host: {PROVIDER_HOST}
Content-Type: application/json
Api-Key: YOUR_API_KEY
```
```
{
"game_id": "block-boost",
"player_id": "player-123",
"currency": "USD",
"mode": "LIVE",
"rtp": 97,
"lang": "en"
}
```
| Field | Type | Required | Description |
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `game_id` | string | yes | `block-boost`, `bingo-bongo`, `magic-double` (Magic Jump) or `flip-jet`. |
| `player_id` | string | yes | Your stable, unique player id. It is sent back in every wallet callback. Do not use personal data (e.g. e-mail): some games show a nickname derived from it to other players (see [Game specifics](https://docs.19flip.com/operator-integration/game-specifics/index.md)). Keep it ≤ 128 characters. |
| `currency` | string | yes | ISO 4217 code in upper case, e.g. `USD`, `EUR`. Must be enabled by 19flip and allowed for your account. The value is matched exactly: `usd` is rejected. |
| `mode` | string | yes | `LIVE` (real money through your wallet) or `DEMO` (no wallet calls). Not every game supports `DEMO`. |
| `rtp` | number | no | Return-to-player percent, e.g. `97` or `96.5`. Must be one of the RTPs enabled for the game. If 19flip has fixed an RTP for your account, omit it or send exactly that value. If omitted, the fixed or default RTP is used. |
| `demo_balance` | number | no | Starting balance of a `DEMO` session. Send it for every `DEMO` launch: games that support demo start from `0` if it is missing. Ignored in `LIVE`. |
| `lang` | string | no | UI language code, e.g. `en`, `ru`. Passed into `launch_url` by games that support it; default `en`. Keep it ≤ 8 characters. |
Unknown fields are ignored. In particular, a `callback_url` field in this request has **no effect**: your callback URL is configured on your account by 19flip.
## Response
`200 OK`:
```
{
"success": true,
"data": {
"session_id": "3f6c2a9e-8d41-4b7a-9c55-1e2f0a7b9d10",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"launch_url": "https://block-boost.example.com/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&lang=en"
}
}
```
| Field | Type | Description |
| ----------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success` | boolean | Always `true` on success. |
| `data.session_id` | string (UUID) | The game session. It appears as `session_id` in every wallet callback for this session. Store it with the player and game if you want to reconcile. |
| `data.token` | string | The session token that is already embedded in `launch_url`. You do not need to use it separately. |
| `data.launch_url` | string | Open this URL for the player (redirect or iframe). |
### Session and URL lifetime
- Every call creates a **new** session. There is no "resume session" call; launch again instead.
- Open `launch_url` right away and do not cache it. For Bingo Bongo the token in the URL is **single-use and valid for 5 minutes**. For Block Boost and Mister Twister it is valid for 24 hours.
- A session never expires for money purposes: wins and rollbacks for a session can arrive long after the player closed the game. See [Wallet callbacks](https://docs.19flip.com/operator-integration/wallet-callbacks/index.md).
## What 19flip applies at launch
The session is created with the settings in force at launch time and keeps them for its whole life, even if your settings change later:
| Setting | Effect |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RTP | Your fixed RTP for the game if 19flip set one; otherwise the requested `rtp`; otherwise the game's default. |
| Bet ladder | The list of bet amounts the game offers, per game and currency, filtered by your minimum and maximum bet. |
| Minimum / maximum bet | Bets outside these limits are rejected by 19flip and **never reach your wallet**. The maximum applies to the whole amount debited in one round: in games where a round can cost more than the bet (an ante or a bonus buy), 19flip passes the limit to the game, and the game disables those options while their price is above it. Exception: a Flip Jet [Golden Flight](https://docs.19flip.com/operator-integration/game-specifics/#flip-jet-golden-flight) purchase is checked against the **base bet**, so its price (20×, 50× or 100× the base bet) may exceed the maximum. |
| Maximum win | Per-round win cap, passed to games that support it: Magic Jump and Flip Jet apply it, the other games do not; see [Game specifics](https://docs.19flip.com/operator-integration/game-specifics/index.md). |
| Visual players | Flip Jet only, off by default: 19flip can turn on, for your account, a range of simulated players (for example 15–20) that add to the "playing" counter and place bets in the "All bets" list. They exist only on screen: no wallet calls, not in your reports, not in the daily top. Seen only by your players. |
| Golden Flight | Flip Jet only, **off by default**: a bonus buy of a personal flight for 20×, 50× or 100× the bet, RTP 97 %. 19flip turns it on for your account only on your request, optionally with a pause between a player's purchases. Check that bonus buy is allowed under your licence first. See [Game specifics](https://docs.19flip.com/operator-integration/game-specifics/#flip-jet-golden-flight). Added in 2.73.0. |
| Game title | Whether the game shows its name (wordmark) in the top-left corner. Shown by default; 19flip can hide it for your account per game on request. Supported by Flip Jet; the other games always show their name. |
## Checks and errors
The checks run in this order. The first failing one is returned. Error body format is described in [Errors](https://docs.19flip.com/operator-integration/errors/index.md).
| Condition | HTTP | `code` | `detail` example |
| ------------------------------------------------------------------------------------------------ | ---- | ------------------------ | ------------------------------------------------------- |
| `Api-Key` missing | 401 | `AUTH_ERROR` | `Api key not provided` |
| Key unknown (or previous key after its grace period) | 401 | `AUTH_ERROR` | `Operator not found` |
| Caller IP not in your allowlist | 403 | `IP_NOT_ALLOWED` | `IP 203.0.113.7 is not allowed for this operator` |
| Account suspended | 403 | `OPERATOR_SUSPENDED` | `Operator is suspended` |
| Body invalid (missing field, wrong type, `mode` not `LIVE`/`DEMO`) | 400 | `REQUEST_ERROR` | see example below |
| Unknown `game_id` | 400 | `REQUEST_ERROR` | `Game roulette not found` |
| Game under maintenance | 403 | `GAME_UNAVAILABLE` | `Game block-boost is under maintenance` |
| Game disabled | 403 | `GAME_UNAVAILABLE` | `Game block-boost is disabled` |
| Game not connected on this 19flip installation | 403 | `GAME_UNAVAILABLE` | `Game block-boost integration is not configured` |
| Game switched off for your account | 403 | `GAME_UNAVAILABLE` | `Game block-boost is not available for this operator` |
| Currency unknown or disabled | 400 | `REQUEST_ERROR` | `Currency not supported` |
| Currency not in your allowed list | 400 | `REQUEST_ERROR` | `Currency GBP is not allowed for this operator` |
| Player blocked by 19flip (`LIVE` only; `DEMO` is still allowed) | 403 | `PLAYER_BLOCKED` | `Player is blocked` |
| `rtp` differs from the RTP fixed for your account | 400 | `REQUEST_ERROR` | `RTP 99 is not allowed for this operator (fixed at 97)` |
| `rtp` not enabled for the game | 400 | `REQUEST_ERROR` | `RTP 95 is not allowed for this game` |
| The game refused to open the session (for example `DEMO` in Bingo Bongo) or could not be reached | 502 | `EXTERNAL_SERVICE_ERROR` | `Game API error: Failed to request: 400 {...}` |
| Anything unexpected | 500 | `UNKNOWN_ERROR` | – |
Validation error example (`player_id` missing):
```
{
"success": false,
"code": "REQUEST_ERROR",
"traceback": "q7ZkP2xY9a",
"detail": "[{'type': 'missing', 'loc': ('body', 'player_id'), 'msg': 'Field required', 'input': {'game_id': 'block-boost', 'currency': 'USD', 'mode': 'LIVE'}, 'url': 'https://errors.pydantic.dev/2.12/v/missing'}]",
"errors": [
{"loc": ["body", "player_id"], "msg": "Field required"}
]
}
```
Use `errors` for machine handling; `detail` is a human-readable dump and its wording may change.
## Retrying a launch
`launch-game` is not idempotent: each successful call creates a new session. It moves no money, so retrying after a timeout or a `5xx` is safe; the unused session is simply never opened.
# Live banners
A live banner is a small widget for your lobby. It shows what is happening in a 19flip game right now, and it updates by itself. You add it with two lines of HTML. When a player clicks it, you get an event and launch the game with your usual [`launch-game`](https://docs.19flip.com/operator-integration/launch-game/index.md) call.
Available banners:
| `game_id` | Banner | What it shows |
| ------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bingo-bongo` | "Lotto BINGO BONGO" marquee board | The next draw with a countdown (`MM:SS`, `HH:MM:SS`, or `13d 04:52` when a day or more away). Then ticket sales, then the live draw: pot, players, cards in play, jackpot, and the last ball. |
The banner takes the full width of the element you put it in, from **280 px** up to the full page width. Its height follows its content (about 140 px). See [Size and layout](#size-and-layout).
## How it works
1. You get your public `banner_key` and a ready embed snippet from `GET /api/operator/v1/banners`.
1. `embed.js` creates an `