Boss HubBoss HubDocs
ChangelogStatus
Getting started/Authentication & signing

Authentication & signing

Operator API requests and wallet callbacks carry the same HMAC signature. It covers the timestamp, the method, the path, the query and the raw body, so a signed request cannot be moved to another endpoint or replayed after 30 seconds. With one key, both directions use its secret; with several, callbacks are signed with the signing key (below).

Credentials

ItemDescription
Operator IDYour fixed identifier, e.g. op_meridyen.
API keyPrefixed by environment: Test (staging) pk_test_, Live (production) pk_live_. The first Test key is created automatically when your account opens; a Live key can be created after production approval. You can open several keys. A key bound to a site works only for that site and must be enabled for your account by Boss Hub.
Signing secretStarts with sk_. Shown only once: on first view for the automatic Test key, at creation for the others. If you lose it, rotate it. Keep it out of your repository and logs.
Signing keyCallbacks are signed with the secret of the signing key of that environment: the newest active key when you first saved the wallet settings. If you rotate that key, signing moves to the new key. X-Api-Key on each callback names the key. The signing key cannot be revoked; rotate it instead.

Headers

HeaderDescription
X-Operator-IdYour operator ID. Required.
X-TimestampUnix seconds. Required. Rejected outside ±30 s.
X-SignatureThe signature, lowercase hex. Required.
X-Request-IdRequired on API write requests (POST, PUT, DELETE), at most 128 characters. Always present on callbacks; stays the same across bet, win and rollback retries.
X-Api-KeyOptional. Selects the right secret when you have several keys; without it, the active keys of that environment are tried.

Calculating the signature

hex(HMAC_SHA256(secret, ts + "." + METHOD + "." + path?query + "." + body))
  • ts: the same value as the X-Timestamp header.
  • METHOD: the HTTP method in uppercase.
  • path?query: the path and query as they appear on the request line, /v1 included, in encoded form. On a callback, the full path of your own address, base path included, e.g. /bossgaming/bet.
  • body: the raw body. For requests without a body it is an empty string, so the signed text ends with a dot.
Raw body: Verify the signature over the bytes you received. If you parse the JSON and write it again, the amount 148.60 becomes 148.6 and the signature no longer matches. Read amounts without converting them to float as well.

Code samples on the other pages use the signing and verification helpers shown here.

import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

// Sign an outgoing request, or rebuild the signature of an incoming callback.
export function sign(secret, ts, method, pathWithQuery, rawBody = '') {
  return createHmac('sha256', secret)
    .update([ts, method.toUpperCase(), pathWithQuery, rawBody].join('.'))
    .digest('hex');
}

export const sha256 = (buf) => createHash('sha256').update(buf).digest('hex');

// Express: keep the raw bytes with
// app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString('utf8'); } }))
export function verifySignature(req) {
  const ts = req.get('X-Timestamp') ?? '';
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 30) return false;
  const expected = sign(process.env.BOSS_SECRET, ts, req.method, req.originalUrl, req.rawBody ?? '');
  const given = (req.get('X-Signature') ?? '').toLowerCase();
  return given.length === expected.length
    && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
POSTSample signature: callback
secret: sk_test_example

1762344000.POST./bossgaming/bet.{"transaction_id":"tx_01HF3K9QZ2YB","amount":2.00}

X-Signature: b0e89e3b7f1f07b0a54e36f262a5c5adb22dc556a8cd9cb41aac31e93adefb85
GETSample signature: API request without a body
secret: sk_test_example

1762344000.GET./v1/games?site_id=site_mrd01&lang=tr.

X-Signature: 8c020cb9d6b2c7688bde1a53987cf957b6ad9632d0f5f113866d3f9a45ec6906

Callback retries

When Boss Hub retries a callback, the body is byte-for-byte the same and X-Request-Id does not change; only X-Timestamp and X-Signature are renewed. Recognise a retry by its transaction_id, not by its signature. Answer a request you cannot verify with HTTP 200 and {"status":"ERROR","code":"INVALID_SIGNATURE"}; a bare 401 counts as uncertain and fails scenario SIG-04 of the test suite. Ignore body fields you do not recognise.

API idempotency

Every write request to the operator API must carry X-Request-Id; if it is missing or longer than 128 characters, you get 422 VALIDATION_ERROR (details.field). IDs are scoped to your operator and environment and kept for 24 hours. This lets you retry safely after a network error without creating a second campaign.

  • Same ID with the same method, path, query and body: the first response comes back as is, with the header X-Idempotent-Replay: true.
  • Same ID with a different request: 409 REQUEST_ID_CONFLICT.
  • While the first request is still running: 409 REQUEST_IN_PROGRESS; retry the same request shortly.
  • 4xx responses are stored too. For example, a request that got GAME_DISABLED or SCOPE_MISSING and is retried with the same ID gets the same error. After fixing the cause, use a new X-Request-Id. Exception: INVALID_JSON and field validation (VALIDATION_ERROR with details.fields) happen before the ID is claimed and are not stored.
  • 5xx responses are not stored; you can retry with the same ID. Signature, IP and rate limit errors (401, 403 IP_NOT_ALLOWED, 429) do not use up the ID.

Scopes

ScopeEndpointsExtra condition
games.readGET /v1/games, GET /v1/games/{game_id}, GET /v1/limits, GET /v1/limits/rules, GET /v1/availability, GET /v1/jackpots, GET /v1/lobby/categories, GET /v1/lobby/recommendationsThe jackpot endpoint needs the jackpot module enabled
game.launchPOST /v1/game/launch
rounds.readGET /v1/rounds, GET /v1/rounds/{round_id}, GET /v1/transactions, GET /v1/jackpots/{pool_id}/wins, GET /v1/live/streamWin history needs the jackpot module, the live stream needs the live module enabled
freerounds.writePOST /v1/freerounds, GET and DELETE /v1/freerounds/{id}, POST and GET /v1/freerounds/{id}/players, DELETE /v1/freerounds/{id}/players/{player_id}The promo module must be enabled
reports.readGET /v1/reports/ggrThe api module must be enabled
limits.writePUT /v1/limits/rules
availability.writePUT /v1/availability/rules
lobby.writePUT /v1/lobby/categories
reconciliation.readGET /v1/reconciliation/stuck-rounds, GET /v1/reconciliation/daily
reconciliation.writePOST /v1/reconciliation/stuck-rounds/{round_id}/resolve, POST /v1/reconciliation/daily
wallet_health.readGET /v1/wallet-health
marketing.readGET /v1/marketing/assets, GET /v1/releases
player_events.writePOST /v1/player-events

Scopes belong to your operator, not to a key, and apply to all your keys. A new account has game.launch, games.read, rounds.read, reports.read and wallet.seamless (no endpoint needs it); Boss Hub enables the others (e.g. freerounds.write, reconciliation.write), so ask your account manager. A missing scope returns 403 SCOPE_MISSING (details.scope); a disabled module returns 403 MODULE_DISABLED. All codes: Error codes.

Rate limit

The limit applies per operator and environment. Unless a value is set for you, it is 3,000 requests per minute. Going over it returns 429 RATE_LIMITED; the Retry-After header and retry_after in the body tell you how many seconds to wait. On Live (production), requests must also pass the IP allowlist.

Error body

API errors return {code, message, request_id}, plus details and retry_after depending on the code. message is in English, or in Turkish if you send Accept-Language: tr; build your logic on code. request_id is the server’s ID and also comes in the X-Boss-Request-Id response header; it is not your X-Request-Id. Quote it in support requests.

← PreviousQuickstartNext →Environments & IP allowlist
Authentication & signing · Documentation · Boss Hub