Documentation

Quick reference for the Cymba API. All endpoints live under /api/v1/.

Authentication

All API requests require a Bearer token. Generate keys from the dashboard.

Authorization: Bearer cy_live_your_api_key_here

Rate limits and spin quotas are enforced per key. The response includes X-RateLimit-Limit and X-RateLimit-Remaining headers.

POST /api/v1/spin

Resolve a spin. Pass a stored config ID or an inline config object.

Request body

{
  "config": "my-config-id",  // string (stored ID) or object (inline config)
  "lines": 20,               // integer, min: 1 — number of active paylines
  "client_seed": "optional"   // string, auto-generated if omitted
}

Response

{
  "result": {
    "round_id": "01JQ5K8X...",         // unique time-ordered identifier
    "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
  },
  "provably_fair": {
    "server_seed_hash": "a3f1c8...",
    "client_seed": "b7c29e...",
    "nonce": 42
  }
}

Win line variants by type

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]]
}

Cascade entries

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
}

Hold & respin entries

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)
}

Free spin entries

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
}

Payline direction

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.

Symbol features

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 }
  }
}

Multiplier wilds

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.

Dynamic reels (megaways)

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)

Dynamic reel count

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.

POST /api/v1/validate

Validate a game config JSON object without resolving a spin.

// Request
{ "config": { ... your game config object ... } }

// Success → 200
{ "valid": true }

// Failure → 422
{ "valid": false, "error": "Invalid symbol frequency..." }

GET /api/v1/rtp/{config_id}

Get the observed RTP for a stored game config, calculated from all logged spins.

{
  "config_id": "my-config-id",
  "total_spins": 12847,
  "rtp": 95.6582
}

Returns "rtp": null if no spins have been logged yet for this config.

POST /api/v1/seed/rotate

Rotate the server seed for provably fair verification. Reveals the previous server seed so clients can verify past spins, 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 spins
}

POST /api/v1/verify

Verify a previous spin by reproducing it from its provably fair seeds. Returns the same result the spin would have produced, plus the server seed hash for comparison.

// Request
{
  "server_seed": "e8d4a1...",  // revealed after seed rotation
  "client_seed": "b7c29e...",  // from the original spin
  "nonce": 42,
  "config": "my-config-id",
  "lines": 20
}

// Response
{
  "server_seed_hash": "3e1c7d...",  // compare with committed hash
  "result": { ... same as /spin result ... },
  "verified": true
}

Verification flow: (1) save the server_seed_hash and result_hash from each spin, (2) call POST /seed/rotate to reveal the server seed, (3) call POST /verify with the revealed seed to reproduce the spin, (4) confirm sha256(server_seed) === server_seed_hash, (5) 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).

// PHP — canonical encoding for result_hash verification
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 fields

Every spin response includes fields for tamper-evident audit trails.

"round_id": "01JQ5K8X..."      // unique time-ordered identifier for each game round
"result_hash": "d4e5f6..."     // HMAC-SHA256 tamper-evident hash

round_id is a unique, time-ordered identifier assigned to each game round — useful for logging, dispute resolution, and correlating spins across systems.

result_hash 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.

Multiplier semantics

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