> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zerodash.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Session token

> The credential that carries a player from your lobby into a Zero-Dash game.

You generate the token. You validate it. You decide when it dies. Zero-Dash only carries it: from the launch URL, back to your [Player authorization](/api-reference/callbacks/player) callback, and into every [debit](/api-reference/callbacks/debit) for that session.

```mermaid theme={null}
flowchart LR
    A["Your platform<br/>mints token"] --> B["Launch URL<br/>?token=…"]
    B --> C["Game client"]
    C --> D["Zero-Dash"]
    D --> E["GET /player?token=…<br/>you validate"]
    D --> F["POST /debit<br/>token included"]
```

## Requirements

<CardGroup cols={2}>
  <Card title="Unique and non-deterministic" icon="shuffle">
    A random, unguessable value. **Never** the player ID, the account ID, an email, or anything derived from them.
  </Card>

  <Card title="At least 2 hours valid" icon="clock">
    Sliding-window expiry is preferred where your platform supports it — the token refreshes while the player is active.
  </Card>

  <Card title="Reusable for the whole session" icon="rotate">
    Multiple API calls and client reloads use the same token. Single-use tokens break every reconnect.
  </Card>

  <Card title="No length limit" icon="ruler">
    Any length is fine. An opaque random string or a signed JWT both work.
  </Card>
</CardGroup>

<Warning>
  The token travels as a **query parameter** in the launch URL. Assume it will be visible in browser history, in a shared screenshot and possibly in an intermediate access log. That is exactly why it must be non-deterministic, scoped to one player, time-limited and revocable.
</Warning>

## Choosing a format

<Tabs>
  <Tab title="Opaque random token">
    A random identifier, with the session state in your store. Simple and instantly revocable.

    ```javascript theme={null}
    import { randomBytes } from 'node:crypto';

    async function mintSessionToken(playerId, currency) {
      const token = randomBytes(32).toString('base64url'); // 256 bits
      await sessions.set(token, { playerId, currency }, { ttl: '2h' });
      return token;
    }
    ```

    <Check>Revoking is a single delete. Preferred when you already run a session store.</Check>
  </Tab>

  <Tab title="Signed JWT">
    Self-contained, no lookup on the hot path — but revocation needs a deny list.

    ```javascript theme={null}
    import jwt from 'jsonwebtoken';

    const mintSessionToken = (playerId, currency) =>
      jwt.sign({ sub: playerId, cur: currency }, process.env.SESSION_SECRET, {
        expiresIn: '2h',
        jwtid: randomUUID(), // gives you something to revoke
      });
    ```

    <Warning>Never put personal data in the payload. A JWT is signed, not encrypted — anyone holding the token can read every claim.</Warning>
  </Tab>
</Tabs>

## Validating it

Your `/player` callback receives the token and answers with the player's identity:

```json theme={null}
{
  "accountId": "acct_8f2c19",
  "displayName": "Matthew S.",
  "balance": { "amount": 12.45, "currency": "USD", "updatedAt": "2026-01-29T14:05:29.678Z" },
  "subOperatorId": "9"
}
```

Reject the request when the token is unknown, expired, revoked, or belongs to a different player than the one the request implies. A rejected authorization stops the game from starting — which is the correct outcome.

<Note>
  Token validity and `accountId` are separate concerns. The token identifies a **session**; the `accountId` identifies the **player**, for life. See [Account ID](/wallet/account-id).
</Note>

## Lifetime in practice

| Moment                       | What the token must do                                                   |
| ---------------------------- | ------------------------------------------------------------------------ |
| Launch URL requested         | Exist and be valid                                                       |
| Game opened, `/player` fires | Validate successfully                                                    |
| Player reloads the tab       | Still validate — the same token is reused                                |
| Player bets, `/debit` fires  | Still validate                                                           |
| Player wins, `/credit` fires | **Irrelevant** — credit carries no token and must work without a session |
| 2 hours later, still playing | Ideally still valid via sliding expiry                                   |

<Warning>
  This is the one that catches people: `/credit` and `/rollback` carry **no token at all** and can arrive days after the round. Never gate them on session validity. See [Errors and retries](/wallet/errors-and-retries).
</Warning>

## Common mistakes

<AccordionGroup>
  <Accordion title="Using the player ID as the token" icon="user-xmark">
    It is deterministic and permanent. Anyone who learns a player ID can open a session as that player, forever. Use random bytes.
  </Accordion>

  <Accordion title="Single-use tokens" icon="ban">
    The game reloads on reconnect, on rotation, on a resumed session. A token consumed by the first `/player` call breaks the second one.
  </Accordion>

  <Accordion title="Tokens shorter than the session" icon="hourglass-end">
    A 15-minute token ends the round for anyone who steps away. Two hours is the floor; sliding expiry is better.
  </Accordion>

  <Accordion title="One token shared across currencies" icon="coins">
    An account ID maps to exactly one currency. If a player holds several wallets, each needs its own session with its own token and its own account ID.
  </Accordion>
</AccordionGroup>
