Authentication
LootBox Solutions uses two auth schemes: HMAC for server-to-server calls (both directions) and bearer session tokens for the in-iframe player API.
HMAC envelope (S2S, both directions)
The same envelope authenticates your calls to us and our calls to you, so one client implementation serves both.
Headers
| Header | Direction | Value |
|---|---|---|
X-Key-Id | inbound only (you → us) | Your public key id (igk_…) |
X-Timestamp | both | Unix seconds at request time |
X-Signature | both | hex(hmac_sha256(secret, canonical)) |
Outbound calls (us → you) omit X-Key-Id — there is a single
outbound secret, not a
keyring. Money operations additionally carry an Idempotency-Key header.
Canonical string
canonical = "{timestamp}\n{METHOD}\n{path}\n{bodyHash}"timestamp— the same value sent inX-Timestamp.METHOD— the HTTP method, upper-cased.path— the request path only (no scheme/host/query), e.g./api/s2s/launches.bodyHash— lowercase hexsha256of the exact request body bytes (empty-string hash for an empty body).
Any change to the body invalidates the signature.
Clock skew
The timestamp must be within ±300 seconds of server time, or the request is
rejected with TIMESTAMP_SKEW. Keep your servers NTP-synced.
Reference implementation
import { createHmac, createHash } from 'node:crypto';
function sign({ secret, method, path, body = '' }) { const timestamp = Math.floor(Date.now() / 1000).toString(); const bodyHash = createHash('sha256').update(body).digest('hex'); const canonical = `${timestamp}\n${method.toUpperCase()}\n${path}\n${bodyHash}`; const signature = createHmac('sha256', secret).update(canonical).digest('hex'); return { timestamp, signature };}$bodyHash = hash('sha256', $body);$canonical = "{$timestamp}\n".strtoupper($method)."\n{$path}\n{$bodyHash}";$signature = hash_hmac('sha256', $canonical, $secret);Auth error codes
| code | meaning |
|---|---|
MISSING_HEADERS | one of X-Key-Id / X-Timestamp / X-Signature is absent |
INVALID_SIGNATURE | unknown key, revoked key, or signature mismatch (deliberately coarse) |
TIMESTAMP_SKEW | timestamp outside ±300s |
Bearer tokens (player API)
The game app authenticates /api/play/* calls with a session token
obtained from session/init. Send it as
Authorization: Bearer <sessionToken>. Tokens are session-scoped, expire on
idle, and are revoked on logout. /api/play/* is CORS-open so the game app can call
it cross-origin from your embedding page.
For the iframe model, the game app manages bearer tokens for you. If you build your own frontend, you manage them — see below.
Authenticating a custom frontend (headless)
Building your own UI instead of embedding the iframe? Authentication still starts server-side — there is no way to authenticate a player from the browser directly, because that would require your API key/secret in client code. The flow is:
your backend ──HMAC──▶ POST /api/s2s/launches → launchToken (signed, trusted)your frontend ───────▶ POST /api/play/session/init → sessionToken (token in body, CORS-open)your frontend ─bearer▶ /api/play/* → play (rounds/open, inventory, …)- Your backend mints a launch token with your API key
(
/launches). This is the authentication step — you assert theplayerExternalIdis yours. - Your frontend exchanges that launch token at
/api/play/session/init— no API key needed, just thelaunchTokenin the body./api/play/*is CORS-open, so any origin can call it. - Your frontend drives the player API with the returned
sessionTokenas a bearer credential.
So you are not forced to use S2S for everything — only to authenticate (mint the launch). After that, a custom frontend can run entirely on the player API. Use the S2S equivalents when you’d rather have your backend drive purchases/cashouts/cancels (server-authoritative control, no live session required) — see Integration models.