Skip to main content

Authentication

Every call to the Cortex API is authenticated. As an integrator you’ll use one of two mechanisms:

  • API key — for backends, game servers, CI and any machine-to-machine calls. This is the one you’ll use most.
  • JWT bearer token — for browser/dashboard sessions where a human has logged in.

Pick the right one and send it with every request.

API keys​

An API key belongs to your account, can be bound to a single project, and carries scopes that decide which resources it can touch. Create and manage keys in the dashboard under your project’s Cortex → API Keys. A key looks like ots_live_... and is shown only once on creation — store it securely.

Send it as a header — either form works:

X-API-Key: ots_live_xxxxxxxxxxxxxxxxxxxx
Authorization: ApiKey ots_live_xxxxxxxxxxxxxxxxxxxx

With the SDK:

const client = new CortexClient({
baseUrl: "https://cortex.odin.4players.io",
apiKey: process.env.CORTEX_API_KEY,
});
Keep keys server-side — and in headers

An API key is a secret. Use it from your backend only — never embed it in a browser app, mobile app or shipped game client. Keys are accepted only in request headers: a key in a URL (?apiKey=...) is ignored and the request is rejected as unauthenticated, because URLs end up in logs, proxies and browser histories. If a key leaks, revoke it in the dashboard and create a new one.

Project binding​

A key can only act on projects that your account owns and has configured for Cortex (activating Cortex on a project does that). On every /api/projects/{projectId}/... route:

  • a key bound to a project can only use that project (an unbound key can use any project of your account), and
  • the project must be configured for Cortex by the same account as the key.

Anything else is answered with 403 — the same response for a project that doesn’t exist and one that belongs to someone else, so a key can’t be used to probe for project ids. Use one key per environment (Dev, Staging, Production) so a credential for one can never reach another. For production integrations, always bind the key to its project.

Scopes​

Every key carries a list of scopes. A request to a route outside the key’s scopes is rejected with 403. Pick only what the integration needs:

ScopeGrants
sessionsCreate, start, stop, list and read sessions; run session actions such as regenerating a summary; generate ODIN Voice tokens for the project
messagesRead session transcripts (including debug audio)
participantsCreate, read, update and erase participants
gatheringsGatherings, their members and invitations, join codes
sanctionsRead and issue, update or revoke sanctions
sanctions.readRead sanctions only — for a backend that enforces sanctions but must not issue them
rooms.push ⚠️Push custom messages into the ODIN room of a live session (Room Push)
pluginsThe plugin catalog, plugin instances and annotations
plugin-endpointsCustom inbound plugin endpoints
webhooksWebhook subscriptions and the delivery log
functionsServerless functions, their settings and runtime; invoking functions that require an API key
settings ⚠️Project settings (transcription provider, announcements, debug audio, encryption, room push)

⚠️ marks sensitive scopes: they act on your players or on how the project is configured, so the dashboard never includes them in a preset and asks for them explicitly.

The scope names follow fixed rules, so you can predict what a scope grants:

  • <family> (e.g. sessions) — read and write of one resource family.
  • <family>.read (e.g. sanctions.read) — only the read routes of that family.
  • <family>.<capability> (e.g. rooms.push) — one narrow capability that the family scope does not include.

There are no wildcards, and scope names are never renamed — when a family is split, the new scope is added and granted to existing keys, so a key never silently loses (or gains) access. The dashboard offers presets for common setups. The full, current catalog — with labels and descriptions — is also available to signed-in users at GET /api/api-keys/scopes (client.apiKeys.getScopes() in the SDK). That catalog is authoritative and may list additional scopes for features in early access.

Scopes also filter real-time events

On the Server-Sent Events stream, a key only receives events for the resource families its scopes cover — for example, sessions delivers session.* and bot.* events, but transcript events (message.*) need messages. A key needs at least one such scope to open a stream at all.

For example, a backend that starts sessions and reads transcripts needs sessions and messages; a game server that checks whether a player is muted or banned needs only sanctions.read.

Rate limits​

Each API key has its own request budget. Responses carry RateLimit-* headers, and an exhausted budget returns 429 with a Retry-After header. See Rate limits.

JWT bearer tokens​

JWTs are for dashboard / user sessions — when a human signs in, the dashboard exchanges their 4Players login for a short-lived Cortex JWT and sends it as a bearer token:

Authorization: Bearer eyJhbGciOi...
const client = new CortexClient({
baseUrl: "https://cortex.odin.4players.io",
accessToken: jwt,
});

A signed-in user has full access to the projects they can see; scopes only apply to API keys. Use JWTs when you’re building UI on behalf of a logged-in user; use API keys for everything server-side.

Inside serverless functions​

Code running in a Cortex serverless function doesn’t need a key at all: ctx.cortex is a client that the platform has already authenticated for the function’s project. The underlying credential is never exposed to your code.

Choosing the right mechanism​