Quick reference for the Cymba API. All endpoints live under /api/v1/.
One API serves every engine. The config picks the math engine — slot games and instant games (scratch cards, dice duels, wheels, plinko) play, validate, and verify through the same endpoints. Stored configs carry their engine; inline configs declare it with a game_type field. The provably-fair spine — committed server seed, monotonic nonce, multipliers-only results — is identical for every round.
Every endpoint except GET /api/v1/health (the public status probe) requires a Bearer token. Generate keys from the dashboard.
Authorization: Bearer cy_live_your_api_key_here
Rate limits are enforced per key on every endpoint; responses include X-RateLimit-Limit and X-RateLimit-Remaining headers. Round quotas bill only on POST /play (and plays through a hosted embed) — validation, verification, RTP, seed, PAR-sheet, presentation, and embed-minting endpoints are free audit surface.
Resolve one round of any game. Pass a stored config ID or an inline config object; the config's engine shapes the response. Billable (one round).
{
"config": "my-config-id", // string (stored ID) or object (inline config)
"game_type": "slots", // inline configs only — "slots" (default) or "instant"
"client_seed": "optional", // string, auto-generated if omitted
"lines": 20, // slots only, optional — active paylines
"stake": 1.0, // instant only, optional — display scaling, never stored
"gamble_rounds": 0 // instant only, optional — double-or-nothing rounds
}
Stored configs carry their own engine — game_type is ignored for them. Params the config's engine doesn't use are ignored. Inline configs are validated on every call.
{
"result": {
"round_id": "9f2c41...", // deterministic — derived from the seeds; a /verify replay echoes it
"spins": [
{
"type": "base",
"reel_positions": [
["A", "wild", "K"],
["K", "A", "A"],
["J", "wild", "K"]
],
"wins": [{
"type": "payline",
"line": 1,
"payline": [0, 0, 0, 0, 0],
"win_symbol_id": "A",
"matching_symbols": 4,
"win_multiplier": 25,
"has_wild": true
}],
"active_reel_count": 5, // only present for dynamic reel count games
"reel_heights": [3,5,4,2,6], // per-reel visible symbols (dynamic reels)
"ways_count": 360, // ways-to-win (dynamic reels/reel count)
"symbol_features": { ... } // wild/mystery/stacked features applied
}
],
"summary": {
"total_multiplier": 25,
"win_capped": false,
"breakdown": { // multiplier by source
"payline": 25,
"scatter": 0
}, // hold_respin, free_spins: present when > 0
"cascade_count": 0, // number of cascades in this spin
"hold_respin_count": 0, // number of hold respins
"free_spin_count": 0, // number of free spins played
"free_spins_retriggered": 0 // free spins won during free spins
},
"result_hash": "d4e5f6..." // HMAC-SHA256 tamper-evident hash (slots only)
},
"provably_fair": {
"server_seed_hash": "a3f1c8...",
"client_seed": "b7c29e...",
"nonce": 42
}
}
Scratch cards, dice duels, wheels, plinko and other reveal mechanics. One play is one round (the RTP divisor is 1). Author configs in the Instant-Win Studio or send them inline with "game_type": "instant".
{
"result": {
"round_id": "9f2c41...", // deterministic — derived from the seeds; a /verify replay echoes it
"game_id": "my-dice-game",
"prize": { "prize_id": "m15", "multiplier": 15, "kind": "win" },
"card": { // what the player reveals — mechanic-specific
"mechanic": "beat_target",
"cells": ["5", "2"], "rows": 1, "cols": 2,
"meta": { "your_value": 5, "target_value": 2, ... }
},
"result": { "total_multiplier": 15, "base_multiplier": 15, "breakdown": { "base": 15 } },
"external_state": { "pack_remaining": {}, "consumed_outcome_count": 3 },
"outcomes_consumed": 3
},
"provably_fair": { "server_seed_hash": "3e1c7d...", "client_seed": "player-chosen", "nonce": 42 }
}
The card's read-back is the verification model: the engine refuses any card whose display doesn't name exactly the drawn prize, so a returned round is self-consistent by construction. breakdown itemizes feature contributions (global multiplier, bonus, gamble) and always sums to total_multiplier.
The type field determines which extra fields appear on each win line.
Ways win
{
"type": "ways",
"win_symbol_id": "K",
"matching_symbols": 4,
"win_multiplier": 10,
"has_wild": false,
"ways_count": 12,
"reel_counts": [2, 1, 3, 2]
}
Cluster win
{
"type": "cluster",
"win_symbol_id": "A",
"matching_symbols": 7,
"win_multiplier": 15,
"has_wild": true,
"cluster_size": 7,
"cluster_positions": [[0,0],[0,1],[1,0],[1,1],[1,2],[2,1],[2,2]]
}
Entries with "type": "cascade" appear in the spins[] array when cascading reels trigger. Summary includes cascade_count.
{
"type": "cascade",
"iteration": 1,
"reel_positions_before": [["A","wild","K"], ...], // grid before cascade removal
"reel_positions": [["A","K","Q"], ...], // grid after cascade fill
"wins": [{ ...win object... }], // wins from this iteration
"positions_removed": [[0,2],[],[1],[],[]], // per-reel row indices removed
"iteration_multiplier": 2 // cascade progression multiplier
}
Entries with "type": "hold_respin" appear in the spins[] array when the feature triggers. Summary includes hold_respin_count; the multiplier contribution is at summary.breakdown.hold_respin.
{
"type": "hold_respin",
"respin_index": 1,
"reel_positions": [["bonus","K","Q"], ...], // grid after respin
"wins": [], // no payline evaluation during respins
"locked_positions": [[0],[],[1,2],[],[]], // per-reel locked row indices
"locked_count": 3, // total locked trigger symbols
"new_triggers_landed": true, // whether new triggers appeared
"respins_remaining": 3 // respins left (resets on new triggers)
}
Entries with "type": "free_spin" appear in the spins[] array when scatter symbols award free spins. The entire sequence is resolved in a single API call. Summary includes free_spins_won, free_spin_count, and free_spins_retriggered; the multiplier contribution is at summary.breakdown.free_spins.
{
"type": "free_spin",
"spin_index": 0,
"spins_remaining": 9,
"reel_positions": [["A","K","wild"], ...],
"wins": [...win objects...], // flat array, same shape as base spin
"win_total": 20.0 // this free spin's total multiplier
}
// Cascades within free spins include parent_type + parent_spin_index:
{
"type": "cascade",
"parent_type": "free_spin",
"parent_spin_index": 0,
"iteration": 1,
// ... same fields as any cascade entry
}
Control which direction paylines are evaluated using the win_evaluation.direction config field.
"win_evaluation": {
"direction": "ltr" // "ltr" (default), "rtl", or "both"
}
"ltr" — left-to-right only (default). "rtl" — right-to-left only. "both" — evaluates paylines in both directions with full-line deduplication to prevent double-counting.
RTL wins use negative line numbers (e.g. -1, -2) to distinguish them from LTR wins in the response.
Present on the base spin entry at spins[0].symbol_features when any symbol transformations occurred. Each sub-field is only included if non-empty.
"symbol_features": {
"expanded_reels": { // reel index → rows filled with wild
"2": [0, 1, 2]
},
"sticky_positions": { // wilds persisted from prior free spin
"1": [1],
"3": [0, 2]
},
"stacked_positions": { // multi-row stacked symbols
"0": [0, 1]
},
"mystery_resolutions": { // [reel][row] → resolved symbol
"0": { "0": "A", "2": "A" },
"3": { "1": "A" }
},
"mystery_uniform_symbol": "A", // only if uniform resolve mode
"wild_multipliers": { // [reel][row] => assigned multiplier value
"1": { "0": 3, "2": 2 }
}
}
Wilds can carry random multiplier values that multiply wins they participate in. Configure by adding multiplier_values to any symbol with role: "wild".
// Symbol config
{
"id": "wild",
"role": "wild",
"multiplier_values": [2, 3, 5], // possible multiplier values
"multiplier_weights": [50, 30, 20] // optional — uniform if omitted
}
Payline mode: when multiple multiplier wilds appear in a winning line, the product of their multipliers is applied to the win.
Ways mode: each wild contributes its multiplier value instead of 1 to the effective ways calculation, amplifying the total ways count.
Present on the base spin entry at spins[0].reel_heights and spins[0].ways_count when the game config uses dynamic reels.
"reel_heights": [3, 5, 4, 2, 6, 3], // visible rows per reel this spin
"ways_count": 2160 // product of reel heights (3×5×4×2×6×3)
Enable variable number of reels per spin via reels.dynamic_reel_count. When active, each spin entry includes active_reel_count.
"reels": {
"dynamic_reel_count": {
"enabled": true,
"min_reels": 3, // minimum active reels
"max_reels": 6, // maximum active reels
"trigger": "per_spin", // "per_spin" or "on_cascade_win"
"reel_count_weights": { // optional — uniform if omitted
"3": 10, "4": 25, "5": 40, "6": 25
}
}
}
"per_spin" — reel count is randomized on every spin. "on_cascade_win" — reel count can increase when a cascade win occurs.
Validate a game config JSON object without resolving a round. game_type picks the engine's ruleset — slots configs run the reel/symbol checks, instant configs run the full prize-pool coherence and per-mechanic feasibility proofs.
// Request
{
"config": { ... your game config object ... },
"game_type": "slots" // "slots" (default) or "instant"
}
// Success → 200
{ "valid": true }
// Failure → 422
{ "valid": false, "error": "Invalid symbol frequency..." }
Get the observed RTP for a stored game config, calculated from all logged rounds. For slots the divisor is lines bet; for instant games one play is one unit, so RTP is the mean multiplier.
{
"config_id": "my-config-id",
"total_rounds": 12847,
"rtp": 95.6582
}
Returns "rtp": null if no rounds have been logged yet for this config.
Fetch the current seed commitment before playing — the hash only proves commitment if you saw it before the round. Per API key and engine-agnostic: one commitment serves every game the key plays. Not billable.
{
"server_seed_hash": "3e1c7d...", // sha256(server_seed) — record this pre-play
"nonce": 41, // rounds played under this seed so far
"next_nonce": 42 // advisory — concurrent rounds may consume it first
}
Rotate the server seed for provably fair verification. Reveals the previous server seed so clients can verify past rounds, then generates a new seed and resets the nonce.
{
"previous_server_seed": "e8d4a1...", // verify: sha256(this) === old server_seed_hash
"new_server_seed_hash": "7b2f9c..." // committed hash for upcoming rounds
}
Replay a past round from its provably-fair inputs. The deterministic round_id derivation makes the replay byte-identical to the original — including the revealed card for instant games — so every engine param (lines, stake, gamble_rounds) must match the original play. Replays skip config re-validation on purpose: a round that was valid when played must stay reproducible even after validation rules tighten.
// Request
{
"server_seed": "e8d4a1...", // revealed after seed rotation
"client_seed": "b7c29e...", // from the original round
"nonce": 42,
"config": "my-config-id", // or inline config (+ game_type)
"lines": 20 // slots — must match; stake + gamble_rounds for instant
}
// Response
{
"server_seed_hash": "3e1c7d...", // compare with committed hash
"result": { ... identical to the original round ... },
"verified": true
}
Verification flow: (1) save the server_seed_hash from each round (and, for slots, the result_hash), (2) call POST /seed/rotate to reveal the server seed, (3) call POST /verify with the revealed seed to reproduce the round, (4) confirm sha256(server_seed) === server_seed_hash, (5) for slots, confirm the result_hash matches by recomputing it with the canonical encoding below (a plain json_encode will not match — PHP doesn't guarantee key order and drops trailing zeros on floats). Instant rounds have no result_hash; their equivalent is the byte-identical replay plus the card read-back audit of /verify-card.
// PHP — canonical encoding for result_hash verification (slots)
function canonicalize($value) {
if (!is_array($value)) return $value;
foreach ($value as $k => $v) $value[$k] = canonicalize($v);
if (!array_is_list($value)) ksort($value); // recursive key-sort, lists untouched
return $value;
}
$payload = $result;
unset($payload['result_hash']); // strip the hash field
$canonical = json_encode(
canonicalize($payload),
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION
);
$expected = hash_hmac('sha256', $canonical, $server_seed);
$verified = hash_equals($expected, $result['result_hash']);
Audit a revealed instant-game card without seeds — the dispute path. Post back the card block of any play result and the engine reads it forward, reporting the prize the card displays, resolved against the pool. A tampered, ambiguous or impossible card is refused with 422 {verified: false, error}. Instant games only — slots rounds are verified via /verify plus result_hash. Not billable.
// Request
{ "config": "my-dice-game", "card": { ... the card block from a play result ... } }
// Response
{ "verified": true, "prize_id": "m15", "kind": "win", "multiplier": 15 }
Works because instant mechanics are built as inverse pairs: presentation renders a prize into a card, and an independent reader derives the prize back from what the card displays.
Export the PAR sheet bundle for a stored config — the auditor-facing math document: canonical config plus its order-independent config_hash, theoretical RTP, and public verification fixtures (deterministic rounds with disclosed seeds an auditor can replay for byte-identity). Same artifact as the dashboard's Audit Center download. Not billable.
The presentation layer — composed design manifests for rendering a game, decoupled from its math config. List a config's presentations (id, name, slug, default flag) or fetch one composed manifest by id. Engine-agnostic; not billable.
Put a playable, provably-fair game on your own site without your API key ever reaching a browser — works for every game type, slots and instant alike. Your backend mints a short-lived embed session; the token it returns is all the page needs. Plays through the embed bill your key like any API round.
// 1. Your server (key stays here)
POST /api/v1/embed/sessions
{
"config_id": "my-game",
"lang": "en", // optional; presentation, client_seed, ttl_seconds, metadata too
}
// → 201
{
"session_token": "cy_emb_…",
"embed_url": "https://…/embed/cy_emb_…",
"expires_at": "2026-01-01T00:00:00Z",
"server_seed_hash": "3e1c7d...", // the key's current commitment — surface it pre-play
"game_type": "slots"
}
<!-- 2a. Your page: plain iframe -->
<iframe src="{embed_url}" style="width:100%;border:0"></iframe>
<!-- 2b. …or the SDK: auto-sizing + DOM events -->
<script src="https://<embed-host>/sdk/cymba.js"></script>
<cymba-game session-token="cy_emb_…"></cymba-game>
<script>
document.querySelector('cymba-game')
.addEventListener('cymba:round', (e) => console.log(e.detail.totalMultiplier));
</script>
The embed shows the seed commitment before play and lets the player set their own client seed. Lock framing to your domains with the key's Embed origins setting in the dashboard; revoke a session early with DELETE /api/v1/embed/sessions/{token}. Events: cymba:ready, cymba:round {totalMultiplier, nonce}, cymba:error {code, message}.
Every round result carries what an auditor needs to reconstruct and verify it.
"round_id": "9f2c41..." // deterministic — derived from (server_seed, client_seed, nonce)
"result_hash": "d4e5f6..." // HMAC-SHA256 tamper-evident hash — slots only
round_id identifies each round for logging, dispute resolution, and correlating rounds across systems. It is derived deterministically from the provably-fair inputs — a /verify replay echoes the exact same id — so it is not time-ordered; order rounds by the nonce instead.
result_hash (slots) is an HMAC-SHA256 hash computed with the server seed over the canonically-encoded result payload (excluding the hash itself). Store it alongside your own records to detect any post-hoc tampering. See the verification flow above for the exact canonical encoding (recursive key-sort + JSON_PRESERVE_ZERO_FRACTION) — a naive json_encode will not match. Instant rounds carry no result_hash: their tamper check is the card read-back (/verify-card) plus the byte-identical seed replay.
Cymba returns multipliers, not monetary amounts. The engine does not handle money — that is the client's responsibility.
To compute a payout from the engine's result:
// Per-line win
line_payout = win_multiplier × bet_per_line × denomination
// Total payout
total_payout = total_multiplier × bet_per_line × denomination