MachineMachine
MachineMachine
MachineGetting startedQuickstartAccounts & access
Your project
AgentsLegacy kortix.tomlManifest referenceModelsAgent permissionsSecrets
Running work
Change requestsRuntime & sandboxSessions
Connect & automate
ComputersConnectorsInkbox: iMessage, SMS, email & callsMCP serverPixio: images, video & audioRemindersSlack & channelsMicrosoft TeamsTriggers
Feature flags
Apps

Develop

CLI
TypeScript SDKFull exampleAuthenticationSign in with KortixSessionsAppsReact hooksSDK reference
Machine as a BackendAPI reference
GitHub
SDK

SDK reference

The full @kortix/sdk API surface — client methods, modules, turns, and distribution.

This page is the full @kortix/sdk API surface: every client method, the framework-free modules, the turns helpers, and how the package ships. Use SDK to get started and Sessions for the session lifecycle in depth.

The client

createKortix(config) returns one client. Every method is a typed call to the platform API. The project(id) and session(pid, sid) handles bind ids so you never repeat them.

typescript
const kortix = createKortix({ backendUrl, getToken });

kortix.accounts; // account / team operations
kortix.accountInvites; // invite accept/decline by token alone
kortix.projects; // top-level project operations
kortix.connectors; // Connector calls scoped by an agent-minted token
kortix.project(id); // id-bound project handle
kortix.session(pid, sid); // id-bound session handle → see Sessions
kortix.github; // GitHub App install + repo linking (account-scoped)
kortix.gitBackend; // the instance git backend ("Kortix managed")
kortix.billing; // credits, subscription, tier, transactions
kortix.sandboxShares; // public share links for a sandbox port
kortix.connectStatus; // easy-connect (Pipedream) status
kortix.marketplace; // public marketplace catalog
kortix.validateToken; // pasted-API-key check
kortix.config; // platform config in effect
kortix.runtime(); // OpenCode REST compatibility client

Accounts — kortix.accounts

methodwhat
list() · get(accountId)accounts you belong to · one account
create({ name }) · updateName(accountId, name)create · rename an account
branding.get(accountId) · branding.update(accountId, { app_name }) · branding.uploadAsset(accountId, kind, file) · branding.removeAsset(accountId, kind) · branding.reset(accountId)organization branding (Enterprise): own logo / icon / favicon and product name; kind is logo · icon · favicon, or logo_dark · icon_dark · favicon_dark for the dark-scheme variant
members(accountId) · invite(accountId, input)list members · invite one
updateMemberRole(accountId, userId, role) · removeMember(accountId, userId)assign the account role · remove a member
invites(accountId)pending invites
cancelInvite(accountId, inviteId) · resendInvite(accountId, inviteId)cancel · resend a pending invite
leave(accountId)leave the account

accounts.tokens mints account-scoped API keys (kortix_pat_...). See SDK auth for the full token model.

methodwhat
tokens.list(accountId?, options?)list API keys — the whole account's, or only your own with { mine: true }
tokens.create(input)mint one — { accountId?, name, expiresAt?, projectId? }
tokens.revoke(tokenId, accountId?)revoke one

accounts.audit is the enterprise reconstruction log. It combines authenticated API requests with semantic session, connector, approval, and computer events.

methodwhat
audit.log(accountId, filters?)list events by project, session, actor, source, outcome, request, correlation, resource, action, or time
audit.export(accountId, filters?)export the same filtered event stream as CSV or JSONL
audit.webhooks.list/create/update/remove(...)manage signed SIEM webhooks for the centralized stream

Each event includes project_id, session_id, actor_type, source, outcome, request_id, trace_id, and correlation_id when the action supplies them. The API does not store request bodies, prompts, secrets, credentials, or raw connector arguments in the centralized event. Connector events can include a bounded argument preview that redacts credential-shaped fields and opaque data.

Access assignments

Kortix has one grant record: an assignment. It binds one principal (user, group, service_account, or pending) to one role, at one scope (account, or one project), optionally narrowed to one object (agent, skill, secret, app, or trigger) and optionally carrying an expires_at. Group access, per-resource access, and custom-role bindings are all assignments. See Accounts & access for the model.

The canonical REST surface is:

Method + pathDoes
GET /v1/accounts/{accountId}/iam/assignmentslist assignments, filtered by principal, scope, object, or role
POST /v1/accounts/{accountId}/iam/assignmentscreate one assignment
DELETE /v1/accounts/{accountId}/iam/assignments/{assignmentId}revoke one assignment
GET /v1/accounts/{accountId}/iam/permissionsthe permission catalog, as data
GET /v1/accounts/{accountId}/iam/roles · …/roles/{roleId}/permissionsroles · one role's permissions

The SDK exposes them as listAssignments, createAssignment, revokeAssignment, and listPermissions. A catalog row carries action, scope_type, resource_type, delegable, description, area, level, and implies — read it instead of hardcoding action strings.

Assigning a custom role needs the account's rbac entitlement; the route answers 402 with code: "entitlement_required" without it.

Account invites — kortix.accountInvites

Reached by invite token alone — the invitee may not be a member yet.

methodwhat
listMine()every pending, unexpired invite addressed to your email, with the workspace and any projects it grants
describe(inviteId) · accept(inviteId) · decline(inviteId)preview · accept · decline an invite

Projects — kortix.projects

methodwhat
list() · listForAccount(accountId)your projects · projects in an account
get(id) · detail(id)summary · full detail
create(input) · createRepo(input)from an existing repo_url · new empty GitHub repo
provision(input)new project on a new Kortix-managed repo, seeded with a starter template — { name, account_id?, seed_starter?, starter_template?, marketplace_items?, source_item_id?, idempotency_key? }
update(id, input) · archive(id)update settings · archive
llmCatalog(id) · modelPicker(id)full · compact model catalog for a selector
sandboxTemplates(id) · sandboxHealth(id)sandbox build templates · build health
sessions(id) · createSession(id, input?)list visible sessions · create a session

provision creates a new project; it does not start an existing project's sandbox. Start a session instead — see Sessions.

Send idempotency_key when a retry is possible — a reload, a second tab, a timeout you retried. provision mints a brand-new managed repo per call, so without a key those all create real duplicate projects. Reuse one key for every attempt at a single logical create and the repeats return the project the first attempt made (201, same project_id, push_token: null). The key identifies the attempt, not the payload — reusing one with a different name returns the first project and ignores the new value, so mint a fresh key per distinct create. Creating a second project with the same name and no key still works.

A repeat that arrives while the first call is still provisioning gets 409 with code: 'provision_in_flight' rather than a project_id that call may still roll back. Retry with the same key.

project(id).sessions.list({ scope: 'project' }) is a lifecycle inventory for a caller with project-manager permissions. It adds accessible unavailable, warm, and soft-deleted sessions with ownership and runtime-state metadata. Both list scopes omit every session the caller cannot open.

sessions.list and sessions.listPage also take the hierarchy and attribution filters. Without them the list stays flat and unchanged.

optionwhat
parent: 'root'Top-level sessions only. Each row carries child_count.
parent: '<sessionId>'That session's children, newest first.
startedBy: 'me' | 'others' | 'automated'Filter by initiator, relative to the caller.
qCase-insensitive search over title, starter label, trigger slug, and session id prefix, across every session the caller may open. With parent: 'root', a root that matched through a child has search_match: 'child'.

Every session row has parent_session_id and initiator ({ type: 'member' | 'trigger' | 'channel' | 'api' | 'system', id, label }). initiator names who started the whole run; a child copies its parent's. sessionParentId(session) reads parent_session_id, then the legacy metadata.spawned_by_session. A cursor is valid only for the filter set that issued it.

GitHub — kortix.github

Account-scoped GitHub App install and repo linking, not project-scoped.

methodwhat
getInstallation(accountId) · listInstallations(accountId)this account's install · installs the user can reach
saveInstallation(input) · deleteInstallation(accountId, installationId?)record · unlink an install
listLinkableInstallations(input)installs the authorized GitHub user may link; each entry carries linked_to_other_accounts, a count of other Kortix accounts holding it — never their names
listRepositories(accountId, installationId?) · listRepositoryBranches(...)repos the install can see · branches and the GitHub default
linkRepository(input)import a repo as a project. source: 'managed' imports one the instance git backend holds (self-host operator only, 403 otherwise) and is mutually exclusive with installation_id (400)
replaceProjectRepository({ project_id, repo_url, expected_repo_url, github_token })replace an existing project's GitHub repository. Use either github_token with Contents read/write access, or installation_id plus temporary github_user_token to verify organization admin access and grant only this repository through the GitHub App. copy_shared_secrets_from_project_id plus copy_shared_secret_identifiers copies selected shared runtime secrets in the same transaction. A stale expected_repo_url returns 409. The old repository remains on GitHub. New sessions use the replacement repository; existing sessions retain their checkout.

This list holds real account connections only. "Kortix managed" is not one of them — it is the instance backend below.

Instance git backend — kortix.gitBackend

One deployment-wide owner plus credential, used for Kortix-managed repositories. It is never an account connection and never appears in kortix.github.listInstallations.

methodwhat
get(){ configured, kind: 'app' | 'pat' | null, owner } — readable by any authenticated user; it exposes only the owner login that every managed repo_url already carries
repositories({ search?, limit? })repositories under that owner. Self-host operators only: on cloud that owner holds every customer's project repo, so the API answers 403 for anyone else

Billing — kortix.billing

Reads for credits, subscription, tier, and transaction history. Checkout, the customer portal, and credit purchases are Stripe flows, app-owned.

methodwhat
accountState(accountId?) · accountStateMinimal(accountId?)full · minimal billing state
transactions(params?) · transactionsSummary(params?)history · summarized totals
creditBreakdown(accountId?) · usageHistory(params?)credit balance by source · usage over time
sessionCosts.list(options?) · sessionCosts.get(sessionId, options?)paginated session-cost records · one detailed session ledger
tierConfigurations()available plan tiers
checkout.createSession(input) · checkout.confirmSession(sessionId, accountId?)start · confirm a Stripe Checkout session
subscription.createPortalSession(...) · subscription.cancel(...) · subscription.reactivate(...)open the customer portal · cancel · reactivate
subscription.scheduleDowngrade(...) · cancelScheduledChange(...) · prorationPreview(...)schedule · cancel · preview a plan change
credits.purchase(input) · credits.autoTopupSettings(...) · credits.configureAutoTopup(...)one-off purchase · read · configure auto-topup

sessionCosts.list() accepts accountId, projectId, limit, and offset. Each row combines finalized LLM cost and billed sandbox compute cost. sessionCosts.get() adds model usage and the discriminated LLM/compute ledger. The list includes a reconciliation total for cost without a session.

Sandbox shares — kortix.sandboxShares

Public share links for one exposed sandbox port. Sandbox-scoped, not project-scoped.

methodwhat
list(sandboxId)active share links
create(input)create one — { sandboxId, port, ttl?, label? }
revoke(sandboxId, token)revoke one

Marketplace catalog — kortix.marketplace

Public catalog browsing, read-only — distinct from project(id).marketplace, which installs an item onto a project.

methodwhat
items(options?) · item(id) · itemFile(id, path)browse · one item · a file inside an item
marketplaces() · featured()all · featured marketplaces
sources.list() · sources.add(input) · sources.remove(id)list · add · remove a source

The project handle — kortix.project(id)

Binds the project id; every sub-resource hangs off it.

typescript
const p = kortix.project(projectId);
await p.detail();
await p.update({ name });
await p.llmCatalog();
methodwhat
get · detail · update · archiveread · full detail · update · archive
llmCatalog · modelPicker · sandboxHealthmodel and sandbox-build reads
onboardingCompletemark project onboarding done
validateManifest(raw)validate a kortix.yaml (or legacy kortix.toml) manifest server-side
gitToken()mint a fresh scoped git push token (409 for a bring-your-own repo)
setAgentScope(agentName, scope)set an agent's allowed secrets and connectors in the manifest — the second binding, not a role

p.tokens — project-scoped API keys

Auto-minted at session create as KORTIX_TOKEN; can also be minted by hand.

methodwhat
list()project API keys
create(input?)mint a new one
revoke(tokenId)revoke one

p.setupLinks — agent-minted setup links

A link a person opens to enter a secret or connect an app, without full project access.

methodwhat
requestSecret(input) · requestConnector(input)link to collect a secret · connect an app

p.secrets — project secrets

methodwhat
list() · upsert(input)list metadata · create or update a write-only value and delivery policy
setStrategy(identifier, strategy, options?)change the exposure and its host list
broker(identifier, request)execute a session-authorized, policy-bound HTTPS request
remove(identifier)delete a secret
setPersonal(name, value) · removePersonal(name)set · remove a per-user override
setGitCredential(input)set a git auth credential

runtime with consumer sandbox is environment exposure — the default, and the only policy that puts a plaintext value in the session. egress with consumer network is egress-enforced exposure: the session holds a handle and Kortix substitutes the real value outside the sandbox, for the exact HTTPS hosts the policy lists. Egress-enforced exposure is experimental; it needs the secrets_egress feature flag (Settings → Feature flags). With the flag off, setStrategy(identifier, 'egress', …) and upsert(...) with an egress policy return 403 feature_disabled. Every broker consumer has no session presence at all. See Secrets. The broker(...) method requires a session-scoped token and an active session handle.

p.access — project assignments, invites, requests

Every method here reads or writes an assignment scoped to this project. Project roles are manager and member.

methodwhat
list() · invite(email, role)principals with access · invite a user
update(userId, role) · revoke(userId)assign a project role · revoke the assignment
pendingInvites() · requests()outstanding invites · pending access requests
resendInvite(inviteId) · revokeInvite(inviteId) · approveRequest(id) · rejectRequest(id)resend/revoke an invite · approve/reject a request
groupGrants() · attachGroupGrant(...) · updateGroupGrant(...) · detachGroupGrant(...)the same assignments, with a group principal

p.access.resourceGrants is the object assignment view: it narrows a principal to one object in the project instead of the whole project. Kortix enforces object assignments on agents and skills today. An agent is closed by default — a member reaches it only when an assignment names them or one of their groups. An object assignment carries no permissions of its own, and it restricts a project manager as much as a member.

methodwhat
resourceGrants.list()object assignments in this project
resourceGrants.create(input)assign one object to a user or a group
resourceGrants.remove(grantId)revoke one object assignment

p.connectors — tool connectors

methodwhat
catalog() · tools()callable Connector catalog · flattened <connector>.<action> tools
search(query, options?) · describe(tool)find · inspect one callable tool
call(tool, args?, options?)call one <connector>.<action> tool through the server-side gateway. options.account names which account to run as — see below.
accounts(slug)the accounts this caller may call slug as, default first — what options.account matches against
uploadAttachment(content, input)stage bytes for input.connector; returns ref ({ $kortix_attachment }) to place in a later call's args — see Attach files
list() · config(connectorId)configured connectors · one connector's config
create(input)add a connector
auth.discover(input)preview auth from an OpenAPI spec, Postman collection, or endpoint
remove(connectorId) · sync()delete a connector · re-sync connectors
setName(connectorId, name) · setSensitive(connectorId, sensitive)rename · mark it sensitive (extra approval gating)
setAuthorizationStrategy(slug, strategy)@deprecated. Still calls the route; the route is a no-op that answers {ok:true, deprecated:true} and changes nothing. owner_type on each connection is the whole access rule now.
setCredentialMode(connectorId, mode) · setCredential(connectorId, input)switch source · set the credential value
policies.get(connectorId) · policies.set(connectorId, policies)read · replace its tool policies
connections.list() · connections.reconcile(input)list · create/update connected accounts
connections.updateCredential(connectionId, input)rotate a connection credential
connections.revoke(connectionId) · connections.activate(connectionId)deny · restore a connection
connections.setDefault(connectionId)make it the default for its owner scope
connections.rename(connectionId, label)change the label only; the account stays authorized. Each connection carries connected_as, the identity it was authorized as.

p.connectors.discover browses the direct-connector catalog. It is experimental and off by default — enable it per project under Settings → Experimental → "Connectors API Discover". Easy Connect (Pipedream) remains the default connector marketplace.

methodwhat
discover.list(query?, cursor?) · discover.detail(id)search OpenAPI/MCP/GraphQL/CLI entries · one entry's detail

p.connectors.pipedream is the optional managed-OAuth path; listApps returns OAuth apps only. Connect API-key apps directly instead.

methodwhat
pipedream.listApps(params?) · pipedream.connect(input) · pipedream.finalize(input)browse the app catalog · start a connect flow · finalize it

A connector defines the tool, provider app, and policies. connections stores its connected accounts — a connector can hold several side by side. A session can pin one for every call with connector_bindings: { alias: { connection_id } }, or a single call can name one with call(tool, args, { account }). Credentials stay encrypted and resolve per request.

connections is the only active authorization facade. The retired authorizations and profiles names are not part of the current SDK surface.

Accounts and the call-time gate

Each connection has an owner_type: project (shared — reachable by anyone the connector is granted to, human or service account) or member (private — reachable only by its own owner, and only in a private session; never by a service account). A shared account can be narrowed to groups and people: connections.list() returns its grants in shared_with, and connectionSharedWithEveryone(connection) answers whether everyone in the project may use it. A narrowed account follows the private rules for the people it names, and usable: false marks one the caller sees only to manage it. Grant or revoke with createAssignment / revokeAssignment (object: { type: 'connection', id }) — see Choose who can use a shared account. p.connectors.accounts(slug) lists every account this caller may reach, default first:

typescript
const accounts = await kortix.project(projectId).connectors.accounts('stripe-read');
// [{ connection_id, label, owner_type, is_default }, ...]

await kortix.project(projectId).connectors.call(
  'stripe-read.get_balance',
  {},
  { account: 'My Stripe' }, // a connection id, a label, or the words `me` / `project`
);

A call that names no account resolves to the caller's own default private account first, then the project's default shared account. A named account that does not resolve is denied with connector_not_connected — never silently substituted — and the denial lists requested_account and available_accounts. Every successful call echoes which account it ran as: result.account = { connection_id, label, owner_type }.

There is no session-create-time connector gate any more. A session used to be refused 409 CONNECTOR_CONNECTION_REQUIRED when a required connector had no usable connection; that refusal is gone (require_connectors is inert — see p.sessions). The gate moved to the call: a denied connector_not_connected carries connect_url, a hosted authorization link, whenever one can be minted for the caller — the whole remedy is one click, surfaced by the transcript's connect button.

p.policies — project policies

methodwhat
list() · set(policies)the project's policies · replace the set

p.triggers — cron and webhook automations

A trigger starts an agent action on a schedule or an inbound webhook. See Triggers for session strategy and payload templating.

methodwhat
list()all triggers
create(input)create one
update(triggerId, input)edit a trigger
remove(triggerId)delete a trigger
fire(triggerId)run it now
setActivation(paused)pause or resume every trigger on the project

create(input) takes { name, type: 'cron' | 'webhook' | 'monitor', prompt_template, slug?, agent?, model?, enabled?, session_mode?, session_id?, cron?, run_at?, timezone?, secret_env?, session_access? }. name and prompt_template are required. cron/run_at are mutually exclusive (type: 'cron'); secret_env (the webhook HMAC secret) applies to type: 'webhook'.

session_access controls who can open sessions the trigger creates. It is { mode: 'private' | 'members' | 'project', memberIds: string[], groupIds: string[] } and defaults to private. This policy is account-local runtime state. It does not enter the portable kortix.yaml manifest. Updating only session_access creates no Git commit. A pinned session keeps its own sharing settings. A project manager can always open trigger-created sessions, including sessions that use private or selected-member access.

session_access is a per-resource visibility setting on top of the role model, not a role. It decides who can open one trigger's sessions. It grants no permission the role verdict denies. See Accounts & access.

p.marketplace / p.registry — installed items

Installs a catalog item's files onto the project's default branch. registry.* is an identical alias of marketplace.*.

methodwhat
marketplace.list() · marketplace.install(id)installed items · install a catalog item
marketplace.updates() · marketplace.update(name) · marketplace.updateAll()available updates · update one · update all
marketplace.remove(name)uninstall an item

p.files — repo files (read)

Read-only access to the project's git tree. To read and write files inside a running session, use the session's file operations — see Files under Modules below.

methodwhat
list(options?) · read(path, ref?)the repo tree · a file's contents at a git ref
search(query)search the repo
archive(options?) · history(path)download a tarball · a file's git history

p.git — history

methodwhat
commits()the commit log
commit(sha) · commitDiff(sha)one commit · its diff
branches() · versionDiff(from, to)branches · diff between two refs

p.changeRequests — lifecycle and merge

A change request (CR) is how a session's work merges into the default branch.

methodwhat
list() · get(crId)open CRs · one CR
diff(crId) · mergePreview(crId)its diff · preview the merge result
open(input) · merge(crId, input?)open · merge a CR
close(crId, input?) · reopen(crId, input?)close without merging · reopen a closed one
requestChanges(crId, input)record feedback, optionally delivered back to the originating session

p.sessions — and the session handle

methodwhat
list()the project's sessions
create(input?)create a session
session(sid)the session handle (same as kortix.session(id, sid))

The session handle is the heart of the runtime — see Sessions.

create(input) accepts connector_bindings keyed by connector-connection slug. Each value names a connection_id. It also accepts secrets for backend-origin secret narrowing. require_connectors is deprecated and inert: the field is still accepted (an old client is never rejected for sending it), but nothing reads it — a session is never refused at create time for a connector with no usable account. See Accounts and the call-time gate.

p.approvals — the connector approval inbox

Pending connector-gated actions awaiting a decision — backs the permission-approval UX (APPROVE / ASK / BLOCK).

methodwhat
list(options?) · sessionsNeedingInput(options?)pending approvals · sessions blocked on a decision
resolve(executionId, decision, scope?)approve or deny one — decision: 'approve' | 'deny', scope: 'once' | 'session' | 'session_all'

p.gateway — LLM observability

Request logs, cost/latency rollups, budgets, and gateway API keys for this project's model traffic.

methodwhat
logs(opts?) · log(logId)request log entries · one log entry
overview(days?) · series(days?) · breakdown(days?) · sessions(days?) · errors(days?)rollups, per-session cost, and errors over a window
budgets() · setBudget(input) · deleteBudget(budgetId)read · create/edit · remove a budget
keys() · createKey(name) · revokeKey(keyId)list · mint · revoke a gateway API key
playground(prompt, models)run one prompt against up to 6 models
routing.get() · routing.set(policy) · routing.reset()read · replace · inherit the routing policy
routing.preview(input)resolve a route without invoking a model

A routing policy holds a default model, a vision model, and an ordered fallback chain, each model attempted at most once; fallbackOn is transient or any-error.

The gateway moves to the next fallback model only when the current model failed before it returned any output:

  • transient: a network error, a timeout, any 5xx, or 402, 403, 429.
  • any-error: every failure, including a 400.

A failure after output started stays with that model. A request on your own provider key (BYOK) never falls back to a Kortix-billed model. Each fallback model gets its own generation defaults.

p.channels — Slack / email / voice

Connector surfaces that let an agent act as a Slack app, an email address, or join a realtime voice call.

methodwhat
slack.installation() · slack.mode() · slack.manifest()current install · mode · app manifest
slack.connect(input) · slack.disconnect()connect · disconnect
slack.getFile(url) · slack.uploadFile(input)download · upload a file via the server proxy
email.installation(connectorSlug?) · email.mode()current install · mode
email.connect(input) · email.disconnect(...) · email.updatePolicy(input)connect · disconnect · update the send/reply policy
voice.setBotName(name)rename the bot in a live call

p.modelDefaults — default model preferences

Account, agent, and project-scoped model defaults, resolved by the gateway.

methodwhat
get() · set(input) · clear(params)read · set a default · clear an override

p.setDefaultAgent — project default agent

p.setDefaultAgent(agentName) checks that the agent is declared and enabled, then sets it as default_agent in the project's kortix.yaml. New sessions prefer this agent unless a user picks another one.

p.updateFeatureFlag — feature flags

p.updateFeatureFlag(feature, enabled) turns one feature flag on or off for the project. Pass enabled: null to clear the override and fall back to the platform default. It calls the canonical PATCH /v1/projects/:id/features.

feature is one of FEATURE_FLAG_KEYS (exported from @kortix/sdk, typed as FeatureFlagKey). The caller needs the project's project.settings.write permission; the route answers 403 otherwise.

Every flag-gated route rejects the same way while the flag is off: HTTP 403 with { error, code: "feature_disabled", feature }. Use isFeatureDisabledError(error) to branch on it and featureDisabledKey(error) to read the flag key — never match on the message text.

p.updateExperimentalFeature(feature, enabled) is the deprecated alias. It keeps calling the deprecated route alias PATCH /v1/projects/:id/experimental so consumers pinned to an older deployed API keep working. Use p.updateFeatureFlag in new code.

p.sandbox — templates and snapshot builds

Sandbox build config beyond sandboxHealth/sandboxTemplates on the project handle: Dockerfile/image/warm-pool templates and their snapshot builds.

methodwhat
list() · snapshots()sandboxes for this project · built snapshots
rebuildSnapshot(slug?) · fixWithAgent()rebuild a snapshot · ask an agent to fix a broken build
createTemplate(input) · updateTemplate(...) · removeTemplate(...) · buildTemplate(...)add · edit · delete · build a template
setProvider(provider)request a provider switch. null (or the platform default / the already-active provider) applies immediately; switching to a different enabled provider starts a durable prepare→verify→activate transition — the current provider keeps serving while the target warm image is built and verified, then activated. The return is a tagged union: kind:'project' (immediate) or kind:'preparation' (poll getProjectSandboxProviderTransition() until activated/failed)

Platform admin hooks (@kortix/sdk/react)

These hooks serve the admin console. The API answers them only for a platform admin. Each mutation invalidates the ['admin', 'accounts'] queries.

HookWhat it does
useAdminRole(userId, options?)read the caller's role from GET /user-roles → { isAdmin, role }. userId keys the cache. A missing userId disables the query. Refetches on mount and focus, so a revoked role stops gating within one navigation.
useAdminGrantCredits()mutation { accountId, amount, description, isExpiring }: add wallet credit. POST /admin/api/accounts/:id/credits. Moves money: confirm in the UI first.
useAdminDebitCredits()mutation { accountId, amount, description }: remove wallet credit. POST /admin/api/accounts/:id/credits/debit. Moves money: confirm in the UI first.
useAdminSetTier()mutation { accountId, tier }: set the plan tier.
useAdminGrantTrial()mutation { accountId, tierKey, seats, durationDays, creditGrant?, note? }: grant or replace a trial. The account behaves as tierKey until the window ends. creditGrant also adds wallet credit.
useAdminRevokeTrial()mutation { accountId }: end an active trial now. Answers 400 when none is active.

Escape hatch

kortix.runtime() returns the typed OpenCode REST client for the active sandbox. On a client created by createScopedKortix (@kortix/sdk/server), it throws — the process-global "active" runtime is another request's sandbox in a multi-tenant server, a cross-tenant leak. Use the session-scoped kortix.session(pid, sid).runtime (call ensureReady() first) instead, which resolves that session's own sandbox.

Modules

The framework-free modules behind the client facade and the React hooks. Reach for them when you need one operation without the facade, a pure helper, or a Node-only isolation layer. Each module carries a stability tier so you know what to build on.

TierMeaning
CanonicalImport from the root @kortix/sdk. Use this for all new code.
SupportedA dedicated subpath (@kortix/sdk/react, @kortix/sdk/server). First-class, not deprecated.
Deprecated aliasAn old subpath that still works. It re-exports code the root already exports. Import from root instead.
InternalOutside semver. Do not import this in host code.

The root entry is canonical. Every framework-free name below is importable straight from @kortix/sdk:

typescript
import { files, getSessionHealth, getClient, authenticatedFetch, backendApi } from '@kortix/sdk';

Canonical modules

ModuleWhat it does
FilesWorkspace file operations: list, read, search, write
Session runtimeHealth probe and preview/proxy URL builders
OpenCode clientThe typed OpenCode REST client and its full type surface
AuthauthenticatedFetch and token accessors
Projects RESTThe raw REST functions the facade wraps
API clientbackendApi, the low-level typed HTTP client
TurnsMessage-to-turn grouping, cost, and status math — see Turns
TranscriptsformatTranscript, a client-side Markdown export
Composer listsThe session composer's agent and model lists: which to offer, which runs

Files

typescript
import { files } from '@kortix/sdk';

const tree = await files.list('/workspace/src');
const { content } = await files.read('/workspace/README.md');
const hits = await files.findText('TODO');
await files.upload(file, '/workspace/uploads', file.name, {
  onProgress: ({ loadedBytes, totalBytes }) => {
    console.log(`${loadedBytes}/${totalBytes}`);
  },
});

Uploads larger than 64 KiB cross the sandbox edge in verified chunks. The progress callback runs after each chunk reaches the session filesystem.

uploadNativeFile({ uri, name, type? }, targetPath, baseUrl) uploads a device file from React Native in one request. The runtime streams the bytes from uri, so they never enter the JS heap. It uses the same auth, deadline and 401 replay as files.upload. It does not chunk.

fetchProjectArchive, fetchCostExportCsv and the sandbox version calls send through the same transport. A failure throws an ApiError with status.

files.write(path, blob) replaces the file at path and fails if path is a folder. files.create(path) creates an empty file and never overwrites: when a file or folder already exists at path, it throws FileExistsError (status 409, code 'FILE_EXISTS').

typescript
import { FileExistsError, files } from '@kortix/sdk';

try {
  await files.create('/workspace/notes.md');
} catch (error) {
  if (error instanceof FileExistsError) console.log(`${error.path} already exists`);
  else throw error;
}

files targets the globally active sandbox. If your host runs more than one session at a time, call s.files on the session handle instead. It always targets that session's own sandbox. See Sessions.

Session runtime helpers

typescript
import { getSessionHealth, isRuntimeReady } from '@kortix/sdk';

const result = await getSessionHealth();
if (result.ok && isRuntimeReady(result.health)) {
  // the sandbox daemon is ready
}

getSessionHealth never throws on a non-2xx status. It returns { status, ok, health, body } and lets you decide what a status means. The same module exports the URL helpers that rewrite an agent's localhost output into a reachable proxy URL: detectLocalhostUrls, rewriteLocalhostUrl, proxyLocalhostUrl, parseLocalhostUrl, and buildWebProxyUrl.

OpenCode client

typescript
import { getClient } from '@kortix/sdk';

const client = getClient();
const { data } = await client.session.list({ limit: 100 });

getClient() returns the typed OpenCode v2 compatibility client for the active sandbox, with auth already injected. Prefer kortix.session(pid, sid).runtime, the same client scoped to one session, over the global getClient() when your host runs more than one session.

Auth helpers

typescript
import { authenticatedFetch, getAuthToken } from '@kortix/sdk';

const res = await authenticatedFetch(`${runtimeUrl}/kortix/health`);
const token = await getAuthToken();

The token comes from the getToken function you passed to createKortix. Most app code does not need this module — the file, session, and facade layers already authenticate for you.

API client

typescript
import { backendApi } from '@kortix/sdk';

const data = await backendApi.get('/some/endpoint');
await backendApi.post('/some/endpoint', { name: 'x' });

backendApi is the typed HTTP client every REST function builds on. Use it only for an endpoint that has no typed wrapper yet.

Composer agent and model lists

Build a session composer's agent picker and model picker with the same rules the Machine web app uses. Every function is pure: fetch the inputs with the client, then pass them in.

typescript
import {
  composerSelectableAgents,
  createModelVisibility,
  flattenModels,
  modelInDefaultView,
  pickerProviderList,
  projectConfigAgentsToRuntimeAgents,
  resolveComposerAgent,
  resolveComposerModel,
  resolveModelDefault,
} from '@kortix/sdk';

// Agents: the roster, the picker list, and the agent the send carries.
const roster = projectConfigAgentsToRuntimeAgents(detail.config);
const pickerAgents = composerSelectableAgents(roster, { enableProjects: false });
const agent = resolveComposerAgent({
  agents: roster,
  boundAgent: session?.agent,
  defaultAgent: detail.config.default_agent,
  selectedAgent: pick,
});
if (agent.disabled) {
  // no accessible agent: refuse the send
}

// Models: gateway projects read /model-picker, native projects merge the
// catalog synthesis with the running sandbox's provider list.
const gatewayEnabled = detail.project.experimental?.llm_gateway === true;
const providers = pickerProviderList({
  gatewayEnabled,
  modelPicker,
  runtimeProviders,
  llmCatalogProviders,
  secretNames,
});
const models = flattenModels(providers, { providerMode: gatewayEnabled ? 'gateway' : 'native' });
const isVisible = createModelVisibility({ catalogModels: models });
const defaultView = models.filter(
  (m) => m.enabled !== false && modelInDefaultView(m, { search: '', isStoreVisible: isVisible, selected }),
);
const { model, explicit } = resolveComposerModel({
  models,
  picks: [sessionPick],
  serverDefault: resolveModelDefault(modelDefaults, agent.selected ?? undefined),
  providers,
});

resolveComposerModel returns explicit: undefined when the composer is on its default. model is the key to display and send. Each flattened model's id is the ref a pick stores; modelRefToKey(ref, gatewayEnabled) reads a stored ref (a session pin, a channel binding, a trigger's model) back into a key.

Supported subpaths

SubpathWhat it does
@kortix/sdk/reactReact hooks — see React hooks
@kortix/sdk/serverRequest-scoped config for multi-tenant backends
@kortix/sdk/wire-message-idThe OpenCode wire message-id clock alone (one module, no imports). The root exports the same names

Wire message ids

A prompt's messageId is its position in the transcript. OpenCode orders messages by msg_ + a 48-bit clock (Date.now() * 0x1000, low 48 bits) + 14 base62 characters, and the clock wraps every 2^36 ms (about 2.2 years). Never encode it by hand.

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

// `after`: the ids already in this session's transcript.
const messageId = mintWireMessageId({ after: transcriptIds });

The mint is dated two minutes back, then lifted one tick above the newest id in after that is at most one hour ahead. A caller that holds the newest position as a clock instead of ids uses mintWireMessageIdAbove({ nowMs, newestKnownTime }), which returns { id, time }. newestWireIdClock(ids) finds that clock. Every comparison is on the ring, so ids on both sides of a wrap keep their true order.

The 48-bit clock wraps every ~2.2 years, so never compare two clocks with > or <=: an id minted just after a wrap is numerically smaller than one minted just before it. Use wireIdClockDelta(clock, reference), the signed distance on the ring (positive when clock is ahead), and maxWireIdClock(clocks) for the newest of several:

typescript
import { maxWireIdClock, wireIdClockDelta } from '@kortix/sdk/wire-message-id';

const floor = maxWireIdClock([transcriptNewest, deliveredNewest]);
const stale = floor !== null && wireIdClockDelta(clientClock, floor) <= 0n;

Server-side isolation

createKortix stores its config, including the token function, in one process-wide variable. That is fine for a browser tab, a CLI, or a single-tenant server. It is unsafe for a Node server that handles concurrent requests for different users, because the last createKortix call wins for every in-flight request. @kortix/sdk/server fixes this with per-request isolation:

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

export async function handler(req: Request) {
  const kortix = createScopedKortix({ backendUrl, getToken: () => tokenFor(req) });
  return kortix.projects.list();
}

createScopedKortix and runWithKortix isolate config per request with Node's AsyncLocalStorage. Never import @kortix/sdk/server from a browser bundle — it statically pulls in node:async_hooks.

A scoped client's top-level runtime() throws (it would resolve another tenant's sandbox). Reach a specific session's runtime via kortix.session(pid, sid).runtime after await s.ensureReady().

Deprecated aliases

About twenty old subpaths still work: /files, /turns, /session, /auth, /projects-client, /api-client, /config, /event-stream, /opencode-client, /platform-client, and more. Each one re-exports code the root @kortix/sdk entry already exports. They stay working so no existing import breaks, but new code should import from the root.

Internal modules

@kortix/sdk/internal/* holds the zustand stores apps/web's own runtime uses internally — session sync state, active-runtime tracking, and reconnect bookkeeping. This subpath is explicitly outside semver. Do not import it in host code; the React hooks already expose the state you need.

Turns

Plain functions that group session messages into turns and classify each message part. Use them to build a custom chat renderer instead of the reference one in apps/web. No React, no DOM — every export is a plain function or type, safe to call from any host.

typescript
import { classifyPart, classifyTurn, toolInfo, toolViewModel } from '@kortix/sdk';

Import every function from the root @kortix/sdk entry. The @kortix/sdk/turns subpath still works, but it is a deprecated alias. New code must use the root entry — see Distribution.

Classify a part

classifyPart(part) normalizes one of the 12 part types of the Machine transcript format (kortix.transcript.v1, KORTIX_TRANSCRIPT_SCHEMA) into a ClassifiedPart — a union keyed by kind. Each variant already resolves the fields a renderer needs: tool status, parsed JSON output, image detection.

typescript
import { classifyPart, type ClassifiedPart } from '@kortix/sdk';

for (const part of message.parts) {
  const classified: ClassifiedPart = classifyPart(part);
  switch (classified.kind) {
    case 'text':
      render(classified.text);
      break;
    case 'tool':
      render(classified.tool.title, classified.tool.status);
      break;
  }
}
kindshape
text{ id, text, synthetic } — skip synthetic parts; they mark shell mode's synthetic prompt
reasoning{ id, text }
tool{ id, tool: ToolView }
file{ id, filename?, mime, url, isImage, isPdf }
subtask{ id, description, agent, prompt, model? }
patch{ id, hash, files, fileCount }
snapshot{ id, snapshot }
agent{ id, name }
retry{ id, attempt, message, createdAt }
compaction{ id, auto, overflow, tailStartId? }
step{ id, phase: 'start' | 'finish', snapshot?, reason?, cost?, tokens? }
unknown{ raw } — a part type this SDK version does not know

An unrecognized wire part degrades to unknown at runtime instead of throwing. This lets an older client talk to a newer server.

A tool part classifies into ToolView:

typescript
interface ToolView {
  name: string;
  title: string;
  status: 'pending' | 'running' | 'done' | 'error';
  input?: Record<string, unknown>;
  output?: string;
  error?: string;
  outputParsed?: unknown; // JSON.parse(output) when it parses, capped at 256KB
  outputText?: string;    // the raw output text, always present
  kind?: ToolKind;        // toolKind(name): the action, whatever the harness calls the tool
  files?: ToolFile[];     // the files a file tool read, wrote or changed: path, patch, additions/deletions
  diff?: string;          // a unified diff of the change
  diagnostics?: Record<string, unknown[]>; // language-server diagnostics, keyed by file path
  answers?: string[][];   // the answers to a `question`, one list per question
  childSessionId?: string; // the child session a delegating call (`task`) runs
}

toToolView(part) builds one straight from a tool part. Read these fields instead of a harness's own state.metadata: the same card then renders an OpenCode edit (filePath, filediff) and a pi edit (path, edits, a unified patch).

Some tools (web_search, image_search, connector calls) report state.status: 'completed' even when their JSON body carries success: false. classifyPart detects this and sets ToolView.status to 'error' in that case too.

classifyTurn(message) classifies every part of one assistant message and returns a ClassifiedTurn with three fields:

  • parts — each part, classified
  • error — from message.info.error, if any
  • isEmpty — true when the turn has no error and no part with visible content

Tool metadata

toolKind(name) is the one tool taxonomy: it maps any harness's tool name (pi's write, OpenCode's oc-apply_patch, a plugin's pty_spawn) to a ToolKind — read, edit, write, bash, web_search, task, question, … or other. Narration families, view models, toolInfo categories and the context-tool group all derive from it. inputPath(input) reads a file tool's path from filePath, file_path or path.

typescript
toolKind('oc-apply_patch'); // 'apply_patch'
toolKind('pty_spawn');      // 'pty'
inputPath({ path: 'app.py' }); // 'app.py'

Two more lookups both describe a tool. Do not confuse them.

  • toolInfo(name) — icon-free, returns { label, category }. classifyPart uses this internally. ToolCategory is 'shell' | 'files' | 'search' | 'edit' | 'web' | 'task' | 'other'.
  • getToolInfo(name, input) — icon-aware, returns { icon, title, subtitle } for the reference tool-card UI. The subtitle comes from the tool's input, for example a file path or a search query.
typescript
toolInfo('bash'); // { label: 'Shell', category: 'shell' }
getToolInfo('write', { filePath: '/workspace/main.go' });
// { icon: 'file-pen', title: 'Write', subtitle: 'main.go /workspace' }

Both functions match tool-name prefixes, so Kortix's plugin tool families (agent_*, session_*, task_*, trigger_*, project_*, pty_*) resolve without a registry update. An unknown tool name never throws — it falls back to humanizeToolName(name) with category 'other'.

Tool view models

toolViewModel(classifiedTool) maps a classified tool part to a shape built for one tool family. A UI can then render it specially instead of as a generic JSON blob.

typescript
const vm = toolViewModel(classifiedTool);
if (vm.kind === 'shell') {
  render(vm.command, vm.stdout, vm.exitCode);
}
kindshapetools
web-search{ query, results?, answer?, error? }web_search, image_search
shell{ command, stdout?, exitCode? }bash
file-read{ path, preview? }read
file-write{ path, preview? }write
file-edit{ path, diff?: DiffLine[] }edit, morph_edit
search{ pattern, matches?: SearchMatch[] }grep, glob
task{ description, agent? }task
todo{ items: TodoItem[] }todowrite
question{ questions: QuestionItem[], answers? }question, ask
generic{ label, inputPretty?, outputPretty? }everything else, always safe to render

DiffLine is { type: 'added' | 'removed' | 'unchanged', text }. String fields are capped so one large tool output never breaks a render: 4000 characters for pretty-printed JSON, 256KB before a diff runs.

Group messages into turns

A turn pairs one user message with the assistant messages that answered it. It is the unit a chat UI renders as one exchange.

typescript
import { groupMessagesIntoTurns, collectTurnParts, type TurnLike } from '@kortix/sdk';

const turns: TurnLike[] = groupMessagesIntoTurns(messages);
for (const turn of turns) {
  const parts = collectTurnParts(turn);
}

groupMessagesIntoTurns links each assistant message to its parent user message. It falls back to message order when a parent link is missing. It also attaches an orphan assistant message — one with no parent that precedes every user message — to the first turn, not the last.

Related helpers:

  • findLastTextPart(parts) — the turn's final response text
  • turnHasSteps(parts) — true if a tool, compaction, snapshot, or patch part exists
  • isShellMode(turn) / getShellModePart(turn) — a turn that is one synthetic prompt driving one bash call

Type guards

Narrow a part by type:

typescript
import { isTextPart, isToolPart, getPartText } from '@kortix/sdk';

if (isTextPart(part)) {
  // part.type narrowed to 'text'
}
const text = getPartText(part); // works for 'text' and 'reasoning' parts

Also available: isReasoningPart, isFilePart, isAgentPart, isCompactionPart, isSnapshotPart, isPatchPart, isStepPart (step-start and step-finish).

Status and errors

typescript
import { getWorkingState, getTurnStatus, formatDuration } from '@kortix/sdk';

const status = getTurnStatus(parts, childMessages); // "Running commands..."
formatDuration(4300); // "4s" — durations under 1s return ''

getTurnStatus scans a turn's parts for the last status line. When the last part is a running task delegation, pass childMessages so the status shows the sub-agent's real activity instead of a generic "Delegating..." line.

typescript
import { getTurnError, getChildSessionError, unwrapError } from '@kortix/sdk';

getTurnError(turn);                    // the first assistant error, unwrapped
getChildSessionError(childMessages);   // newest error in a sub-agent's messages
unwrapError(rawError);                 // normalizes double-JSON and mixed error shapes

Cost and token totals

typescript
import { getTurnCost, getSessionCost, formatCost, formatTokens, COST_MARKUP } from '@kortix/sdk';

const info = getTurnCost(partsWithMessage, modelPricingLookup);
const sessionCost = getSessionCost(messages, modelPricingLookup);

formatCost(0.0032);  // "$0.003"
formatTokens(12345); // "12k"

Both functions read cost and token totals from step-finish parts. When a part reports zero, they estimate cost from token counts using a ModelPricingLookup. Every total is multiplied by COST_MARKUP (1.2) to match what Kortix bills.

ChatGPT/Codex subscription messages contribute $0.00, including historical messages with nonzero runtime costs. Token and context usage remain visible. Mixed sessions retain charges for paid API messages. OpenAI API models remain billable, including API models whose names contain codex.

Child sessions and pending requests

A project session's conversation tree (the root and the subagent conversations it spawned) comes from its runtime_sessions list. These helpers read it, preferring the current names over the pre-W4 ones:

typescript
import { directSubsessions, projectSessionForRuntimeId, rootRuntimeSession, runtimeSessionsOf } from '@kortix/sdk';

const root = rootRuntimeSession(projectSession);            // the root conversation id
const children = directSubsessions(projectSession);         // its subagent conversations
const owner = projectSessionForRuntimeId(projectSessions, childId); // the session a conversation belongs to

A task tool call delegates to a child session. These helpers connect a parent turn to that child's own messages.

typescript
import { getChildSessionId, getChildSessionToolParts } from '@kortix/sdk';

const childId = getChildSessionId(taskToolPart);
const steps = getChildSessionToolParts(childMessages);

Match a pending permission or question request to its tool call, and find which tool parts to hide while one is active:

typescript
import { getPermissionForTool, getHiddenToolParts, isToolPartHidden } from '@kortix/sdk';

const permission = getPermissionForTool(permissions, callID);
const hidden = getHiddenToolParts(activePermission, activeQuestion);

Formatting and lists

typescript
import { getFilename, getDirectory, relativizePath, stripAnsi } from '@kortix/sdk';

getFilename('/workspace/src/main.go');               // "main.go"
getDirectory('/workspace/src/main.go');               // "/workspace/src"
relativizePath('/workspace/src/main.go', '/workspace'); // "src/main.go"

Session-list helpers operate on the same session data:

typescript
import { sortSessions, childMapByParent, allDescendantIds } from '@kortix/sdk';

sessions.sort(sortSessions(Date.now())); // pins sessions updated in the last 60s
const childMap = childMapByParent(sessions);
const descendants = allDescendantIds(childMap, sessionId);

Retry state: getRetryInfo(sessionStatus) returns { attempt, message, next, details? } when status.type === 'retry', with message capped to 60 characters. details preserves a structured LLM-gateway envelope when present: the final provider, gateway code, request ID, suggestion, upstream status, and ordered candidate failures. Each candidate failure contains attempt, provider, routeModel, resolvedModel, stage, optional status, code, and message. Legacy plain-text retries return details: undefined. getRetryMessage(sessionStatus) returns the full unwrapped message. When OpenCode retains only that message, the gateway composite still includes the request ID and each candidate's provider, resolved model, HTTP status, code, and bounded message.

Structural types

The grouping and status functions accept minimal structural types — PartLike, MessageInfoLike, TurnLike, ToolStateLike, SessionStatusLike — instead of the concrete transcript types. Your own message and part shapes flow through unchanged as long as they match the required fields. classifyPart and classifyTurn are the exception. They type against the Machine Part union (kortix.transcript.v1), so their exhaustiveness check catches a new part type at build time.

Distribution

bash
npm install @kortix/sdk

The package ships as compiled ESM with full TypeScript type declarations. react and @tanstack/react-query are optional peer dependencies. If you use @kortix/sdk/react, install both.

typescript
import { createKortix } from '@kortix/sdk'; // framework-free core
import { useSession } from '@kortix/sdk/react'; // optional React layer
import { createScopedKortix } from '@kortix/sdk/server'; // Node and Bun servers

Entry points and stability

The root entry, @kortix/sdk, is canonical. It exports the full framework-free surface and runs in browsers, Node 18+, Bun, and edge runtimes.

EntryTierContract
@kortix/sdkCanonicalFramework-free. Never imports React or node:*.
@kortix/sdk/reactSupportedReact hooks. The only entry that imports React.
@kortix/sdk/serverSupportedPer-request config isolation for Node and Bun servers, via node:async_hooks. Never bundle it into a browser.
Legacy subpaths (/projects-client, /turns, /files, and more)DeprecatedStill work. Each re-exports from the root. Import from the root instead.
@kortix/sdk/internal/*InternalUsed by the Kortix web app only. Not a supported API.

CDN bundles

The package also ships two browser bundles, built by tsup: an ESM bundle (dist/kortix.esm.min.js) and an IIFE global (dist/kortix.global.js) that defines window.Kortix.

html
<script src="https://unpkg.com/@kortix/sdk/dist/kortix.global.js"></script>
<script>
  const kortix = Kortix.createKortix({
    backendUrl: 'https://api.kortix.com/v1',
    getToken: async () => KEY,
  });
</script>

window.Kortix exposes the same root entry as the npm import: Kortix.createKortix, Kortix.classifyTurn, Kortix.ApiError.

See also

  • SDK — install and your first session.
  • Sessions — lifecycle, streaming, and error handling.
  • React hooks — the reactive layer built on these modules.

Agent repository access

Agent configuration accepts repository_access?: boolean (default true). Set false to run new sessions without the project repository or repository API access. Git, secret, connector, and tool permissions remain separate. Existing sessions retain their saved policy. AgentConfigBlock.workspace is deprecated. The SDK maps legacy branch/runtime to the boolean field. A legacy read write requires an explicit repository_access choice; it does not enable read-only repository access.

Provider and model access

Read kortix.projects.modelAccess(projectId). Change one target with kortix.projects.setModelAccess(projectId, { target: 'provider' | 'model', id, enabled }).

Use provider ID kortix for Kortix Managed Models, or a catalog provider ID for BYOK, Codex, and custom providers. Use the full gateway wire ID for a model. Provider disable covers future models and preserves individual model preferences and saved credentials.

The current default model and its provider require a replacement before disabling (409 cannot_disable_default). Direct gateway requests to disabled targets fail with model_disabled or provider_disabled. Existing in-flight upstream requests finish normally. The response's enforced flag is false when the project uses native OpenCode instead of the gateway.

React consumers use useModelAccess(projectId) from @kortix/sdk/react. Its setEnabled(change) mutation refreshes the management catalog and session picker.

PreviousReact hooksNextMachine as a Backend

On this page

The clientAccounts — kortix.accountsAccess assignmentsAccount invites — kortix.accountInvitesProjects — kortix.projectsGitHub — kortix.githubInstance git backend — kortix.gitBackendBilling — kortix.billingSandbox shares — kortix.sandboxSharesMarketplace catalog — kortix.marketplaceThe project handle — kortix.project(id)p.tokens — project-scoped API keysp.setupLinks — agent-minted setup linksp.secrets — project secretsp.access — project assignments, invites, requestsp.connectors — tool connectorsAccounts and the call-time gatep.policies — project policiesp.triggers — cron and webhook automationsp.marketplace / p.registry — installed itemsp.files — repo files (read)p.git — historyp.changeRequests — lifecycle and mergep.sessions — and the session handlep.approvals — the connector approval inboxp.gateway — LLM observabilityp.channels — Slack / email / voicep.modelDefaults — default model preferencesp.setDefaultAgent — project default agentp.updateFeatureFlag — feature flagsp.sandbox — templates and snapshot buildsPlatform admin hooks (@kortix/sdk/react)Escape hatchModulesCanonical modulesFilesSession runtime helpersOpenCode clientAuth helpersAPI clientComposer agent and model listsSupported subpathsWire message idsServer-side isolationDeprecated aliasesInternal modulesTurnsClassify a partTool metadataTool view modelsGroup messages into turnsType guardsStatus and errorsCost and token totalsChild sessions and pending requestsFormatting and listsStructural typesDistributionEntry points and stabilityCDN bundlesSee alsoAgent repository accessProvider and model access