← All docs

Fox Reel Studio Integration Guide

Version 2.0 · For operator casinos integrating Fox Reel Studio games.

Questions at any point: [email protected].

1. Overview

Fox Reel Studio provides provably-fair casino games. You, the operator, keep the player relationship and the money. Two integration modes are supported; your account is configured for exactly one:

Mode Where player money lives What you implement
Seamless wallet (recommended) Your wallet. We call your callbacks on every bet and win. 4 HTTPS endpoints (Section 6)
Wallet transfer Our wallet during play. You move funds in and out. Nothing. You call our API (Section 7)

Your team also gets the operator portal (Section 10): reports, transactions, rounds, game and RTP settings within your contract, API credentials, wallet callbacks, IP allow-list, webhooks and team access.

1a. Live docs and catalog

These docs are served from the API host so your team can read them without repository access:

2. Environments

Environment API base URL Operator portal
Staging https://api-staging.foxreelstudio.com https://portal-staging.foxreelstudio.com
Production https://api.foxreelstudio.com https://portal.foxreelstudio.com

Staging and production are separate deployments with separate data and separate portal accounts. Everything you configure (credentials, IPs, callback URLs, webhooks) belongs to the deployment you configured it on: a staging key does not exist in production, and vice versa. API requests and payloads carry no environment field. You complete certification on staging (Section 9) before production access is set up.

3. What we exchange at onboarding

You give us:

  1. A technical contact email and the email of the person who should own your portal account. That person receives an invite and can then add the rest of your team.
  2. Your egress IP addresses or CIDR ranges for each deployment. You can also manage these yourself in the portal. Requests from other IPs are rejected with IP_NOT_WHITELISTED.
  3. Seamless mode only: your wallet callback base URL for each deployment (rules in Section 6.6). Also editable in the portal.

We give you:

  1. A portal invite (valid 7 days).
  2. api_key and hmac_secret for each deployment, or you create them yourself in the portal. The secret is shown once. Store it in a secrets manager immediately.
  3. Our egress IPs to allow on your side, if your firewall needs them (seamless callbacks and webhooks originate from them).
  4. This guide and the OpenAPI spec.

4. Authentication and request signing

Every request to /api/v1/* carries three headers:

X-Api-Key:   ck_<32 hex chars>
X-Timestamp: <unix seconds>
X-Signature: <hex HMAC>

The signature is:

X-Signature = hex( HMAC-SHA256( hmac_secret, X-Timestamp + "." + raw_request_body ) )

Rules:

Worked example

hmac_secret = "mysecret"
X-Timestamp = "1700000000"
body        = {"a":1}

string to sign = 1700000000.{"a":1}
X-Signature    = 239d0cbc5357b9e36b7b991b5f55e1bcf02cf51acd4e2d1918d3194d24d4c4c9

Check that your implementation reproduces that exact signature before going further:

printf '1700000000.{"a":1}' | openssl dgst -sha256 -hmac "mysecret"

We sign our seamless wallet callbacks to you the same way, with the same secret. While you have more than one active credential (during a key rotation), callbacks are signed with the oldest one until you revoke it. Verify X-Signature on every callback and reject mismatches with HTTP 401. Webhooks use a separate per-endpoint secret (Section 11).

5. Game launch

Your backend requests a launch URL, then opens it for the player in an iframe or as a redirect:

POST /api/v1/launch
{
  "player_id": "your-player-id",   // your stable ID for the player, never their PII
  "game_code": "test-game",
  "currency": "USD",               // optional, defaults to your account currency
  "lang": "en",                    // optional
  "brand_code": "lucky-red"        // optional, see Section 7d
}

200 → { "status": "OK", "url": "https://api.foxreelstudio.com/launch/<one-time-token>" }

The URL is single-session and expires in 15 minutes. Launching a game that is disabled for your account returns 403 GAME_DISABLED. A player's currency is fixed by their first launch: launching the same player_id with a different currency returns 422 CURRENCY_MISMATCH (use a separate player_id per currency).

5a. Games catalog

Two views of the same catalog:

Public catalog, no auth. Safe to call from a browser (CORS *, cached for 60 seconds).

GET /api/games                     every game Fox Reel Studio has live
GET /api/games?operator=<code>     only games enabled for operator <code>

Use the operator form for a public lobby page: games you have disabled are left out. An unknown or suspended operator code returns an empty list, never the full catalog.

Your lobby, signed. Call from your backend with the usual signing headers. Returns only the games enabled for you, with your bet limits and the RTP you run. Not cached.

GET /api/v1/games

200 → {
  "status": "OK",
  "provider": "Fox Reel Studio",
  "count": 19,
  "games": [
    {
      "code": "dice",
      "title": "Dice",
      "description": "Roll over or under your target …",
      "category": "instant",
      "thumbnail": "https://lockeandcrow-games.s3.eu-west-1.amazonaws.com/thumbnails/dice.jpg",
      "rtp_default": 99.0,
      "rtp": 97.0,
      "min_bet": 10,
      "max_bet": 1000000,
      "max_win": 100000000,
      "features": ["provably-fair", "auto-bet", "free-spins"],
      "launch_code": "dice"
    }
  ]
}

Pass launch_code as game_code when launching. rtp, min_bet, max_bet and max_win appear only in the signed view. A game we disable globally leaves both views within a minute and stops accepting bets in sessions that are already open.

6. Seamless wallet: endpoints you implement

Configure a base URL (for example https://casino.example.com/provider-wallet) in the portal or with us. We POST JSON to base_url + path. Default paths are /balance, /debit, /credit and /rollback; each can be changed. All four endpoints must respond within 5 seconds with HTTP 200 and a JSON body:

{ "status": "<STATUS>", "balance": <int minor units>, "currency": "USD" }

status values we understand: OK, INSUFFICIENT_FUNDS, DUPLICATE, PLAYER_BLOCKED, ERROR.

6.1 POST {base}/balance

Request: { "player_id": "...", "currency": "USD" }. Return the current balance.

6.2 POST {base}/debit (bet)

{ "player_id": "...", "currency": "USD", "amount": 100,
  "transaction_uuid": "bet-…", "game_code": "…", "round_id": "…" }

6.3 POST {base}/credit (win)

Same schema as debit. Increase the balance. Same idempotency rule. Credits can arrive late: we retry failed credits with exponential backoff for up to 30 attempts. Always accept them for known players.

6.4 POST {base}/rollback

{ "player_id": "...", "currency": "USD", "related_uuid": "bet-…" }

Undo the debit identified by related_uuid:

6.5 Failure semantics (what we do)

Event Our behaviour
Debit returns non-200 or times out Round is aborted. We send rollback and retry it with exponential backoff until you acknowledge.
Debit returns INSUFFICIENT_FUNDS Bet rejected, player informed. No rollback.
Credit fails for any reason We retry with exponential backoff (up to about 72 hours). Credits are never dropped.

6.6 Callback URL rules

These rules apply to wallet callback URLs and webhook URLs alike:

Use Test callback in the portal (Integration page) to check connectivity before you go live.

7. Wallet transfer mode: endpoints we provide

You move funds into a play wallet we hold; bets and wins settle internally.

POST /api/v1/wallet/deposit    { "player_id","amount","transaction_uuid","currency" }
POST /api/v1/wallet/withdraw   { "player_id","amount","transaction_uuid","currency" }
POST /api/v1/wallet/balance    { "player_id" }

Responses: { "status": "OK|DUPLICATE|INSUFFICIENT_FUNDS|ERROR", "balance": <int>, "currency": "..." }. transaction_uuid must be unique per operation. Replays return DUPLICATE with the current balance and are safe. On a seamless account these endpoints return 409 WRONG_MODE.

7a. Game limits and provably fair

7b. Free spins

Grant promotional spins, funded by you, at a fixed bet size. Idempotent on award_ref:

POST /api/v1/freespins/award
{ "player_id": "...", "game_code": "coin-flip", "award_ref": "promo-123",
  "spins": 10, "bet_amount": 100, "expires_at": "2026-08-01T00:00:00Z" }

200/201 → { "status": "OK" | "DUPLICATE", "award_id": 1 }

While an award is active, the player's plays in that game use spins first. There is no wallet debit; wins are credited normally through your wallet.

Progressive bonus free spins (optional module)

Ask us to enable the bonus free spins module on your account, then pass "mode": "bonus" with the extra fields below. Instead of paying each win straight to your wallet, winnings collect in a bonus pot on our side. The player must wager it in cash before it is released, in one credit call at the end.

POST /api/v1/freespins/award
{ "player_id": "...", "game_code": "rocket", "award_ref": "promo-124",
  "spins": 10, "bet_amount": 10, "expires_at": "2026-09-21T00:00:00Z",
  "mode": "bonus",
  "max_win": 10000,        // pot cap (minor units); wins above it are discarded
  "wager_multiplier": 1,   // pot × this must be wagered in cash; 0 = no wagering
  "wager_max_bet": 10 }    // max real-money bet while wagering (larger bets → BET_LIMIT)

403 → { "status": "FEATURE_DISABLED" }   module not enabled for your account

Lifecycle, as the player sees it in every game:

  1. Pending. The game shows a gift icon and "You are gifted N free bets". The player presses Claim.
  2. Active. Bets in that game are free bets at bet_amount. Wins collect in the pot, capped at max_win. Nothing is debited or credited.
  3. Wagering. After the last free bet, the player must wager pot × wager_multiplier in real-money bets on any Fox Reel Studio game. While wagering, any bet above wager_max_bet is rejected with BET_LIMIT; bets at or under it are debited from your wallet as usual and count in full. A pot of 0 ends the award with nothing to wager.
  4. Claimable, then paid. The player presses Get bonus money and we send one normal credit callback for the pot (round id bonus-<award_id>).

Awards expire at expires_at at any stage and the pot is forfeited. Players can discard an award. You can cancel one at any non-terminal stage (idempotent):

POST /api/v1/freespins/cancel
{ "award_ref": "promo-124" }

200 → { "status": "OK", "award_status": "cancelled" }     404 → NOT_FOUND

7c. Player limits (responsible gaming)

POST /api/v1/player/limits
{ "player_id": "...", "daily_loss_limit": 50000, "session_minutes": 60 }

daily_loss_limit is in minor units, net of wins, and resets at UTC midnight. Bets that would exceed it are rejected in-game with LIMIT_REACHED. session_minutes caps game session length (default 4 hours). Send null to clear either limit.

7d. Brands

If you run several casino brands, register their codes with us and pass brand_code in the launch call. Reporting can then be split per brand. Unknown codes are rejected with UNKNOWN_BRAND.

7e. Rate limits

API requests are limited per API key (default 20 req/s, burst 40). Excess requests receive HTTP 429 RATE_LIMITED. Back off and retry.

8. Error codes (HTTP layer)

HTTP body.status Meaning
401 MISSING_AUTH_HEADERS / INVALID_API_KEY / INVALID_SIGNATURE / TIMESTAMP_OUT_OF_RANGE / INVALID_TIMESTAMP Fix auth
403 IP_NOT_WHITELISTED Source IP not on your allow-list
403 OPERATOR_SUSPENDED Contact us
403 GAME_DISABLED Game is off for your account
422 CURRENCY_MISMATCH player_id already exists with another currency
409 WRONG_MODE Endpoint not available in your integration mode
400 BAD_REQUEST / BAD_BODY Malformed body
429 RATE_LIMITED Slow down

9. Certification (go-live checklist)

On staging, using the test game (game_code: "test-game"):

When every box passes, we set up production: your production portal account, then you add production credentials, IPs and callback URLs (https only) and we schedule go-live.

10. Operator portal

The portal is where your team runs the integration day to day. Staging and production each have their own portal and their own accounts; each portal manages only its own deployment:

Sign-in is email and password, with TOTP two-factor authentication (any authenticator app). We can require 2FA for your whole team; when we do, a member without it can only reach the 2FA setup page until they enrol. Accounts lock for a short period after repeated failed logins. Sessions can be reviewed and revoked from your profile. Forgotten passwords are reset by email.

What you can do yourself

Area What you can do
Overview and reports GGR, turnover and per-game performance by day, CSV export
Transactions Every bet, win and rollback, with filters and CSV export
Rounds Round history with the full provably-fair detail, for disputes
Games Enable or disable each game, set min and max bet
RTP Set each game's RTP within the range in your contract
API credentials Create and revoke API keys and HMAC secrets
Integration Wallet callback URL and paths, callback connectivity test, IP allow-list
Webhooks Up to 10 endpoints, event selection, secret rotation, test delivery, delivery log, redelivery
Team Invite teammates, set roles or custom permissions, resend invites, reset a member's 2FA, disable access
Audit log Who changed what, when, and from which IP

Things that stay with Fox Reel Studio: your contract terms (RTP range, max win, integration mode, currency), suspending an account, enabling optional modules such as bonus free spins, and adding new games to the catalog.

Roles and permissions

Everyone on the team can read the integration setup: API keys (not secrets), callbacks, IP allow-list, webhooks and the game list. Everything else is gated by permission:

Permission Allows
reports:read GGR, turnover and per-game performance, CSV export
transactions:read Every bet, win and rollback, CSV export
rounds:read Round history and provably-fair detail for disputes
games:write Enable or disable games and set bet limits
rtp:write Change each game's RTP within the contracted range
credentials:write Create and revoke API keys and HMAC secrets
integration:write Wallet callback URLs and the IP allow-list
webhooks:write Create endpoints, rotate secrets, redeliver events
team:write Invite teammates and change their access
audit:read Who changed what, when, from where

Roles are presets of those permissions:

Role reports transactions rounds games rtp credentials integration webhooks team audit
owner yes yes yes yes yes yes yes yes yes yes
admin yes yes yes yes yes yes yes yes yes yes
finance yes yes yes yes
developer yes yes yes yes yes yes
viewer yes yes yes
custom any combination you choose

Invites are valid for 7 days and can be resent.

11. Webhooks

Webhooks tell your systems about events as they happen, so you do not have to poll. Create endpoints in the portal under Webhooks (needs webhooks:write). Each endpoint has its own signing secret (shown once, starts with whsec_), and subscribes to a list of events or to * for all of them.

11.1 Events

Event When data
transaction.confirmed A bet, win or rollback is settled Transaction fields (below)
transaction.failed A wallet operation failed Transaction fields
transaction.rolled_back A transaction was rolled back Transaction fields
game.settings_updated A game's settings changed for your account (portal or Fox Reel Studio) game_id, game_code, enabled, min_bet, max_bet, max_win, config (includes rtp_bp when set)
credential.created An API credential was created id, api_key
credential.revoked An API credential was revoked id, api_key
freespins.awarded Free spins were awarded Reserved; not emitted yet
webhook.test You pressed Send test in the portal message

Transaction fields: transaction_uuid, related_uuid, kind (bet, win, rollback, deposit or withdraw), amount (minor units), currency, player_id (your ID), game_code, round_id, created_at.

11.2 Request format

We send POST with a JSON body:

POST https://casino.example.com/hooks/foxreel
Content-Type: application/json
User-Agent: FoxReel-Webhooks/1
X-LC-Event: transaction.confirmed
X-LC-Delivery: 81723
X-LC-Timestamp: 1760000000
X-LC-Signature: 5f2c…

{
  "event": "transaction.confirmed",
  "created_at": "2026-10-13T09:21:07.114Z",
  "data": {
    "transaction_uuid": "win-…",
    "related_uuid": null,
    "kind": "win",
    "amount": 250,
    "currency": "USD",
    "player_id": "p-1001",
    "game_code": "dice",
    "round_id": 4411,
    "created_at": "2026-10-13T09:21:07.090Z"
  }
}

Respond with any 2xx within 10 seconds. Do the work asynchronously if it takes longer.

11.3 Verifying the signature

X-LC-Signature = hex( HMAC-SHA256( webhook_secret, X-LC-Timestamp + "." + raw_body ) )

The same scheme as API signing, with the endpoint's own whsec_… secret. Always:

  1. Compute the HMAC over the raw request body, before any JSON parsing.
  2. Compare with a constant-time function.
  3. Reject timestamps more than 5 minutes from your clock, to block replays.

Node.js (Express):

const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.LC_WEBHOOK_SECRET; // whsec_…

app.post('/hooks/foxreel', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-LC-Timestamp') || '';
  const sig = req.get('X-LC-Signature') || '';
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);

  const expected = crypto.createHmac('sha256', SECRET)
    .update(ts + '.').update(req.body) // req.body is a Buffer of the raw bytes
    .digest('hex');
  const a = Buffer.from(sig, 'hex'), b = Buffer.from(expected, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  const deliveryId = req.get('X-LC-Delivery');
  // if (alreadyProcessed(deliveryId)) return res.sendStatus(200);
  const event = JSON.parse(req.body);
  // queue event for processing, record deliveryId
  res.sendStatus(200);
});

Python (Flask):

import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["LC_WEBHOOK_SECRET"].encode()  # whsec_…

@app.post("/hooks/foxreel")
def foxreel_hook():
    ts = request.headers.get("X-LC-Timestamp", "")
    sig = request.headers.get("X-LC-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(401)
    raw = request.get_data()  # raw bytes, before JSON parsing
    expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(401)
    delivery_id = request.headers["X-LC-Delivery"]
    # if already_processed(delivery_id): return "", 200
    event = request.get_json()
    # queue event for processing, record delivery_id
    return "", 200

11.4 Retries and idempotency

Deliveries can arrive more than once (for example after a timeout on a request you actually processed, or a manual redelivery) and are not guaranteed to arrive in order. X-LC-Delivery is the same on every attempt of a delivery, so store it and ignore repeats. For transaction events, transaction_uuid is also a safe business key. Use created_at to order events, not arrival time.

The URL rules in Section 6.6 apply: production endpoints must be https:// on a public address.

11.5 Rotating a secret

Rotate secret in the portal issues a new secret immediately and shows it once. Deliveries after that are signed with the new secret, so update your receiver straight away. Retries of older deliveries are also signed with the new secret at send time.