Full example

One file that lists projects, starts a session, and streams a reply.

This page shows the whole SDK in one file. It uses no framework and needs no build step beyond TypeScript. The file uses every core concept in order: the client, the platform REST surface, session readiness, live streaming, sending a prompt, and turn classification. Each numbered step links to a deep dive.

The complete script

import { ApiError, classifyTurn, createKortix, narrowChatEvent } from '@kortix/sdk';
import type { MessageWithParts } from '@kortix/sdk';

async function main() {
  // 1. One client, one auth seam. getToken returns your API key
  //    (kortix_pat_…) or a logged-in user's Supabase JWT — nothing else.
  const kortix = createKortix({
    backendUrl: 'https://api.kortix.com/v1',
    getToken: async () => process.env.KORTIX_API_KEY!,
  });

  // 2. Platform REST: list projects, pick one (or provision your first).
  const projects = await kortix.projects.list();
  const project = projects[0] ?? (await kortix.projects.provision({ name: 'sdk-quickstart' }));
  console.log(`using project ${project.name} (${project.project_id})`);

  // 3. Create a session — a cheap platform call. No sandbox exists yet.
  const created = await kortix.projects.createSession(project.project_id, {
    name: 'sdk full example',
  });
  const session = kortix.session(project.project_id, created.session_id);

  // 4. Ready the session. This provisions (or resumes) the real cloud
  //    sandbox. ensureReady() polls /start (each call long-polls up to 30s)
  //    until the runtime is ready or its deadline (~3 min) elapses, so a
  //    cold boot just takes longer rather than throwing. The
  //    retryUntilReady wrapper below is optional — keep it only if you want
  //    a longer total budget than the default.
  const { opencodeSessionId } = await retryUntilReady(() => session.ensureReady());

  // 5. Connect the event stream before you send, so no early events are
  //    missed. narrowChatEvent() collapses the wire events into a small
  //    typed union you can switch over.
  let resolveIdle!: () => void;
  const idle = new Promise<void>((resolve) => (resolveIdle = resolve));

  const stream = await session.stream({
    onEvent: (event) => {
      const e = narrowChatEvent(event);
      if (!e) return;
      if (e.type === 'message.part.updated') process.stdout.write('.');
      if (e.type === 'session.error') console.error('\nerror:', e.error);
      if (e.type === 'session.idle' && e.sessionID === opencodeSessionId) {
        resolveIdle(); // the turn is finished
      }
    },
  });

  // 6. Send. Per-send overrides pick the model and the agent for this
  //    prompt only (ids come from projects.modelPicker() and
  //    projects.detail().config.agents).
  await session.send('What files are in this repo?', {
    model: { providerID: 'kortix', modelID: 'claude-sonnet-4.6' },
  });

  // 7. Wait for the turn to finish — the session.idle event, not a sleep.
  await idle;
  stream.close();

  // 8. Render the transcript. classifyTurn() turns the wire part variants
  //    into one union, so a renderer can switch on part.kind and
  //    TypeScript proves no case is missed.
  const result = await session.runtime.session.messages({
    sessionID: opencodeSessionId,
  });
  for (const message of (result.data ?? []) as MessageWithParts[]) {
    for (const part of classifyTurn(message).parts) {
      if (part.kind === 'text') console.log(`\n[${message.info.role}] ${part.text}`);
    }
  }
}

/** Optional outer-budget wrapper — ensureReady() already polls internally. */
async function retryUntilReady<T>(ensure: () => Promise<T>): Promise<T> {
  const deadline = Date.now() + 300_000;
  for (;;) {
    try {
      return await ensure();
    } catch (error) {
      const provisioning = error instanceof ApiError && error.code === 'RUNTIME_UNAVAILABLE';
      if (!provisioning || Date.now() > deadline) throw error;
      await new Promise((r) => setTimeout(r, 3_000));
    }
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Run it with Node 18 or later, Bun, or tsx:

KORTIX_API_KEY=kortix_pat_... npx tsx full-example.ts

The first ensureReady() call on a fresh session provisions a real cloud sandbox, so the ready step takes a while on the first run. Later runs resume the same sandbox and finish fast.

What each step teaches

StepConceptDeep dive
1One client, one token, one auth seamAuthentication
2–3The platform REST surface: projects and sessionsReference
4Session readiness, the bridge from platform to runtimeSessions
5, 7Live SSE events, narrowChatEvent, session.idleSessions
6Per-send { model, agent } overridesSessions
8classifyTurn and the exhaustive part unionReference

Going further from here

The same client reaches the rest of the platform through one facade.

const project = kortix.project(projectId);

// Workspace files inside the session's live sandbox
const tree = await session.files.list('/workspace');
const readme = await session.files.read('/workspace/README.md');

// Project secrets — env vars every session's agent can read at runtime
await project.secrets.upsert({ name: 'MY_API_KEY', value: 'sk-…' });

// The project's agents and skills (config files in the repo)
const { config } = await kortix.projects.detail(projectId);
console.log(
  config.agents.map((a) => a.name),
  config.skills.length,
);

// LLM gateway observability — cost, latency, per-model breakdown
const overview = await project.gateway.overview(7);
const routing = await project.gateway.routing.get();

The package ships runnable examples that cover each of these steps, in packages/sdk/examples/. They include a minimal client, streaming, a server wrapper, transcript rendering, and files and secrets.

Full example – Kortix Docs