> For the complete documentation index, see [llms.txt](https://docs.hub88.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hub88.io/developer-docs/operator-sdks/tournaments-sdk.md).

# Tournaments SDK

The **Tournaments SDK** is a TypeScript library that puts Hub88 Tournaments on your casino's website. It connects the player to Hub88, lets them join a tournament with a single call, receives soft-realtime leaderboard updates, and renders them on your page as the tournament runs.

**Tournaments** turn ordinary play into a **competition**, with a **join button**, a **live leaderboard** and a r**eal prize pool**. That's a proven way to increase session length and bring players back. Hub88 configures, runs and settles every tournament, you don't need to build any of that. Your only job is to add the SDK to your page.

{% hint style="info" %}
**This package is the browser half only.**&#x20;

Tournaments are configured, run and settled by Hub88. Your page just displays what Hub88 sends it.&#x20;
{% endhint %}

### Get started in 5 minutes

{% stepper %}
{% step %}
Confirm your tournaments contract is active (see [Before you start](#before-you-start)).
{% endstep %}

{% step %}
Access the package: <https://cdn.hub88.io/tournaments-sdk/hub88-tournaments-sdk-1.0.0.js>
{% endstep %}

{% step %}
Call [`/tournaments/authenticate`](#step-1-authenticate-the-player-backend) from your backend and pass the result to your page.
{% endstep %}

{% step %}
Initialise the SDK, attach it to a container, and listen for the `leaderboard` event.
{% endstep %}

{% step %}
Add a join button that calls `sdk.optIn(tournamentUuid)` . See [Step 5](#step-5-let-the-player-join-a-tournament).
{% endstep %}
{% endstepper %}

### Key terms

* **Tournament** is a time-boxed competition Hub88 runs across one or more games, with a prize pool split across ranks.
* **Leaderboard** is the ranked list of players in a tournament. Hub88 pushes you a fresh one as scores change.
* **Scoring template** is how Hub88 calculated the score: `biggest_single_win` is a money amount, `win_multiplier`, `sum_multiplier` and `points_per_round` are a number. It tells you how to read the numbers.
* **Opt-in** logic, where a player joining a tournament, via `sdk.optIn()`. Hub88 decides whether the join succeeds.
* **Broadcaster** is the Hub88 service that streams leaderboard updates to your page over a WebSocket. Its URL comes from your contract, not from you.

## How it works?

There are two halves, and they never overlap:

1. **Your backend** signs a call to [`POST /operator/generic/v2/tournaments/authenticate`](#step-1-authenticate-the-player-backend) and receives an `sdk_init_token`.
2. **Your page** hands that token to `@hub88/tournaments-sdk`, which connects to Hub88 and emits `leaderboard` events as positions change.

```
Your backend  ──authenticate + signature──▶  Hub88
Your backend  ◀──────sdk_init_token──────────  Hub88
Your page     ──sdk_init_token──▶  SDK.init()
Your page     ──attach()────────▶  Hub88 (broadcaster)
Your page     ◀────leaderboard events────────  Hub88 (broadcaster)
```

Your operator private key never leaves your backend. The browser only ever holds the `sdk_init_token`.

### Before you start

The following three requirements must already be set up on the Hub88 side before you start building the integration with the SDK. If for some reason you don't have them, please contact your Account Manager.

| Requirement                                    | Why it's needed                               | What you'll see if it's missing                                                     |
| ---------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------- |
| Your RSA public key is registered with Hub88   | Hub88 verifies your request signature with it | `401 Missing relevant operator settings.`                                           |
| You hold a tournaments contract                | It carries your tournaments configuration     | `404 Tournaments contract was not found`                                            |
| That contract carries a provider configuration | It tells the SDK where to connect             | `500` from the endpoint, or `attach()` failing because `broadcaster_url` is missing |

You'll also need your own user-info API reachable from Hub88. Every authentication call confirms the player against your system synchronously, so keep that endpoint fast and available.

{% hint style="info" %}
Request signing follows the same logic as for the general Operator API. [See more](/developer-docs/operator-api-reference/getting-started.md#request-signing-logic-for-operator-api).
{% endhint %}

## Integration steps

{% stepper %}
{% step %}

### Step 1: Authenticate the player (backend)

Sign and send a request to the authenticate endpoint from your backend — never from the browser.

```json
POST https://<your-hub88-api-host>/operator/generic/v2/tournaments/authenticate
content-type: application/json
x-hub88-signature: <base64 RSA-SHA256 signature of the raw request body>

{
  "operator_id": 42,
  "user": "player_9f2a",
  "token": "<the player's current session token in your wallet>",
  "currency": "EUR",
  "country": "EE"
}
```

| Field         | Required                | Notes                                                                                               |
| ------------- | ----------------------- | --------------------------------------------------------------------------------------------------- |
| `operator_id` | Yes                     | Your Hub88 operator ID. Hub88 uses it to pick which public key to verify against.                   |
| `user`        | Yes                     | The player, in your own system's naming. Must be non-empty.                                         |
| `token`       | Include if you have one | The player's current session token in your wallet. Lets Hub88 tie their play to the tournament.     |
| `currency`    | Include if you have one | The player's wallet currency, as an ISO 4217 code. Hub88 uses it to check tournament eligibility.   |
| `country`     | Include if you have one | The player's country, as an ISO 3166-1 alpha-2 code. Hub88 uses it to check tournament eligibility. |

{% hint style="info" %}
`token`, `currency` and `country` aren't covered by the published error table below the way `operator_id` and `user` are. Send them whenever you have them, tournaments that restrict eligibility by currency, country or wagering need them to place the player correctly.
{% endhint %}

**Sign the request** with a base64-encoded RSA-SHA256 (PKCS#1 v1.5) signature over the exact bytes of the body. This is the same scheme as every other `/operator/generic` endpoint, so if you already call Hub88, you already have this code.

Two rules cause almost every first-time failure:

* **Serialise once, sign those bytes, send those bytes.** Re-encoding the JSON between signing and sending changes the bytes, and the signature stops matching. Watch for HTTP clients that quietly re-serialise a body object for you.
* **The signature covers the body only** — no path, no timestamp, no nonce. A captured `(body, signature)` pair keeps verifying forever, so use TLS and never log the pair.

```bash
BODY='{"operator_id":42,"user":"player_9f2a","token":"<wallet session token>","currency":"EUR","country":"EE"}'

SIG=$(printf '%s' "$BODY" \
  | openssl dgst -sha256 -sign operator_private_key.pem \
  | openssl base64 -A)

curl -sS "https://$HUB88_API_HOST/operator/generic/v2/tournaments/authenticate" \
  -H 'content-type: application/json' \
  -H "x-hub88-signature: $SIG" \
  --data-binary "$BODY"
```

```ts
import { createSign } from 'node:crypto'

const body = JSON.stringify({
  operator_id: 42,
  user: 'player_9f2a',
  token: req.user.walletSessionToken,
  currency: req.user.currency, // ISO 4217, e.g. 'EUR'
  country: req.user.country    // ISO 3166-1 alpha-2, e.g. 'EE'
})

const signature = createSign('RSA-SHA256')
  .update(body)
  .sign(process.env.OPERATOR_PRIVATE_KEY_PEM!, 'base64')

const res = await fetch(
  `https://${process.env.HUB88_API_HOST}/operator/generic/v2/tournaments/authenticate`,
  {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'x-hub88-signature': signature },
    body // the same string you signed — not a re-serialised object
  }
)

const authPayload = await res.json()
```

A successful call returns:

{% code title="Authentication successful response:" %}

```json
{
  "sdk_init_token": {
    "token": "<opaque access token>",
    "attributes": "eyJwcm92aWRlcl9tZXRhIjp7ImJyb2FkY2FzdGVyX3VybCI6Ii4uLiJ9fQ=="
  }
}
```

{% endcode %}

`attributes` is base64-encoded JSON holding one key.

{% hint style="warning" %}
Don't read keys out of `provider_meta`, validate it, or hardcode your own `broadcaster_url`. It's server-controlled on purpose, so Hub88 can repoint you at a different deployment with a contract change and no deploy on your side.
{% endhint %}

**About the token:**

* **It never expires and has no renewal or revocation.** Treat it as a bearer credential - keep it out of URLs, logs, analytics and error reports.
* **It's issued per player.** Never reuse one across sessions belonging to different players, and don't cache one server-side for everybody.
* **Re-authenticating doesn't invalidate an earlier token**, so a page that reloads and authenticates again is fine.
* **Authenticate once per player session**, not on every page interaction.
  {% endstep %}

{% step %}

### Step 2: Pass the token to your page

Your own backend endpoint should return `{ sdk_init_token }` to your frontend — nothing else. Your private key, operator credentials and Hub88 API host all stay on your backend.

Pass `sdk_init_token` through unchanged. The SDK decodes `attributes` itself, so reshaping the payload is the most common reason `init()` fails.
{% endstep %}

{% step %}

### Step 3: Initialise the SDK (frontend)

Load the bundle straight from Hub88's CDN instead:

```html
<script src="https://cdn.hub88.io/tournaments-sdk/hub88-tournaments-sdk-1.0.0.js"></script>
```

This defines a global `Hub88Tournaments` object holding everything the SDK offers — `Hub88Tournaments.init`, `Hub88Tournaments.EventManager`, and so on. Every example below uses the `import` form; swap `init(...)` for `Hub88Tournaments.init(...)` if you're using the global. Pin the version in the URL — a published version never changes, so upgrading is a matter of changing the URL once you've read the changelog.

Add a container element to your page. This is what the SDK renders into:

```html
<div id="hub88-tournaments"></div>
```

Then initialise the SDK with the payload from Step 2:

```ts
import { init } from '@hub88/tournaments-sdk'

// authPayload is what your backend returned: { sdk_init_token: { token, attributes } }
const sdk = await init({
  ...authPayload,
  dev: { log: true } // useful while integrating — turn off in production
})
```

`init()` only unpacks the payload. It doesn't connect yet — that's the next step.
{% endstep %}

{% step %}

### Step 4: Subscribe to events and attach

Subscribe before you attach, so nothing arriving during the handshake is missed:

{% code lineNumbers="true" %}

```ts
import { EventManager, type LeaderboardEvent } from '@hub88/tournaments-sdk'

const off = EventManager.addEventListenerByType<LeaderboardEvent>(
  'leaderboard',
  (event) => {
    // Every message is a complete, already-sorted board. Replace what you hold.
    render(event.tournament_uuid, event.rankings)
  }
)

try {
  // container_id must name an element that already exists on the page.
  await sdk.attach({ container_id: 'hub88-tournaments' })
} catch (error) {
  off()
  throw error // refused: the payload is stale, wrong, or points at the wrong deployment
}
```

{% endcode %}

`attach()` is what connects, and it rejects if the connection is refused — there's no partially-connected state to handle.

A page does nothing to opt into a leaderboard. Once attached, rankings arrive automatically for every tournament the player is in, and stop when they're no longer in one. Which boards a player receives is Hub88's decision — there's no API for asking which ones they're on, and no way to subscribe to one yourself.
{% endstep %}

{% step %}

### Step 5: Let the player join a tournament

Add a button that calls `optIn()` with the tournament's ID:

```ts
document.getElementById('join').addEventListener('click', async () => {
  const outcome = await sdk.optIn('0f9c1c1e-9a4e-4f1a-8f0f-2f9d3b8a7c11')
  console.log('Join result', outcome.state, outcome.reason)
})
```

Your backend gets that tournament ID from the [List tournaments endpoint](/developer-docs/operator-api-reference/tournaments-api.md#post-operator-generic-v2-tournaments-list), that's how you build a lobby of joinable tournaments.

`optIn()` works on its own — you can call it without ever calling `attach()`, so a page that only needs a join button doesn't have to connect to the broadcaster at all. It never rejects, so always branch on the outcome it resolves with; see Joining a tournament for the full set of outcomes.
{% endstep %}

{% step %}

### Step 6: Detach when the player leaves

```ts
// On unmount or page leave:
off()
await sdk.detach()
```

`detach()` tears down listeners, releases the connection, and leaves the container as it found it. It has no effect on an opt-in, a player who joined stays joined after you detach.
{% endstep %}
{% endstepper %}

## Full Example

```ts
import {
  init,
  EventManager,
  type LeaderboardEvent
} from '@hub88/tournaments-sdk'

// 1. authPayload came from your own backend endpoint, which called
//    POST /operator/generic/v2/tournaments/authenticate and returned
//    { sdk_init_token: { token, attributes } } unchanged.
const sdk = await init(authPayload)

// 2. Subscribe before attaching, so nothing is missed during the handshake.
const off = EventManager.addEventListenerByType<LeaderboardEvent>(
  'leaderboard',
  (event) => {
    render(event.tournament_uuid, event.rankings)
  }
)

// 3. Attach to a container that already exists in the DOM.
try {
  await sdk.attach({ container_id: 'hub88-tournaments' })
} catch (error) {
  off()
  throw error
}

// 4. Let the player join — this works even if attach() above failed.
document.getElementById('join').addEventListener('click', async () => {
  const outcome = await sdk.optIn('0f9c1c1e-9a4e-4f1a-8f0f-2f9d3b8a7c11')
  console.log('Join result', outcome.state, outcome.reason)
})

// 5. On unmount / page leave:
// off()
// await sdk.detach()
```

## Reading a leaderboard event

You can get the live leaderboard events through the SDK leaderboard event as follows.&#x20;

{% hint style="info" %}
To get the final leaderboard, you can call the [POST /tournaments/rankings](/developer-docs/operator-api-reference/tournaments-api.md#post-operator-generic-v2-tournaments-rankings) API endpoint.&#x20;
{% endhint %}

```json
{
  "name": "leaderboard",
  "instanceId": "9f1c0e2a-6d14-4f8b-9a3c-2e7b5d1a4c60",
  "timestamp": 1786550400000,
  "tournament_uuid": "2b8f1c4e-9a7d-4b3e-8f21-6c5d4e3a2b19",
  "version": 1234,
  "emitted_at": "2026-08-11T12:00:00.000Z",
  "tournament": {
    "state": "active",
    "scoring_template": "biggest_single_win",
    "event_time": {
      "start_time": "2026-08-01T00:00:00.000Z",
      "end_time": "2026-08-31T23:59:59.000Z"
    },
    "prize_pool": {
      "currency_code": "EUR",
      "total": "1025.0",
      "tiers": [
        { "rank": 1, "amount": "500.0" },
        { "rank": 2, "amount": "300.0" },
        { "rank": 3, "amount": "225.0" }
      ]
    }
  },
  "rankings": [
    { "rank": 1, "operator_id": 42, "operator_user": "alice", "score": "84.62" },
    { "rank": 2, "operator_id": 42, "operator_user": "bob", "score": "73.10" },
    { "rank": 3, "operator_id": 42, "operator_user": "carol", "score": "69.05" }
  ]
}
```

| Field                         | Type               | Notes                                                                                 |
| ----------------------------- | ------------------ | ------------------------------------------------------------------------------------- |
| `name`                        | `'leaderboard'`    | Outbound event name.                                                                  |
| `instanceId`                  | `string`           | The SDK instance that dispatched it.                                                  |
| `timestamp`                   | `number`           | When the SDK dispatched it, ms since epoch.                                           |
| `tournament_uuid`             | `string`           | Canonical UUID. Key your own state on this.                                           |
| `version`                     | `number`           | Per-tournament board counter — see below.                                             |
| `emitted_at`                  | `string`           | ISO 8601, set by the publisher.                                                       |
| `tournament.state`            | `string`           | Tells a running board apart from a settled one.                                       |
| `tournament.scoring_template` | `string`           | One of `biggest_single_win`, `win_multiplier`, `sum_multiplier`, `points_per_round` . |
| `tournament.event_time`       | `EventTime?`       | `start_time`, `end_time`, both ISO 8601.                                              |
| `tournament.prize_pool`       | `PrizePool?`       | `currency_code`, `tiers[]`, and `total` when one currency covers them.                |
| `rankings[]`                  | `LeaderboardRow[]` | `rank`, `operator_id`, `operator_user`, `score`. An empty array is valid.             |

Four rules decide whether a board renders correctly:

* **Every message is the whole truth.** The full ranked list, already sorted, with ranks assigned. Replace what you hold — nothing to merge, nothing to sort. A message you missed needs no recovery, because the next one is complete.
* **Discard a message whose `version` is not above the one you're showing.** Boards for one tournament can arrive out of order, and this is what stops a stale one overwriting a newer one. A gap in the sequence means nothing.
* **`scoring_template` is what interprets a `score`.** It tells `8462` as money apart from `8462` as a multiplier or a round count.
* **`score`, `amount` and `total` are integers in minor units, as strings — keep them as strings.** Rank maps to prize money, and a float comparison shouldn't decide it. Hub88's currency precision isn't universally two decimals, so `'1025000'` can't be formatted from `currency_code` alone. The SDK does no formatting or currency lookups — that part is your page's job.

An empty `rankings` array is valid and meaningful: it distinguishes *started, nobody has scored yet* from *not started*. A player enrolled but not yet scored simply has no row.

Positions move on the publisher's cadence, a few seconds apart — design for a periodic refresh, not a live feed. Diffing two consecutive messages gives you exactly the rows to animate.

## Joining a Tournament

`sdk.optIn(tournamentUuid)` joins the player your session was created for. You pass only the tournament's ID — Hub88 identifies the player and the operator from the session token, so your page can't join anyone else.

`optIn()` never rejects. It always resolves with an outcome that your page branches on:

```ts
type OptInOutcome =
  /** Hub88 recorded the player as a participant. */
  | { state: 'joined' }
  /**
   * Hub88 declined the join. `reason` is 'not_started', 'ended' or
   * 'not_joinable'. Treat an unknown reason as a plain refusal.
   */
  | { state: 'refused'; reason?: string }
  /** Hub88 didn't accept the session token. Create a new session and try again. */
  | { state: 'unauthenticated' }
  /**
   * Nothing was sent, because the call couldn't be made. `reason` is
   * 'missing_tournament_uuid', 'missing_token', 'missing_endpoint_configuration'
   * or 'not_initialized'.
   */
  | { state: 'invalid'; reason: string }
  /** Hub88 couldn't be reached, or didn't answer in time. Let the player try again. */
  | { state: 'unavailable' }
```

A few behaviours worth designing around:

* **`not_joinable` covers an unknown tournament and one that belongs to another operator alike** — Hub88 never reveals which, the same way the authenticate endpoint never reveals why a player failed.
* **`missing_endpoint_configuration` means your Hub88 setup is incomplete.** Contact your Hub88 Account Manager rather than retrying.
* **Joining twice succeeds both times**, and each call gives up after ten seconds.
* **You don't need to call `attach()` first.** A page that only needs a join button never has to connect to the broadcaster.
* **After a successful join, the player's board can take up to about a minute to start arriving.** Show the joined state from the outcome itself, rather than waiting for the first board.

To find a `tournamentUuid` to pass in, call [List tournaments](/developer-docs/operator-api-reference/tournaments-api.md#post-operator-generic-v2-tournaments-list) from your backend.

{% hint style="info" %}
No JavaScript API answers a question on the player's behalf — if a tournament needs the player to answer something to join, that happens through a Hub88 popup, not through `optIn()`.&#x20;
{% endhint %}

### Operator Tournament API Endpoints

Two REST endpoints let your backend read the tournaments Hub88 runs for your operator and see how your players rank in them. Call them from your server only, never from the browser, and sign them exactly as you signed the authenticate call in [Step 1 ](#step-1-authenticate-the-player-backend)— same scheme, same operator key.

Money amounts in these responses are decimal strings at the currency's own precision, the same convention as the `leaderboard` event. Any request field Hub88 doesn't recognise is ignored, so sending extra data is safe.

* [Tournaments List API ](/developer-docs/operator-api-reference/tournaments-api.md#post-operator-generic-v2-tournaments-list)
* [Tournaments Ranking API](/developer-docs/operator-api-reference/tournaments-api.md#post-operator-generic-v2-tournaments-rankings)

## Popups

Hub88 can send a popup to a player — for example, to nudge them about a tournament. The SDK renders it automatically inside your `container_id`, so you get it for free with no extra code. It also dispatches a `popup` event to your page, which you can use for analytics or custom handling; rendering is additive, so ignoring the event still shows the popup. If your page renders `popup` itself as well, it will draw twice.

If the popup asks the player a question, the SDK collects the answer and returns it to Hub88 — there's no JavaScript API for answering one yourself.

### Configuration

```ts
const sdk = await init({
  ...authPayload,
  dev: { log: true },
  connection: { retryAttempts: 5, retryDelayMs: 3000 } // the defaults
})
```

| Option                     | Default | Meaning                                                      |
| -------------------------- | ------- | ------------------------------------------------------------ |
| `dev.log`                  | `false` | Report connection outcome and diagnostics to the console.    |
| `connection.retryAttempts` | `5`     | Attempts after a **lost** connection. `0` disables retrying. |
| `connection.retryDelayMs`  | `3000`  | Milliseconds between attempts. Fixed, not growing.           |

A connection Hub88 **refuses** is never retried — the server has already answered, and `attach()` rejecting is the signal that your authenticate payload is stale or wrong. A connection that was established and then **dropped** is retried within the budget above. That budget bounds a single outage, not a session: it resets after every recovery, so only an unbroken run of failures exhausts it.

The SDK renders nothing during an outage, so telling your player is your page's job.

{% hint style="info" %}
`dev.log` is process-global — two SDK instances on one page share whichever value was configured last. With it off, the SDK only logs when something is broken (a listener threw, or the retry budget ran out). The token and connection URL never appear in console output either way, so a transcript is safe to paste into a support ticket.&#x20;
{% endhint %}

## SDK Methods

| Method                                             | Purpose                                                    |
| -------------------------------------------------- | ---------------------------------------------------------- |
| `init(arg)`                                        | Unpack the authenticate payload and establish the session. |
| `sdk.attach({ container_id })`                     | Connect and start emitting events.                         |
| `sdk.detach()`                                     | Tear down listeners and release the connection.            |
| `EventManager.addEventListener(cb)`                | Subscribe to every outbound event.                         |
| `EventManager.addEventListenerByType<T>(name, cb)` | Subscribe to `'leaderboard'` (or `'popup'`).               |

Events also reach the page as a `hub88-tournaments` `CustomEvent` on `window`, with the event under `e.detail.event`, if you'd rather not use `EventManager`.

`container_id` must name an element that exists — `attach()` rejects otherwise rather than connecting with nowhere to draw. `detach()` leaves the container as it found it.

## Error handling and troubleshooting

### Authenticate endpoint errors

Every error body is `{"error": "<message>"}`.

| Status | Error                                                           | Cause                                                                    | What to do                                                                    |
| ------ | --------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| 400    | `Missing required parameter - operator_id.`                     | No `operator_id` in the body                                             | Fix the request.                                                              |
| 400    | `Request signature header x-hub88-signature is missing.`        | Signature header absent                                                  | Fix the request.                                                              |
| 400    | `A parameter user is missing or invalid`                        | `user` absent or empty                                                   | Fix the request.                                                              |
| 401    | `Request signature is invalid.`                                 | The body bytes and the signature disagree                                | Sign the exact bytes you send.                                                |
| 401    | `Missing relevant operator settings.`                           | No public key registered for you                                         | Contact your Hub88 Account Manager.                                           |
| 401    | `Authentication failed`                                         | The player is unknown or disabled (deliberately indistinguishable)       | Check the player in your own system. Hub88 support can tell you which it was. |
| 404    | `The operator_id does not correspond to any existing Operator.` | Unknown `operator_id`                                                    | Check the ID and the environment you're calling.                              |
| 404    | `Tournaments contract was not found`                            | No tournaments contract                                                  | Contact your Hub88 Account Manager.                                           |
| 500    | `Tournaments contract is missing provider configuration`        | Configuration gap on Hub88's side                                        | Contact your Hub88 Account Manager.                                           |
| 500    | `Broadcaster access token signing is not configured`            | Configuration gap on Hub88's side                                        | Contact your Hub88 Account Manager.                                           |
| 502    | `Unable to confirm the user with the operator`                  | Your user-info API errored, was unreachable, or doesn't support the call | Check your own API, not the player.                                           |

Player problems return one uniform `401` on purpose, so nobody — including you — can use this endpoint to discover which of your players exist. Configuration problems stay individually diagnosable, since they say nothing about any player. If you need to know which player problem it was, Hub88 support can look it up.

### SDK and connection errors

| What you see                                                               | What it means                                                                                                                         |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `init: missing sdk_init_token.attributes in authenticate payload`          | You passed something other than the whole `sdk_init_token`, or the call failed and you passed the error body instead.                 |
| `init: sdk_init_token.attributes is not valid base64 / not valid JSON`     | The payload was reshaped or re-encoded between your backend and your page.                                                            |
| `init: decoded attributes missing provider_meta configuration`             | Your configuration has no provider section. Contact Hub88.                                                                            |
| `attach: container "x" not found`                                          | `container_id` names an element that doesn't exist yet. Render the element first.                                                     |
| `attach: already attached`                                                 | `attach()` was called twice on one instance. Call `detach()` first.                                                                   |
| `no access token in the authenticate payload`                              | `sdk_init_token.token` is absent. Check what your backend forwarded.                                                                  |
| `provider_meta.broadcaster_url is missing from the contract configuration` | Your configuration predates tournaments realtime delivery. Contact Hub88.                                                             |
| `the broadcaster refused the connection (socket stage)`                    | The token was refused. Check it came from a current authenticate call, and that `broadcaster_url` points at the right deployment.     |
| `refused N of the topic joins (channel stage)`                             | The credential was accepted but the subscription wasn't. Same checks, then share the message with Hub88 support — it's safe to share. |
| `the connection attempt timed out (socket stage)`                          | No answer within 10 seconds. A network or deployment problem, not your token.                                                         |
| `the connection ... could not be restored after N attempts`                | The credential and URL were both fine and the network wasn't. Nothing to change on your side.                                         |
| Connected, no errors, no events                                            | Normal when the player is in no tournament. A board can also start a few seconds late, on first load and after a reconnect.           |

## Limits worth knowing

* **A board can start a few seconds later than you expect**, on a first load and again after a reconnect. Nothing is queued for you in the meantime — the next board supersedes whatever you missed.
* **Two browser tabs can disagree briefly** — up to about half a minute — over which tournaments the player is in.
* **Nothing is persisted across page loads.** A fresh load starts from the next board rather than from what the previous one held.

### Support

If something's wrong and the tables above don't settle it, Hub88 support needs: your `operator_id`, the player identifier you sent, the HTTP status and error body you received, and roughly when it happened. A console transcript is safe to include. Never send your private key or an `sdk_init_token`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.hub88.io/developer-docs/operator-sdks/tournaments-sdk.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
