Skip to content
KortixKortix
Esc
↑↓navigate↵open⌘Jpreview
On this page

Sessions from code

The session handle: create, wait for readiness, send and steer prompts, stream events, read transcripts, handle errors, and start sessions from your own backend.

A session is one agent run in its own sandbox, on its own git branch. This page drives a session from TypeScript with @kortix/sdk, from a script or from a server that starts sessions for its own customers. For the concept, see Sessions; to set up the client, see SDK.

The shortest complete run:

import { createKortix } from '@kortix/sdk';

const kortix = createKortix({
  backendUrl: 'https://api.kortix.com/v1',
  getToken: async () => process.env.KORTIX_API_KEY!,
});

const created = await kortix.project(projectId).sessions.create({ agent_name: 'my-agent' });
const s = kortix.session(projectId, created.session_id);

const { runtimeSessionId } = await s.ensureReady();
let finish!: () => void;
const turnDone = new Promise<void>((resolve) => (finish = resolve));

const stream = await s.stream({
  onEvent: (event) => {
    if (event.type === 'session.idle' && event.properties.sessionID === runtimeSessionId) finish();
  },
});

await s.send('List the open TODO comments in this repository.');
await turnDone;
stream.close();

const { messages } = await s.messages({ limit: 1 }); // the agent's last message: { info, parts }

The session handle

const s = kortix.session(projectId, sessionId);
// the same handle: kortix.project(projectId).session(sessionId)
  • A handle resolves only its own sandbox. It never falls back to the runtime of another session, so two handles in one process never cross.
  • The session_id is also the name of the session’s git branch.
  • ensureReady() returns sandboxId. That is the sandbox provider’s id, a different value from session_id.
  • Creating a handle sends no request.

Create a session

const created = await kortix.project(projectId).sessions.create({
  agent_name: 'my-agent',
  model: 'kimi-k3',
  initial_prompt: 'Triage the open issues labeled bug.',
  labels: ['triage'],
  metadata: { app_ticket: 'T-142' },
});
created.session_id;

Every field is optional. With no input, the session runs the project’s default agent and model and waits for a prompt. A create starts provisioning the sandbox at once. If the session needs a new secret value or a new connector credential, write it before the create.

Field Meaning
initial_prompt The first prompt. The server delivers it when the runtime is up, so you need no send().
agent_name The agent to run. Default: the project’s default agent.
model The model id (see Models). With the LLM gateway off, use the native provider/model form; a bare id returns 400 INVALID_SESSION_MODEL. A model the account cannot use returns 400.
name The session title. Without it, the server makes a title from the first prompt.
labels Up to 20 labels, each 1 to 64 characters after trimming. Duplicates drop. Matching is exact and case-sensitive.
metadata A JSON object of your own keys, at most 16,384 characters of JSON per write. Server-managed keys, such as source, trigger_kind and trigger_slug, return 400. Prefix your keys, for example app_.
session_id A v4 UUID you generate, so you know the id before the create returns. A second create with the same id returns 409.
base_ref The branch or ref the session branch starts from. Default: the project’s default branch.
sandbox_slug The sandbox to boot, by its slug in kortix.yaml. Default: the agent’s sandbox, then the project default, then default.
provider The sandbox provider: daytona, platinum or e2b. Default: the project’s pinned provider, else the platform’s choice.
persistent_machine true keeps the whole root disk across stops. The session then runs on Platinum. Only for an account with Volumes on; without Volumes, Kortix ignores the field and returns no error. See Persistent and ephemeral machines.
runtime_context Non-secret values for the agent. See Start sessions from your server.
connector_bindings Pins one connector account per connector alias: { gmail: { connection_id } }.
inherit_unbound With connector_bindings set, a connector you did not bind gets no account. true lets it use the project default. Default false.
secrets Narrows which project secrets the sandbox receives, by identifier. A personal access key or service account key only.
provider_secret_pools HTTP body only. Selects up to 10 account secret ids per model provider, for up to 20 providers. Needs the pooled_provider_secrets flag and the LLM gateway.

opencode_model is the deprecated name of model; model wins when you send both. require_connectors is accepted and ignored.

Update a session later:

await s.update({ name: 'Bug triage', labels: ['triage', 'done'], metadata: { app_priority: null } });
const { items } = await kortix.project(projectId).sessions.listPage({ labels: ['triage', 'done'] });

labels replaces the list. metadata merges keys, and a null value removes a key. listPage({ labels }) returns only sessions that carry every label. For a pre-booted session that is ready before the first prompt, see Warm sessions.

Wait until ready

const { runtimeSessionId, runtimeUrl, sandboxId } = await s.ensureReady({ readyTimeoutMs: 300_000 });

ensureReady() provisions or resumes the sandbox and waits for its runtime.

  • It asks the API to start the session in long polls of up to 30 s, with a 1 s pause, while the answer is provisioning or starting with retriable: true.
  • The default deadline is 180 s. Pass readyTimeoutMs to wait longer.
  • It throws an ApiError with code: 'RUNTIME_UNAVAILABLE' at the deadline, or at once on a terminal failed or stopped answer.
  • Concurrent calls for one session share one start request. A second call on a ready handle returns at once.

Which methods ready the session for you:

Behavior Methods
Call ensureReady() themselves send, abort, rewind, restoreRewind, stream, messages, pending, answerPermission, answerQuestion, compact, every files.* method
Throw SessionNotReadyError before a ready previewUrl, proxyUrl, sandboxPortUrl
Read only the Kortix API, never the sandbox get, update, cost, scope, transcript, transcriptSync, messageAuthors, modelUsage, turn, attachments.read

s.health() never throws SessionNotReadyError. Before this handle has a runtime it returns { status: 0, ok: false }.

A failed start is a cooldown, not a final verdict. A start after the cooldown tries again. The cooldown grows from 2 to 5 to 10 minutes. After 5 consecutive failures, ensureReady() throws RUNTIME_UNAVAILABLE at once. That verdict expires 30 minutes after the last failure. s.restart() clears it at once.

Send prompts

await s.send('Refactor the auth module.');
await s.send('Write the tests.', { agent: 'tester' }); // this call only

s.setModel({ providerID, modelID }); // later send() calls on this handle
s.setAgent('reviewer');

await s.abort(); // end the running turn; the session stays
  • send() puts the prompt in the session’s durable prompt inbox, the same inbox the web app and the CLI use. It resolves when the prompt is stored, with { prompt_id, state, message_id }. It does not wait for the turn. Read the reply from stream() or the transcript.
  • send() throws an ApiError with code: 'PROMPT_FAILED' when the inbox row already failed.
  • Model and agent precedence: the send() option, then setModel() / setAgent() on this handle, then the session’s saved default.
  • setModel() stays on the handle. To change the session’s saved model, call changeModel():
const { applied_live } = await s.changeModel('kimi-k3');

The same model-id rule applies as for model at create. changeModel() restarts the runtime when the model changes on a running session, so a running turn ends. applied_live is true when a running session took the model now, and false when it applies at the next start. push_failed: true means the model is stored, but the running runtime still uses the old one. On a stopped, failed or completed session it returns 409 SESSION_NOT_RUNNING. Only the session owner or a project manager may call it. Anyone else gets 403.

Steer, queue or interrupt

send() queues: the prompt waits for the running turn to end. To choose how a prompt reaches a running turn, write it with s.prompts.create():

import { mintWireMessageId } from '@kortix/sdk';

const messageId = mintWireMessageId();
await s.prompts.create({
  clientMessageId: messageId,
  messageId,
  remintOnDelivery: true,
  delivery: 'steer',
  parts: [{ type: 'text', text: 'Use the staging database.' }],
});
delivery While a turn runs
steer The running turn reads the prompt at its next step. The turn does not stop.
queue The prompt waits for the turn to end, then runs as its own turn.
interrupt The turn ends after the running tool. Then the prompt runs as its own turn.

With no turn running, all three start a turn. A steer needs the runtime capability session.steer and must come from the member who started the turn. Otherwise the server delivers it as queue, and the inbox row shows steer_fallback:

  • unsupported: the runtime does not list session.steer.
  • not_prompter: another member started the running turn.
  • turn_ended: the turn ended before the prompt reached it.

The inbox lists only prompts not yet delivered:

Method What it does
s.prompts.list() Prompts the session still owes, oldest first, with state, reason and steer_fallback
s.prompts.edit(promptId, text) Replaces the text of a waiting prompt. 409 when the prompt is already on its way to the runtime
s.prompts.interrupt(promptId) Turns a waiting prompt into an interrupt
s.prompts.retry(promptId) Retries a failed prompt
s.prompts.remove(promptId) Removes a prompt and returns it. 409 when the agent already reads it
s.prompts.hold(true | false) Holds or releases the whole queue
s.turn() Running turns (turns, empty when idle), how the last turn ended (last_ended), and recent failures (recent_failures)

Attach files

Upload a file to the project, then send its id in a file part:

const file = await kortix.project(projectId).attachments.upload(blobFile);
const fileMessageId = mintWireMessageId();
await s.prompts.create({
  clientMessageId: fileMessageId,
  messageId: fileMessageId,
  remintOnDelivery: true,
  parts: [
    { type: 'text', text: 'Summarize this report.' },
    { type: 'file', attachment_id: file.attachment_id, filename: file.filename, mime: file.mime },
  ],
});
  • Limits: 50 MiB per file, 100 MiB per message, 20 files per message. A file must not be empty.
  • An upload needs no running sandbox. Before the prompt runs, the server writes each file to /workspace/uploads/.kortix-inbox/.
  • An upload that no prompt uses expires after 24 hours.
  • A browser composer uses usePromptAttachments. See React.

Stream events

const stream = await s.stream({
  onEvent: (event) => { /* one runtime event */ },
  onGapRehydrate: (gapMs) => { /* re-read state you cache */ },
  signal: abortController.signal,
});
stream.close();

stream() readies the session, then opens the session runtime’s event stream through the Kortix API. It is server-sent events over a streaming fetch. There is no WebSocket. The same stream serves both harnesses.

  1. Open the stream before you send, so you do not miss the first events.
  2. Send the prompt.
  3. Close the stream on session.idle for your runtime session id. A subagent’s conversation sends its own session.idle with its own id.

The SDK keeps the connection alive for you:

  • It reconnects at once (250 ms) after a healthy stream drops, else with backoff from 1 s up to 30 s plus up to 50% jitter, and resumes with Last-Event-ID.
  • The sandbox sends a keepalive every 20 s. After 60 s with no frame, the SDK drops the connection and reconnects.
  • onGapRehydrate(gapMs) runs after a reconnect when the dropped stream had delivered events, or when the gap was longer than 5 s.
  • After 8 consecutive failed connects (a stopped or deleted sandbox), the stream stops. Open a new one after the session runs again.
  • Events arrive in batches every 16 ms. Consecutive message.part.delta events for one part field arrive as one event: properties.delta holds the joined text, id is the last event’s id, and coalesced lists the events it replaced. Append delta as for a single event.

The events a chat needs:

type When it fires
message.updated / message.removed A message changed or was deleted.
message.part.updated / message.part.removed A part (text, tool call, file) changed or was removed.
message.part.delta Text was appended to a part field.
session.status A conversation’s busy state changed.
session.idle A turn finished.
session.error A turn failed. The event carries the error.
permission.asked / permission.replied The agent asked for a permission, and the answer.
question.asked / question.replied / question.rejected The agent asked a question, and the answer.
todo.updated The agent’s todo list changed.

Each event has type and properties, for example event.properties.sessionID. The stream carries more event types than these. narrowChatEvent(event) from @kortix/sdk maps the events above, except message.part.delta, to typed chat events, maps server.connected to a connection event, and returns null for every other event. It merges question.replied and question.rejected into question.answered.

A host whose fetch cannot stream a response body, such as React Native, passes its own transport. The SDK keeps reconnect, resume, backoff and the keepalive check:

import { configureKortix, type RuntimeEventTransport } from '@kortix/sdk';

const transport: RuntimeEventTransport = async function* ({ url, headers, signal }) {
  // Yield { data, id?, retry? } for each server-sent message of ONE connection.
  // Return when the server ends it. Throw (with `status` for a non-2xx answer) when it fails.
};
configureKortix({ backendUrl, getToken, eventStreamTransport: transport });

A host without visibilitychange and online events calls notifyHostSignal('visible' | 'online' | 'retry'). The stream that useSession holds reconnects on it. A stream() handle does not listen to it.

A React app uses useSession instead of stream(). See React.

Read the conversation

Method Returns Wakes the sandbox
s.messages({ limit?, before?, conversationId? }) A page of messages, oldest first, as { info, parts } (kortix.transcript.v1), and hasMore. conversationId reads a subagent’s conversation. Yes
s.transcriptSync({ limit?, before?, child? }) The copy the server saves after every turn: every part, with tool inputs and outputs. limit at most 500. Pass next_cursor as before for the older window. child reads a subagent’s saved transcript. No
s.transcript({ limit?, chars? }) A short digest: text and tool calls without inputs and outputs. limit 1 to 500 (default 40), chars 80 to 5,000 per message (default 700). No
s.get() The session record: status, agent, model, labels, metadata. No
s.messageAuthors() The member or session that sent each message, keyed by message id. No
s.modelUsage() The model that answered each turn, and fallback_from when a fallback model answered. No

A transcriptSync window also stops at 1,000,000 characters of JSON. It keeps its newest messages, and next_cursor then names the oldest one it kept.

Answer the agent

const { statuses, permissions, questions } = await s.pending();
await s.answerPermission(permissions[0].id, 'once'); // 'once' | 'always' | 'reject'
await s.answerQuestion(questions[0].id, [['Yes']]); // one answer list per question; null dismisses

Control the session

Method What it does
s.abort() Ends the running turn.
s.rewind(messageId) / s.restoreRewind() Rolls the conversation back to a user message, and undoes that before the next prompt. Needs session.rewind.
s.compact(model?) Summarizes the conversation into a shorter context. Needs session.compact.
s.stop() Stops the sandbox. The disk, the branch and the conversation stay.
s.restart() Stops and starts the sandbox in place. Clears a failed-start verdict.
s.delete() Deletes the session and its sandbox. This cannot be undone.
s.configState() / s.reloadConfig({ force? }) Reads whether the session runs the current config from the base branch, and loads that config into the running session. The reload restarts the runtime. While a turn runs it returns 409 SESSION_BUSY; force: true ends the turn.
s.setSharing(intent) Sets who can open the session. See Who can open a session.
s.publicShares.create({ transcript: true }) Makes one read-only link to the conversation. See Public links.
s.reminders.create(input) Schedules a prompt into this session. See Triggers & reminders.

The full method list is in SDK reference.

Files and previews

s.files reads and writes the session’s live sandbox. Every call readies the session first.

Method What it does
list(dir), read(path), readBlob(path) Lists a directory and reads a file as text or as a Blob.
status() The git status of changed files.
findFiles(query, { type?, limit? }), findText(pattern) Finds files by name and by content.
write(path, blob) Writes a file and overwrites it in place.
upload(file, targetPath?, filename?) Writes a new file. It never overwrites: a taken name gets a unique name.
create, copy, remove, mkdir, rename Edits the file tree.

Previews need a ready handle:

await s.ensureReady();
const url = s.previewUrl(3000, '/docs'); // a URL a browser tab opens
const fixed = s.proxyUrl('http://localhost:3000/'); // rewrite a localhost URL the agent printed
const direct = s.sandboxPortUrl(3000); // the authenticated API proxy URL, for a bearer-token client
const ports = await s.previews(); // ports the runtime exposes

See Ports and previews.

Runtime capabilities

A session runs on one of two harnesses, OpenCode or pi. s.health() lists what the runtime supports in health.capabilities. Hide a control when its capability is absent:

import { runtimeSupports } from '@kortix/sdk';

await s.ensureReady();
const { health } = await s.health();
if (!runtimeSupports(health?.capabilities, 'session.compact')) hideCompactButton();
Capability Feature
session.rewind Roll back to a message, and restore it
session.compact Summarize the conversation on demand
session.commands Project slash commands
session.fork Fork a session at a message
session.subagents Subagent child sessions
session.mcp MCP servers the runtime connects itself
session.todo The runtime’s todo list
session.shell A shell command run as a turn
session.attach Attach the harness’s own terminal client
session.config A runtime config document a client reads and patches
session.steer A prompt read by the running turn at its next step
session.log.v2 The runtime event stream serves the session log as log.* frames (?schema=v2)
  • OpenCode lists all 12. It lists session.steer only on OpenCode 1.18.15 or later.
  • pi lists session.subagents, session.compact, session.commands, session.steer and session.log.v2.
  • A route for an unlisted feature answers 501 feature_not_supported.
  • runtimeSupports returns true before a health answer arrives, and for a runtime that lists no session.* capability.

See Harnesses: OpenCode and pi.

Start sessions from your server

Your server holds one Kortix credential and starts sessions for its own customers. Your customers never sign in to Kortix.

  1. Get a credential. A personal access key (kortix_pat_…) acts as you. A service account key (kortix_sa_…) acts as itself and needs a project role first. See API keys and tokens.
  2. Create the client with createScopedKortix from @kortix/sdk/server. It never writes the process-wide config, so one request never reads another request’s config or sandbox. See Servers that act for many users.

Both credentials give the session the origin backend. The request body cannot set the origin. A token from inside a session sandbox is never backend.

import { createScopedKortix } from '@kortix/sdk/server';

const kortix = createScopedKortix({
  backendUrl: process.env.KORTIX_API_URL!,
  getToken: async () => process.env.KORTIX_API_KEY!,
});

const created = await kortix.project(projectId).sessions.create({
  agent_name: 'support',
  initial_prompt: 'Answer ticket ' + ticket.id + '.',
  labels: ['support', 'customer:' + customer.id],
  metadata: { app_customer_id: customer.id, app_ticket_id: ticket.id },
  runtime_context: { customer_id: customer.id, plan_tier: customer.tier },
  connector_bindings: { gmail: { connection_id: supportInboxConnectionId } },
  secrets: ['HELPDESK_API_KEY'],
});

On a scoped client, kortix.runtime() throws. Reach a session through kortix.session(projectId, sessionId).

The same kind of create over HTTP, with an idempotency key:

curl -X POST "https://api.kortix.com/v1/projects/<project_id>/sessions" \
  -H "Authorization: Bearer kortix_pat_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-app:ticket-123:reply" \
  -d '{
    "agent_name": "support",
    "initial_prompt": "Answer ticket 123.",
    "runtime_context": { "customer_id": "c-42" },
    "secrets": ["HELPDESK_API_KEY"]
  }'

The API answers 201 with the session. The full route reference is at https://api.kortix.com/v1/docs.

Retry a create safely

  • Send one Idempotency-Key per logical create: 1 to 255 characters of A-Z a-z 0-9 . _ : + / = -. A replay with the same key returns the same session.
  • A key is unique across all of Kortix. A key that another project or account already used returns 409 IDEMPOTENCY_KEY_CONFLICT. Put your own unique ids in the key.
  • A replay while the first create still runs answers 202 with status and session_id.
  • A replay with different connector_bindings, provider_secret_pools, secrets or runtime_context returns 409 IDEMPOTENCY_BINDING_CONFLICT, IDEMPOTENCY_PROVIDER_POOL_CONFLICT, IDEMPOTENCY_SECRETS_CONFLICT or IDEMPOTENCY_CONTEXT_CONFLICT.
  • A key whose session was deleted returns 409 IDEMPOTENCY_KEY_SESSION_DELETED. Use a new key.
  • sessions.create() in the SDK sends no Idempotency-Key. To retry an SDK create, generate session_id yourself. A retry that answers 409 session_already_exists means the first create succeeded.

Pass context, accounts and secrets

runtime_context gives the agent non-secret values, such as your customer id.

  • Values are strings (at most 4,096 characters), numbers, booleans or null.
  • Keys are 1 to 64 characters: a lowercase letter, then lowercase letters, digits, ., _ or -. A key with a segment such as token, secret, password, credential, api_key, private_key, authorization or cookie is rejected.
  • At most 64 keys and 16 KiB of JSON.
  • The sandbox receives the object as JSON in the environment variable KORTIX_SESSION_CONTEXT.
  • You set it at create. Scope changes do not touch it.

connector_bindings pins the connector account each alias uses, as { alias: { connection_id } }, at most 64 entries.

  • When you set it, an alias you did not bind gets no account. Pass inherit_unbound: true to let unbound aliases use the project default.
  • A shared (project) account works for a personal access key and a service account key.
  • A private (member) account works only for its owner, and only in a private session. A new session is private. Binding a private account to a shared session returns 409. A service account never uses a private account.
  • The credential never enters the sandbox. Kortix resolves it on every connector call. See Connectors, connections and accounts.

secrets narrows the project secrets the sandbox receives, from the agent’s grant down to the identifiers you list. It never widens the grant. [] delivers no project secrets. Only a backend-origin caller may set it; any other caller gets 403 origin_override_forbidden. See Secrets.

Tag each session with your own ids in labels and metadata. Find a customer’s sessions with listPage({ labels: ['customer:c-42'] }).

Scope and cost

Read and change a running session’s secrets and connector accounts:

const scope = await s.scope();
scope.secrets_allowlist; // null: the agent's grant applies
scope.connector_bindings_configured; // false: the session uses the project defaults

const next = await s.rescope({
  secrets: ['HELPDESK_API_KEY'],
  connector_bindings: { gmail: { connection_id: otherConnectionId } },
});
next.applied_live; // true when the sandbox took the new secrets now

Each field you send replaces its whole previous value. A field you leave out stays as it is.

Value secrets connector_bindings
null Use the agent’s secret grant Remove the override and use the project defaults
[] / {} No project secrets No connector account at all, project defaults included
A list / a map Exactly these secrets, inside the agent’s grant Exactly these accounts
  • Sending {} where you meant null blocks every connector.
  • A connector change applies on the next connector call.
  • A secret change restarts the runtime when the delivered set changes. applied_live says whether it applied now or applies at the next start. push_failed: true means the change is stored, but the running runtime still has the old secrets.
  • A removed secret stops reaching the sandbox. It cannot leave a model context or a process that already read it. Rotate the secret when that matters.
  • A rescope cannot widen past the agent’s grant.
  • Only the session owner or a project manager may change the scope.

Read usage without waking the sandbox:

const record = await s.cost(); // LLM and compute totals, tokens, model usage, compute time, ledger entries

const page = await kortix.billing.sessionCosts.list({ accountId, projectId, from, to, limit: 25 });

sessionCosts.list covers every session of the account in a half-open UTC window [from, to). The default window is the last 30 days. Filter by projectId or ownerId. See Track usage.

Session errors

A session create returns these errors. The code field of the JSON body holds the code.

Status Code Meaning
400 INVALID_SESSION_MODEL The model id is malformed or the account cannot use the model.
400 INVALID_SESSION_CONNECTOR_BINDINGS The binding map is malformed.
400 INVALID_SESSION_RUNTIME_CONTEXT The runtime context breaks a key, value, entry or size rule.
400 INVALID_SESSION_SECRETS The secrets list is malformed or has more than 128 identifiers.
400 INVALID_IDEMPOTENCY_KEY The Idempotency-Key header is longer than 255 characters or has a character outside the allowed set.
400 UNKNOWN_SANDBOX_TEMPLATE The sandbox slug names no sandbox in the project.
400 PERSISTENT_MACHINE_UNSUPPORTED A persistent machine needs the Platinum provider.
402 subscription_required / insufficient_credits The account cannot start a session.
403 origin_override_forbidden A caller that is not a backend origin sent a secrets list.
403 CONNECTOR_NOT_ASSIGNED The agent is not granted a connector you bound.
404 CONNECTOR_CONNECTION_NOT_FOUND The account does not exist for that connector in this project, or the caller may not use it.
404 SECRET_IDENTIFIER_NOT_FOUND The secrets list names an unknown project secret.
409 CONNECTOR_CONNECTION_INACTIVE The account or its connector is inactive, disabled or not connected.
409 PERSONAL_CONNECTOR_CONNECTION_REQUIRES_PRIVATE_SESSION A private account is bound to a session that is not private.
409 SECRET_IDENTIFIER_KEY_COLLISION Two listed secrets set the same environment variable.
409 session_already_exists A session with this session id exists.
409 IDEMPOTENCY_*_CONFLICT Another create already holds the key, or this key was used before with a different body.
409 IDEMPOTENCY_KEY_SESSION_DELETED The key belongs to a deleted session.
500 SESSION_CREATE_FAILED The create failed. The body has retry set to true: retry it.
503 SESSION_MODEL_RESOLUTION_FAILED The server could not resolve the default model. Retry it.

s.rescope() returns these errors:

Status Code Meaning
400 INVALID_SESSION_SCOPE The body is malformed, or it has neither field.
403 NOT_IN_AGENT_GRANT A secret is outside the agent’s grant.
403 NOT_GRANTED_CONNECTOR A connector is outside the agent’s grant.
403 SECRET_IDENTIFIER_NOT_AVAILABLE A secret does not exist or the session may not use it.
403 CONNECTOR_CONNECTION_NOT_FOUND / CONNECTOR_CONNECTION_INACTIVE The account is unknown, unusable or inactive.
409 AGENT_GRANT_UNRESOLVED The server could not read the agent’s grant. Retry it.
409 SECRET_IDENTIFIER_KEY_COLLISION Two listed secrets set the same environment variable.
409 PERSONAL_CONNECTOR_CONNECTION_REQUIRES_PRIVATE_SESSION A private account is bound to a session that is not private.

A rescope by anyone other than the session owner or a project manager returns 403 with no code.

Errors and retries

Every SDK call rejects with an Error subclass. Check instanceof, then branch on status or code:

import { ApiError, BillingError, SessionNotReadyError, formatBillingErrorForUI } from '@kortix/sdk';

try {
  await kortix.project(projectId).sessions.create({ agent_name: 'my-agent' });
} catch (err) {
  if (err instanceof BillingError) {
    const ui = formatBillingErrorForUI(err); // { alertTitle, alertSubtitle }
  } else if (err instanceof ApiError) {
    console.error(err.status, err.code, err.detail);
  } else {
    throw err;
  }
}
Class When it throws Key fields
ApiError Any failed request: an error status, a network failure, a timeout or an abort status, code, detail, response, url, endpoint, timeout
AuthError (extends ApiError) getToken() returned null. No request was sent code is NO_SESSION
BillingError (extends ApiError) Every 402 code, for example insufficient_credits; detail.message
RequestTooLargeError (extends ApiError) A 431, usually too many files in one request detail.suggestion
SessionNotReadyError A preview method ran before ensureReady() name is SessionNotReadyError
  • code is the API’s error code, or the status as a string.
  • An aborted request has name: 'AbortError' and code: 'ABORTED'. It is not a failure.
  • A request that passes its own timeout has code: 'TIMEOUT'. The default timeout is 30 s per request.
  • formatBillingErrorForUI(err) returns null for any error that is not a billing error.
  • isSandboxNotReadyError(err) and isRuntimeStartingError(err) are true while the sandbox is starting, stopped or parked. Show a waiting state and retry.
  • A 401 is sent once more with a fresh token from getToken(). See getToken and 401 recovery.

The SDK retries some requests before your code sees an error:

Method Retried Never retried
GET, HEAD Up to 2 times, after 250 ms and 500 ms, on 502, 503, 504 or a network failure A 500 or any other status
DELETE Up to 2 times on a network failure with no response Any received status
POST, PUT, PATCH Never —

To make a create retry-safe, see Retry a create safely.

Was this page helpful?