Skip to main content

TypeScript SDK

@4players/odin-cortex is the official TypeScript SDK for ODIN Cortex. It wraps the entire REST API in a typed, ergonomic client — and adds live objects and Firestore-style real-time subscriptions on top.

It’s the same SDK the 4Players console uses for Cortex, and the same one your serverless functions get pre-authenticated as ctx.cortex.

Installation​

npm install @4players/odin-cortex

The SDK is published as a stable release — the latest tag on npm (currently 1.13.x). It follows semantic versioning: minor releases add features, patch releases fix bugs.

Coming from a beta?

Earlier versions were published as prereleases (npm install @4players/odin-cortex@beta). Switch to the stable release with npm install @4players/odin-cortex@latest.

Requires Node.js 18+ (it uses the global fetch for streaming). It also works in modern browsers — but keep API keys server-side.

Creating a client​

Authenticate with an API key for server-to-server use:

import { CortexClient } from "@4players/odin-cortex";

const client = new CortexClient({
baseUrl: "https://cortex.odin.4players.io",
apiKey: process.env.CORTEX_API_KEY, // ots_live_...
});

For a browser/dashboard session where a user has logged in, pass a JWT instead:

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

See Authentication for when to use each.

Project scope​

Project-scoped resources are reached through client.project(projectId):

const project = client.project("your-project-id");

await project.sessions.list();
await project.gatherings.create({ name: "Standup", type: "appointment" });
await project.participants.create({ externalUserId: "u1", displayName: "Alice" });

Available resource managers on a project scope include sessions, gatherings, participants, sanctions, plugins, webhooks, functions, settings and rooms. A few resources hang off the client directly: client.auth, client.apiKeys and client.pluginCatalog.

Each call needs the matching API-key scope — for example sessions for project.sessions, messages for reading a transcript and rooms.push for project.rooms.send().

Live objects​

Methods like create() and get() return live objects. Every field from the API response is available directly on the object (e.g. session.title), and the object also exposes action methods and knows its own identity — no need to thread ids back through every call:

const session = await project.sessions.create({
title: "Team Meeting",
externalRoomId: "room-42",
idleTimeout: 120,
});

// Fields are on the object directly:
console.log(session.title, session.status);

// ...and so are the action methods:
await session.start();
const messages = await session.getMessages();
await session.regenerateSummary();
await session.stop();

A live object is also plain data: JSON.stringify(session) returns exactly the API payload (no client internals), and a Session is assignable to SessionResponse. The older session.data.title form still works but is deprecated — prefer session.title.

Paging and polling​

Collections that can grow large have paged variants. Sessions page by cursor:

let cursor: string | undefined;
do {
const page = await project.sessions.listPaged({ status: "inactive", limit: 100, cursor });
for (const s of page.sessions) console.log(s.title, s.endedAt);
cursor = page.nextCursor ?? undefined;
} while (cursor);

A transcript can be read incrementally with session.listMessages() — pass the last seen seq as after, and the previous ETag as ifNoneMatch. When nothing new has arrived the call resolves to null (HTTP 304), which is cheap for both sides:

let after = 0;
const page = await session.listMessages(
{ after, limit: 200 },
{ ifNoneMatch: `"${after}"` },
);
if (page) {
for (const m of page.messages) {
console.log(`${m.senderName}: ${m.content}`);
after = m.seq; // the next poll starts after the last message you processed
}
}

See Polling instead of streaming for the full loop.

Real-time watch()​

Every list() / get() returns a one-shot snapshot. To receive live updates instead, call watch() — it loads the initial state over REST and keeps it in sync over Server-Sent Events:

const sub = project.sessions.watch();

sub.onSnapshot((sessions, changes) => {
for (const c of changes) console.log(c.type, c.id); // added | modified | removed
});
sub.onError((err) => console.error(err));

console.log(sub.current); // latest array, readable synchronously
sub.unsubscribe(); // stop listening

Watch a single object by id, a session’s transcript, or a gathering’s members and invitations:

project.sessions.watch(sessionId);          // one session

const session = await project.sessions.get(sessionId);
session.watchMessages(); // transcript streams in live

const gathering = await project.gatherings.get(gatheringId);
gathering.watchMembers();
gathering.watchInvitations();

All watchers for a project share one auto-reconnecting SSE connection, and every reconnect re-syncs state — so a dropped connection never leaves you stale. Full walkthrough: Real-time updates.

Typed errors​

The SDK throws typed errors keyed to HTTP status codes:

import {
CortexAuthError, // 401, 403 — missing credential, missing scope, or a project the key can't reach
CortexNotFoundError, // 404
CortexRateLimitError, // 429
CortexApiError, // any other HTTP error, e.g. 400 (validation) or 409 (conflict)
CortexError, // base class — every error above extends it
} from "@4players/odin-cortex";

try {
await project.sessions.get("nope");
} catch (err) {
if (err instanceof CortexNotFoundError) {
// handle missing session
} else if (err instanceof CortexApiError && err.statusCode === 400) {
// the request body was rejected — err.details has the server's response
}
}

Every error carries the HTTP status as err.statusCode. The SDK does not retry automatically — on a CortexRateLimitError, back off before trying again (see Rate limits).

Debug logging​

Set debug: true to log every request and response (method, URL, status, body) to console.debug:

const client = new CortexClient({ baseUrl, apiKey, debug: true });

API reference​

Every class, method and type is documented in the generated reference:

Browse the SDK API reference