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

# Client Credentials

> Keep your API key on the server and let browsers connect directly with short-lived credentials.

## Why You Need This

If you're building a web app, you have a problem: `connect()` needs an API key, but anything you ship to the browser is public.
Bundle it in your JS, and anyone can open DevTools and grab it.

Client credentials fix this by splitting the connection into two steps:

1. Your **server** uses the API key to mint a short-lived token.
2. Your **client** connects with that token. No API key required.

```mermaid theme={null}
sequenceDiagram
  participant Browser
  participant Your Server
  participant Odyssey API
  participant Streamer

  Browser->>Your Server: Request credentials
  Your Server->>Odyssey API: createClientCredentials() (with API key)
  Odyssey API-->>Your Server: { sessionId, signalingUrl, sessionToken, expiresIn }
  Your Server-->>Browser: Forward credentials (JSON)
  Browser->>Streamer: connectWithCredentials() (no API key)
  Streamer-->>Browser: MediaStream (video)
```

The API key stays on your server.
Tokens expire after 10 minutes and are locked to a single session, so even if someone intercepts one, the blast radius is minimal.

***

## Quick Start

### Step 1: Mint credentials on your server

<CodeGroup>
  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI
  from odyssey import Odyssey

  app = FastAPI()
  server = Odyssey(api_key="ody_your_api_key")

  @app.post("/credentials")
  async def get_credentials():
      creds = await server.create_client_credentials()
      return creds.to_dict()
  ```

  ```typescript Node.js (Express) theme={null}
  import express from 'express';
  import { Odyssey, credentialsToDict } from '@odysseyml/odyssey';

  const app = express();
  const server = new Odyssey({ apiKey: 'ody_your_api_key' });

  app.post('/credentials', async (req, res) => {
    const creds = await server.createClientCredentials();
    res.json(credentialsToDict(creds));
  });
  ```
</CodeGroup>

Your endpoint returns a JSON object with these fields:

| Field           | Type                  | Description                                                                                                                      |
| :-------------- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
| `session_id`    | `string`              | Unique session identifier, derived from the JWT                                                                                  |
| `signaling_url` | `string`              | WebSocket URL for the signaling server                                                                                           |
| `session_token` | `string`              | Short-lived JWT for session authentication                                                                                       |
| `expires_in`    | `number`              | Token lifetime in seconds                                                                                                        |
| `capabilities`  | `object \| undefined` | Streamer capabilities (e.g. `{ image_to_video: true }`). Present when the assigned streamer supports image-to-video. *(v1.3.0+)* |

### Step 2: Connect from the browser

<CodeGroup>
  ```typescript JavaScript (browser) theme={null}
  import { Odyssey, credentialsFromDict } from '@odysseyml/odyssey';

  // Fetch credentials from your server
  const res = await fetch('/credentials', { method: 'POST' });
  const credentials = credentialsFromDict(await res.json());

  // Connect without an API key
  const client = new Odyssey();
  const mediaStream = await client.connectWithCredentials(credentials);

  document.getElementById('video').srcObject = mediaStream;
  await client.startStream({ prompt: 'A mountain lake at sunrise' });
  ```

  ```typescript React (useOdyssey hook) theme={null}
  import { useOdyssey } from '@odysseyml/odyssey/react';
  import { credentialsFromDict } from '@odysseyml/odyssey';

  function VideoStream() {
    const { connectWithCredentials, startStream, videoRef } = useOdyssey();

    async function handleConnect() {
      const res = await fetch('/credentials', { method: 'POST' });
      const credentials = credentialsFromDict(await res.json());
      await connectWithCredentials(credentials);
      await startStream({ prompt: 'A mountain lake at sunrise' });
    }

    return (
      <>
        <video ref={videoRef} autoPlay playsInline />
        <button onClick={handleConnect}>Connect</button>
      </>
    );
  }
  ```
</CodeGroup>

That's it. The browser never sees your API key.

***

## Cross-SDK Serialization

Both SDKs produce the same JSON format.
You can mint credentials on a Python backend and consume them in JavaScript on the frontend (or the other way around).

<CodeGroup>
  ```python Python theme={null}
  creds_dict = creds.to_dict()
  # {"session_id": "...", "signaling_url": "...", "session_token": "...", "expires_in": 600, "capabilities": {"image_to_video": true}}

  from odyssey.types import ClientCredentials
  restored = ClientCredentials.from_dict(creds_dict)
  ```

  ```typescript JavaScript theme={null}
  import { credentialsToDict, credentialsFromDict } from '@odysseyml/odyssey';

  const dict = credentialsToDict(creds);
  // {session_id: "...", signaling_url: "...", session_token: "...", expires_in: 600, capabilities: {image_to_video: true}}

  const restored = credentialsFromDict(dict);
  ```
</CodeGroup>

<Info>
  The `session_id` in the dict is informational.
  On deserialization, it gets re-derived from the JWT to prevent tampering.
</Info>

***

## Session Lifecycle and Token Expiration

When you call `createClientCredentials()`, Odyssey provisions a session and returns a `session_token` (a JWT) that's valid for `expires_in` seconds (currently 600s).
Your client needs to call `connectWithCredentials()` before that window closes.

Once the client is connected and streaming, **the token expiration doesn't interrupt an active stream.**
The connection stays alive as long as the WebRTC session is open.
Token expiration only matters for the initial connection.

Here's what happens when a session ends:

* **Client disconnects cleanly:** The session is dropped immediately and your concurrency slot is freed.
* **Client loses connection (crash, network drop):** The session is dropped once the token expires.
* **Client never connects:** Unused sessions are automatically cleaned up after 15 minutes of idle time.

<Note>
  If a client tries to connect after the token has expired, the connection will be rejected.
  The unused session is cleaned up automatically, but your concurrency slot may stay occupied for a few minutes until that happens.
</Note>

Each `createClientCredentials()` call provisions a new session.
You can't reuse a credential for a different session.
There's no token refresh. If a token expires before the client connects, mint a new one from your server.

### Error Handling

<CodeGroup>
  ```python Python theme={null}
  from odyssey.exceptions import OdysseyAuthError, OdysseyConnectionError

  try:
      creds = await server.create_client_credentials()
  except OdysseyAuthError:
      # Invalid or expired API key
      ...
  except OdysseyConnectionError:
      # Network issue or no streamers available
      ...
  ```

  ```typescript JavaScript theme={null}
  try {
    const creds = await server.createClientCredentials();
  } catch (error) {
    // Common causes: invalid API key, network errors, no streamers available
  }
  ```
</CodeGroup>

***

## Client Credentials vs Broadcast

Both patterns let end users watch Odyssey streams without an API key.
The difference is whether each user gets their own world or everyone shares one.

|                      | Client Credentials                                | Broadcast                                          |
| :------------------- | :------------------------------------------------ | :------------------------------------------------- |
| **Sessions**         | One per user                                      | One shared by all viewers                          |
| **Token**            | `session_token` (full control)                    | `spectator_token` (view only)                      |
| **User interaction** | Yes: `startStream()`, `interact()`, `endStream()` | View only (you orchestrate input at the app level) |
| **GPU cost**         | One GPU per user                                  | One GPU total                                      |
| **Good for**         | Interactive web apps                              | Live demos, watch parties, Twitch-style streams    |

Use **client credentials** when each user needs their own session to interact with.

Use **[Broadcast](/broadcast)** when you want many people watching the same stream.
The Broadcast guide covers `spectator_token` and `connectToStream()`.

***

## Full Example

A minimal Express server with a vanilla HTML frontend:

<CodeGroup>
  ```typescript server.ts theme={null}
  import express from 'express';
  import { Odyssey, credentialsToDict } from '@odysseyml/odyssey';

  const app = express();
  const server = new Odyssey({ apiKey: process.env.ODYSSEY_API_KEY! });

  app.use(express.static('public'));

  app.post('/api/credentials', async (req, res) => {
    try {
      const creds = await server.createClientCredentials();
      res.json(credentialsToDict(creds));
    } catch (err: any) {
      res.status(500).json({ error: err.message });
    }
  });

  app.listen(3000, () => console.log('Server running on :3000'));
  ```

  ```html public/index.html theme={null}
  <!DOCTYPE html>
  <html>
  <body>
    <video id="video" autoplay playsinline muted></video>
    <button id="connect">Connect</button>
    <input id="prompt" placeholder="Enter prompt..." />
    <button id="send">Send</button>

    <script type="module">
      import { Odyssey, credentialsFromDict } from 'https://esm.sh/@odysseyml/odyssey';

      const client = new Odyssey();
      let connected = false;

      document.getElementById('connect').onclick = async () => {
        const res = await fetch('/api/credentials', { method: 'POST' });
        const creds = credentialsFromDict(await res.json());

        const stream = await client.connectWithCredentials(creds);
        document.getElementById('video').srcObject = stream;
        connected = true;
      };

      document.getElementById('send').onclick = async () => {
        if (!connected) return;
        const prompt = document.getElementById('prompt').value;
        await client.startStream({ prompt });
      };

      window.addEventListener('beforeunload', () => client.disconnect());
    </script>
  </body>
  </html>
  ```
</CodeGroup>

## SDK Reference

<CardGroup cols={2}>
  <Card icon="js" href="/sdk/javascript/odyssey-class" title="JavaScript SDK">
    `createClientCredentials()` and `connectWithCredentials()` reference.
  </Card>

  <Card icon="python" href="/sdk/python/client" title="Python SDK">
    `create_client_credentials()` and `connect_with_credentials()` reference.
  </Card>
</CardGroup>
