Документ для интеграции · S7 Wallet API v1Integration document · S7 Wallet API v1

Подключение казино к играм S7Connecting a casino to S7 games

Со стороны казино это ровно две вещи: эндпоинт кошелька, который зовём мы, и ссылка на игру, которую вы открываете в iframe. Больше ничего. From the casino's side this is exactly two things: a wallet endpoint we call, and a launch link you open in an iframe. Nothing else.

Кому:For: бэкенд и фронтенд казиноthe casino's backend and frontend Кастомер:Customer: {customer} Версия:Version: Wallet API v1
Подставить свои значенияFill in your own values

Значение подставится по всему тексту, включая примеры. Хранится только в вашем браузере — в документ оно не сохраняется, так что каждый читатель видит своё. The value is substituted throughout the text, examples included. It lives in your browser only — nothing is saved into the document, so every reader sees their own.

1Как устроена сессияHow a session works

Границу ответственности проще всего увидеть по шагам.The division of labour is easiest to see step by step.

1.  Игрок нажимает «Играть» у вас на сайте.
2.  Вы выпускаете сессионный токен и открываете нашу ссылку в <iframe>.
3.  Мы зовём POST /auth на вашем кошельке с этим токеном.
       Вы отвечаете, кто это и сколько у него денег —
       именно отсюда мы узнаём валюту.
4.  Идёт игра. Каждая ставка — POST /bet, каждая выплата — POST /win.
       Ставку, которую нужно вернуть, отменяет POST /rollback.
5.  Игрок уходит. /win и /rollback обязаны работать и после этого.
1.  The player presses Play on your site.
2.  You mint a session token and open our link in an <iframe>.
3.  We call POST /auth on your wallet with that token.
       You answer with the player and their balance —
       this is where we learn the currency.
4.  The game runs. Every stake is POST /bet, every payout POST /win.
       A stake that has to be given back is POST /rollback.
5.  The player leaves. /win and /rollback still have to work.

Шаги 3–5 держите вы. Всё остальное — мы.You host steps 3–5. We host everything else.

Валюту называете вы, а не URL.The currency comes from you, not from the URL.

Параметр currency в ссылке необязателен. Если его нет, мы спросим кошелёк — потому что единственный, кто точно знает, в чём номинирован счёт игрока, это тот, кто этот счёт держит. The currency parameter is optional. Leave it out and we ask your wallet — because the only party that knows what a player's account is denominated in is the one holding it.

Токен не одноразовый.The token is not single-use.

Каждая загрузка ссылки зовёт /auth с этим токеном, то есть перезагрузка страницы — это ещё один /auth с тем же токеном, и игра может проверить его ещё раз при старте. Принимайте повторные /auth, пока токен жив, и держите его живым всю сессию: /getBalance и /bet пользуются им всё это время. После перезагрузки игрок возвращается в ту же сессию, вместе со ставками, которые уже сделал в текущем раунде. Every load of the launch link calls /auth with it, so a reload is another /auth with the same token, and the game may verify it once more when it starts. Accept repeated /auth calls for as long as the token is valid, and keep it valid for the whole session: /getBalance and /bet use it throughout. A reload puts the player back in the same session, with the bets they already have in the current round.

2Ссылка на игруThe launch link

Её вы собираете у себя и открываете в iframe.You build it on your side and open it in an iframe.

https://play.afr.stellar-kiosk.com/game/{customer}/{module}/{game}/?<params>
play.afr.stellar-kiosk.com Адрес площадки. Любой другой адрес, который вы от нас видели, — демо или тестовая среда, и ссылкой на игру он не является.The launch host. Any other address you have seen from us is a demo or a test environment and is not a launch host.
{customer} Идентификатор, который мы вам присваиваем: строчными буквами, без пробелов.The id we assign you: lower case, no spaces.
{module} / {game} Называют игру; оба даём мы, по каждому тайтлу.Name the game; we give you both per title.

2.1ПараметрыParameters

ПараметрParameter Что кладёмWhat goes in it
tokenобяз.required Сессионный токен игрока. Вернём его вам в каждом вызове кошелька.The player's session token. We send it back on every wallet call.
partnerIdopt. Бренд, к которому относится игрок, если на одном вашем кошельке их несколько. Мы вернём его в каждом вызове кошелька как operatorId, и один и тот же токен под двумя брендами останется двумя разными игроками. При одном бренде параметр не кладите. Это не тот partnerId, что лежит в теле запроса, — см. 6.1.The brand this player belongs to, when one wallet of yours serves several. We return it on every wallet call as operatorId, so one token string under two brands stays two different players. With a single brand, leave it out. It is not the partnerId in the wallet body — see 6.1.
currencyopt. ISO 4217. Без него спросим кошелёк.ISO 4217. Omit it and we ask your wallet.
languageopt. ISO 639-1: en, ru. en-US тоже принимаем, регион сохраняем.ISO 639-1: en, ru. en-US is accepted; the region is kept.
openTypeopt. real или fun. Всё, чего мы не узнаём, и отсутствие параметра — это реальные деньги.real or fun. Anything we do not recognise, and anything absent, means real money.
devicetypeidopt. 1 web, 2 мобильный web, 3 iOS, 4 Android.1 web, 2 mobile web, 3 iOS, 4 Android.
gameIdopt. Ваш каталожный id игры, если он у вас есть.Your own catalogue id for the game, if you have one.
exitURLopt. Куда внутриигровая кнопка «выход» уводила бы игрока. Значение до игры доезжает уже сейчас, самой кнопки пока нет — см. 2.3.Where an in-game exit control would send the player. Carried to the game today; the control itself is not built yet — see 2.3.
depositURLopt. То же для кнопки кассы. Не пришлёте — кассы внутри игры не будет вовсе.The same, for a cashier control. Send nothing and there will be no cashier inside the game at all.
https://play.afr.stellar-kiosk.com/game/{customer}/crash/default/
  ?token=abc123&language=en&openType=real
  &devicetypeid=1&exitURL=https://casino.example/lobby

2.2Три вещи, которые стоит знатьThree things worth knowing

Регистр и разделители не важны. auth_token, authToken и AUTH-TOKEN — для нас один и тот же параметр. Если ваша платформа уже отдаёт другое написание — присылайте как есть: скорее всего мы его уже принимаем, а если нет, добавить его — это правка конфигурации у нас, а не у вас. Case and separators do not matter. auth_token, authToken and AUTH-TOKEN are the same parameter to us. If your platform already emits a different spelling, send it: we probably accept it already, and if not, accepting it is a configuration change on our side, not a change on yours.

Незнакомые параметры мы не выбрасываем. Они возвращаются в диагностике, так что если вы что-то прислали, а мы будто не заметили — видно ровно то, что пришло. Anything we do not recognise is kept, not dropped. It comes back in the diagnostics, so if you send us something and we appear to ignore it, you can see exactly what arrived.

Откройте ссылку в браузере до того, как напишете код кошелька. Мы отдадим страницу, которая перечисляет каждый присланный параметр, во что мы его сопоставили, и что ваш кошелёк ответил про токен. Это самый быстрый способ найти ссылку, собранную не на того кастомера. Open the launch link in a browser before writing any wallet code. We serve a page listing every parameter you sent, what we matched it to, and what your wallet said about the token. It is the fastest way to find a link built against the wrong customer.

В ссылке нет ваших секретов — и не должно быть.The link carries none of your secrets, and must not.

Общий ключ подписи не участвует в запуске: он живёт только на вашем бэкенде и на нашем. URL уходит в браузер игрока, где его видно целиком, поэтому единственное, что там важно, — сессионный токен: короткоживущий, и выпускаете его вы. The shared signing key takes no part in a launch: it lives on your backend and ours. The URL goes into a player's browser where all of it is visible, so the only thing in it that matters is the session token — short-lived, and yours to issue.

2.3Кнопки выхода и кассыThe exit and cashier controls

Как есть сегодня: значения передаются, кнопок ещё нет.State today: the values are carried, the buttons are not built.

Всё, что вы положите в exitURL и depositURL, доезжает до клиента игры корректно — это видно на странице запуска, — но ни одна игра пока не рисует для них контрол. Мы предпочитаем сказать это здесь, а не дать вам обнаружить это на тестах. Whatever you put in exitURL and depositURL reaches the game client correctly — you can see it on the launch page — but no game we serve draws a control for it yet. We would rather say so here than let you discover it in testing.

Ничего из того, что вы строите сейчас, менять не придётся: правила уже зафиксированы.Nothing you build now has to change when they appear, because the rules are already fixed:

  1. Нет адреса — нет кнопки. Не серая, не неработающая — контрола не существует. Не слать exitURL и нарисовать свой выход вокруг iframe — поддерживаемая конфигурация, а не обходной путь, и она останется верной и потом.No url, no button. Not a greyed-out one, not one that does nothing — the control does not exist. Leaving exitURL off and drawing your own exit around the iframe is a supported configuration, not a workaround, and it stays correct afterwards.
  2. Адрес может прийти из ссылки или из настроек. Если игроки всегда возвращаются в одно лобби, назовите адрес один раз — мы его сохраним. Игра разницы не видит.A url may come from the link or from your settings. If players always return to the same lobby, tell us the address once and we hold it. The game cannot tell the two apart.
  3. Переход заменяет всю страницу, а не фрейм. Лобби, отрисованное внутри прямоугольника игры, в её рамке и с её полосами прокрутки, — не то, что мы стали бы выпускать.The control replaces the whole page, never the frame. A lobby rendered inside the game's rectangle, still wrapped in the game's frame and scrollbars, is not something we would ship.

Проходят только обычные http(s)-адреса. Это единственные два параметра ссылки запуска, по которым что-либо переходит, поэтому значение вида javascript: или data: отбрасывается, а не передаётся дальше. Если вы прислали такое и игра ведёт себя так, будто вы не прислали ничего, — причина в этом. У правила 3 есть одно требование к вашей стороне — см. 3. Only ordinary http(s) values survive. These two are the only parameters on a launch link that anything navigates to, so a javascript: or data: value is discarded rather than passed on. If you send one and the game behaves as though you sent nothing, that is why. Rule 3 has one requirement on your side — see 3.

3Встраивание iframeEmbedding the iframe

Игра — обычная страница на нашем домене. От вас нужны размер и разрешение на звук.The game is an ordinary page on our domain. What it needs from you is a size and permission to make sound.

<iframe
    src="https://play.afr.stellar-kiosk.com/game/{customer}/crash/default/?token=abc123&openType=real"
    allow="autoplay; fullscreen"
    title="Crash"
    style="border:0; width:100%; height:100%; display:block">
</iframe>
  • Размер задаёте вы. Игра тянется на весь фрейм; своей высоты у неё нет. Дайте контейнеру реальную высоту — height:100% внутри блока нулевой высоты даёт пустой прямоугольник.
  • allow="autoplay" — без него в игре не будет звука до первого касания. fullscreen нужен, если даёте кнопку на весь экран.
  • Не ставьте sandbox, если не уверены в наборе флагов: игре нужны скрипты, свой origin и хранилище. Урезанный sandbox ломает её молча. Если вы всё же ставите sandbox и хотите, чтобы работали внутриигровые выход и касса из 2.3, нужен ещё allow-top-navigation-by-user-activation — без него браузер не даст игре заменить страницу, и откажет молча.
  • Новая сессия — новый элемент. Не переиспользуйте один iframe, меняя src: создавайте элемент заново, чтобы перезапуск был настоящей перезагрузкой, а не страницей, сохранившей прежнее состояние.
  • The size is yours to set. The game fills the frame and has no height of its own. Give the container a real height — height:100% inside a zero-height block is an empty rectangle.
  • allow="autoplay" — without it there is no sound until the first tap. fullscreen is needed if you offer a full-screen button.
  • Do not set sandbox unless you are sure of the flags: the game needs scripts, its own origin and storage. A trimmed sandbox breaks it silently. If you do sandbox the frame and you want the in-game exit or cashier of 2.3 to work, it also needs allow-top-navigation-by-user-activation — without it the browser refuses to let the game replace the page, and refuses silently.
  • A new session is a new element. Do not reuse one iframe by changing src; create the element again, so reopening is a real reload rather than a page that kept its state.

Только https, с обеих сторон.https on both sides, always.

Ваша страница по https не может открыть фрейм по http — браузер режет это как mixed content, молча и без диалога. Наш адрес всегда https; следите, чтобы и ваш был. A page on https cannot open a frame on http — browsers block it as mixed content, silently and with no prompt. Our address is always https; keep yours that way too.

4Эндпоинт кошелькаThe wallet endpoint

Пять методов. Вы даёте один базовый URL, пути мы дописываем сами.Five endpoints. You give us one base URL; we append the paths.

POST {base}/auth          проверить токен → кто игрок + баланс
POST {base}/getBalance    прочитать баланс, ничего не двигает
POST {base}/bet           списать ставку
POST {base}/win           начислить выплату
POST {base}/rollback      вернуть ставку
POST {base}/auth          verify a token → who the player is + balance
POST {base}/getBalance    read the balance, moves nothing
POST {base}/bet           debit a stake
POST {base}/win           credit a payout
POST {base}/rollback      give a stake back
  • Content-Type: application/json в обе стороны.
  • Всегда отвечайте HTTP 200, в том числе на отказ. Вердикт — это поле code в теле. Кошелёк, который говорит «недостаточно средств» четырёхсотым, неотличим от прокси, который говорит «плохой запрос», а нам эти два случая нужно различать: от них зависит, повторять операцию или нет.
  • Отвечайте быстро. Игра сдаётся по собственному вызову через 5 секунд и сообщает игроку, что ставка не прошла; мы ждём 6, прежде чем счесть запрос потерянным и повторить. То есть 5 с — ваш SLA, а 6 с — только наше терпение: кошелёк, который регулярно отвечает между ними, показывает игрокам ошибки на ставках, которые потом придётся возвращать (см. 12). Цель — меньше секунды.
  • Content-Type: application/json, both ways.
  • Always answer HTTP 200, including for a refusal. The verdict is the code in the body. A wallet that says "insufficient funds" with a 400 is indistinguishable from a proxy saying "bad request", and we have to tell those apart to decide whether to retry.
  • Answer quickly. The game gives up on its own call after 5 seconds and tells the player the bet failed; we wait 6 before treating the request as lost and retrying it. So 5 s is your service level and 6 s is only our patience — a wallet that regularly answers in between shows players errors on bets that then have to be given back (see 12). Under one second is the target.

5ПодписьSignature

Каждый запрос несёт дайджест собственного тела, в заголовке с именем ровно Auth.Every request carries a digest of its own body, in a header named exactly Auth.

Auth: sha256(<сырые байты тела> + <общий ключ>)     hex, нижний регистр
Auth: sha256(<raw request body bytes> + <shared key>)     hex, lower case

Ни префикса, ни разделителя: ключ дописывается прямо к байтам тела, а значением идёт hex дайджеста в нижнем регистре. Проверить свою реализацию — одна строка: No prefix and no separator: the key is appended directly to the body bytes, and the value is the lower-case hex of the digest. Checking your implementation is one line:

printf '%s' "$body$shared_key" | shasum -a 256

Общий ключ обменивается при интеграции и по проводу не ходит.The shared key is exchanged at integration and never travels on the wire.

Проверяйте по тем байтам, которые пришли.Verify against the bytes you received.

Не по повторной сериализации разобранного тела. Пересборка меняет порядок ключей и пробелы, и расхождение выглядит как ошибка аутентификации — ровно до тех пор, пока кто-нибудь не заподозрит кодировщик. Прочитайте тело один раз, проверьте подпись, потом разбирайте. Not against a re-serialisation of the parsed body. Re-encoding reorders keys and changes spacing, and the resulting mismatch presents as an authentication failure for as long as nobody suspects the encoder. Read the body once, verify, then parse it.

6Запросы и ответыRequests and responses

В каждом запросе есть partnerId и token. Остальное зависит от операции. Every request carries partnerId and token. The rest depends on the operation.

6.1Что присылаем мыWhat we send

// POST /auth  и  POST /getBalance
{
  "partnerId":  "198",
  "operatorId": "brand-7",  // только если бренд был в ссылке
  "token":      "abc123",
  "currency":   "USD"       // только getBalance; в auth его нет
}

// POST /bet
{
  "partnerId":     "198",
  "token":         "abc123",
  "transactionId": "bet:alice:1187:0",   // наш; уникален навсегда
  "amount":        2.5,
  "currency":      "USD",
  "gameId":        "555",                // ВАШ каталожный id
  "roundId":       "crash:USD:1187"
}

// POST /win — то же плюс ставка, которую выплачивает
{
  "partnerId":        "198",
  "token":            "abc123",          // МОЖЕТ БЫТЬ ПРОСРОЧЕН — см. 8
  "transactionId":    "take:alice:1187:0",
  "betTransactionId": "bet:alice:1187:0",
  "amount":           7,
  "currency":         "USD",
  "gameId":           "555",
  "roundId":          "crash:USD:1187"
}

// POST /rollback — вернуть ставку
{
  "partnerId":        "198",
  "token":            "abc123",          // МОЖЕТ БЫТЬ ПРОСРОЧЕН — см. 8
  "transactionId":    "cancel:alice:1187:0",
  "betTransactionId": "bet:alice:1187:0",
  "amount":           2.5,
  "currency":         "USD",
  "roundId":          "crash:USD:1187",
  "reason":           "cancel"           // cancel | void | rollback
}
// POST /auth  and  POST /getBalance
{
  "partnerId":  "198",
  "operatorId": "brand-7",  // only when the link named a brand
  "token":      "abc123",
  "currency":   "USD"       // getBalance only; absent on auth
}

// POST /bet
{
  "partnerId":     "198",
  "token":         "abc123",
  "transactionId": "bet:alice:1187:0",   // ours; unique forever
  "amount":        2.5,
  "currency":      "USD",
  "gameId":        "555",                // YOUR catalogue id
  "roundId":       "crash:USD:1187"
}

// POST /win — the same, plus the bet it pays out
{
  "partnerId":        "198",
  "token":            "abc123",          // MAY BE EXPIRED — see 8
  "transactionId":    "take:alice:1187:0",
  "betTransactionId": "bet:alice:1187:0",
  "amount":           7,
  "currency":         "USD",
  "gameId":           "555",
  "roundId":          "crash:USD:1187"
}

// POST /rollback — give a stake back
{
  "partnerId":        "198",
  "token":            "abc123",          // MAY BE EXPIRED — see 8
  "transactionId":    "cancel:alice:1187:0",
  "betTransactionId": "bet:alice:1187:0",
  "amount":           2.5,
  "currency":         "USD",
  "roundId":          "crash:USD:1187",
  "reason":           "cancel"           // cancel | void | rollback
}

roundId собран из игры, валюты и номера раунда: каждая игра нумерует свои раунды сама, поэтому голый номер уникален только внутри одной игры — составной id не даёт пятому раунду двух разных игр слиться в ваших отчётах. roundId is composed of the game, the currency and the round number. Each game numbers its own rounds, so the bare number is unique only within one game — a composed id keeps round 5 of two different games apart in your reports.

Выплата называет ставку, которую закрывает.A payout names the bet it settles.

betTransactionId — единственный способ их сопоставить: у игрока может быть несколько ставок в одном раунде, а номер места по этому проводу не ходит. Начисление, которое не закрывает никакой ставки — джекпот, — его не несёт. betTransactionId is the only way to match the two: a player can hold more than one bet in the same round, and the seat is not on this wire. A credit that settles no bet of its own — a jackpot — carries none.

reason говорит, какой это откат.reason says which kind of rollback this is.

cancel — игрок сам отменил ставку до старта раунда. void — ваш кошелёк подтвердил ставку уже после того, как мы сказали игре, что она не прошла, и теперь мы возвращаем деньги (см. 12). rollback — всё остальное. cancel — the player cancelled the stake before the round started. void — your wallet confirmed a bet after we had already told the game it failed, so we are giving the stake back (see 12). rollback — anything else.

partnerId и operatorId — не два имени одного.partnerId and operatorId are not two names for one thing.

partnerId — это учётные данные: наш идентификатор в вашей системе, одно значение на всю интеграцию. operatorId приезжает с каждым запуском и говорит, к какому вашему бренду относится игрок. Он отсутствует целиком, если в ссылке бренда не было — так что при одном бренде про него можно забыть. partnerId is a credential: our id in your system, one value for the whole integration. operatorId arrives with each launch and says which of your brands the player belongs to. It is absent entirely when the link named no brand, so with one brand you can ignore it.

Поля roundFinished больше нет. Раньше оно было в этих телах и всегда приходило false, потому что ни одна игра не могла заполнить его честно: у раунда нет закрывающего сообщения, проигранная ставка не шлёт ничего (см. 8), а /win закрывает ровно ту ставку, которую называет. There is no roundFinished. It used to be in these bodies and was always false, because nothing in the games could set it truthfully: a round has no closing message, a losing bet sends nothing at all (see 8), and a /win settles exactly the bet it names.

6.2Что отвечаете выWhat you answer

{
  "code":          0,
  "balance":       997.50,     // ПОСЛЕ операции
  "currency":      "USD",
  "transactionId": "bet:alice:1187:0",   // эхо нашего
  "message":       "",         // свободный текст, для человека в логах

  // только /auth:
  "playerId":      "p-1001",
  "username":      "alice"
}
{
  "code":          0,
  "balance":       997.50,     // AFTER the operation
  "currency":      "USD",
  "transactionId": "bet:alice:1187:0",   // echo ours
  "message":       "",         // free text, for humans reading logs

  // /auth only:
  "playerId":      "p-1001",
  "username":      "alice"
}

balance — баланс после операции, в той валюте, о которой спрашивали. На отказе присылайте баланс как есть, если он у вас под рукой: вам это ничего не стоит, а нам экономит вызов. balance is the balance after the operation, in the currency you were asked about. On a refusal send the unchanged balance if you have it: it costs you nothing and saves us a call.

7КодыCodes

Одна таблица на все методы. Что мы сделаем с кодом, зависит от того, в какую сторону шли деньги: отказ по списанию не двигает ничего и окончателен, а начисление — это деньги игрока, и попросить их ещё раз за нас уже некому. One table for every endpoint. What we do with a code depends on which way the money was going: refusing a debit moves nothing and is final, while a credit is money that belongs to the player, and nothing upstream of us will ask for it a second time.

КодCode ЗначениеMeaning /auth, /getBalance, /bet /win, /rollback
0OK примененоit took effect примененоit took effect
110 Уже примененоAlready processed прошло раньше; считаем один разit took effect earlier; counted once прошло раньше; считаем один разit took effect earlier; counted once
1 Общая ошибкаGeneral error окончательный отказ, игрок его видитfinal refusal; the player is told повторяемwe retry
3 Недостаточно средствInsufficient funds окончательный отказ, игрок его видитfinal refusal; the player sees it повторяем (на начислении это бессмыслица)we retry (it means nothing on a credit)
4 Токен не найден или просроченToken not found or expired окончательный отказ, сессия оконченаfinal refusal; the session is over не используйтеdo not use — см. 8; повторяем— see 8; we retry
5 Превышен лимитLimit exceeded окончательный отказ, игрок его видитfinal refusal; the player is told повторяемwe retry
6 Игрок заблокирован или самоисключёнPlayer blocked or self-excluded окончательный отказfinal refusal повторяемwe retry
7 Транзакция не найденаTransaction not found /rollback: считаем закрытым. /win: повторяем — см. ниже/rollback: settled, we stop. /win: we retry — see below

7 — единственный код, смысл которого зависит от направления. На /rollback он закрывает вопрос: если ставки у вас не было, деньги и так там, где должны быть. На /win он не закрывает ничего — игрок выиграл и не получил, — поэтому мы продолжаем слать, а если на горизонте всё ещё 7, выплату забирает человек на нашей стороне. Не отказывайте выплате кодом 7: кода, которым это можно сделать, нет вовсе, и это сделано намеренно. 7 is the one code whose meaning depends on the direction. On a /rollback it settles the matter: if you never had the stake, the money is already where it belongs. On a /win it settles nothing — the player won and has not been paid — so we keep re-sending, and if it is still 7 at the horizon a human on our side picks it up. Do not use 7 to decline a payout; there is no code that does that, by design.

Незнакомый код и тело вообще без кода читаются ровно как 1. A code we do not know, and a body with no code at all, is read exactly like 1.

110 стоит перечитать дважды.110 is worth reading twice.

Это не синоним нуля. Он говорит, что деньги двинулись ровно один раз, а этот вызов был повтором — и именно он велит нам записать одну транзакцию вместо двух. На нём сходятся цифры оборота у вас и у нас. It is not a synonym for 0. It says the money moved exactly once and this call was a repeat — which is what tells us to record one transaction instead of two, and keeps the turnover figures on both sides agreeing.

8ПравилаThe rules

1 · Идемпотентность: ключ — это transactionId.1 · Idempotency: transactionId is the key.

Повтор уже виденного не должен двигать деньги снова и должен отвечать 110 с текущим балансом. Мы повторяем: потерянный ответ не говорит нам, применили вы операцию или нет, — поэтому мы шлём тот же самый transactionId, пока не узнаем. Это самый важный пункт всего документа. Проверка должна быть атомарной — уникальный индекс, а не «прочитал-записал»: повтор может прийти к вам, пока первый запрос ещё в работе. A repeat of one you have seen must not move money again, and must answer 110 with the balance as it stands. We retry: a lost response does not tell us whether you applied the operation, so we re-send the same transactionId until we know. This is the single most important clause in this document. Make the check atomic — a unique constraint, not read-then-write: a retry can reach you while the original request is still being processed.

2 · /win и /rollback работают на просроченном токене.2 · A /win and a /rollback work on an expired token.

Конец сессии игрока — не причина оставить у себя ни его ставку, ни его выигрыш, а раунд может пережить сессию, в которой начался. Не проверяйте токен ни там, ни там. Код 4 — это про /auth, /getBalance и /bet. A player's session ending is not a reason to keep their stake or their winnings, and a round can outlive the session it started in. Do not check the token on either. Code 4 belongs to /auth, /getBalance and /bet.

3 · /win и /rollback мы не бросаем.3 · A /win and a /rollback are never dropped by us.

Всё, кроме 0 / 110 — и 7 на откате, — оставляет операцию неурегулированной, и она уйдёт снова под тем же transactionId, пока не пройдёт или пока не истечёт горизонт (см. 12). Так что не отказывайте в выплате из-за временной проблемы: увидите её снова. Anything but 0 / 110 — and 7 on a rollback — leaves the operation unsettled, and it goes out again under the same transactionId until it lands or the horizon passes (see 12). So do not refuse a payout for something transient, and expect to see it again if you do.

4 · Rollback того, чего у вас нет, — это 7, а не 1.4 · A rollback of something you do not have is 7, not 1.

Семёрку мы считаем окончательным ответом: деньги в любом случае там, где должны быть, и мы останавливаемся. Но идентификатор всё равно запомните: /bet с этим id, который придёт следом — задержавшись где-то по дороге, — не должен забирать деньги. На него ответьте 110. We treat 7 as settled — the money is where it should be either way — and stop. Record the id all the same: a /bet under it that turns up afterwards, held up somewhere on the way, must not take the money. Answer that one 110.

5 · Суммы никогда не отрицательные, а ноль — не ошибка.5 · Amounts are never negative, and zero is not an error.

Направление задаёт эндпоинт: /bet списывает, /win начисляет, /rollback возвращает. Минуса не бывает никогда — исправление делается противоположным методом, а не суммой со знаком. The direction is the endpoint: /bet debits, /win credits, /rollback returns. There is never a minus sign — a correction is the opposite endpoint, not a negative amount.

Ноль приходит ровно в двух случаях, и оба у части операторов выключены: счастливая ставка/bet на 0, раунд за счёт заведения (не списывайте ничего, ответьте 0 и запомните transactionId: за ней может прийти /win); и проигранный раунд в тех модулях, которые о нём сообщают, — /win на 0 (правило 6). Не отказывайте ни в том, ни в другом: нулевая сумма, на которую ответили ошибкой, остаётся у нас нерассчитанной и вернётся повтором. Zero reaches you in exactly two cases, and both are switched off for some operators: a lucky bet — a /bet of 0, a round the house is paying for (debit nothing, answer 0, and record the transactionId: a /win may follow it); and a lost round, in the modules that report one — a /win of 0 (rule 6). Refuse neither: a zero amount answered with an error leaves the round unsettled on our side and comes back as a retry.

6 · Проигранный раунд не шлёт ничего.6 · A losing round sends nothing.

Ни /win, ни /rollback. Ставка, после которой ничего не пришло, а раунд закончился, — проиграна: не держите её открытой и не возвращайте. (Часть наших игр сообщает о проигранном раунде нулевым /win. Если он придёт — примите; но ждать его не надо.) No /win, no /rollback. A bet with nothing after it once the round is over has lost: do not hold it open, and do not give it back. (Some of our games do report a lost round as a /win of zero. Accept one if it comes — never wait for it.)

9ДеньгиMoney

Суммы — JSON-числа в мажорных единицах: 2.5 для 250 минорных единиц двухзначной валюты. JSON-число не хранит хвостовых нулей, поэтому 2.50 приходит как 2.5, а 10.00 — как 10. Разбирайте их как десятичные — не как float и не как строку заданного вида. Amounts are JSON numbers in major units: 2.5 for 250 minor units of a two-decimal currency. A JSON number keeps no trailing zeros, so 2.50 arrives as 2.5 and 10.00 as 10. Parse them as decimals — not as floats, and not as text of a fixed shape.

У себя мы держим деньги целым числом минорных единиц и конвертируем один раз — здесь, на краю провода. Конвертация, которая потеряла бы долю минорной единицы, падает, а не округляет: мы не станем молча срезать часть выплаты и предпочтём, чтобы интеграция громко сломалась на тестах. We hold money as an integer count of minor units and convert once, here at the wire edge. A conversion that would lose a fraction of a minor unit fails rather than rounding — we will not silently shave value off a payout, and we would rather an integration break loudly during testing.

Сделайте так же.Do the same on your side.

amount * 100 через float — это то, как 2.50 превращается в 249. amount * 100 through a float is how 2.50 becomes 249.

12Таймауты и повторыTiming and retries

Ответить за 5 с — ваш SLAAnswer within 5 s — your service level игра сдаётся по собственному вызову на 5 секунде и сообщает игроку, что ставка не прошла. Всё, что придёт позже, до игрока как удачная ставка уже не дойдёт.the game gives up on its own call at 5 seconds and tells the player the bet failed. Nothing you send after that reaches the player as a bet that worked.
Мы ждём 6 с — наш таймаутWe wait 6 s — our timeout после этого считаем запрос потерянным и повторяем. Лишняя секунда — на переходы между игрой, нашим шлюзом и вашим кошельком: ответ на 5,5 с для игрока уже опоздал, но закрывает транзакцию и экономит повтор. Реальная цель — меньше секунды.past it we treat the request as lost and retry it. The extra second is for the hop between the game, our gateway and your wallet — an answer at 5.5 s is too late for the player and still worth having, because it settles the transaction and saves a retry. Under one second is the real target.
ПовторяемWe re-send тот же transactionId, то же тело: если ответа нет вообще (таймаут, обрыв соединения), если HTTP 5xx, если тело не-JSON; а на /win и /rollbackпри любом коде, кроме 0 и 110, плюс 7, который закрывает только /rollback.the same transactionId and the same body: when there is no answer at all (timeout, connection error), on HTTP 5xx, on a body that is not JSON; and on /win and /rollback, for any code but 0 and 110 — plus 7, which settles a /rollback only.
Не повторяемWe do not re-send /bet, на который вы ответили кодом. Этот код и есть вердикт, каким бы ни был HTTP-статус.a /bet you answered with a code. That code is the verdict, whatever the HTTP status.
РасписаниеSchedule первый повтор — через несколько секунд, дальше интервал удваивается, до одного раза в минутуthe first re-send within a few seconds, then the interval doubles, up to once a minute
ГоризонтHorizon столько, сколько вы держите transactionId. Назовите число — мы подстроимся; после этого операция уходит на разбор человеку у нас.as long as you keep a transactionId. Tell us the number and we match it; after that the operation is flagged for a human on our side.

Один счёт — один запрос за раз, с нашей стороны. Пока /bet, /win или /rollback по игроку не получил ответа, мы придерживаем /getBalance и /auth по тому же игроку до его завершения. Баланс, прочитанный в середине записи, — это число, которое вот-вот перестанет быть правдой, а попадёт оно на экран игрока как текущее состояние. Ставки с разных мест в одном раунде по-прежнему приходят одновременно, каждая со своим transactionId, — поэтому проверка идемпотентности должна быть атомарной (8, правило 1), и именно она делает параллельные записи безопасными. One account, one request at a time — from us. While a /bet, /win or /rollback for a player is still unanswered, we hold back any /getBalance or /auth for that same player until it settles. A balance read taken mid-write is a number that is about to stop being true, and it would reach the player's screen as though it were the state. Bets at different seats in one round still arrive together, each under its own transactionId — so your idempotency check must be atomic (8, rule 1), and it is the thing that makes overlapping writes safe.

Ставку, которую мы не смогли подтвердить, мы возвращаем, а не оставляем.A bet we could not confirm is given back, not kept.

Игра выбрасывает ставку, вызов по которой не удался: место не зафиксировано, окно приёма ставок закрылось. Мы продолжаем слать эту ставку, пока вы не ответите, — и если ответ окажется «применено», игрок заплатил за раунд, которого у него не было. Поэтому мы возвращаем деньги откатом с reason: "void". Он уходит только после вашего вердикта по ставке, так что обогнать саму ставку он не может. The game drops a bet whose call failed: no seat is recorded and the betting window closes behind it. We keep re-sending that bet until you answer — and if the answer turns out to be "applied", the player has paid for a round their game never had. So we give the stake back with a /rollback carrying reason: "void". It is sent only after your verdict on the bet, so it can never race the bet itself.

/bet никогда не приходит после своего же /rollback.A /bet never arrives after its own /rollback.

Мы не откатываем ставку, исход которой нам неизвестен: неотвеченная ставка уходит повторно как та же самая ставка. Правило 4 в разделе 8 — это страховка на случай, если запрос задержало что-то между нами. We do not roll back a bet whose outcome we do not know; an unanswered bet is re-sent as the same bet. Rule 4 in section 8 is the belt for the case where something between us holds a request up anyway.

13Лимиты и лестница ставокLimits and the stake ladder

Лимиты обеспечиваем мы, по числам, которые выбираете вы. По каждой валюте и каждой игре у нас лежат: Limits are ours to enforce, from values you choose. Per currency and per game we hold:

  • лестница ставок — суммы, которые может выбрать игрок. Она же задаёт минимальную и максимальную ставку: всё, что вне её, отклоняется ещё до вашего кошелька;
  • максимальный выигрыш на ставку — максимум, который может вернуть одна ставка. Именно на ставку, а не на раунд, и применяется он ограничением РАУНДА, а не урезанием выплаты — см. примечание ниже.
  • a stake ladder — the amounts a player can pick. It is also the minimum and the maximum stake: anything outside it is refused before it reaches your wallet;
  • a maximum win per bet — the most a single bet can return. Per bet, not per round, and applied by capping the ROUND rather than by trimming a payout — see the note below.

Пришлите нужные числа по каждой валюте — мы их выставим; поменять потом это конфигурация у нас, а не релиз. Отказать в ставке вы, разумеется, можете и сами — кодом 5. Send us the numbers you want, per currency, and we set them; changing them later is configuration on our side, not a release. You may of course refuse a stake yourself with code 5.

Как именно применяется максимальный выигрыш — ответ не тот, который напрашивается.How the maximum win is applied, because the answer is not the obvious one.

Это не урезанная выплата и не принудительный автокэшаут. В момент постановки ставки точка краша раунда для этого игрока ограничивается значением max_win / ставка, и ракета просто разбивается там. Игрок видит обычный проигранный раунд на этом коэффициенте; ваш кошелёк получает обычный /win не выше кэпа — или не получает ничего. Нет выплаты, которую надо сверять с лимитом, и нет сообщения о том, что выигрыш урезали. It is not a trimmed payout and not a forced cash-out. When a bet is placed, the round's crash point is capped for that player at max_win / stake, and the rocket simply crashes there. The player sees an ordinary losing round at that multiplier; your wallet receives an ordinary /win at or below the cap, or nothing at all. There is no payout to reconcile against a limit, and no message saying a win was reduced.

Потолок полёта может сработать раньше. Полёт в этой игре ограничен 1001×. Ставка, для которой ставка × 1001 меньше вашего максимального выигрыша, до кэпа не доходит вовсе. Несколько ставок в одном раунде делят самый строгий кэп: раунд держит НАИМЕНЬШИЙ кэп среди своих ставок. А если ставка равна максимальному выигрышу или больше, кэп упирается в 1.00× и раунд разбивается сразу — игрок проигрывает ставку, и это задуманное поведение. The flight cap can bind first. This game's flight is capped at 1001×, so a stake small enough that stake × 1001 is below your maximum win never reaches the cap at all. Several bets in one round share the strictest cap: the round keeps the LOWEST cap among its bets. And if a stake is at or above the maximum win, the cap floors at 1.00× and the round crashes immediately — the player loses the stake, which is the intended behaviour.

10Чек-листChecklist

Интеграция готова, когда выполняется всё перечисленное.Your integration is done when all of these hold.

  1. /auth с валидным токеном возвращает игрока, валюту и баланс./auth with a valid token returns the player, the currency and a balance.
  2. /bet уменьшает баланс ровно на сумму./bet decreases the balance by exactly the amount.
  3. /win увеличивает его ровно на сумму./win increases it by exactly the amount.
  4. Один и тот же transactionId дважды не двигает деньги дважды, и второй ответ — 110.The same transactionId twice does not move money twice, and the second answer is 110.
  5. /rollback восстанавливает баланс, а его повтор отвечает 110./rollback restores the balance, and a repeat of it answers 110.
  6. /win и /rollback работают на просроченном токене./win and /rollback work with an expired token.
  7. /rollback неизвестного betTransactionId отвечает 7./rollback of an unknown betTransactionId answers 7.
  8. Ставка больше баланса отвечает 3, и баланс не меняется.A bet larger than the balance answers 3, and the balance is unchanged.
  9. /win с нулевой суммой принят.A zero-amount /win is accepted.
  10. 2.5 на проводе — это 250 минорных единиц в вашем реестре, в обе стороны.2.5 on the wire is 250 minor units in your ledger, both ways.
  11. /bet с идентификатором, который вы уже откатили, не двигает денег и отвечает 110.A /bet whose id you have already rolled back moves nothing and answers 110.
  12. Запрос с неверным дайджестом в Auth отвергается.A request with a wrong Auth digest is refused.

Четвёртый и шестой — те, что падают в проде. Проверить их дёшево, а обнаружить дорого. Numbers 4 and 6 are the ones that fail in production. They are cheap to test and expensive to discover.

У нас есть эталонная реализация — попросите, если нужна.There is a reference implementation — ask if you want one.

Это кошелёк, который реализует этот документ буквально и работает строго: неверный дайджест тела он отвергает, а не терпит, — так что ошибка в подписи всплывает сейчас, а не на боевом запуске. Он же умеет ронять ответ после того, как деньги двинулись: это тот случай, который решает, спишется у игрока один раз или два, и его не даст на заказ ни одна песочница. Скачиваемой сборки мы не раздаём: скажите, что она вам нужна, и мы поднимем такой кошелёк для вас и пришлём адрес с ключом. Но для проверки ВАШЕЙ стороны полезнее следующий пункт. It is a wallet that implements this document exactly and is strict: a wrong body digest is refused rather than tolerated, so a signature mistake shows up now instead of at go-live. It also drops the response after the money has moved — the case that decides whether a player is charged once or twice, and the one no sandbox gives you on demand. We do not hand out a download: say you want one and we will stand it up for you and send the address and a key. For checking YOUR side, though, the next item is the better tool.

И мы прогоняем чек-лист по ВАШЕМУ кошельку.And we run the checklist against YOUR wallet.

Пришлите адрес тестового кошелька, тестовый токен и один просроченный — мы пройдём все двенадцать пунктов по вашему эндпоинту тем же кодом, который ходит к вам в проде. Он двигает одну ставку и возвращает её на место, а мы присылаем вам запрос и ответ по каждому шагу. Send us a test wallet URL, a test token and one expired token, and we walk all twelve items against your endpoint with the same code that talks to you in production. It moves a stake and puts it back, and we send you the request and the answer for every step.

11Что прислать намWhat to send us

Базовый URL кошелькаWallet base URL один; пути /auth, /bet, … допишем самиone; we append /auth, /bet, …
Общий ключShared key для дайджеста телаfor the body digest
Ваш идентификаторYour operator id ляжет в partnerIdgoes in partnerId
Несколько ли брендов на одном кошелькеWhether you run several brands off one wallet если да — кладите бренд в ссылку и ждите его обратно как operatorIdif so, put the brand on the link and expect it back as operatorId
ВалютыCurrencies какие обслуживает эта интеграцияwhich ones this integration serves
Каталожные id игрGame ids ваш id для каждой нашей игры, если пользуетесь своимиyour catalogue id for each of our games, if you use your own
Горизонт повторовRetry horizon сколько вы держите transactionId, прежде чем считаете операцию окончательной — мы подстроимся, чтобы не сдаться раньше васhow long you keep a transactionId before you consider it settled — we match it, so that we never give up before you do
Тестовый кошелёкA test wallet его адрес и ключ, тестовый токен и один просроченный: это то, что нужно для прогона чек-листаits URL and key, a test token and one expired token: that is what the checklist run needs
Лимиты ставокStake limits лестница и максимальный выигрыш на ставку, по каждой валюте — см. 13the ladder and the maximum win per bet, per currency — see 13

Если что-то из этого потом поменяется — всё это конфигурация на нашей стороне. Ни один пункт не требует деплоя. If any of it changes later, all of it is configuration on our side. None of it is a deploy.