Notell API Docs
Русский

Integration

Wallet callbacks

The platform keeps no balance of its own. For every player action that touches money it calls your wallet endpoint. You implement one HTTPS endpoint that handles several method values.

Transport#

PropertyValue
URLPOST <callback URL>/<currency>, e.g. https://casino.example/wallet/EUR. The currency in the path is any of the 157 codes of Currencies.
Where to set the URLAdmin panel → Projects → your project → Integration (see Admin panel)
BodyJSON object with sorted keys; includes api_key. Amounts are JSON numbers with two decimals (1.50).
HeadersContent-Type: application/json, X-Notell-Timestamp, X-Notell-Signature (Callback signature)
AuthenticationThe project api_key in the body: compare it with your stored key. Every request is also signed.
Time-out2 seconds per attempt; redirects are not followed
SuccessHTTP 2xx with the JSON answer described in The answer. A non-2xx status counts as a failure even if the body says ok
IdempotencyEvery bet / win / refund carries a unique meta.transaction; a retry repeats the same id and body

Connections. If the host of your callback URL is not behind Cloudflare, the platform keeps connections to it alive (idle connections are closed after 60 seconds, and a request that fails on a reused connection is repeated once on a fresh one). Otherwise every request is sent with Connection: close. If your wallet sits behind Cloudflare or hides its origin, the Direct IP setting of the project (admin panel, Integration) pins the connection to a public, non-Cloudflare IP address (the TLS certificate name is still checked).

If a project has no callback URL configured, the platform uses a built-in test wallet (starting balance 10,000.00 per account) so you can try games before your wallet is ready.

Callback signature#

Every callback carries two headers, so you can prove that a request comes from the platform and was not altered (the api_key in the body stays as it was; checking the signature is optional but recommended):

HeaderValue
X-Notell-TimestampUnix time in seconds when this attempt was signed (a retry is signed again)
X-Notell-Signaturev1= + lowercase hex HMAC-SHA256

Canonical string (UTF-8, lines separated by a single \n, no trailing newline):

<X-Notell-Timestamp>
POST
<URL path of the request, e.g. /wallet/EUR>
<raw request body, byte for byte>

Secret. By default it is derived from your project api_key: secret = hex(HMAC-SHA256(key = api_key, message = "notell-callback-signing-v1")), so the key itself is never used as an HMAC key and a rotated API key changes the secret. If you prefer an independent secret, ask the account manager to configure an explicit one for your project.

Hash the raw bytes of the body as received. Amounts are written as 1.50; a parser that re-serialises the JSON turns it into 1.5 and the signature no longer matches.

js
import {createHmac, timingSafeEqual} from 'node:crypto';
const secret = createHmac('sha256', API_KEY).update('notell-callback-signing-v1').digest('hex');
function verify(req, rawBody) {            // rawBody: the exact bytes received
  const ts = req.headers['x-notell-timestamp'], sig = req.headers['x-notell-signature'] ?? '';
  if (!/^\d{9,12}$/.test(ts ?? '') || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;   // replay window
  const mac = createHmac('sha256', secret).update(`${ts}\nPOST\n${new URL(req.url, 'http://x').pathname}\n${rawBody}`).digest('hex');
  return sig.length === mac.length + 3 && timingSafeEqual(Buffer.from(sig), Buffer.from('v1=' + mac));
}

Test vector. With api_key = YOUR_API_KEY the secret is

43a216923c5d33308ab3944616a873baed88975e9781a84ab18618cb710b0942

For timestamp 1800000000, path /wallet/EUR and the raw body (a minimal body; real ones carry more meta fields)

json
{"amount":1.50,"api_key":"YOUR_API_KEY","meta":{"round_id":"b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234","transaction":"6f1d0c9e-3b7a-4c52-8e14-a9d05b6c7e21"},"method":"bet","player_id":"player-1024"}

the header is

X-Notell-Signature: v1=78d40d425a7ed15ef7a9eaeaedb28baba83babc3d25b2d1c84873f4db608932b

Our callback IP addresses#

Callbacks leave the platform from fixed addresses. GET /capabilities lists them in callbackSourceIps; allow exactly those on your firewall. The values per environment are on the Environments page. Tell the account manager before you move your wallet behind a new address range; the callback URL itself is set in the admin panel.

The answer#

Every method is answered with the same JSON object, also when it reports an error:

json
{"status": "ok", "balance": 998.50, "currency": "EUR"}
FieldTypeRequiredDescription
statusenumyes"ok" or "error".
balancenumberyesThe player's balance after the operation, as a JSON number (not a string): non-negative, at most 2 decimals, up to 13 digits before the point, no exponent (998.5, 1000, 0.50 are valid; 1e3, -1, 988.5000000001 are not). Required for "error" answers too.
currencystringyesMust equal the currency in the request path; otherwise the answer is rejected as invalid. Required for "error" answers too.
messagestringwith status "error"A code such as INSUFFICIENT_FUNDS.

An answer that breaks these rules (missing or malformed balance, wrong currency, not JSON) is not read as a refusal but as a broken wallet: INVALID_CALLBACK_RESPONSE. The operation is then treated as unknown (see Retries and failures), and a launch fails.

session_info#

CALLBACKsession_info

Sent once at game launch (not when an unfinished round is resumed). Nothing is changed; answer with the current balance.

Request#

curl
curl -X POST https://casino.example/wallet/EUR \
  -H 'Content-Type: application/json' \
  -d '{"api_key":"YOUR_API_KEY","method":"session_info","player_id":"player-1024","session_id":"9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21"}'
JSON
{
  "api_key": "YOUR_API_KEY",
  "method": "session_info",
  "player_id": "player-1024",
  "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21"
}
FieldTypeRequiredDescription
methodstringyes"session_info".
player_idstringyesYour player id.
session_idstringyesSession created by the launch.
api_keystringyesYour project's API key.

Response#

json
{"status": "ok", "balance": 1000.00, "currency": "EUR"}
FieldTypeDescription
status, balance, currencyAs in The answer.

Errors#

CodeHTTPWhen
BALANCE_UNAVAILABLE400 (returned by the launch)You answer "status":"error"; the launch fails and no session is created.
CALLBACK_HTTP_ERROR400 (returned by the launch)You answer a non-2xx HTTP status.
INVALID_CALLBACK_RESPONSE400 (returned by the launch)The answer is not valid.

bet#

CALLBACKbet

Sent before a paid round is played. Debit amount and answer.

Request#

curl
curl -X POST https://casino.example/wallet/EUR \
  -H 'Content-Type: application/json' \
  -d '{"amount":1.50,"api_key":"YOUR_API_KEY","meta":{"round_id":"b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234","session_id":"9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21","symbol":"gates-of-olympus","time":1800000000,"transaction":"6f1d0c9e-3b7a-4c52-8e14-a9d05b6c7e21"},"method":"bet","player_id":"player-1024"}'
JSON
{
  "amount": 1.50,
  "api_key": "YOUR_API_KEY",
  "meta": {
    "round_id": "b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234",
    "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21",
    "symbol": "gates-of-olympus",
    "time": 1800000000,
    "transaction": "6f1d0c9e-3b7a-4c52-8e14-a9d05b6c7e21"
  },
  "method": "bet",
  "player_id": "player-1024"
}
JSON (free-spin round)
{
  "amount": 0.00,
  "api_key": "YOUR_API_KEY",
  "meta": {
    "freespins": {"external_id": "welcome-2027-01", "grant_id": "7b1c4e92-0a3d-4f68-b5c1-2e9d8a6f3b40", "nominal_bet": 1, "remaining": 9},
    "is_freespins": true,
    "round_id": "d4e8a1b6-93c2-4075-8f1e-5b0a7c2d9e63",
    "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21",
    "symbol": "gates-of-olympus",
    "time": 1800000300,
    "transaction": "0b6e5d3a-8c14-4f97-a2d0-1e7c9b4f6a58"
  },
  "method": "bet",
  "player_id": "player-1024"
}
FieldTypeRequiredDescription
methodstringyes"bet".
player_idstringyesYour player id.
amountnumberyesStake in currency units, positive; 0 for a free-spin round.
meta.transactionstringyesUnique id of this debit; a retry repeats it.
meta.round_idstringyesRound id.
meta.session_idstringyesSession id.
meta.symbolstringyesGame id.
meta.timeintegeryesUnix time in seconds.
meta.is_freespinsbooleanfree-spin roundstrue when the round belongs to an operator free-spins grant; accept the zero bet.
meta.freespinsobjectfree-spin roundsgrant_id, external_id (or null), nominal_bet (number) and remaining spins.
api_keystringyesYour project's API key.

Response#

Debit the stake and answer with the new balance:

json
{"status": "ok", "balance": 998.50, "currency": "EUR"}

For a real lack of funds answer HTTP 200 with:

json
{"status": "error", "message": "INSUFFICIENT_FUNDS", "balance": 0.50, "currency": "EUR"}

The round is rejected and nothing is played. Return this code only for a real lack of funds.

Errors#

CodeHTTPWhen
INSUFFICIENT_FUNDS200Your answer: balance too low. The round is rejected at once; nothing is retried.
TRANSACTION_CANCELLED200Your answer: the debit arrives after you recorded a refund for this transaction.
any other4xx except 408 and 429Your HTTP status: a definitive refusal. The round is rejected at once and a refund is queued.
any other5xx, 408, 429, time-outThe debit is unknown and retried (Retries and failures).

win#

CALLBACKwin

Sent when a round pays. amount is the total win of the round (positive; zero wins are not sent).

Request#

curl
curl -X POST https://casino.example/wallet/EUR \
  -H 'Content-Type: application/json' \
  -d '{"amount":12.75,"api_key":"YOUR_API_KEY","meta":{"round_id":"b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234","session_id":"9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21","symbol":"gates-of-olympus","time":1800000012,"transaction":"0b6e5d3a-8c14-4f97-a2d0-1e7c9b4f6a58"},"method":"win","player_id":"player-1024"}'
JSON
{
  "amount": 12.75,
  "api_key": "YOUR_API_KEY",
  "meta": {
    "round_id": "b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234",
    "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21",
    "symbol": "gates-of-olympus",
    "time": 1800000012,
    "transaction": "0b6e5d3a-8c14-4f97-a2d0-1e7c9b4f6a58"
  },
  "method": "win",
  "player_id": "player-1024"
}
JSON (free-spins final credit)
{
  "amount": 23.50,
  "api_key": "YOUR_API_KEY",
  "meta": {
    "freespins": {"external_id": "welcome-2027-01", "final_credit": true, "grant_id": "7b1c4e92-0a3d-4f68-b5c1-2e9d8a6f3b40", "nominal_bet": 1, "total_win": 23.5},
    "is_freespins": true,
    "round_id": "e7a02c58-1f94-4b36-9d8a-3c5e6b7f0a12",
    "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21",
    "symbol": "gates-of-olympus",
    "time": 1800000900,
    "transaction": "a3f6c1d8-52e7-4b09-8c14-7d2e9a0b5f63"
  },
  "method": "win",
  "player_id": "player-1024"
}
FieldTypeRequiredDescription
methodstringyes"win".
player_idstringyesYour player id.
amountnumberyesTotal win of the round, positive. For the free-spins final credit: the sum of all wins of the grant.
meta.transactionstringyesUnique id of this credit; a retry repeats it.
meta.round_id, meta.session_id, meta.symbol, meta.timeyesAs in bet. For the final credit round_id is the last round of the grant, which may differ from the round of an earlier bet.
meta.is_freespinsbooleanfree-spins final credittrue.
meta.freespinsobjectfree-spins final creditgrant_id, external_id (or null), nominal_bet, total_win (equals amount) and final_credit: true.
api_keystringyesYour project's API key.

Free-spin wins are paid once. The wins of the rounds of an operator free-spins grant are not sent per spin. After the last spin the platform sends one win for the whole grant, with meta.freespins.final_credit: true, before the freespins_finished summary. The summary's total_win equals that credit. Do not look for a win per winning spin, and do not reconcile such a win with the bet of its round_id.

Response#

json
{"status": "ok", "balance": 1011.25, "currency": "EUR"}

Errors#

CodeHTTPWhen
anyanyA credit is never cancelled: if you do not answer ok it is retried until confirmed.

refund#

CALLBACKrefund

Sent when a round is cancelled after its bet may have been debited: the bet stayed unconfirmed past the cancel policy (10 minutes or 600 attempts, whichever comes first), the wallet definitively refused it with a 4xx status, or you cancelled the round with round_cancel.

Request#

curl
curl -X POST https://casino.example/wallet/EUR \
  -H 'Content-Type: application/json' \
  -d '{"amount":1.50,"api_key":"YOUR_API_KEY","meta":{"original_transaction":"6f1d0c9e-3b7a-4c52-8e14-a9d05b6c7e21","reason":"wallet_unconfirmed","round_id":"b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234","session_id":"9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21","symbol":"gates-of-olympus","time":1800000600,"transaction":"c41e8a07-6d2b-4f35-9e1c-0a8b7d3f5c92"},"method":"refund","player_id":"player-1024"}'
JSON
{
  "amount": 1.50,
  "api_key": "YOUR_API_KEY",
  "meta": {
    "original_transaction": "6f1d0c9e-3b7a-4c52-8e14-a9d05b6c7e21",
    "reason": "wallet_unconfirmed",
    "round_id": "b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234",
    "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21",
    "symbol": "gates-of-olympus",
    "time": 1800000600,
    "transaction": "c41e8a07-6d2b-4f35-9e1c-0a8b7d3f5c92"
  },
  "method": "refund",
  "player_id": "player-1024"
}
FieldTypeRequiredDescription
methodstringyes"refund".
player_idstringyesYour player id.
amountnumberyesEquals the amount of the bet.
meta.transactionstringyesId of the refund itself.
meta.original_transactionstringyesThe meta.transaction of the bet being refunded.
meta.reasonenumyeswallet_unconfirmed or operator_cancel.
meta.round_id, meta.session_id, meta.symbol, meta.timeyesAs in bet.
api_keystringyesYour project's API key.

Rules for your wallet:

  • The original bet was debited and not yet refunded: credit amount back, answer ok with the new balance.
  • The original bet was never received (or you rejected it): remember that this transaction id is cancelled, change nothing and answer ok. If that bet arrives later, refuse it with {"status":"error","message":"TRANSACTION_CANCELLED","balance":1000.00,"currency":"EUR"}.
  • A repeat of the same refund (same meta.transaction) returns the first answer. One bet is refunded at most once.
  • An error answer is retried until you answer ok; the platform never sends the bet again after it created the refund.

Response#

json
{"status": "ok", "balance": 1000.00, "currency": "EUR"}

Errors#

CodeHTTPWhen
anyanyIf you do not answer ok the refund is retried (2 s, doubling, at most 5 minutes) until confirmed.

round_info#

CALLBACKround_info

Informational round notification: record it, change nothing, answer ok with the current balance. Two are sent per round, in order: start (after the debit is confirmed) and finish (after the credit is confirmed, or after the round ended without a win). finish is delivered only after start was acknowledged.

Request#

curl
curl -X POST https://casino.example/wallet/EUR \
  -H 'Content-Type: application/json' \
  -d '{"api_key":"YOUR_API_KEY","bet_amount":1.50,"bet_type":"spin","bonus_buy":false,"bonus_trigger":true,"currency":"EUR","game_symbol":"gates-of-olympus","has_win":true,"method":"round_info","multiplier":8.5,"player_id":"player-1024","round_id":"b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234","session_id":"9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21","status":"finish","timestamp":1800000015,"win_amount":12.75}'
JSON
{
  "api_key": "YOUR_API_KEY",
  "bet_amount": 1.50,
  "bet_type": "spin",
  "bonus_buy": false,
  "bonus_trigger": true,
  "currency": "EUR",
  "game_symbol": "gates-of-olympus",
  "has_win": true,
  "method": "round_info",
  "multiplier": 8.5,
  "player_id": "player-1024",
  "round_id": "b2a7c1d4-5e60-4f1b-9a3c-7d2e8f901234",
  "session_id": "9d0c3f7e-41b8-4a52-9c0e-6d3f5a7b8c21",
  "status": "finish",
  "timestamp": 1800000015,
  "win_amount": 12.75
}
FieldTypeRequiredDescription
methodstringyes"round_info".
statusenumyesstart or finish.
player_id, round_id, session_id, game_symbol, currencystringyesIdentifiers of the round.
timestampintegeryesUnix time in seconds.
bet_amountnumberyesStake of the round (nominal bet for free-spin rounds).
bet_typestringyesType of bet of the round, for example spin or purchase.
bonus_buybooleanyesThe round was a bonus buy.
bonus_triggerbooleanyesThe round triggered a bonus feature.
win_amountnumberon finishTotal win of the round.
multipliernumberon finishWin divided by the bet.
has_winbooleanon finishWhether the win is positive.
forcedbooleannotrue when the round was produced for an operator target outcome.
api_keystringyesYour project's API key.

The finish notification is the round summary: use it for reporting; money has already moved through bet and win.

Response#

json
{"status": "ok", "balance": 1011.25, "currency": "EUR"}

Errors#

CodeHTTPWhen
anyanyIf you do not answer ok it is retried later; a project that does not respond is skipped for a while so that it never blocks others.

freespins_finished#

CALLBACKfreespins_finished

One per finished free-spins grant, sent after the final-credit win. Answer in the usual format; it is repeated until you do.

Request#

curl
curl -X POST https://casino.example/wallet/EUR \
  -H 'Content-Type: application/json' \
  -d '{"api_key":"YOUR_API_KEY","bet":"1.00","currency":"EUR","external_id":"welcome-2027-01","finished_at":"2027-01-15T12:30:00.000Z","game":"gates-of-olympus","grant_id":"7b1c4e92-0a3d-4f68-b5c1-2e9d8a6f3b40","method":"freespins_finished","played":10,"player_id":"player-1024","status":"used","total":10,"total_win":"23.50"}'
JSON
{
  "api_key": "YOUR_API_KEY",
  "bet": "1.00",
  "currency": "EUR",
  "external_id": "welcome-2027-01",
  "finished_at": "2027-01-15T12:30:00.000Z",
  "game": "gates-of-olympus",
  "grant_id": "7b1c4e92-0a3d-4f68-b5c1-2e9d8a6f3b40",
  "method": "freespins_finished",
  "played": 10,
  "player_id": "player-1024",
  "status": "used",
  "total": 10,
  "total_win": "23.50"
}
FieldTypeRequiredDescription
methodstringyes"freespins_finished".
grant_idstringyesId of the grant.
external_idstringnoYour id of the grant, if you gave one.
player_id, currency, gamestringyesPlayer, currency and game of the grant.
statusenumyesFinal status of the grant, for example used; values: Free spins.
totalintegeryesSpins granted.
playedintegeryesSpins played.
betmoneyyesBet of each spin.
total_winmoneyyesTotal win of the grant; equals the amount of the final-credit win.
finished_atstringyesISO 8601 time.
api_keystringyesYour project's API key.

Response#

json
{"status": "ok", "balance": 1011.25, "currency": "EUR"}

Errors#

CodeHTTPWhen
anyanyIf you do not answer ok it is retried: 10 s × 2<sup>attempts</sup>, at most 1 hour.

Order of calls in a round#

bet  ->  (debit confirmed)  ->  round_info start  ->  [play]  ->  win (if > 0)  ->  (credit confirmed)  ->  round_info finish

Free-spin grants: bet (amount 0) per spin, no win per spin, then one win with meta.freespins.final_credit: true, then freespins_finished.

Retries and failures#

SituationWhat the platform does
bet answered INSUFFICIENT_FUNDSThe round is rejected; nothing is retried
bet answered with HTTP 4xx except 408 and 429Definitive refusal: the round is rejected at once and a refund is queued
bet with HTTP 5xx, 408, 429, a time-out, a network error or an invalid answerThe debit is unknown and retried with the same transaction id and body: the first 4 attempts immediately, then after 1 s, doubling up to 60 s. If it is still unconfirmed after 10 minutes or 600 attempts (whichever comes first) the round is cancelled and a refund is sent. The round stays pending meanwhile
The same on winRetried until confirmed (a win is never cancelled)
refund not answered okRetried with growing delays (2 s doubling, at most 5 minutes) until confirmed
round_info not answered okRetried later; a project that does not respond is skipped for a while so that it never blocks others
freespins_finished not answered okRetried with growing delays: 10 s × 2<sup>attempts</sup>, at most 1 hour

Because retries are expected, your wallet must be idempotent: remember the answer you gave for each meta.transaction and return it again for a repeat of the same request. A repeat must never debit or credit twice.

Checklist for your wallet#

  • [ ] Verify api_key; serve HTTPS only; answer within 2 seconds.
  • [ ] Parse amount as a decimal; return balance as a JSON number (non-negative, at most 2 decimals) in every answer, errors included.
  • [ ] Echo the currency from the URL path in currency.
  • [ ] Idempotency by meta.transaction (same id, same body → same answer).
  • [ ] INSUFFICIENT_FUNDS as HTTP 200; no HTTP 4xx for temporary problems.
  • [ ] Accept amount: 0 for bet when meta.is_freespins is true; expect one final-credit win per free-spins grant.
  • [ ] Answer round_info and freespins_finished with ok (status, balance, currency).
  • [ ] Implement refund (idempotent, tombstone for unknown transactions) and, if you can, verify X-Notell-Signature over the raw body.
  • [ ] Allow the callback source IPs from /capabilities.
  • [ ] Use Test connection in the admin panel and watch API Logs while testing.
Documentation for operators and integrators.TEST_ONLY platform: non-withdrawable units, no real payments.