Skip to main content

Viewer tokens

POST /battles/{id}/viewer-tokens scope: viewer:mint → 200

A viewer token is the credential a browser presents to watch a battle live. It is the only SlotBattle credential that reaches untrusted code, which is why it is small, short-lived and scoped to exactly one battle.

{
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImsxIn0...",
"expires_at": "2026-08-03T03:55:42Z",
"ws_url": "wss://slotbattle.example.com/ws?battle_id=btl_CIkE8lctOrFDe1hJlG0T",
"ws_subprotocol": "slotbattle.jwt"
}

What is inside

An Ed25519-signed JWT. Decoded, from a real instance:

// header
{ "alg": "EdDSA", "typ": "JWT", "kid": "k1" }

// claims
{
"iss": "slotbattle",
"aud": "slotbattle-ws",
"tenant_id": "acme",
"battle_id": "btl_CIkE8lctOrFDe1hJlG0T",
"sub": "docs verification",
"role": "viewer",
"iat": 1785728442,
"exp": 1785729342
}

Lifetime is 15 minutes. battle_id is baked in, so a token for one battle is useless for another. The kid names the signing key, which is what makes rotation survivable. See Keys and credentials.

When you need this route

POST /battles and POST /battles/{id}/seats already return a token. Call this route when:

  • The token expired. A lobby that sat open twenty minutes outlived its token.
  • A new viewer arrives. Someone watching a battle they did not create or join.
  • You are re-rendering. A page reload past the 15-minute mark.

Mint one per viewer. They are cheap, and the token is the only revocation granularity available: one token shared across an audience cannot be withdrawn from a single person.

Never put the token in a URL

The token belongs in a response body your front end reads, then in the WebSocket subprotocol header. Putting it in ws_url as a query parameter, or in any URL, leaks it into access logs, proxy logs and browser history.

The API never does this: ws_url carries only battle_id, and the token is a separate field.

Failures

StatusCodeMeaning
403forbiddenThe key lacks viewer:mint
404not_foundNo such battle for this casino
500internalThe instance has no signing key
503unavailableThe instance has no token issuer at all, or is still starting

The 500 is a configuration problem, not a bug

{ "error": { "code": "internal", "message": "failed to mint viewer token" } }

The instance has no viewer signing key. Battles still open and seats still fill; only live viewing is unavailable, and ws_token is absent from create and seat responses because the field is optional.

Report it to your host, quoting that exact message. The signing key is platform-scoped, since one key signs for every casino on the instance, so configuring it is theirs to do.

Handling expiry in a front end

The token outlives most lobbies but not all of them. A workable pattern:

  1. Use the ws_token from the create or seat response for the first connection.
  2. Keep ws_token_expires_at. Re-mint when it is within a minute of expiring, or when the socket closes and a reconnect is needed.
  3. Route the mint through your backend, which holds the API key. Never call SlotBattle from the browser.

The tutorial implements exactly this.

Next