1. Check your client configuration

Call this from your backend during setup and cache the response:

GET https://arcade.puntlab.solutions/api/client-config
Authorization: Bearer <api-key>

{
  "data": {
    "clientName": "Example production",
    "allowedOrigins": [
      "https://www.example.com"
    ],
    "enabledGames": [
      "elite-drop",
      "elite-wheel",
      "elite-deal",
      "elite-mines"
    ],
    "callback": {
      "mode": "registered",
      "configured": true,
      "url": "https://api.example.com/elitearcade/callback",
      "secretFingerprint": "sha256:1a2b3c4d",
      "perLaunchOverridesAllowed": false,
      "launchFields": "omit"
    },
    "launchContract": "config-v1",
    "expiresInSeconds": {
      "minimum": 60,
      "maximum": 86400,
      "default": 900
    }
  }
}

This returns your registered origins, enabled games, expiry limits, callback mode, and a short SHA-256 secret fingerprint. It never returns the callback secret.

If callback.launchFields is omit, do not send callback fields at launch. If it is required, send callbackUrl and callbackSecret together. Supplied secrets must contain 16–256 characters. Compare callback.secretFingerprint with the first eight hex characters of your secret's SHA-256 hash before testing signatures.

2. Create a game session

Call this endpoint from your backend, never from the browser:

POST https://arcade.puntlab.solutions/api/launch-sessions
Authorization: Bearer <api-key>
Content-Type: application/json

{
  "externalSessionId": "123456",
  "gameId": "elite-drop",
  "campaignId": "123456",
  "allowedOrigins": [
    "https://www.example.com"
  ],
  "expiresInSeconds": 900,
  "config": {
    "rewardType": "dollar",
    "rewardValues": [
      "?",
      50,
      60,
      40,
      20,
      40,
      60,
      "?"
    ],
    "end-game-challenge": false,
    "expectedReturn": 45
  },
  "promo": {
    "subtitleHTML": "Drop ball to reveal your <strong>deposit bonus</strong>.",
    "claimText": "CLAIM BONUS BET",
    "rewardDescriptionTemplate": "You won a {value} bonus bet."
  }
}
externalSessionId
Your unique ID for this eligible play. Reusing it safely returns the original session.
campaignId
Your campaign or promotion reference.
gameId
elite-drop, elite-wheel, elite-deal, or elite-mines.
allowedOrigins
HTTPS origins allowed to embed this session. Every value must appear in the client configuration response.
expiresInSeconds
Session lifetime from 60 to 86400 seconds.
config.expectedReturn
The required target average whole-dollar reward used to build the game.
config.rewardValues
Optional Drop bucket or Wheel one-use boost values. Drop supports ? mystery buckets. Wheel accepts 2–12 positive whole-dollar values. Every Wheel slice is equally likely, and each landed boost becomes an equal-size Lose Boost slice. When omitted, EliteArcade calculates them.
config.bankLevel
Optional for Elite Mines. Sets the guaranteed bank after the first safe pick.
config.progressionValues
Optional for Elite Mines. Positive whole-dollar increments for later safe picks, for example [2, 2, 3, 5, 6, 11]. The ladder must produce expectedReturn using the configured mine count's natural odds.
config.mineCount
Optional for Elite Mines. Sets 1–14 mines on the 4×4 board. The default is 2. After the guaranteed opening pick, risk is mines divided by remaining tiles.
config.caseValues
Optional for Elite Deal. Supply exactly 12 whole-dollar case values. Manual values are accepted as supplied and do not need to average to expectedReturn. Values form an unordered prize pool and are drawn naturally without replacement.
config.rewardType
Optional. The supported value and default is dollar.
config.end-game-challenge
Optional. The default is false.
promo
Optional display copy. subtitleHTML allows plain text and <strong> only.
callbackUrl
Send only when callback.launchFields is required. Production URLs must use HTTPS and resolve publicly.
callbackSecret
Send with callbackUrl when required. It must be 16–256 characters and remain backend-only.

Response

{
  "data": {
    "sessionId": "54ef91fc-b3d2-4eb6-93a7-b2572cfab652",
    "expiresAt": "2026-07-30T12:15:00.000Z",
    "callbackSecretFingerprint": "sha256:1a2b3c4d",
    "iframeUrl": "https://arcade.puntlab.solutions/api/game/frame/54ef91fc-b3d2-4eb6-93a7-b2572cfab652#launch=<signed-token>"
  }
}

Compare callbackSecretFingerprint on your backend. Pass only sessionId, expiresAt, and iframeUrl to your frontend. Do not extract, log, or rewrite the URL fragment.

Errors

INVALID_REQUEST
Fix each field in the structured issues array.
LAUNCH_CONFIGURATION_INVALID
Fix every game, origin, or callback issue listed; compare it with /api/client-config.
EMBED_ORIGIN_NOT_REGISTERED
Use only origins listed by /api/client-config.
CLIENT_ORIGINS_NOT_CONFIGURED
Ask EliteArcade to register at least one iframe origin for the client.
WILDCARD_ORIGIN_FORBIDDEN
Replace * with explicit HTTPS origins.
INSECURE_EMBED_ORIGIN
Use HTTPS for every production iframe origin.
CALLBACK_OVERRIDE_FORBIDDEN
A callback is already registered. Omit callbackUrl and callbackSecret.
CALLBACK_REQUIRED
No callback is registered. Supply both callback fields together.
CALLBACK_URL_INVALID
Use a public HTTPS callback URL without embedded credentials.
GAME_DISABLED
Use one of the enabled games returned by /api/client-config.
API_KEY_INVALID
Check the backend bearer API key.
EXTERNAL_SESSION_CONFLICT
The ID was already used with different immutable settings. Retry the original payload or use a new eligible-play ID.
RATE_LIMITED / 5xx
Retry with backoff using the same payload and externalSessionId.

3. Display the iframe

<iframe
  id="elitearcade-game"
  title="EliteArcade game"
  src="https://arcade.puntlab.solutions/api/game/frame/54ef91fc-b3d2-4eb6-93a7-b2572cfab652#launch=<signed-token>"
  style="border:0;width:100%;max-width:430px;min-height:680px"
  allow="autoplay"
></iframe>

The games are mobile-first at 390–430px and scale to desktop. EliteArcade restricts the iframe to the supplied allowedOrigins.

Iframe messages control the host UI. Signed callbacks update your backend. Only the signed completed callback is settlement evidence.

Event states

Launch response
Your backend created the session. Embed the returned iframeUrl; this is not an iframe event or callback.
elitebet:ready
Iframe message only. The game loaded and is ready to show.
elitebet:commenced
Iframe message and signed callback. The first game action was accepted; no reward exists yet.
elitebet:completed
Iframe message and signed callback. The outcome settled; grant only from the verified callback.
elitebet:done
Iframe message only. The player pressed Done; close the host modal or iframe.
elitebet:timeout
Iframe message and signed callback. The session expired before play began; request a new session to play.
elitebet:error
Iframe message only. The game could not load; show an error and close or replace the iframe.
elitebet:resize
Iframe message only. Optionally set the iframe height from detail.height.

Normal play follows ready → commenced → completed → done. An unplayed session follows ready → timeout. Once commenced, that session cannot time out. These are logical UI states; signed callbacks are delivered independently and may arrive later or be retried.

Listen for iframe messages

window.addEventListener("message", (event) => {
  const frame = document.querySelector("#elitearcade-game");
  if (event.origin !== "https://arcade.puntlab.solutions") return;
  if (event.source !== frame.contentWindow) return;

  const { type, detail } = event.data || {};
  switch (type) {
    case "elitebet:ready": showGame(); break;
    case "elitebet:commenced": markGameStarted(); break;
    case "elitebet:completed": showSettledState(); break;
    case "elitebet:done": closeGameModal(); break;
    case "elitebet:timeout": showExpiredMessage(); break;
    case "elitebet:error": showGameError(detail?.message); break;
    case "elitebet:resize":
      if (Number.isFinite(detail?.height)) {
        frame.style.height = Math.ceil(detail.height) + "px";
      }
      break;
  }
});

Every lifecycle message has { type, detail } and an eventStatus inside detail. The resize message instead supplies detail.gameId and detail.height.

4. Verify callbacks

Your backend receives exactly three signed callback statuses:

commenced
The player started. It has no settlement value and is sent once.
completed
The canonical result. Verify it before granting the reward.
timeout
The session expired unplayed. It has no settlement value and is mutually exclusive with commenced.

Process callbacks by eventStatus and eventId; do not depend on delivery order.

Commenced callback

{
  "event": "elitebet.game_event",
  "eventStatus": "commenced",
  "eventId": "54ef91fc-b3d2-4eb6-93a7-b2572cfab652:commenced",
  "sessionId": "54ef91fc-b3d2-4eb6-93a7-b2572cfab652",
  "externalSessionId": "123456",
  "campaignId": "123456",
  "gameId": "elite-drop",
  "resultType": null,
  "outcomeId": null,
  "reward": null,
  "createdAt": "2026-07-30T12:04:31.120Z"
}

Timeout callback

{
  "event": "elitebet.game_event",
  "eventStatus": "timeout",
  "eventId": "54ef91fc-b3d2-4eb6-93a7-b2572cfab652:timeout",
  "sessionId": "54ef91fc-b3d2-4eb6-93a7-b2572cfab652",
  "externalSessionId": "123456",
  "campaignId": "123456",
  "gameId": "elite-drop",
  "resultType": null,
  "outcomeId": null,
  "reward": null,
  "createdAt": "2026-07-30T12:15:00.000Z"
}

Completed callback

{
  "event": "elitebet.game_result",
  "eventStatus": "completed",
  "eventId": "0e8cd393-391c-42e3-92d7-a92db0096e70",
  "sessionId": "54ef91fc-b3d2-4eb6-93a7-b2572cfab652",
  "externalSessionId": "123456",
  "campaignId": "123456",
  "gameId": "elite-drop",
  "resultType": "drop-claim",
  "outcomeId": "55d1b5f4-4177-4202-a23a-ef46ca65d7e4",
  "reward": {
    "kind": "bonus-bet",
    "value": 45
  },
  "createdAt": "2026-07-30T12:05:16.927Z"
}

outcomeId is nullable. It is normally null for deal-accept, deal-final, wheel-cashout, mines-cashout, and a declined endgame-final. Use the always-present eventId for idempotency and reconciliation.

Signature headers

elitebet-event-id: 0e8cd393-391c-42e3-92d7-a92db0096e70
elitebet-timestamp: 1785393916
elitebet-signature: sha256=<hex-hmac>

The signature is calculated over the exact raw request body:

HMAC_SHA256(webhookSecret, timestamp + "." + rawRequestBody)
  1. Read the body as raw bytes before parsing JSON.
  2. Reject timestamps older than five minutes.
  3. Compare the signature in constant time.
  4. Check the header and payload event IDs match.
  5. Grant each eventId only once.
  6. Return the acknowledgement below after durable processing.
{
  "status": "Success",
  "statuscode": 200
}