Skip to main content

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.
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

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()
Your endpoint returns a JSON object with these fields:
FieldTypeDescription
session_idstringUnique session identifier, derived from the JWT
signaling_urlstringWebSocket URL for the signaling server
session_tokenstringShort-lived JWT for session authentication
expires_innumberToken lifetime in seconds
capabilitiesobject | undefinedStreamer 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

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' });
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).
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)
The session_id in the dict is informational. On deserialization, it gets re-derived from the JWT to prevent tampering.

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.
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.
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

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
    ...

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 CredentialsBroadcast
SessionsOne per userOne shared by all viewers
Tokensession_token (full control)spectator_token (view only)
User interactionYes: startStream(), interact(), endStream()View only (you orchestrate input at the app level)
GPU costOne GPU per userOne GPU total
Good forInteractive web appsLive demos, watch parties, Twitch-style streams
Use client credentials when each user needs their own session to interact with. Use 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:
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'));

SDK Reference

JavaScript SDK

createClientCredentials() and connectWithCredentials() reference.

Python SDK

create_client_credentials() and connect_with_credentials() reference.