Acceptance Testing
Self-certify your wallet integration before go-live
Acceptance testing lets you certify your wallet integration on your own, before
go-live. It replays the real signed wallet callbacks (/balance, /bet, /win,
/rollback) against your staging wallet and returns a pass/fail report — so you can
confirm everything works end-to-end without waiting on a coordinated test.
Runs on staging only (acceptance.stagingmarbles.io). Each money-moving check
places a real, small bet/win/rollback on your staging wallet for the test
player you specify, so make sure that player is funded.
Run it yourself
You drive the run with two calls:
POST /acceptance/run— kicks off the suite and returns the full report in the response body when it finishes (a run takes up to ~90s).GET /acceptance/run/{runId}— re-fetches a stored run by itsrunId(handy to re-read or share a result; you can only fetch your own runs).
POST https://acceptance.stagingmarbles.io/acceptance/run
GET https://acceptance.stagingmarbles.io/acceptance/run/{runId}Authentication
Authentication is by source IP — call from one of the IP addresses you whitelisted
with us when your integration was set up. There is no token or signature to manage; we
resolve which integration you are from the calling IP, so you do not send an
integrationId (any integrationId in the body is ignored for operator calls). A
request from a non-whitelisted IP is rejected with 401.
Request
Field names are PascalCase under testPlayer (they map directly to the harness'
TestPlayer fields):
POST /acceptance/run HTTP/1.1
Host: acceptance.stagingmarbles.io
Content-Type: application/json
{
"operatorId": "your-operator-id",
"partnerId": "your-partner-id",
"testPlayer": {
"ClientPlayerID": "a funded standing test player",
"ClientSessionID": "that player's last real session id",
"Currency": "USD",
"GameID": "a gameId whitelisted for you (GameAccessRules)"
}
}Omit checks to run all applicable checks. To run a subset, add a checks array
of check ids (e.g. "checks": ["balance_read"]); ["all"] is the same as omitting it.
Request fields
| Field | Type | Description |
|---|---|---|
operatorId | string | Your operator identifier (the operatorId you send in /game/url). Used for the game-whitelist check and the wallet-URL fallback. |
partnerId | string | Your brand / partner identifier (the partnerId you send in /game/url). Needed for the game_whitelisted launch check. |
testPlayer.ClientPlayerID | string | A funded standing test player on your staging wallet. |
testPlayer.ClientSessionID | string | A current/recent session id for that player (some wallets validate it). |
testPlayer.Currency | string | ISO 4217 currency code (e.g. USD). |
testPlayer.GameID | string | A gameId whitelisted for your integration (its GameAccessRules). |
checks | array | Optional. Subset of check ids, or ["all"]. Defaults to all applicable checks. |
Where do these values come from?
These are the same identifiers you already send us when you launch a game via
POST /game/url — there's nothing new to generate:
ClientPlayerID— your own unique id for the player. Use a real, funded test player on your staging wallet (the money-moving checks place small real bets, so it needs a balance). Tip: launch a game once as that player and copy theclientPlayerId/clientSessionIdfrom your own/game/urlrequest — those are exactly the values to use.ClientSessionID— your session id for that player. Use a recent/live one your wallet recognises (the same value you'd pass in/game/url).GameID— any game whitelisted for you (thegame_whitelistedcheck below confirms it is). You can list your games withPOST /game/games.operatorId/partnerId— the same values you send in every/game/urlcall.
Try it with curl
Start a run from a whitelisted integration IP:
curl -sS -X POST https://acceptance.stagingmarbles.io/acceptance/run \
-H "Content-Type: application/json" \
-d '{
"operatorId": "your-operator-id",
"partnerId": "your-partner-id",
"testPlayer": {
"ClientPlayerID": "your-funded-test-player",
"ClientSessionID": "that-players-session-id",
"Currency": "USD",
"GameID": "a-whitelisted-game-id"
}
}'The POST returns the full report. To re-fetch it later by its runId:
curl -sS https://acceptance.stagingmarbles.io/acceptance/run/run_327b8db7-0da9-448c-80cb-b78d09a5ce91For a first, no-risk run, pass only the read-only checks:
"checks": ["signature_accepted", "signature_rejected", "unknown_player", "balance_read"].
These never move money — they confirm reachability, signature handling, and that we
can read the test player's balance.
The checks
The suite runs in four phases. Phase 1 is reachability/auth, Phase 2 confirms the game is launchable, Phase 3 exercises the full wallet lifecycle, and Phase 4 verifies amount precision. Advisory checks warn rather than fail — a not-met advisory never blocks go-live.
Phase 1 — Reachability & auth (read-only)
| Check | What it proves |
|---|---|
signature_accepted | Confirms we can reach your wallet and your endpoint accepts our signed /balance request. Fail = your wallet is unreachable, the endpoint is wrong, or it rejected our request (signature, IP allowlist, or TLS). |
signature_rejected (advisory) | A request signed with the wrong key should be refused. Advisory — if you secure the wallet by allowlisting our egress IPs instead of verifying the X-Marbles-Signature, this warns rather than fails. |
unknown_player (advisory) | A /balance for a non-existent player should return an error, not a balance. Advisory — returning a balance for an unknown player is sloppy but not a blocker. |
Phase 2 — Launch (read-only)
| Check | What it proves |
|---|---|
game_whitelisted | The gameId you launch must be in your operator's allowed games (GameAccessRules). Fail = this game isn't whitelisted for you — players would get a 'game not found' on launch. Ask us to add it to your allowed games. |
This check needs both operatorId and partnerId; it is skipped if either is missing.
Phase 3 — Wallet lifecycle
| Check | What it proves |
|---|---|
balance_read | We can read the test player's balance via /balance. Fail = your /balance didn't return a usable balance for the configured test player/session. |
settle_netzero | A bet followed by an equal win returns the balance to where it started. Fail = your wallet didn't reconcile bet+win to the same balance. |
bet_debit | A /bet debits exactly the stake. Fail = a different amount was debited — most often the wallet rounds/truncates amounts (we use 5-digit precision) or applies a fee. |
insufficient_funds | A bet larger than the balance must be rejected with INSUFFICIENT_BALANCE and no debit. Fail = you accepted an over-balance bet or debited anyway. |
win_credit | A /win credits exactly the payout. Fail = the credited amount didn't match the payout — usually amount rounding/precision. |
loss | Settling a bet as a loss (/win with amount 0) leaves only the stake debited. Fail = the balance moved by an unexpected amount on a zero-amount settle. |
rollback_zero | A /rollback of a bet returns the balance to exactly where it started. Fail (common: re-debit) = your rollback moved money the wrong way instead of refunding the stake. |
free_bet | A free bet (isFree:true, amount 0) is accepted with zero debit and settles cleanly. Fail = your wallet rejected the isFree payload or errored settling it. Free bets are enabled by default for new operators. |
idempotency | A replayed /win or /bet with the same transactionId must be deduplicated (no double credit/debit, and a duplicate /bet is rejected). Fail = you re-processed a duplicate — add transactionId dedup, or a network retry will double-charge players. |
Phase 4 — Precision
| Check | What it proves |
|---|---|
precision | Odd amounts (e.g. 1.23457) reconcile to the exact cent at 5-digit precision. Fail = your wallet rounds or truncates amounts (e.g. to 2 decimals), so balances drift from ours. |
The report
The POST (and the GET) return the full report — HTTP 200 even when the run
failed; the verdict is in the top-level status. Each check carries its own
status, a human-readable reason, the same operator-facing description from the
table above, and evidence (balances before/after, HTTP status, etc) — so a failed
check tells you exactly what to fix.
{
"runId": "run_327b8db7-0da9-448c-80cb-b78d09a5ce91",
"integrationId": "your-integration",
"environment": "staging",
"status": "passed_with_warnings",
"checks": [
{
"id": "signature_accepted",
"phase": 1,
"title": "Our signature is accepted",
"status": "pass",
"reason": "correctly-signed /balance accepted",
"evidence": { "httpStatus": 200 }
},
{
"id": "signature_rejected",
"phase": 1,
"title": "A bad signature is rejected",
"status": "warn",
"reason": "operator ACCEPTED a request signed with a wrong key (HTTP 200) — it is not verifying signatures"
},
{
"id": "balance_read",
"phase": 3,
"title": "Balance reads for the test player",
"status": "pass",
"reason": "balance read",
"evidence": { "balance": 3564378000 }
},
{
"id": "rollback_zero",
"phase": 3,
"title": "Rollback nets to zero",
"status": "pass",
"reason": "rollback returns to start (catches re-debit operators)",
"evidence": { "before": 3564378000, "after": 3564378000, "expectedDelta": 0, "actualDelta": 0 }
}
]
}Run status
status | Meaning |
|---|---|
running | The run is still in progress — transient; you only see this if you poll GET /acceptance/run/{runId} before it finishes. |
passed | Every applicable check passed — you're good to go live. |
passed_with_warnings | All hard checks passed; an advisory check was flagged (not a blocker). |
failed | At least one hard check failed — see the checks array. |
blocked | A prerequisite was missing (e.g. no funded test player). |
Per-check status
status | Meaning |
|---|---|
pass | The check passed. |
fail | The check failed — read reason and description for the fix. |
warn | An advisory check was not met (e.g. you allowlist our IPs instead of verifying our signature). Not a go-live blocker. |
blocked | A prerequisite was missing for this check (e.g. no funded test player). |
Caveats
Staging only, and the test player must be funded
- The harness runs on staging only (
acceptance.stagingmarbles.io). Operator self-cert against the production endpoint is refused with403— production is internally gated. - Use a real, funded standing test player. The money-moving Phase-3/4 checks place small real bets/wins/rollbacks on that player's staging wallet; with no balance they fail (or block). The read-only checks never move money.
- Advisory checks (
signature_rejected,unknown_player) warn rather than fail — a not-met advisory yieldspassed_with_warnings, notfailed.
Error statuses
| HTTP | Reason |
|---|---|
400 | Missing testPlayer.GameID, or invalid JSON body. |
401 | Calling IP is not whitelisted for any integration. |
403 | Operator self-cert was attempted against the production endpoint (staging only). |
404 | The requested runId doesn't exist (or isn't yours). |
For the wallet-callback error statuses your endpoint returns (e.g.
INSUFFICIENT_BALANCE), see Error Codes.