Skip to documentation
GAME_ALLIGATOR
ProductsGamificationIntegrationDemos
Let’s talk
ProductsGamificationIntegrationDemos
Let’s talk
Integration
OverviewGuidesAPI referenceResources
Integration overview
Integration architectureFundamentalsGetting startedCertification
Products
Aggregation
Part I · Fundamentals
Core flowRequest & signingEnvelope, money and data formatsErrors: one modelIdempotency & retriesRate limits, pagination, networkMoney Path Rules
Part II · Operator API
Getting startedGames APIWallet APIFree rounds APIReports APIFeatures APIEvent stream
Part III · Certification
Run the checks from the Operator PortalThe command-line tool (for your CI)The checklistCertification checklist
Part IV · Changelog and status
ChangelogDocument ControlChangelog & migration guide: Operator API v2
Appendix
Numbers to rememberWhole guide on one page
API reference
Get player balanceAuthenticate player sessionDebit player balance (bet)Credit player balance (win/bonus)Atomic debit and creditRollback transactionClose game roundSettle free-round grantReconcile uncertain transactionNon-financial notificationgetCapabilitieslistGamesupdateGamelaunchGamelaunchDemocloseSessionlistSessionslistRoundsexportRoundsgetAggregatesissueGrantlistGrantscancelGrantsubscribeackgetOperatorCapabilitiesgetGameFeatureslistBonusBuyTypesissueBonusBuycancelBonusBuygetJackpotsgetBetRangesgetRoundReplaygetRoundDetailslistCampaignscancelCampaignqueryProviderTransactionslistTournamentsgetTournamentgetLeaderboard
Gamification
Integration
Quick startWidgets on your siteSigned-in playersLaunching gamesGameplay eventsTransportsOutcomes and your obligationsGame cataloguesRewardsOnboarding and go-liveReasons, numbers, currencies
Poker
Integration
OverviewEmbed the poker clientServer APIEvents and webhooks
llms-full.txt
Start here
Aggregation / Part I · FundamentalsCore flow

Game Alligators (GA) puts game studios' games into your casino. Traffic runs in two directions, each with its own key pair. Don't mix them. Direction Who calls whom What it's for Key you use Chapter You → GA You call https://api.rexplay.sit

Aggregation / Part I · FundamentalsRequest & signing

Every call GA sends to your wallet looks like this: http POST /v2/wallet/debit HTTP/1.1 Host: wallet.operator.example Content Type: application/json X API Key Id: key live 01 X Request Id: 0198a1d0 9a30 7f08 a7dd 713e4fd33db0 Idempotency Ke

Aggregation / Part I · FundamentalsEnvelope, money and data formats

Every wallet request has the same outer shape. payload is the action specific part and the per call ids live in payload.meta . json { "request id": "0198a1d0 9a30 7f08 a7dd 713e4fd33db0", "ts": "2026 09 13T12:00:00Z", "operator id": "0197aa

Aggregation / Part I · FundamentalsErrors: one model

Your wallet's codes are Appendix A.1. What GA returns to you is Appendix A.2. Every error from /v2/aggregator/ and /v2/features/ has a non 200 HTTP status and one JSON body, Content Type: application/json : json { "code": "ERROR CODE MAINTE

Aggregation / Part I · FundamentalsIdempotency & retries

Certification tests these hardest. Build them in from day one. The full normative text is in Money Path Rules. Keep every op id with its stored answer for at least 4 months . GA resends for 72 hours, and the margin covers reconciliation dis

Aggregation / Part I · FundamentalsRate limits, pagination, network

Your wallet must answer within 5 seconds per call (§2.7). Aim for well under 1 second. GA doesn't filter your source IP on the Operator API. If you restrict inbound traffic to your wallet, allow GA's outbound addresses: 49.13.169.177 and 46

Aggregation / Part I · FundamentalsMoney Path Rules

The rules below govern the v2 wallet contract when a call goes wrong. The cases are a timeout, a duplicate, a rollback of an operation you never saw, and a win that arrives after the session closed. They're additive: GA removes or renames n

Aggregation / Part II · Operator APIGetting started

1. Get sandbox credentials. Email [integration@gamealligator.com](mailto:integration@gamealligator.com). You receive an operator profile with operator id , the Operator API key pair, and a login to https://operator.rexplay.site . There you

↑ ↓ navigate↵ openesc close
  1. Home
  2. /Integration
  3. /Aggregation
  4. /Whole guide on one page
Aggregation

Whole guide on one page

MarkdownSource

Document Control

ItemValue
Contract version2.0.0 (frozen, additive-only)
Document revision2026-09-25
Exact field typesproto/*.proto and openapi/*.yaml in this package. Where this guide and those files differ, the files govern; tell us at integration@gamealligator.com
Money rules in fullmoney-path/OPERATOR-API-MONEY-PATH-RULES.md (the Money Path Rules page on the docs site)
Postmanpostman/GA_Operator_API_v2 (your wallet), postman/GA_Aggregator_API_v2 and postman/GA_Features_API_v2 (calls to GA), environment postman/GA_Sandbox_v2
CertificationOperator Portal → Integration → Callback test, or the ga-operator-certify tool for CI; sign-off form certification/GA_Operator_Certification_Checklist_v2.md (chapter 9)
Supportintegration@gamealligator.com

1. Quick start

1.1 What you are building

Game Alligators (GA) puts game studios’ games into your casino. Traffic runs in two directions, each with its own key pair. Don’t mix them.

DirectionWho calls whomWhat it’s forKey you useChapter
You → GAYou call https://api.rexplay.site/v2/…Launch a game, read the catalog, issue free rounds, read reportsOperator API key + hash-based message authentication code (HMAC) secret, from your onboarding profile4
GA → youGA calls your wallet at https://<your-host>/v2/wallet/*Balance, bets, wins, rollbacksWallet signing key, from the Operator Portal. GA signs, you verify5

Transports: HTTP/JSON, which this guide covers, or native gRPC with the same messages. On gRPC there is no HMAC signature in either direction: the credential travels as metadata (x-api-key plus ga-brand-id on calls to your wallet, x-api-key plus x-brand-id on your calls to GA). If you already run a Softgaming-compatible wallet API, ask GA about the compatibility adapter. A separate document covers it.

1.2 One session, end to end

 Player            Your site            GA                       Your wallet
   |  click game       |                 |                            |
   |------------------>| launch_game     |                            |
   |                   |---------------->| (token, player, currency)  |
   |                   |<----------------| launch_url, session 24h    |
   |<------------------| open launch_url |                            |
   |                   |                 | get_balance / authenticate |
   |                   |                 |--------------------------->|
   |                   |                 |<---------------------------| balance
   |  spin             |                 | debit  (op_id A)           |
   |                   |                 |--------------------------->|
   |                   |                 |<---------------------------| ok, balance_after
   |                   |                 | credit (op_id B, win)      |
   |                   |                 |--------------------------->|
   |                   |                 |<---------------------------| ok
   |  spin, no answer  |                 | debit  (op_id C)  ... 5 s per attempt, 8 s per bet
   |                   |                 |--------------------------->|  (timeout)
   |                   |                 | rollback (original C)      |
   |                   |                 |--------------------------->|
   |                   |                 |<---------------------------| ok, original_found: false
   |                   |                 |   (late debit C arrives -> you refuse ALREADY_ROLLED_BACK)

1.3 Six steps to go-live

  1. Get sandbox credentials. Email integration@gamealligator.com. You receive an operator profile with operator_id, the Operator API key pair, and a login to https://operator.rexplay.site. There you rotate the wallet signing key once and copy the secret. The portal shows it only at that moment.
  2. Give GA your sandbox wallet URL and the optional wallet actions you support. The URL uses HTTPS and is reachable from the internet. Also give your currencies with their exponent, a 24/7 technical contact, and a finance contact.
  3. Build the five mandatory wallet actions (chapter 5) with the four money rules (chapter 3). Test your signature check with the worked example in §2.2 first.
  4. Run the Postman collection GA_Operator_API_v2 against your wallet. Set wallet_base_url, ga_wallet_key_id, ga_wallet_hmac_secret. Done when all ten requests answer a valid envelope.
  5. Run the certification checks from the Operator Portal (chapter 9) until every check passes, then sign the checklist with your GA integration manager.
  6. Repeat steps 2, 4, and 5 with production credentials and your production wallet URL. Production credentials are separate. GA never derives them from sandbox ones.

Addresses:

SurfaceSandboxProduction
GA API (chapter 4)https://api.rexplay.sitehttps://api.game-alligator.com
Operator Portal (wallet key, reports)https://operator.rexplay.sitehttps://operator.game-alligator.com
Your walletyou host it, you give GA the URLseparate URL and credentials
GA calls your wallet from49.13.169.177, 46.224.156.149.13.169.177

2. Common to every call

2.1 Headers and signature: GA → your wallet

Every call GA sends to your wallet looks like this:

POST /v2/wallet/debit HTTP/1.1
Host: wallet.operator.example
Content-Type: application/json
X-API-Key-Id: key-live-01
X-Request-Id: 0198a1d0-9a30-7f08-a7dd-713e4fd33db0
Idempotency-Key: op-bet-48912
X-GA-Signature: t=1783944000,v1=8a75f608c2a8e178d89a055198050e9a2d5341c2f390ac1d2c2c68e433eb5f2d
  • X-API-Key-Id names the wallet signing key. It’s the key id you see in the Operator Portal.
  • X-Request-Id is this one network attempt. It equals request_id in the body and payload.meta.request_id, and changes on every retry.
  • Idempotency-Key equals payload.meta.op_id, the business operation. It stays the same across retries. Use it, or the body field, as your idempotency key.
  • X-GA-Signature carries the timestamp t (unix seconds, the same instant as the body’s ts) and the signature v1.

Verify in this order, before you parse the body:

  1. Find your secret by X-API-Key-Id.
  2. Split X-GA-Signature into t and v1.
  3. Reject if t is more than 300 seconds away from your clock.
  4. Compute SHA256_HEX of the raw body bytes exactly as received. Don’t re-serialize.
  5. Compute HMAC-SHA256(secret, t + "." + request_id + "." + body_sha256) where request_id is the top-level request_id field of the body (identical to the X-Request-Id header). Compare with v1 using a constant-time compare.
  6. Check that operator_id in the body is your operator id.

If any step fails, answer HTTP 401 or 403 with no body. GA treats that as “no answer” and retries. A bet that never verifies ends in a rollback.

2.2 Worked example

Use these values to test your verification code before GA sends anything. Secret, body, and timestamp are fixed, so your output must match to the byte.

InputValue
Wallet HMAC secretsec-cert-01
t1783944000 (the body’s ts 2026-07-13T12:00:00Z)
request_id0198a1d0-9a30-7f08-a7dd-713e4fd33db0
Raw body (one line, no trailing newline){"request_id":"0198a1d0-9a30-7f08-a7dd-713e4fd33db0","ts":"2026-07-13T12:00:00Z","operator_id":"0197aaaa-0000-7000-a000-000000000002","action":"get_balance","payload":{"meta":{"request_id":"0198a1d0-9a30-7f08-a7dd-713e4fd33db0","op_id":"op-bal-1001","operator_id":"0197aaaa-0000-7000-a000-000000000002","player_ref":"player-10428"},"currency":"EUR"}}
SHA256_HEX(body)f6ebf9ad22209d716b34fff30cb66d7232e96a2d403e86ed0dc23e5da9f8ad6b
Signing input1783944000.0198a1d0-9a30-7f08-a7dd-713e4fd33db0.f6ebf9ad22209d716b34fff30cb66d7232e96a2d403e86ed0dc23e5da9f8ad6b
Expected v18a75f608c2a8e178d89a055198050e9a2d5341c2f390ac1d2c2c68e433eb5f2d

Node.js:

const crypto = require("crypto");
function verify(rawBody, headers, secret) {
  const m = /t=(\d+),v1=([0-9a-f]{64})/.exec(headers["x-ga-signature"] || "");
  if (!m) return false;
  const [, t, v1] = m;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const requestId = JSON.parse(rawBody).request_id;
  const bodySha = crypto.createHash("sha256").update(rawBody).digest("hex");
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${requestId}.${bodySha}`).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(v1, "hex"));
}

PHP:

function verify(string $rawBody, array $headers, string $secret): bool {
    if (!preg_match('/t=(\d+),v1=([0-9a-f]{64})/', $headers['X-GA-Signature'] ?? '', $m)) return false;
    [, $t, $v1] = $m;
    if (abs(time() - (int)$t) > 300) return false;
    $requestId = json_decode($rawBody, true)['request_id'] ?? '';
    $input = $t . '.' . $requestId . '.' . hash('sha256', $rawBody);
    return hash_equals(hash_hmac('sha256', $input, $secret), $v1);
}

Go:

func verify(rawBody []byte, sigHeader, secret string) bool {
    var t int64; var v1 string
    if _, err := fmt.Sscanf(sigHeader, "t=%d,v1=%s", &t, &v1); err != nil { return false }
    if d := time.Now().Unix() - t; d > 300 || d < -300 { return false }
    var env struct{ RequestID string `json:"request_id"` }
    _ = json.Unmarshal(rawBody, &env)
    sum := sha256.Sum256(rawBody)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(fmt.Sprintf("%d.%s.%s", t, env.RequestID, hex.EncodeToString(sum[:]))))
    want, _ := hex.DecodeString(v1)
    return hmac.Equal(mac.Sum(nil), want)
}

Python:

import hashlib, hmac, json, re, time
def verify(raw_body: bytes, sig_header: str, secret: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", sig_header or "")
    if not m: return False
    t, v1 = m.groups()
    if abs(time.time() - int(t)) > 300: return False
    request_id = json.loads(raw_body)["request_id"]
    signing_input = f"{t}.{request_id}.{hashlib.sha256(raw_body).hexdigest()}"
    expected = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Java:

static boolean verify(byte[] rawBody, String sigHeader, String secret) throws Exception {
    var m = java.util.regex.Pattern.compile("t=(\\d+),v1=([0-9a-f]{64})").matcher(java.util.Objects.requireNonNullElse(sigHeader, ""));
    if (!m.matches()) return false;
    long t = Long.parseLong(m.group(1)); String v1 = m.group(2);
    if (Math.abs(System.currentTimeMillis() / 1000 - t) > 300) return false;
    String requestId = new com.fasterxml.jackson.databind.ObjectMapper().readTree(rawBody).get("request_id").asText();
    String bodySha = java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(rawBody));
    var mac = javax.crypto.Mac.getInstance("HmacSHA256");
    mac.init(new javax.crypto.spec.SecretKeySpec(secret.getBytes(java.nio.charset.StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] expected = mac.doFinal((t + "." + requestId + "." + bodySha).getBytes(java.nio.charset.StandardCharsets.UTF_8));
    return java.security.MessageDigest.isEqual(expected, java.util.HexFormat.of().parseHex(v1));
}

2.3 Signature: your calls to GA

Same header names (X-API-Key-Id, X-GA-Signature: t=…,v1=…), your Operator API key pair, the same 300-second window, but a different signing input:

SIGNING_INPUT = METHOD + "\n" + PATH + "\n" + t + "\n" + SHA256_HEX(raw_body)
v1            = HMAC-SHA256(operator_api_secret, SIGNING_INPUT)

METHOD is POST, PATH is the URL path only (/v2/aggregator/launch_game), t is unix seconds. The Postman collections GA_Aggregator_API_v2 and GA_Features_API_v2 compute this for you. Set ga_api_key_id and ga_hmac_secret.

Use these values to test your signing code before you call GA. Secret, body, and timestamp are fixed, so your output must match to the byte.

InputValue
Operator API HMAC secretsec-op-cert-01
METHODPOST
PATH/v2/aggregator/launch_game
t1783944000
Raw body (one line, no trailing newline){"player_ref":"player-10428","game_id":"pragmatic-vs20olympgate","currency":"EUR","token":"lt-cert-556677","return_url":"https://operator.example/lobby"}
SHA256_HEX(body)fd1f9b4dff076382033df7e7f9e575f62cebefb6cdc28276fc6e95ec4f411eb0
Signing inputPOST\n/v2/aggregator/launch_game\n1783944000\nfd1f9b4dff076382033df7e7f9e575f62cebefb6cdc28276fc6e95ec4f411eb0
Expected v1d1de5557c4a004e53eec6e34930e331638497c15884d17e2a649a75ce82d5283

The header you send is:

X-API-Key-Id: key-cert-op-01
X-GA-Signature: t=1783944000,v1=d1de5557c4a004e53eec6e34930e331638497c15884d17e2a649a75ce82d5283

Note the separator. Unlike §2.1’s GA→wallet signature (which joins t, request_id, and the body hash with .), the signing input here joins METHOD, PATH, t, and the body hash with newlines (\n). There is no trailing newline after the hash.

2.4 The wallet envelope

Every wallet request has the same outer shape. payload is the action-specific part and the per-call ids live in payload.meta.

{
  "request_id": "0198a1d0-9a30-7f08-a7dd-713e4fd33db0",
  "ts": "2026-09-13T12:00:00Z",
  "operator_id": "0197aaaa-0000-7000-a000-000000000002",
  "action": "debit",
  "correlation": {
    "provider_slug": "pragmatic",
    "provider_round": "round-82721",
    "provider_ref": "spin-99182"
  },
  "payload": {
    "meta": {
      "request_id": "0198a1d0-9a30-7f08-a7dd-713e4fd33db0",
      "op_id": "op-bet-48912",
      "operator_id": "0197aaaa-0000-7000-a000-000000000002",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "player_ref": "player-10428",
      "launch_token": "lt-abcdef123456",
      "provider_slug": "pragmatic",
      "game_code": "vs20olympgate",
      "round_id": "round-82721",
      "provider_ref": "spin-99182",
      "ts": "2026-09-13T12:00:00Z"
    },
    "money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
FieldMeaning
op_idThe business operation. The same bet has the same op_id on every retry. Store your answer under it.
request_idThis one network attempt. Changes on every retry. Logs only.
round_idGroups the operations of one game round. Never an idempotency key.
session_id, launch_tokenThe player session and the token you gave at launch. Absent on session-less credits, such as promo payouts.
player_refYour player id, passed through unchanged.
correlationThe game provider’s own ids, for support tickets.

Ignore fields you don’t know. New optional fields may appear. GA never removes or renames anything published.

2.5 Two answers

Success:

{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982103",
    "balance_after": {
      "currency": "EUR",
      "amount": 14900,
      "exponent": 2
    },
    "debited": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}

Refusal, always HTTP 200, the reason in the body, no balance:

{
  "status": "error",
  "error": {
    "code": "ERROR_CODE_INSUFFICIENT_FUNDS",
    "message": "Player available balance is below requested debit amount",
    "retryable": false
  }
}

You may add retry_after (a duration such as "5s") to a retryable code. attributes is a free-form string map for your own diagnostics. GA doesn’t read it.

2.6 Money

{
  "currency": "EUR",
  "amount": 100,
  "exponent": 2
}
  • amount is an integer in minor units. 100 with exponent: 2 is 1.00 EUR. exponent is 0 to 18. GA never sends less than 2.
  • Take the exponent from the message, never from your own currency table. If it differs from what you hold the player’s currency in, refuse with ERROR_CODE_CURRENCY_MISMATCH.
  • On your wallet amount is a JSON number: 100. On GA’s own APIs (chapter 4) it’s a JSON string: "100", because proto3 JSON encodes every 64-bit integer as a string. Same value, same minor units. A parser that accepts both is the safe choice.

2.7 Timeouts and retries

The most important number: GA waits 5 seconds for each wallet call. Answer faster than that, or GA treats the call as lost.

GA sees only two kinds of outcome:

  • Refused. A valid envelope with a non-retryable code, such as INSUFFICIENT_FUNDS. Final: GA doesn’t resend that op_id.
  • No answer. A timeout, a dropped connection, a bare HTTP status with no envelope, or a valid envelope with a retryable code (RATE_LIMITED, MAINTENANCE, INTERNAL). GA doesn’t know whether you applied the operation and retries with the same op_id and a new request_id.
OperationOn “no answer” GA does
debit, debit_creditUp to 4 attempts, 100 ms, 500 ms, and 2 s apart, all inside one 8-second budget for the bet. Each attempt waits at most 5 s or what’s left of the budget, whichever is shorter. GA starts a retry only if the budget still holds the pause before it. So a wallet that doesn’t answer at all gets 2 attempts (5 s, then the rest of the 8 s). All 4 happen only when your wallet answers fast with a retryable code. Still nothing: the bet fails towards the game and GA sends one rollback for that op_id.
credit, settle_grantThe same quick attempts, then a background resend with the same op_id for up to 72 hours. Backoff starts at 1 second and doubles up to a cap of 10 minutes. The limit is the 72 hours, not a number of attempts. GA never rolls back a win.
rollbackThe same quick attempts, then the same 72-hour resend. Stops at the first valid envelope: success or refusal.
notifyOne attempt, no retry on HTTP/JSON. On gRPC it gets the same quick attempts as every other call.

GA honours a Retry-After header or retry_after field, capped at 10 seconds inside the quick attempts. When the 72 hours are over, GA flags the operation for manual review and alerts its team. GA never drops anything silently.

What this means for your wallet:

  1. A bet can reach you after GA already rolled it back. Rule 4 (chapter 3) handles it.
  2. A win can reach you days later, after the session ended. Rule 3 handles it.
  3. Every retry carries the same op_id, so Rule 1 makes retries harmless.

2.8 Errors on GA’s side, in one place

Your wallet’s codes are Appendix A.1. What GA returns to you is Appendix A.2. Every error from /v2/aggregator/* and /v2/features/* has a non-200 HTTP status and one JSON body, Content-Type: application/json:

{
  "code": "ERROR_CODE_MAINTENANCE",
  "message": "provider_unavailable",
  "retryable": true
}

code is one of the 15 ERROR_CODE_* values of Appendix A.1. message is for your logs and GA support. It starts with a stable token (signature_invalid, game_unavailable, provider_timeout, …). GA omits retryable when false. This holds for signature and key problems (401, 403) and for a read-only key on a write call (403) too.

Retry only 429, 503, and 504 (retryable: true in the body), with backoff. Every other status, 502 included, is final for that request.


3. Idempotency: the four rules that protect money

Certification tests these hardest. Build them in from day one. The full normative text is in Money Path Rules.

Rule 1. A repeated op_id gets the same answer

Keep every op_id with its stored answer for at least 4 months. GA resends for 72 hours, and the margin covers reconciliation disputes.

  • Same op_id, same content: return the stored answer, move no money. Even if the balance is now too low or the session expired.
  • Same op_id, different content (player, operation type, currency, amount with exponent, round, original, grant): refuse with ERROR_CODE_IDEMPOTENCY_CONFLICT, move no money.
  • Two identical requests at the same moment: the second waits for the first and gets the same answer. Never apply twice.

Rule 2. Check things in this order

signature -> schema -> op_id lookup -> player + currency/exponent
  -> [bets only: session, player status, limits, balance]
  -> round state -> apply and store the answer in one atomic step

The op_id lookup comes before session, player, and balance checks. Otherwise a repeated bet answers “insufficient funds” or “session expired” where it must answer the stored success, and you and GA disagree about whether the bet exists.

Rule 3. Never refuse a win, a rollback, or a grant settlement for session, block, or limits

A win can arrive hours after the session ended, for a blocked player, or past a deposit limit. Accept it. The only final refusals for a credit are: player not found, currency mismatch, invalid request, idempotency conflict, unknown original, already rolled back. After any other refusal, GA resends the credit for 72 hours, and then it lands in manual review.

A promo, bonus, jackpot, or tournament payout may arrive with no bet in the round and with no session. Don’t require either.

Rule 4. A rollback of something you never saw is a success, and you remember it

If a rollback names an original_op_id you don’t have:

  1. Return status: ok with "original_found": false. Move no money.
  2. Store that original_op_id as “rolled back”.
  3. If the original debit or credit arrives later, refuse it with ERROR_CODE_ALREADY_ROLLED_BACK.

Never return ERROR_CODE_UNKNOWN_ORIGINAL to a rollback. Step 2 is what stops you from charging a player for a bet GA already cancelled.


4. You call GA: launch, catalog, free rounds, reports

Base URL https://api.rexplay.site (sandbox) or https://api.game-alligator.com (production). Every call is POST with a JSON body and the signature of §2.3. Paths are /v2/aggregator/<call> in snake_case. The sandbox also accepts the proto method name (/v2/aggregator/LaunchGame, case-sensitive). Sign exactly the path you call. Success is 200 with the call’s own response. Amounts are strings in minor units (§2.6). Timestamps are Request for Comments (RFC) 3339. Paginated calls take page: {cursor, limit} and return page: {next_cursor, has_more, total_count}. limit defaults to 50, with a maximum of 500.

Start from Postman: import postman/GA_Aggregator_API_v2.postman_collection.json with postman/GA_Sandbox_v2.postman_environment.json, set ga_api_key_id and ga_hmac_secret, and send launch_game first. The pack with both files is /downloads/GA_Operator_Integration_Pack_v2.0.0.zip on the docs site.

4.1 Launch a game

POST /v2/aggregator/launch_game

FieldRequiredMeaning
player_refyesYour player id. Opaque to GA, returned unchanged in every wallet call.
game_idyesFrom the catalog (§4.3).
currencyyesThe currency of this session. Uppercase code. GA takes the exponent from your onboarding profile.
tokenyesYour launch token. GA stores it and sends it back as launch_token in every wallet call of this session. It’s how you tie a wallet call to a player session.
return_urlnoWhere the game’s “back to lobby” button leads.
attributesnoString map. attributes["language"] = two-letter lowercase code ("de"). Anything else falls back to "en".

Response: session_id, launch_url, expires_at (the session lives 24 hours), plus running total_bet, total_win, rounds.

launch_game is idempotent by token while the session is open. The same token with the same player, game, and currency returns the first response again: the same session_id, launch_url, and expires_at. So a page reload doesn’t open a second session, and GA doesn’t call the game provider again.

GA refuses the same token with 409 ERROR_CODE_IDEMPOTENCY_CONFLICT (session_token_conflict) in these cases:

  • the token comes with another player, game, or currency;
  • the session of that token is closed: by close_session, or by a newer launch of the same game for the same player, because a launch closes that player’s earlier sessions of the game;
  • the session of that token is past its expires_at.

GA never reopens a closed session. Don’t retry the 409: launch again with a fresh token. If the first launch with the token is still running, GA answers 503 ERROR_CODE_INTERNAL with retryable: true (launch_in_progress). Retry with the same token to get the first response. Use a fresh token for every new session.

Open launch_url in an iframe or by redirect. Both work. The URL is single-session: launch again for a new session. Device, IP, and country aren’t fields on this call.

If you embed launch_url in an iframe, set a sandbox attribute. Don’t omit it, and don’t use an unrestricted frame. launch_url always points to a real address that you load via src, never as inline HTML, so the minimal working set is:

<iframe
  src="https://play.rexplay.site/s/0198a1cb-4f37-7ee8-8e7d-08d39925aec0"
  sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
  allow="autoplay; fullscreen"
  allowfullscreen>
</iframe>
  • allow-scripts—the game is a script-driven client. Without it, nothing runs.
  • allow-same-origin—the frame loads from src, so this keeps it same-origin with GA’s domain, not yours, which the game’s own websocket connection and storage need. It doesn’t weaken your page’s origin boundary.
  • allow-forms—some game and payment flows submit forms inside the frame.
  • allow-popups—rule and payment/cashier windows some games open are new browser contexts, not just navigation inside the frame.
  • allow="autoplay; fullscreen" and allowfullscreen—background/video-driven games and fullscreen mode need these. Playback or fullscreen silently fails without them.
{
  "player_ref": "player-10428",
  "game_id": "0198a2aa-1111-7000-a000-000000000077",
  "currency": "EUR",
  "token": "lt-abcdef123456",
  "return_url": "https://casino.example/lobby",
  "attributes": {
    "language": "de"
  }
}
{
  "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
  "player_ref": "player-10428",
  "game_id": "0198a2aa-1111-7000-a000-000000000077",
  "currency": "EUR",
  "launch_url": "https://play.rexplay.site/s/0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
  "expires_at": "2026-09-14T12:00:00Z",
  "total_bet": {
    "currency": "EUR",
    "amount": "0",
    "exponent": 2
  },
  "total_win": {
    "currency": "EUR",
    "amount": "0",
    "exponent": 2
  },
  "rounds": 0
}

4.2 Launch a demo

POST /v2/aggregator/launch_demo with game_id and optional return_url. No player, no token, no currency, no session: a demo never reaches your wallet. Response: launch_url, game_id. Games with demo_supported: false refuse with 412 (DEMO_NOT_SUPPORTED, Appendix A.2).

4.3 Catalog

POST /v2/aggregator/games with optional filters query, vertical, category, provider, tags[], updated_since, page.

Each Game carries: game_id, title, vertical, category, provider, enabled, demo_supported, wager_weight_bp, tags[], assets[], limits, fee_bp, fee_source, rtp, features[], updated_at. wager_weight_bp is how much a bet on this game contributes to bonus wagering and loyalty accrual, in basis points, 0 to 10000. 10000 bp = 100 %, 1000 bp = 10 %, 0 bp = 0 %. assets[] is {kind, url} with kinds banner, thumbnail, background, icon. limits is min_bet, max_bet, max_win. fee_bp is GA’s fee for this game on your brand, in basis points, 0 to 10000. It’s an optional field—present only once GA has resolved a fee for this game/brand pair, absent otherwise, so check presence rather than treating “absent” as 0. fee_source is which fee-ladder tier produced fee_bp: FEE_SOURCE_UNSPECIFIED, FEE_SOURCE_PROVIDER, FEE_SOURCE_GAME. rtp is the return to player (RTP) in percent, for example 96.5. The wire value is also 0 when GA has recorded no RTP for this game, so a 0 doesn’t mean “0 % RTP”.

features[] is always empty in this call. Call get_game_features (§6.1) to get a game’s actual features[].

Practical rules:

  • Poll with updated_since (the last updated_at you stored) rather than reloading the whole catalog. A daily full reload is enough.
  • Download assets[].url images to your own content delivery network (CDN) and serve them from there. Don’t hotlink them from your lobby.
  • Show only enabled: true games. A disabled game refuses to launch.

Other catalog calls are capabilities, operator_capabilities, and update_game. capabilities is what GA supports for you: currencies, settlement modes, streaming. operator_capabilities is what GA has on record for your wallet: wallet_capabilities with rpcs, credit_kinds, debit_modes, notify_kinds, authenticate, and supported_transports. update_game changes your own settings on a game, partial update with update_mask and a reason for audit.

4.4 Free rounds you issue

POST /v2/aggregator/issue_grant

FieldRequiredMeaning
player_ref, game_ids[]yesWho and on which games.
rounds_totalyesNumber of free rounds.
round_valueyesStake value of one round (Money).
bet_level, linesnoProvider bet level and lines, from get_bet_ranges (§6.1) where the game has levels.
settlement_modenoGA supports only SETTLEMENT_MODE_ON_COMPLETION. Leave it out.
idempotency_keyrecommendedThe same key returns the same grant. GA refuses a different body under the same key with IDEMPOTENCY_CONFLICT. Without a key every call creates a new grant.
valid_untilyesHard end date. GA refuses rounds after it.
external_refnoYour campaign or bonus id.

Response: Grant with grant_id, status (active, exhausted, expired, cancelled, settled), rounds_remaining, rounds_played, accumulated_win.

How the money flows for a grant you issued: no bet and no win reaches your wallet while the player plays the rounds. The win accumulates on the grant. When the grant is exhausted, expired, or cancelled with a win, GA sends one settle_grant to your wallet with total_win, and that’s when you credit the player (§5.9). GA also settles a zero-win grant, with total_win: 0.

list_grants (by player_ref and status) and cancel_grant (grant_id, reason) complete the set. You can cancel only an active grant, and GA still settles the win already earned.

4.5 Sessions, rounds, and reports

CallSendGet back
close_sessionsession_id, reason (player request, self-exclusion, limit, operator request, suspicion, timeout)the closed session
sessionsplayer_ref, pagesessions
roundssession_id or player_ref, start_time, end_time, pagerounds with total_bet, total_win, status (SETTLED, REFUNDED, VOIDED, FAILED, REFUND_FAILED, see §4.6), placed_at, settled_at
export_roundssame filters plus currencythe same rounds streamed as NDJSON lines {"result": …}, for bulk reconciliation
aggregatesdate (YYYY-MM-DD), currencytotal_rounds, total_bet_amount, total_win_amount, total_refund_amount, net_ggr, total_fee

Use aggregates for the daily settlement check and export_rounds when a number doesn’t match.

4.6 Event stream (optional)

subscribe (gRPC server stream) delivers round, grant, session, and notification events with a monotonic sequence per brand. ack confirms the cursor. It’s an alternative to polling rounds, and a first integration doesn’t need it.

Round events carry a Round. Keep the latest one by sequence per (player_ref, round_id):

event_typeRound.statusMeaning
round.settled.rawSETTLEDThe round closed without a rollback.
round.refundedREFUNDED / VOIDEDPart or all of the round’s money was rolled back.
round.failedFAILEDYour wallet declined an operation of the round. That operation moved no money. total_bet / total_win hold the declined amount.
round.failedREFUND_FAILEDGA owed a rollback and couldn’t complete it: retries ran out, or your wallet refused the rollback. The stake is still debited. Reconcile the round manually. total_bet (or total_win for an unpaid re-issued credit) holds the stuck amount.

5. GA calls your wallet: the actions

GA sends POST to https://<your-host>/v2/wallet/<action>. Ten actions exist. Five are mandatory.

ActionMandatoryWhat GA asks
get_balanceyes”How much can this player play with?”
debityes”Take the stake for this bet.”
credityes”Pay this win.”
rollbackyes”Undo this earlier operation.”
reconcileyes”Did this operation go through?”
authenticateno”This player is opening a game. Who are they and what’s their balance?”
debit_creditno”Take the stake and pay the win in one step.”
close_roundno”This round is over. Here are the totals.”
settle_grantno (mandatory if you issue free rounds)“This free-round package is finished. Here is the total win.”
notifyno”Something non-monetary happened.”

For an optional action you don’t support, return the error envelope with ERROR_CODE_UNSUPPORTED_ACTION on HTTP 200 or 404. GA reads the envelope and stops calling that action. A bare 404 without the envelope is “no answer”, and GA retries it.

The examples below show the envelope reduced to the fields that matter. ts, operator_id, correlation, and the headers are as in chapter 2. All values are illustrative.

5.1 get_balance

Return the playable balance in the requested currency. Change nothing.

{
  "request_id": "req-0101",
  "action": "get_balance",
  "payload": {
    "meta": {
      "request_id": "req-0101",
      "op_id": "op-bal-1001",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0"
    },
    "currency": "EUR"
  }
}
{
  "status": "ok",
  "data": {
    "balance": {
      "currency": "EUR",
      "amount": 15000,
      "exponent": 2
    },
    "bonus_balance": {
      "currency": "EUR",
      "amount": 500,
      "exponent": 2
    }
  }
}

Refuse only for: player not found, currency mismatch, invalid request.

5.2 authenticate (optional)

When: the player opens a game. GA sends the launch_token you passed at launch. Return who the player is, the currency, and the balance. Keep the token valid: it comes back in every wallet call of the session.

{
  "request_id": "req-0102",
  "action": "authenticate",
  "payload": {
    "meta": {
      "request_id": "req-0102",
      "op_id": "op-auth-1002",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456"
    }
  }
}
{
  "status": "ok",
  "data": {
    "player_ref": "player-10428",
    "currency": "EUR",
    "balance": {
      "currency": "EUR",
      "amount": 15000,
      "exponent": 2
    },
    "bonus_balance": {
      "currency": "EUR",
      "amount": 500,
      "exponent": 2
    },
    "attributes": {
      "country": "DE",
      "nickname": "lucky_ann"
    }
  }
}

Refuse for: unknown or expired token (SESSION_EXPIRED), blocked player (PLAYER_BLOCKED), player not found.

5.3 debit

Do: take the stake. Return your transaction id, the balance after, the amount debited.

Refuse when:

  • amount is zero or negative: INVALID_REQUEST.
  • the balance can’t cover it: INSUFFICIENT_FUNDS. Final: GA doesn’t resend that bet.
  • the session or token is expired or missing on a call that names a session: SESSION_EXPIRED / INVALID_REQUEST.
  • the player is blocked or over a limit: PLAYER_BLOCKED / LIMIT_EXCEEDED.
  • the round is already closed or voided: BUSINESS_REJECTED.

Never refuse a repeated debit (Rule 1). A debit is also how a balance correction from the game provider reaches you, sometimes outside a live session. Apply it the same way.

{
  "request_id": "req-0103",
  "action": "debit",
  "payload": {
    "meta": {
      "request_id": "req-0103",
      "op_id": "op-bet-48912",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99182"
    },
    "money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982103",
    "balance_after": {
      "currency": "EUR",
      "amount": 14900,
      "exponent": 2
    },
    "debited": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}

5.4 credit

Do: add the win. Rule 3 applies: never refuse for session, block, or limits. Return your transaction id and the balance after.

  • amount may be 0 for a losing spin that closes the round. Answer ok with the unchanged balance.
  • kind says what the payout is: CREDIT_KIND_WIN, _BONUS, _JACKPOT, _PROMO, _FREE_ROUND, _TOURNAMENT, _CASHBACK, _ADJUSTMENT.
  • components[] is an optional breakdown (KIND_BASE, KIND_JACKPOT, KIND_PROMO, KIND_BONUS, KIND_CONTRIBUTION). The parts sum to money.
  • round_finish: true means this credit ends the round. Still accept a credit into a round you already closed.
  • campaign_ref names the provider promo on a FREE_ROUND credit.

Refuse only for: player not found, currency mismatch, invalid request, idempotency conflict, unknown original, already rolled back.

{
  "request_id": "req-0104",
  "action": "credit",
  "payload": {
    "meta": {
      "request_id": "req-0104",
      "op_id": "op-win-48913",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99182"
    },
    "money": {
      "currency": "EUR",
      "amount": 2500,
      "exponent": 2
    },
    "kind": "CREDIT_KIND_WIN",
    "round_finish": true
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982104",
    "balance_after": {
      "currency": "EUR",
      "amount": 17400,
      "exponent": 2
    }
  }
}

5.5 debit_credit (optional)

Do: take the stake and pay the win in one atomic step. Check the stake against the current balance first, exactly like a debit. If it doesn’t cover debit_money, refuse the whole call with INSUFFICIENT_FUNDS and change nothing. Never apply only one half.

{
  "request_id": "req-0105",
  "action": "debit_credit",
  "payload": {
    "meta": {
      "request_id": "req-0105",
      "op_id": "op-step-48914",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99183"
    },
    "debit_money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    },
    "credit_money": {
      "currency": "EUR",
      "amount": 300,
      "exponent": 2
    },
    "credit_kind": "CREDIT_KIND_WIN",
    "round_finish": true
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982105",
    "balance_after": {
      "currency": "EUR",
      "amount": 17600,
      "exponent": 2
    },
    "debited": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}

5.6 rollback

Do: undo the operation named by original_op_id. Rule 4 applies when you don’t know it. Return your transaction id, the balance after, and original_found.

  • Rolling back a debit is always for the full stored amount: return the stake to the player.
  • Rolling back a credit takes money back. debit_mode says what to do when the balance is too low. DEBIT_MODE_REJECT means refuse with INSUFFICIENT_FUNDS. GA’s own rollbacks use this mode. DEBIT_MODE_ALLOW_NEGATIVE means go negative. DEBIT_MODE_PARTIAL means take what’s there, down to zero, and report the taken amount in debited. partial: true marks a partial rollback of a credit.
  • A second rollback of the same original returns the first answer and moves nothing.
  • If you’re still committing the original, answer an error envelope with retryable: true. GA comes back.
  • If the player or currency doesn’t match the original, refuse with INVALID_REQUEST or CURRENCY_MISMATCH and move nothing.

Never refuse a rollback for session, block, or limits. Any final refusal you do send (for example INSUFFICIENT_FUNDS under DEBIT_MODE_REJECT) stops GA’s retries at once and puts the operation into manual review on GA’s side.

{
  "request_id": "req-0106",
  "action": "rollback",
  "payload": {
    "meta": {
      "request_id": "req-0106",
      "op_id": "op-rb-48915",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0",
      "launch_token": "lt-abcdef123456",
      "round_id": "round-82721",
      "provider_ref": "spin-99182"
    },
    "original_op_id": "op-bet-48912",
    "original_kind": "ORIGINAL_KIND_DEBIT",
    "money": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982106",
    "balance_after": {
      "currency": "EUR",
      "amount": 17700,
      "exponent": 2
    },
    "original_found": true
  }
}

Unknown original: same shape with "original_found": false, no money moved, and the id stored as rolled back.

5.7 close_round (optional)

Do: record that the round is over. net_win, bet_total, win_total are for your reports. No money moves on this call.

{
  "request_id": "req-0107",
  "action": "close_round",
  "payload": {
    "meta": {
      "request_id": "req-0107",
      "op_id": "op-close-48916",
      "player_ref": "player-10428",
      "round_id": "round-82721"
    },
    "net_win": {
      "currency": "EUR",
      "amount": 200,
      "exponent": 2
    },
    "bet_total": {
      "currency": "EUR",
      "amount": 100,
      "exponent": 2
    },
    "win_total": {
      "currency": "EUR",
      "amount": 300,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982107",
    "balance_after": {
      "currency": "EUR",
      "amount": 17700,
      "exponent": 2
    }
  }
}

5.8 reconcile

When: GA got no clear answer to an operation and asks what happened. Do: look up original_op_id and report. Change nothing and remember nothing. This is a read. Never return UNKNOWN_ORIGINAL here.

Your record of original_op_idReturn state
not thereRECONCILE_STATE_NOT_APPLIED
still being committedRECONCILE_STATE_UNKNOWN
appliedRECONCILE_STATE_APPLIED with the original operator_tx_id and balance_after
rolled back, or remembered as rolled back (Rule 4)RECONCILE_STATE_NOT_APPLIED
{
  "request_id": "req-0110",
  "action": "reconcile",
  "payload": {
    "meta": {
      "request_id": "req-0110",
      "op_id": "op-rec-48918",
      "player_ref": "player-10428"
    },
    "original_op_id": "op-bet-48912"
  }
}
{
  "status": "ok",
  "data": {
    "state": "RECONCILE_STATE_APPLIED",
    "operator_tx_id": "tx-op-982103",
    "balance_after": {
      "currency": "EUR",
      "amount": 14900,
      "exponent": 2
    }
  }
}

5.9 settle_grant (mandatory if you issue free rounds)

When: a grant you issued with issue_grant (§4.4) is finished: exhausted, expired, or cancelled with a win. Do: credit total_win to the player, once per grant_id. The call is idempotent by op_id. GA sent no debits and no credits for the rounds, so this call is the only place the win reaches the player. total_win: 0 needs only an ok. Exception (Money Path Rules §2.10 item 7): a win of a round played before the grant ended can reach GA after the grant ended (settled via settle_grant, or cancelled or expired before payout). Such a win arrives as a separate credit with kind = CREDIT_KIND_FREE_ROUND and campaign_ref set to the grant_id, idempotent by op_id like any other credit. A new round reported on a grant that has already ended is still refused to GA and never reaches you.

Two free-round mechanisms exist and they never overlap for the same win:

MechanismWho starts itHow the win reaches your wallet
Grant you issue (issue_grant)youone settle_grant with total_win at the end
Promo free spins from the game providerthe provider or a GA campaignan ordinary credit with CREDIT_KIND_FREE_ROUND and campaign_ref per winning spin

Free-round wins may arrive with no session_id and no launch_token. Accept them (Rule 3).

{
  "request_id": "req-0108",
  "action": "settle_grant",
  "payload": {
    "meta": {
      "request_id": "req-0108",
      "op_id": "op-grant-7781",
      "player_ref": "player-10428"
    },
    "grant_id": "grant-7781",
    "total_win": {
      "currency": "EUR",
      "amount": 1250,
      "exponent": 2
    },
    "rounds_played": 20
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-op-982108",
    "balance_after": {
      "currency": "EUR",
      "amount": 18950,
      "exponent": 2
    }
  }
}

5.10 notify (optional)

Do: record the event and answer ok. Nothing to move. GA sends it once and doesn’t retry.

Kinds: NOTIFY_KIND_FREE_ROUNDS_STARTED, _TOKEN_RESET, _SESSION_EXPIRED, _REALITY_CHECK, _PRIZE_AWARDED, _FREESPIN_ISSUED, _FREESPIN_FINISHED, _BONUS_BUY_FINISHED.

{
  "request_id": "req-0109",
  "action": "notify",
  "payload": {
    "meta": {
      "request_id": "req-0109",
      "op_id": "op-ntf-48917",
      "player_ref": "player-10428",
      "session_id": "0198a1cb-4f37-7ee8-8e7d-08d39925aec0"
    },
    "kind": "NOTIFY_KIND_SESSION_EXPIRED",
    "data": {
      "attributes": {
        "reason": "idle_timeout"
      }
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "ntf-op-1"
  }
}

6. Optional: provider features

POST https://api.rexplay.site/v2/features/<call>, signed as in §2.3. Each game supports only some features. Call get_game_features first and look at features[]. Postman: postman/GA_Features_API_v2.postman_collection.json.

6.1 The calls

What you wantCallSendGet back
Which features does this game support?get_game_featuresgame_code, provider_slugfeatures[] (FEATURE_FREE_ROUNDS_GRANT, FEATURE_BONUS_BUY, FEATURE_JACKPOT_FEED, FEATURE_BET_RANGES, FEATURE_ROUND_REPLAY, FEATURE_TOURNAMENT, FEATURE_DEMO, …), limits
Allowed stake levels for a gameget_bet_rangesprovider_slug, game_code, currencybet_ranges[] with bet_level, bet_value, min_bet, max_bet, step. Use bet_level in issue_grant.
Bonus buylist_bonus_buy_types, issue_bonus_buy, cancel_bonus_buyprovider_slug, game_code, player_ref, bonus_type, amount, currency, attributes["idempotency_key"]provider_ref
Current jackpot valuesget_jackpotsprovider_slug, currencyjackpots[]
Replay or details of a disputed roundget_round_replay, get_round_detailsprovider_slug, round_id, player_refreplay_url / details
Provider campaignslist_campaigns, cancel_campaignprovider_slug, campaign_idcampaigns
Tournamentslist_tournaments, get_tournament, get_leaderboardprovider_slug, tournament_idtournaments, entries[]
Provider-side statement for reconciliationquery_provider_transactionsprovider_slug, player_ref, date_from, date_totransactions[], totals

Errors: the {"code", "message", "retryable"} body of §2.8 with a non-200 status, Appendix A.2.


7. Data formats

ValueFormat
CurrencyUppercase letters and digits, 3 to 10 characters. International Organization for Standardization (ISO) 4217 for fiat (EUR), exchange ticker for crypto (USDT). You and GA agree the exponent per currency at onboarding.
Money{currency, amount, exponent}. See §2.6. Number on your wallet, string on GA’s APIs.
TimestampsRFC 3339 in Coordinated Universal Time (UTC): 2026-09-13T12:00:00Z. aggregates.date is YYYY-MM-DD.
LanguageTwo lowercase letters: en, de. Regional forms (en-GB) fall back to en.
Ids (player_ref, op_id, round_id, game_id, session_id)Opaque strings. GA sets no length limit. Keep yours under 128 characters. GA’s session ids are universally unique identifiers (UUIDs).
wager_weight_bp, fee_bpBasis points, 0 to 10000.

Unknown fields in any message must be ignored. Unknown enum values in a known field are refused with INVALID_REQUEST.


8. Network and limits

  • Your wallet must answer within 5 seconds per call (§2.7). Aim for well under 1 second.
  • GA doesn’t filter your source IP on the Operator API. If you restrict inbound traffic to your wallet, allow GA’s outbound addresses: 49.13.169.177 and 46.224.156.1 for the sandbox, 49.13.169.177 for production.
  • GA enforces no request rate limit on the Operator API today. Keep catalog polling to once a minute or less and use updated_since. GA reserves a 429 with ERROR_CODE_RATE_LIMITED for abuse.
  • Secrets never go into source control, browser code, tickets, or logs.

9. Certification and go-live

9.1 Run the checks from the Operator Portal

Log in to the Operator Portal with an operator_admin account and open Integration. The Callback test section runs the certification checks against your sandbox wallet:

  1. Save your wallet endpoint URL. For the v2 contract the URL must contain /v2 (for example https://wallet.operator.example/v2/wallet). That’s how the portal picks the v2 suite.
  2. Create a test player with the person_add button. The portal creates and funds it. The player reference doubles as the launch token in the run.
  3. Choose Full suite, set the currency and bet amount, and press Run suite.

The run shows every check with its pass/fail state, balance deltas, and a hint for each failure. You can replay bet and refund steps one by one. The checks are the same 55 W2-* checks the command-line tool runs. Six of them (W2-D1 to W2-D6) test the four rules of chapter 3. You’re done when every check passes.

9.1a The command-line tool (for your CI)

The same suite exists as ga-operator-certify for automated runs in your pipeline. Ask your GA integration manager for the binary for your platform.

./ga-operator-certify \
  --binding http \
  --target https://wallet.operator.example \
  --api-key key-cert-01 \
  --secret sec-cert-01 \
  --strict

Before running, create in your wallet a test player certify-player-001 (or pass --player-ref) with a funded EUR balance (or --currency), and a valid launch token for that player passed with --launch-token (default token-cert-001). The tool creates neither. --json writes a report with the request and response of every failed check.

9.2 The checklist

Go through certification/GA_Operator_Certification_Checklist_v2.md with your GA integration manager and sign it on both sides. Then repeat the Postman run, the tool, and the checklist against your production wallet with production credentials.

9.3 Changelog

Contract changes are additive only, and GA lists them in CHANGELOG.md in this package. GA never removes or renames anything published. New optional fields may appear at any time, which is why your parser must ignore unknown fields.


Appendix A. Every error code

A.1 Codes your wallet returns to GA

Always HTTP 200 with {"status":"error","error":{"code":…}} (§2.5). 15 codes. The list is closed.

CodeGA retries?When to return it
ERROR_CODE_INSUFFICIENT_FUNDSnoThe balance can’t cover the stake, or a credit rollback under DEBIT_MODE_REJECT
ERROR_CODE_PLAYER_NOT_FOUNDnoNo such player
ERROR_CODE_PLAYER_BLOCKEDnoPlayer blocked, self-excluded, or disabled. New bets only. Still accept wins and rollbacks
ERROR_CODE_CURRENCY_MISMATCHnoCurrency or exponent doesn’t match the player’s account
ERROR_CODE_BUSINESS_REJECTEDnoYour own business rule refused it. Also a new bet into a closed or voided round
ERROR_CODE_UNKNOWN_ORIGINALnoA credit names an original you don’t have. Never for a rollback or a reconcile
ERROR_CODE_ALREADY_ROLLED_BACKnoYou already rolled back the op_id (Rule 4), or the round is voided
ERROR_CODE_UNSUPPORTED_ACTIONnoYou don’t implement this optional action (HTTP 200 or 404, with the envelope)
ERROR_CODE_RATE_LIMITEDyesYou are rate-limiting GA (HTTP 200 or 429, with the envelope)
ERROR_CODE_MAINTENANCEyesYour wallet is down for maintenance
ERROR_CODE_INVALID_REQUESTnoMalformed request: missing field, zero or negative amount, overflow, unknown enum value, missing token on a session call
ERROR_CODE_SESSION_EXPIREDnoLaunch token or session expired. New bets only
ERROR_CODE_LIMIT_EXCEEDEDnoDeposit or loss limit reached. New bets only
ERROR_CODE_INTERNALyesUnexpected error on your side (HTTP 200 or 500, with the envelope)
ERROR_CODE_IDEMPOTENCY_CONFLICTnoSame op_id, different content (Rule 1)

Bare statuses without the envelope (401, 403, 404, 429, 5xx) are all “no answer”: GA retries, and a bet ends in a rollback.

A.2 What GA returns to you

Every /v2/aggregator/* and /v2/features/* error is the body of §2.8. message starts with the token in the table.

HTTPcodeRetry?message starts withMeaning
400ERROR_CODE_INVALID_REQUESTnoinvalid json body, invalid request, invalid_request, invalid_provider_ref, capability_not_supported, bonus_buy_not_cancellableMalformed JSON, missing or invalid field, GA can’t read the body, or the game provider doesn’t offer the capability
401ERROR_CODE_INVALID_REQUESTnounauthorized, signature_missing, signature_invalid, signature_staleWrong or missing key id, signature, or a timestamp outside 300 s
403ERROR_CODE_BUSINESS_REJECTEDnoforbiddenThe operator is suspended, or the key isn’t allowed to do this (for example a read-only key on launch_game)
403ERROR_CODE_BUSINESS_REJECTED or ERROR_CODE_INVALID_REQUESTnooperator_suspended, operator forbidden, restricted, game_unavailable, PLAYER_SELF_EXCLUDED, PLAYER_COOL_OFFNot allowed for this operator, game, or player
404ERROR_CODE_INVALID_REQUESTnoprovider not found, provider_not_found, game not found, player_not_found, bonus_buy_not_foundProvider, game, player, grant, or bonus not found
409ERROR_CODE_IDEMPOTENCY_CONFLICTnosession_token_conflictYou reused the token of launch_game with another player, game, or currency, or after its session closed or expired, or the idempotency_key of issue_grant with a different body (§4.1, §4.4)
412ERROR_CODE_INVALID_REQUESTnoDEMO_NOT_SUPPORTEDlaunch_demo on a game with demo_supported: false
422ERROR_CODE_INVALID_REQUESTnounsupported actionAction not supported for this game or provider
429ERROR_CODE_RATE_LIMITEDyesRate limit
500ERROR_CODE_INTERNALnointernal errorGA-side failure. GA gets an alert. Contact support if it repeats
501ERROR_CODE_UNSUPPORTED_ACTIONnoCall not implemented on this binding (settlement_mode PER_ROUND)
502ERROR_CODE_INTERNALno (retryable not set)provider_errorThe game provider returned an error. Repeating the same call returns the same error
503ERROR_CODE_MAINTENANCEyesprovider_unavailableThe game provider is unavailable
503ERROR_CODE_INTERNALyeslaunch_in_progressThe first launch_game with this token is still running. Retry with the same token (§4.1)
504ERROR_CODE_INTERNALyesprovider_timeoutThe game provider timed out

Retry only 429, 503, and 504, with backoff.


Appendix B. Numbers to remember

WhatValue
GA waits for one wallet call5 seconds
Quick attempts on “no answer”up to 4, spaced 100 ms, 500 ms, 2 s, inside an 8 s budget per bet (a silent wallet gets 2)
Background resend of a credit, rollback, or grant settlementup to 72 hours, backoff 1 s doubling to a 10-minute cap
Retry-After honoured up to10 seconds inside the quick attempts
Signature clock window, both directions±300 seconds
GA outbound IPs49.13.169.177, 46.224.156.1 (sandbox). 49.13.169.177 (production)
Keep op_id records for4 months
Game session lifetime24 hours
Money exponent0 to 18, from the message. GA sends 2 or more
Catalog page size50 by default, 500 maximum
Wallet HMAC secret shownonce, at rotation in the Operator Portal
Certification checks55, of which 6 are money-path checks

Money Path Rules

The rules below govern the v2 wallet contract when a call goes wrong. The cases are a timeout, a duplicate, a rollback of an operation you never saw, and a win that arrives after the session closed. They’re additive: GA removes or renames nothing published in proto/ or openapi/.

Conventions.

  • Normative keywords (MUST, MUST NOT, SHOULD, MAY) are used as in Request for Comments (RFC) 2119.
  • Error codes are written in prose without the ERROR_CODE_ prefix. On the wire they use the published enum form (for example ERROR_CODE_ALREADY_ROLLED_BACK). The same applies to RECONCILE_STATE_*, CREDIT_KIND_*, DEBIT_MODE_* and ORIGINAL_KIND_* values.
  • A business refusal travels on HTTP 200 with {"status":"error","error":{code,message,retryable,retry_after,attributes}}. A success travels as {"status":"ok","data":{…}}.
  • Request examples are reduced to the relevant envelope fields (ts, operator_id, correlation and signature headers are omitted unless relevant). All values are illustrative.

1. Summary

#Rule
1Rollback of an unknown original is a success with original_found=false. The operator remembers the original’s id. A late Debit/Credit with that id gets ALREADY_ROLLED_BACK. The operator never returns UNKNOWN_ORIGINAL to a Rollback
2Mandatory order of checks at the operator
3New enum value IDEMPOTENCY_CONFLICT and the fingerprint field list
4The operator never refuses Credit, SettleGrant, and Rollback for session, player block, or limits. Closed list of terminal Credit refusals
5Reconcile for an unknown operation returns an ok envelope + NOT_APPLIED. It’s a read, never a barrier
6Concurrent duplicates: the second request waits for the commit and receives the first response
7Numbers: op_id retention, GA attempts, resend schedule and horizon, signature window
8Zero amount, exponent mismatch, int64 overflow, unknown fields, and unknown enum values
9Rollback must match the original’s player and currency. DebitCredit checks the stake first and is all-or-nothing. Balance-reducing corrections arrive as an ordinary Debit
10Free rounds and bonus-type credits: what the operator receives, the token rule, no bet required for promo payouts, accrued wins are always settled
11What GA does on an unknown outcome, per operation type
12Deliberate differences from other aggregators

2. Items

2.1 Rollback of an unknown original

Normative text

  1. If the operator holds no record of original_op_id, a Rollback MUST be answered with a success envelope, MUST move no money, and MUST carry original_found=false and the player’s current balance.
  2. The operator MUST remember the ORIGINAL’s id (original_op_id) as rolled back. This is the barrier. Its key is the original’s id, not the Rollback’s op_id.
  3. A Debit MUST check the barrier in the same atomic step that records the debit, before any money moves. The operator serialises a Debit and the Rollback of that Debit on the ORIGINAL’s id.
  4. The operator MUST refuse a later Debit or Credit that carries that op_id with ALREADY_ROLLED_BACK, regardless of its fingerprint.
  5. A Rollback naming an original that the operator is still processing MUST get an error envelope with retryable=true. GA re-drives it until the original resolves.
  6. The operator MUST NOT return UNKNOWN_ORIGINAL to a Rollback. The value stays in the enum.
  7. A repeated Rollback of the same original MUST return the original response and MUST NOT reverse money again.

Example—the operator has never seen op-bet-90:

{
  "request_id": "req-0002",
  "action": "rollback",
  "payload": {
    "meta": {
      "request_id": "req-0002",
      "op_id": "op-rb-91",
      "round_id": "rnd-55"
    },
    "original_op_id": "op-bet-90",
    "original_kind": "ORIGINAL_KIND_DEBIT",
    "money": {
      "currency": "EUR",
      "amount": 1000,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-8841",
    "balance_after": {
      "currency": "EUR",
      "amount": 25000,
      "exponent": 2
    },
    "original_found": false
  }
}

The Debit with op_id op-bet-90 then arrives late:

{
  "status": "error",
  "error": {
    "code": "ERROR_CODE_ALREADY_ROLLED_BACK",
    "message": "operation was rolled back",
    "retryable": false
  }
}

2.2 Mandatory order of checks

Normative text

  1. The operator MUST evaluate checks in this order: signature → schema → op_id lookup (replay / conflict / already rolled back) → player + currency/exponent → [Debit/DebitCredit only: session, player status, limits, balance] → round state → apply and store the result atomically.
  2. The op_id lookup MUST precede the session, player-status, limit, and balance checks.
  3. A duplicate of an applied Debit MUST return the stored success, even when the balance is now insufficient or the session has expired.
  4. A signature or time failure is a transport-level 401/403 without a code.
  5. For a new Debit, an expired session is SESSION_EXPIRED.

Example—a stake of 10.00 took the balance to 0.00. GA resends the same Debit:

{
  "request_id": "req-0011",
  "action": "debit",
  "payload": {
    "meta": {
      "request_id": "req-0011",
      "op_id": "op-bet-77",
      "round_id": "rnd-40",
      "launch_token": "lt-abc"
    },
    "money": {
      "currency": "EUR",
      "amount": 1000,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9001",
    "balance_after": {
      "currency": "EUR",
      "amount": 0,
      "exponent": 2
    }
  }
}

This response is identical to the response to the first request.


2.3 IDEMPOTENCY_CONFLICT

Normative text

  1. The closed error enum has 15 codes plus the ERROR_CODE_UNSPECIFIED default. IDEMPOTENCY_CONFLICT is the only code added after the initial publication.
  2. For the same op_id with a different fingerprint, the operator MUST refuse with IDEMPOTENCY_CONFLICT, and no money moves.
  3. The fingerprint is: player, operation type, currency, amount together with exponent, round, original, grant reference.
  4. This applies while the operation is APPLIED. An op_id that the operator has rolled back, or remembers as rolled back, answers per item 2.1, whatever the fingerprint.
  5. The same op_id with the same fingerprint returns the stored success.

Example—the operator applied op-bet-77 for 10.00. GA now sends the same op_id with 20.00:

{
  "request_id": "req-0031",
  "action": "debit",
  "payload": {
    "meta": {
      "request_id": "req-0031",
      "op_id": "op-bet-77",
      "round_id": "rnd-40",
      "launch_token": "lt-abc"
    },
    "money": {
      "currency": "EUR",
      "amount": 2000,
      "exponent": 2
    }
  }
}
{
  "status": "error",
  "error": {
    "code": "ERROR_CODE_IDEMPOTENCY_CONFLICT",
    "message": "op_id reused with a different fingerprint",
    "retryable": false
  }
}

2.4 What’s never refused

Normative text

  1. The operator MUST NOT refuse Credit and Rollback because the session or token has expired. GA still sends the launch token of that session. Only its expiry isn’t a reason to refuse.
  2. A blocked or disabled player can’t place NEW bets. The operator MUST still accept Credit and Rollback for such a player.
  3. The operator MUST NOT refuse Credit and Rollback for limits.
  4. SettleGrant is a money operation like a Credit and follows the same rule: the operator MUST NOT refuse it for session, player block, or limits.
  5. The terminal refusals of a Credit are a closed list:
    • player not found (PLAYER_NOT_FOUND)
    • currency mismatch (CURRENCY_MISMATCH)
    • invalid request (INVALID_REQUEST)
    • IDEMPOTENCY_CONFLICT
    • “the bet this win refers to is unknown to the operator or was rolled back” (UNKNOWN_ORIGINAL / ALREADY_ROLLED_BACK)
  6. After any other refusal of a Credit, GA resends it.
  7. Round state: the operator refuses a new Debit in a CLOSED or VOIDED round with BUSINESS_REJECTED. It accepts a Credit in OPEN rounds and, as a late win, in CLOSED rounds. It refuses a Credit in a VOIDED round with ALREADY_ROLLED_BACK. It accepts Rollback and Reconcile in any round state.

Example—the player’s session ended hours ago, but the win still arrives:

{
  "request_id": "req-0042",
  "action": "credit",
  "payload": {
    "meta": {
      "request_id": "req-0042",
      "op_id": "op-win-77",
      "round_id": "rnd-40",
      "launch_token": "lt-abc"
    },
    "money": {
      "currency": "EUR",
      "amount": 2500,
      "exponent": 2
    },
    "kind": "CREDIT_KIND_WIN",
    "round_finish": true
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9002",
    "balance_after": {
      "currency": "EUR",
      "amount": 2500,
      "exponent": 2
    }
  }
}

2.5 Reconcile

Normative text

  1. Reconcile is a read. It MUST always return an ok envelope.

  2. The operator derives the state from its record of original_op_id:

    Operator’s recordReconcile state
    absentNOT_APPLIED
    still being committedUNKNOWN
    appliedAPPLIED
    rolled back / remembered as rolled backNOT_APPLIED
  3. Reconcile MUST NOT record anything. It’s never a barrier.

  4. The operator MUST NOT return UNKNOWN_ORIGINAL to a Reconcile.

  5. For NOT_APPLIED of a Credit, GA’s reading is “resend it”.

Example—the operator never saw op-bet-90:

{
  "request_id": "req-0050",
  "action": "reconcile",
  "payload": {
    "meta": {
      "request_id": "req-0050",
      "op_id": "op-rc-12"
    },
    "original_op_id": "op-bet-90"
  }
}
{
  "status": "ok",
  "data": {
    "state": "RECONCILE_STATE_NOT_APPLIED"
  }
}

The operator processes a later Debit with op_id op-bet-90 normally, because the Reconcile recorded nothing.


2.6 Concurrent duplicates

Normative text

  1. The operator MUST serialise per op_id: it inserts a unique key before business logic runs.
  2. The second request MUST wait for the commit of the first.
  3. If its fingerprint matches, the second request MUST receive the same response as the first. Otherwise it receives IDEMPOTENCY_CONFLICT.
  4. The operator never applies the operation a second time.
  5. The “same response” rule concerns operations that WERE processed. A refused request leaves the op_id unused, and the operator MAY evaluate it again when resent. GA never resends a Debit after your business refusal. GA resends a refused Credit until you accept it. If an earlier attempt of the same Debit ended without an answer, GA treats the Debit as unknown even after a later refusal and sends a Rollback. Keep the barrier (2.4): the earlier request may still reach you.

Example—two requests arrive at once. Both carry op_id op-bet-88, with request_id req-0061 and req-0062. Both receive:

{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9100",
    "balance_after": {
      "currency": "EUR",
      "amount": 4000,
      "exponent": 2
    }
  }
}

The balance changed once.


2.7 Numbers

Normative text

  1. Both sides MUST keep op_id records for at least 4 months. The 7-day window of the published v1 wallet spec remains the hard minimum for v1 bridges.
  2. For a Debit/DebitCredit in unknown state, GA makes up to 4 quick attempts with the same op_id, 100 ms, 500 ms, and 2 s apart. A Retry-After may stretch a pause up to 10 s. Each attempt waits at most 5 s. All attempts share the operation budget of item 2.11.7 (8 s by default), so a silent operator gets 2 attempts: 5 s, then the rest of the budget.
  3. For a Rollback, and on the same schedule for a Credit/SettleGrant in unknown state, GA resends for up to 72 hours. The backoff is exponential (1 s, doubling, capped at 10 minutes). GA stops at the first logical, valid-envelope response. The limit is the 72-hour horizon, not a number of attempts. This is inside the 7-day v1 window and inside the 4-month retention.
  4. When the 72 hours are over, GA sets manual_review and raises an alert. The state stays unknown, and GA never marks it “failed”.
  5. Every attempt carries a new request_id and the same op_id.
  6. The signature window is 300 s: X-GA-Signature: t=,v1=, rejected when |now − t| > 300. The failure is a transport-level 401/403 without a code.

Example—attempt 2 of a Credit resend (new request_id, same op_id):

X-GA-Signature: t=1790000000,v1=<hex>
{
  "request_id": "req-0072",
  "action": "credit",
  "payload": {
    "meta": {
      "request_id": "req-0072",
      "op_id": "op-win-91",
      "round_id": "rnd-60",
      "launch_token": "lt-abc"
    },
    "money": {
      "currency": "EUR",
      "amount": 500,
      "exponent": 2
    },
    "kind": "CREDIT_KIND_WIN"
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9200",
    "balance_after": {
      "currency": "EUR",
      "amount": 1500,
      "exponent": 2
    }
  }
}

This is the stored success of attempt 1, which the operator had committed but GA never received.


2.8 Amount and schema validation

Normative text

  1. Money is {currency, amount:int64, exponent:0..18}.
  2. A Debit amount MUST be > 0. A Credit amount MAY be 0 and is then valid. A zero Credit closes a round. A negative amount is INVALID_REQUEST.
  3. The operator MUST refuse an exponent mismatch or an exponent outside 0..18 with CURRENCY_MISMATCH.
  4. The operator MUST refuse an int64 overflow with INVALID_REQUEST.
  5. Both refusals happen before money moves, and the op_id stays unused.
  6. The operator MUST ignore unknown fields.
  7. The operator MUST refuse an unknown enum value in a known field with INVALID_REQUEST.

Example—the operator holds EUR with exponent 2:

{
  "request_id": "req-0080",
  "action": "debit",
  "payload": {
    "meta": {
      "request_id": "req-0080",
      "op_id": "op-bet-95",
      "round_id": "rnd-70",
      "launch_token": "lt-abc"
    },
    "money": {
      "currency": "EUR",
      "amount": 1000,
      "exponent": 3
    }
  }
}
{
  "status": "error",
  "error": {
    "code": "ERROR_CODE_CURRENCY_MISMATCH",
    "message": "exponent mismatch",
    "retryable": false
  }
}

2.9 Rollback matching, DebitCredit, balance-reducing corrections

Normative text

  1. The operator MUST execute a Rollback only if its player and currency match the original’s. Otherwise it refuses the Rollback terminally with INVALID_REQUEST or CURRENCY_MISMATCH, and no money moves.
  2. A Rollback of a Debit is always full: money equals the stored amount of the original. A partial rollback exists only for a Credit, through partial / debit_mode / debited.
  3. DebitCredit MUST check the stake against the current balance FIRST, as for a separate Debit, and only then pay the win of the same call. The operator applies or refuses stake and win together. It’s all-or-nothing.
  4. A provider correction (adjustment) that REDUCES the player’s balance arrives as an ordinary Debit, idempotent by an op_id that GA derives from the provider’s own correction id. One that INCREASES it arrives as a Credit of kind ADJUSTMENT. The correction names no earlier operation and rolls nothing back.
  5. Apply your usual Debit checks. The only refusal the providers define for a correction is insufficient funds (INSUFFICIENT_FUNDS). GA relays your refusal to the provider as a refusal and never turns a refused correction into another operation.
  6. The Debit carries no correction marker in v1.0. A correction can arrive outside an active game session, for example in live-casino settlements and tournaments. If you reject Debits on an expired session, you refuse such a correction, and the provider resolves it on its side. The same applies to any other charge a provider sends outside a session: a participation fee, a contribution to a promo pool, a retried bet that the provider flags as offline.

Example—a 30.00 balance-reducing correction:

{
  "request_id": "req-0090",
  "action": "debit",
  "payload": {
    "meta": {
      "request_id": "req-0090",
      "op_id": "op-corr-5",
      "round_id": "adj-5531"
    },
    "money": {
      "currency": "EUR",
      "amount": 3000,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9300",
    "balance_after": {
      "currency": "EUR",
      "amount": 10000,
      "exponent": 2
    }
  }
}

2.10 Free rounds and bonus-type credits

Normative text

  1. GA has two free-round mechanisms. They never share a callback.
  2. Operator-issued grant. The operator calls IssueGrant / ListGrants / CancelGrant. A round played on a grant sends NO Debit and NO Credit to the operator. The win accumulates on the grant, and SettleGrant pays it ONCE (carrying grant_id, total_win, rounds_played) when the grant is exhausted. GA settles a zero-win grant too. Grant states are ACTIVE / EXHAUSTED / CANCELLED / EXPIRED / SETTLED. For an operator-issued grant, SettleGrant is the only money call the operator receives.
  3. Provider-issued promo. The operator receives, per winning spin, an ordinary Credit of kind FREE_ROUND carrying the campaign reference (campaign_ref), or one SettleGrant at the end.
  4. IssueGrant is idempotent by the operator’s idempotency_key. The same key returns the same grant. GA refuses the same key with different parameters. Without a key every call creates a new grant, so the operator SHOULD always send one.
  5. You can cancel only an ACTIVE grant. A repeated cancel returns the same result. GA allows cancelling a partly played ACTIVE grant.
  6. A grant has a hard end date (valid_until). GA refuses a free round consumed after that date.
  7. GA never drops money already won on a grant. GA pays a win of a round played before the grant ended (cancelled, expired, or settled). SettleGrant pays it for the rounds actually played. When the win arrives after settlement, a separate Credit of kind FREE_ROUND carrying the grant reference pays it. GA refuses to the provider a NEW round reported on a grant that has already ended, and that round never reaches you.
  8. Token rule. For free-round, promo, and bonus credits, the operator MUST NOT validate the EXPIRY of the session/token. They may arrive when the player is offline. The token, when sent, must still be one the operator knows.
  9. Credits of kind BONUS, PROMO, JACKPOT, TOURNAMENT, CASHBACK, ADJUSTMENT, FREE_ROUND are ordinary Credits: items 2.2–2.7 apply in full. They’re idempotent by op_id. The operator never refuses them for session, block, or limits. GA resends them until delivered and never rolls them back.
  10. No bet required. A promo, bonus, jackpot, or tournament payout may come without any bet or debit in the round. The operator MUST NOT require one. An ordinary win belongs to its round as usual.
  11. The operator answers success to a provider’s rollback of a free-round bet. The rollback moves no money, because the bet took none. SettleGrant is never a Rollback target.
  12. SettleGrant is one per grant and idempotent by op_id. On an unknown outcome, GA resends it per item 2.7 until acknowledged.

Example—a grant is exhausted, and GA settles it once:

{
  "request_id": "req-0100",
  "action": "settle_grant",
  "payload": {
    "meta": {
      "request_id": "req-0100",
      "op_id": "op-sg-3"
    },
    "grant_id": "grant-2041",
    "total_win": {
      "currency": "EUR",
      "amount": 1800,
      "exponent": 2
    },
    "rounds_played": 20
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-9400",
    "balance_after": {
      "currency": "EUR",
      "amount": 11800,
      "exponent": 2
    }
  }
}

2.11 What GA does on an unknown outcome

Normative text

  1. An unknown outcome is any of: a timeout, a disconnect, any response that isn’t a valid envelope with a known code (including 401/403/404/5xx from a proxy). A valid envelope with a retryable code (RATE_LIMITED, MAINTENANCE, INTERNAL) on a money operation is also an unknown outcome.
  2. Debit / DebitCredit.
    • GA makes up to 4 quick attempts with the same op_id, 100 ms, 500 ms, and 2 s apart, inside the budget of item 2.11.7 (numbers in item 2.7).
    • If the outcome is still unknown, GA fails the bet towards the provider with a retryable error, so the round doesn’t start. GA also generates ONE linked Rollback.
  3. Credit / SettleGrant. GA resends with the same op_id until success or until the resend limit of item 2.7 runs out. GA never rolls back a bet because it couldn’t deliver a win. Only the provider may send that rollback.
  4. Rollback.
    • GA resends with the same op_id per item 2.7 (up to 72 hours, exponential backoff, stopping at the first logical response).
    • A non-retryable refusal, meaning a valid envelope with a non-retryable code, stops resending at once: GA sets manual_review and raises an alert.
  5. A Rollback is never in flight concurrently with its original. GA records a provider’s cancel that arrives while the Debit is in flight and answers it retryable. GA executes it after the Debit resolves.
  6. There is one Rollback per original, and GA reads its amount from the stored original.
  7. An overall operation budget bounds the retry sequence (GA’s own sender setting, 8s by default): the first attempt always goes out. GA starts no retry and waits no backoff the remaining budget can’t hold. It answers the provider retryable with an unknown outcome before the provider’s own timeout expires.

Example—a Debit op-bet-90 gets no valid response within the budget (two attempts on a silent wallet: two request_id values, same op_id). GA fails the bet towards the provider and sends the linked Rollback:

{
  "request_id": "req-0205",
  "action": "rollback",
  "payload": {
    "meta": {
      "request_id": "req-0205",
      "op_id": "op-rb-91",
      "round_id": "rnd-55"
    },
    "original_op_id": "op-bet-90",
    "original_kind": "ORIGINAL_KIND_DEBIT",
    "money": {
      "currency": "EUR",
      "amount": 1000,
      "exponent": 2
    }
  }
}
{
  "status": "ok",
  "data": {
    "operator_tx_id": "tx-8841",
    "balance_after": {
      "currency": "EUR",
      "amount": 25000,
      "exponent": 2
    },
    "original_found": false
  }
}

If the Debit reaches the operator later, the operator refuses it with ALREADY_ROLLED_BACK (item 2.1).


2.12 Deliberate differences from other aggregators

The published GA contract governs. Some of its choices differ from what other aggregators do. Don’t “fix” them when porting an existing wallet integration:

  • A business refusal travels on HTTP 200 with the status in the body. Non-200 statuses are transport-level only.
  • An error response carries no balance.
  • Money is int64 in minor units plus an explicit exponent, never a fixed scale, and never a decimal string.
  • A signature failure is a transport-level 401/403 without a body status.
  • A partial rollback exists, but only for a Credit (partial, debit_mode, debited). A rollback of a Debit is always full.
  • A Credit is never refused for limits or session (item 2.4).
  • A round may stay OPEN forever. GA doesn’t require every round to be closed.
  • A duplicate returns the stored success (item 2.6), never a conflict status.
  • GA doesn’t send free-round bets to the operator (item 2.10). The operator learns the rounds from SettleGrant.

3. Conformance additions

Check idSetupActionExpected
W2-DELTA-01 debit after rollback-of-unknownThe operator has no record of op_id D. The player has a known balance B1. Rollback with original_op_id = D. 2. Debit with op_id = D: once with the original amount, once with a different amount1. ok envelope, original_found=false, balance B, no money moved. 2. Both Debits refused with ALREADY_ROLLED_BACK. Balance still B
W2-DELTA-02 duplicate debit when balance is now insufficientDebit D (stake S) applied. The balance is now below S. Also run with the session expiredThe same Debit resent: same op_id, same fingerprint, new request_idok envelope identical to the first response (same operator_tx_id and balance_after). Balance unchanged
W2-DELTA-03 duplicate with a different amountDebit D (amount A) appliedDebit with the same op_id and amount A′ ≠ AIDEMPOTENCY_CONFLICT. Balance unchanged. The stored result of D unchanged
W2-DELTA-04 credit after session expiryDebit applied in a session. The session/token has since expiredCredit for the same round, carrying the launch token of that sessionok envelope. The operator credits the balance exactly once
W2-DELTA-05 concurrent duplicatesA fresh op_id, a known balanceTwo or more simultaneous identical Debits (different request_id, same op_id)Every request gets the same ok response. The operator applies exactly one debit
W2-DELTA-06 exponent mismatchThe player’s currency has exponent e at the operatorDebit with the same currency and an exponent ≠ eCURRENCY_MISMATCH. No money moved. The op_id stays unused

Certification checklist

Operator: ____________________ Environment: ____________________ Transport Binding: [ ] HTTP/JSON v2     [ ] Native gRPC v2 Execution Date: ____________________ GA Certification Engineer: ____________________ Operator Lead Engineer: ____________________


0. Scope of certification

A PASS here means one thing: your wallet endpoint follows the v2 contract on a test player. This includes the money rules that certification can exercise (idempotency, order of checks, rollback barrier, never-refused credits). It’s a necessary condition for go-live, not the whole of it.

It does not prove:

  • that your production wallet, with its own URL, credentials, and infrastructure, behaves the same. Run the tool against production before go-live.
  • that your calls to GA work: game launch, catalog, free rounds, reports. Cover them with the GA_Aggregator_API_v2 Postman collection and one real end-to-end session in the sandbox. Launch a game, place a bet, and see it in rounds and in the Operator Portal.
  • that the numbers reconcile: compare one day of aggregates with your own ledger before the first settlement.
  • performance under load: the tool sends one call at a time. Your wallet must answer every call within 5 seconds at your peak traffic.

Go-live needs all four plus this signed checklist.

Files you need

WhatWhere
Certification checksOperator Portal → Integration → Callback test (operator_admin role, wallet URL with /v2). The same suite as the command-line tool ga-operator-certify, which your GA integration manager provides for CI runs
Postman: the calls GA sends to your wallet../postman/GA_Operator_API_v2.postman_collection.json + ../postman/GA_Sandbox_v2.postman_environment.json
Postman: your calls to GA../postman/GA_Aggregator_API_v2.postman_collection.json, ../postman/GA_Features_API_v2.postman_collection.json
All the above in one zip/downloads/GA_Operator_Integration_Pack_v2.0.0.zip on the docs site
The guide/docs on the docs site, or GA_Operator_Integration_Guide_v2.pdf in the zip

1. Conformance tooling verification

Prerequisites to confirm before running

  • A dedicated test player exists in the operator wallet under the --player-ref passed to the tool (default certify-player-001) with a funded balance in the --currency (default EUR).
  • You issue a valid launch token for that player and pass it via --launch-token (default token-cert-001). The tool doesn’t create players or tokens.
  • The API key/secret pair passed via --api-key/--secret is the one the wallet validates signatures with.
  • The target is a sandbox/staging wallet, never production.

Certification requires one run in which every check passes: either from the Operator Portal (Integration → Callback test) or with ga-operator-certify, a binary your GA integration manager provides:

## HTTP Binding:
./ga-operator-certify --binding http --target https://wallet.operator.example --api-key <key-id> --secret <secret> --strict

## gRPC Binding:
./ga-operator-certify --binding grpc --target wallet.operator.example:9090 --api-key <key-id> --secret <secret> --strict

2. Test cases & obligations (all 55 W2-* checks)

2.1 Money primitives & idempotency

  • W2-G1-OPID-REQUIRED: Your wallet refuses an empty op_id with INVALID_REQUEST.
  • W2-G1-DEBIT-ANSWER-SHAPE: A debit answer carries operator_tx_id and balance_after.
  • W2-G1-OPID-REPLAY-MOVES-MONEY-ONCE: Replaying an op_id returns stored result and does NOT move money twice.
  • W2-G1-TWO-WINS-ONE-ROUND-BOTH-SETTLE: Two Credits in one round_id with distinct op_ids both settle.
  • W2-DEBIT-INSUFFICIENT-FUNDS: A debit exceeding balance answers business refusal INSUFFICIENT_FUNDS.
  • W2-DEBIT-ZERO-AMOUNT: Your wallet refuses a debit with zero amount as INVALID_REQUEST.
  • W2-CREDIT-ZERO-AMOUNT: A credit with zero amount succeeds cleanly without altering balance.
  • W2-KIND-EACH-CREDIT-KIND-ACCEPTED: Credit accepts all defined CreditKind variants (WIN, BONUS, JACKPOT, PROMO, FREE_ROUND, TOURNAMENT, CASHBACK, ADJUSTMENT).
  • W2-COMPONENTS-SUM-EQUALS-AMOUNT: A credit with MoneyComponent breakdown matches total amount.

2.2 Atomic debit & credit

  • W2-DEBITCREDIT-HAPPY: Atomic DebitCredit applies net difference to player balance.
  • W2-DEBITCREDIT-INSUFFICIENT: Atomic DebitCredit with bet > balance refuses with INSUFFICIENT_FUNDS.
  • W2-DEBITCREDIT-ATOMIC-ON-CREDIT-FAILURE: Atomic DebitCredit leaves balance intact if operation can’t complete.
  • W2-DEBITCREDIT-REPLAY: Atomic DebitCredit replayed returns identical stored response.

2.3 Reversals & rollbacks

  • W2-G4-ROLLBACK-IDEMPOTENT-ON-ITS-OWN-OPID: The same Rollback delivered twice reverses exactly once.
  • W2-G4-ROLLBACK-OF-UNKNOWN-ORIGINAL-IS-OK-NOOP: A Rollback naming an unknown original answers OK and moves nothing.
  • W2-G4-DEBIT-AFTER-ROLLBACK-OF-UNKNOWN-REJECTED: Your wallet rejects a Debit whose op_id it already rolled back as an unknown original. The barrier holds.
  • W2-ROLLBACK-PARTIAL: A Rollback with partial amount reverses only specified amount.
  • W2-ROLLBACK-DEBIT-MODE-REJECT: Rollback with debit_mode=REJECT refuses if balance insufficient.
  • W2-ROLLBACK-DEBIT-MODE-ALLOW_NEGATIVE: Rollback with debit_mode=ALLOW_NEGATIVE permits negative balance.
  • W2-ROLLBACK-DEBIT-MODE-PARTIAL: Rollback with debit_mode=PARTIAL drains balance up to available funds.

2.4 Rounds & grants

  • W2-CLOSEROUND-IDEMPOTENT: Your wallet acknowledges CloseRound, and the call is idempotent on op_id.
  • W2-CLOSEROUND-WITH-NETTO: CloseRound carries the round summary net_win, bet_total, win_total.
  • W2-G6-SETTLEGRANT-IS-ACKNOWLEDGED: Your wallet acknowledges SettleGrant with OK, including zero total.
  • W2-G6-SETTLEGRANT-IDEMPOTENT: SettleGrant replayed for one grant pays once.
  • W2-G6-SETTLEGRANT-CREDITS-TOTAL-WIN: SettleGrant credits total_win (§5.9 of the Integration Guide): right after the ok reply the balance is up by exactly total_win. An unchanged balance fails: this call is the only place the free-round win reaches the player.

2.5 Transaction reconciliation

  • W2-RECONCILE-APPLIED: Reconcile reports APPLIED for a completed op_id.
  • W2-RECONCILE-NOT_APPLIED: Reconcile reports NOT_APPLIED for an unknown op_id.
  • W2-RECONCILE-UNKNOWN: Reconcile reports valid ReconcileState enum.
  • W2-RECONCILE-CARRIES-OPERATOR-TX: Reconcile for applied operation returns original operator_tx_id.

2.6 Non-financial lifecycle & notifications

  • W2-NOTIFY-EACH-KIND-ACK: Notify acknowledges non-monetary lifecycle events.
  • W2-NOTIFY-FREEROUNDS-STARTED: Notify NOTIFY_KIND_FREE_ROUNDS_STARTED event acknowledged.
  • W2-NOTIFY-FREESPIN-FINISHED: Notify NOTIFY_KIND_FREESPIN_FINISHED event acknowledged.
  • W2-NOTIFY-SESSION-EXPIRED: Notify session_expired event acknowledged.

2.7 Metadata, authentication & scope isolation

  • W2-PROVIDER-DATA-ECHOED: ProviderData with raw payload in CallMeta accepted.
  • W2-AUTH-TOKEN: Authenticate validates launch token and answers session profile.
  • W2-AUTH-INVALID-TOKEN: Your wallet cleanly rejects Authenticate with an invalid launch token.
  • W2-TOKEN-EMPTY-IS-REJECTED: Your wallet refuses a money call arriving with empty launch_token.
  • W2-TOKEN-BALANCE-WITHOUT-TOKEN: GetBalance without launch_token answers from player_ref or refuses cleanly.
  • W2-G7-PLAYER-REF-IS-OPAQUE: The player reference is carried through unparsed and unaltered.
  • W2-G8-BRAND-ISOLATION: A session’s money can’t be moved from another brand.
  • W2-G5-GRANT-ISSUANCE-IDEMPOTENCY: The aggregator, not the wallet, handles grant issuance idempotency.
  • W2-G10-DEMO-CANNOT-REACH-THE-WALLET: A demo launch has no field that could ever address a wallet.

2.8 Error handling, retries & capabilities

  • W2-ERR-REFUSAL-IS-FAILED-PRECONDITION: a business refusal answers with a typed error code on HTTP 200 (on gRPC: a status carrying ErrorDetail), never a bare FAILED_PRECONDITION and never an internal error.
  • W2-ERR-UNDECIDED-RETRY-SAME-OPID: An undecided call retried with the SAME op_id moves money once.
  • W2-ERR-MALFORMED-IS-INVALID-REQUEST: Your wallet refuses a malformed money request as INVALID_REQUEST, not 500.
  • W2-ERR-EACH-ERRORCODE-SHAPE: Structured error responses conform to ErrorCode contract.
  • W2-RETRY-SAME-OPID-NEW-REQUESTID: Your wallet recognizes a retry with SAME op_id and NEW request_id as an idempotent retry.
  • W2-CURRENCY-MISMATCH-REFUSED: Your wallet cleanly rejects a request with currency mismatch against the player account.
  • W2-CAPS-DECLARED-EQUALS-BEHAVIOUR: Declared capabilities in wallet_capabilities match runtime behavior.

2.9 Money-path delta (W2-D1 … W2-D6)

The six checks of ../money-path/OPERATOR-API-MONEY-PATH-RULES.md §3, required of every operator certifying on the v2 contract. The document numbers them W2-DELTA-01 … W2-DELTA-06. ga-operator-certify prints them under the ids below.

  • W2-D1-DEBIT-AFTER-ROLLBACK-OF-UNKNOWN: After a Rollback of an unknown original, your wallet refuses a Debit with that op_id with ALREADY_ROLLED_BACK. This holds at the original amount and at a different one. The balance never moves.
  • W2-D2-DUPLICATE-DEBIT-BALANCE-NOW-INSUFFICIENT: A resent Debit whose stake no longer fits the balance returns the identical stored success. The check also runs with the session expired.
  • W2-D3-DUPLICATE-DEBIT-DIFFERENT-AMOUNT: For the same op_id with a different amount, your wallet refuses with IDEMPOTENCY_CONFLICT. The stored result stays unchanged.
  • W2-D4-CREDIT-AFTER-SESSION-EXPIRY: Your wallet accepts a Credit carrying the launch token of an expired session and credits it exactly once.
  • W2-D5-CONCURRENT-DUPLICATES: Simultaneous identical Debits all get the same response. Your wallet applies exactly one debit.
  • W2-D6-EXPONENT-MISMATCH: For a Debit with an exponent the player’s currency doesn’t use, your wallet refuses with CURRENCY_MISMATCH. No money moves, and the op_id stays unused.

3. Decision & sign-off

ResultStatusNotes
PASS[ ]All 55 checks passed, allowing the structurally PARTIAL results the tool reports (W2-G8-BRAND-ISOLATION on the HTTP binding)—0 errors and 0 failures.
FAIL[ ]One or more tests failed with unexpected state or money loss.
BLOCKED[ ]Network connectivity or authentication issue prevented suite completion.

Certification Signatures:

GA Integration Engineer: _________________________     Date: ______________ Operator Tech Lead: _____________________________     Date: ______________

PreviousNumbers to remember
Integration support: integration@gamealligator.comIntegration center
On this page
Document Control1. Quick start2. Common to every call3. Idempotency: the four rules that protect money4. You call GA: launch, catalog, free rounds, reports5. GA calls your wallet: the actions6. Optional: provider features7. Data formats8. Network and limits9. Certification and go-liveAppendix A. Every error codeAppendix B. Numbers to rememberMoney Path Rules1. Summary2. Items3. Conformance additionsCertification checklist0. Scope of certification1. Conformance tooling verification2. Test cases & obligations (all 55 W2-* checks)3. Decision & sign-off
↑ Back to top