Skip to content

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

HeaderDirectionValue
X-Key-Idinbound only (you → us)Your public key id (igk_…)
X-TimestampbothUnix seconds at request time
X-Signaturebothhex(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 in X-Timestamp.
  • METHOD — the HTTP method, upper-cased.
  • path — the request path only (no scheme/host/query), e.g. /api/s2s/launches.
  • bodyHash — lowercase hex sha256 of 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

codemeaning
MISSING_HEADERSone of X-Key-Id / X-Timestamp / X-Signature is absent
INVALID_SIGNATUREunknown key, revoked key, or signature mismatch (deliberately coarse)
TIMESTAMP_SKEWtimestamp 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, …)
  1. Your backend mints a launch token with your API key (/launches). This is the authentication step — you assert the playerExternalId is yours.
  2. Your frontend exchanges that launch token at /api/play/session/init — no API key needed, just the launchToken in the body. /api/play/* is CORS-open, so any origin can call it.
  3. Your frontend drives the player API with the returned sessionToken as 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.