---
title: REST API
description: "Call the Kortix API directly: base URL, authentication, errors, retries, and where the full route reference lives."
---

The Kortix REST API is the HTTP interface to accounts, projects, sessions,
secrets, connectors, triggers, and Apps. The web app, the CLI, the SDK, and the
Kortix MCP server all call it. Use it directly from any language that can send
an HTTPS request with a bearer token.

The shortest path: create a personal access key, list your projects, and start
a session.

```bash
kortix tokens new my-script --expires 30d    # prints kortix_pat_… once
export TOKEN=kortix_pat_...

curl -s https://api.kortix.com/v1/projects \
  -H "Authorization: Bearer $TOKEN"

curl -s https://api.kortix.com/v1/projects/<project_id>/sessions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"initial_prompt": "Summarize the open change requests."}'
```

`GET /v1/projects` returns an array of projects, each with `project_id`. The
session create returns `201` with the session, including `session_id` and
`branch_name`. It returns `202` with `status`, `command_id`, and `session_id`
when Kortix queues the create. Read the session with
`GET /v1/projects/<project_id>/sessions/<session_id>` until it appears.

## Pick a surface

All four surfaces reach the same API with the same permissions.

| Surface | Use it for | Credential |
|---|---|---|
| REST API | Any language. Integrations, scripts, and servers that are not TypeScript. | A bearer token from [Authenticate](#authenticate) |
| [SDK](/docs/sdk) (`@kortix/sdk`) | TypeScript and React. Sessions with live streaming, typed errors, automatic retries, and the session runtime. | `getToken` returns a bearer token |
| [CLI](/docs/cli) (`kortix`) | Terminals, CI jobs, and agents inside a session sandbox. | `kortix login`, or a token in CI |
| [Kortix MCP server](/docs/connect/mcp) | Claude, Cursor, ChatGPT, and other MCP clients that act as you. Its `search_api`, `describe_api`, and `call_api` tools read and call this API. | OAuth sign-in from the client |

Prefer the SDK for anything that drives a session: it owns the start, the
readiness wait, the event stream, and the runtime routes that the REST reference
does not list.

## Base URL and reference

| Deployment | Base URL |
|---|---|
| Kortix Cloud | `https://api.kortix.com/v1` |
| Self-hosted | `https://api.<domain>/v1` by default. See [Self-hosting](/docs/host). |

Every deployment serves its own route reference:

- `GET /v1/docs`: the interactive reference. The **API reference** tab at the
  top of these docs opens the Kortix Cloud copy.
- `GET /v1/openapi.json`: the OpenAPI 3.1 document. Its `servers` entry is the
  origin you requested it from, so a client generated from a self-hosted
  instance points at that instance.
- `GET /v1/health`: no token needed. Returns `status`, `version`, and `commit`.

The reference leaves out operator routes. A self-host operator turns off both
`/v1/docs` and `/v1/openapi.json` with `OPENAPI_PUBLIC_DOCS=false` in the API
environment.

## Authenticate

Send one header on every request:

```http
Authorization: Bearer <token>
```

The token decides who the request acts as. Every Kortix credential starts with
a prefix that names its kind. Create and manage them on
[Sign-in, SSO & API keys](/docs/accounts/identity#api-keys-and-tokens).

| Prefix | Credential | Acts as |
|---|---|---|
| `kortix_pat_` | Personal access key (**Settings → Personal access keys**, `kortix tokens new`, `kortix login`) | You, with your roles. A key bound to one project gets `403` everywhere else. |
| `kortix_sa_` | Service account key (**Account settings → API keys**) | The service account. It has no access until someone assigns it a role. |
| `kortix_oat_` | OAuth access token from [Sign in with Kortix](/docs/sdk/auth#sign-in-with-kortix). It lasts 1 hour. A `kortix_ort_` refresh token gets a new one. | The person who approved the app. |
| `kortix_gw_` | Project gateway key (`kortix gateway keys new`) | The LLM gateway inference routes of one project, and nothing else. See [Models](/docs/project/models#use-the-gateway-from-your-own-tools). |
| `kortix_scim_` | SCIM token (**Account settings → Identity**) | Directory sync under `/scim/v2` for one account. |
| `kortix_pat_` in `KORTIX_TOKEN` | Session token. Kortix sets it in every session sandbox. | The agent of that one session, limited to its project. It ends with the session. |

An OAuth token without the `kortix` scope reaches only `/v1/accounts/me`,
`/v1/oauth/userinfo`, and `/v1/oauth/revoke`. Other routes answer `403`, and
`message` starts with `insufficient_scope`.

A person's access token from [headless sign-in](/docs/sdk/auth#headless-sign-in)
also works as a bearer token. It acts as that person.

A missing or invalid token returns `401`. A `401` with
`code: "session_token_revoked"` means the credential is revoked, expired, or
belongs to a closed session. It never works again: stop retrying and get a new
token.

The same tokens open the project's git origin. See [Git](/docs/git).

## Errors

An error response is JSON. Every body has `error`. The generic shape adds
`message` and `status`. A typed error adds `code` and can add more fields.

| Field | Type | Meaning |
|---|---|---|
| `error` | `true` or text | Text is a human-readable reason. |
| `message` | text | A human-readable reason. |
| `code` | text | A stable machine code. Branch on this, never on the text. |
| `status` | number | The HTTP status, repeated. |

Some errors add fields. A failed body, query, or path validation returns:

```json
{ "error": true, "message": "Validation failed", "status": 400, "issues": [ … ] }
```

`issues` lists each invalid field with its `path` and `message`.

| Status | Meaning | What to do |
|---|---|---|
| `400` | The request is not valid. | Read `issues` or `message`. Fix the request. |
| `401` | No valid credential. | Send a valid token. Stop on `session_token_revoked`. |
| `402` | The account cannot pay for this, or its plan does not include it. | Read `code`: `insufficient_credits`, `subscription_required`, `no_account`, `entitlement_required`, `budget_exceeded`. A billing `402` (`insufficient_credits`, `subscription_required`, `no_account`) also carries `billing_state`, `balance`, and `account_id`. Branch on `billing_state` to choose between subscribe and top up. See [Common error codes](#common-error-codes). |
| `403` | The credential is valid, but the action is not allowed. | A permission denial names the `action` it needed and a `code`. `feature_disabled` adds `feature`, the flag key. |
| `404` | The object does not exist, or you may not see it. Kortix answers `404`, not `403`, for an object you may not see. | Check the id. Check your access to the object. |
| `409` | The request conflicts with the current state. | Read `code`. Reload the object and decide again. |
| `422` | The request is valid, but the operation cannot apply. | Read `code`, for example `CR_HEAD_NOT_AHEAD`. |
| `429` | A rate limit. | Wait `Retry-After` seconds. See [Retries and idempotency](#retries-and-idempotency). |
| `500` | A defect in Kortix. | Do not retry in a loop. Report it with the `X-Request-Id` response header. |
| `502`, `503`, `504` | A temporary failure. A `503` usually carries `Retry-After`. | Retry reads. Check the state before you repeat a write. |

Every response carries an `X-Request-Id` header. Quote it when you report a
problem.

## Common error codes

These are the codes clients branch on most. The linked page explains each fix.

| `code` | Status | Cause | Fix on |
|---|---|---|---|
| `session_token_revoked` | `401` | The credential can never work again. | [Sign-in, SSO & API keys](/docs/accounts/identity#api-keys-and-tokens) |
| `insufficient_credits` | `402` | The account's credit balance is empty. | [Plans & credits](/docs/credits#running-out) |
| `subscription_required` | `402` | The account has no plan for Kortix-billed usage. | [Plans & credits](/docs/credits#running-out) |
| `no_account` | `402` | The account has no credit account yet. Its setup is not complete. | [Plans & credits](/docs/credits#running-out) |
| `entitlement_required` | `402` | The plan lacks `sso`, `scim`, `rbac`, `auditAccess`, or `branding`. The body names `entitlement`. | [Sign-in, SSO & API keys](/docs/accounts/identity#enterprise-features) |
| `budget_exceeded` | `402` | A model budget set to block reached its limit. | [Models](/docs/project/models#budgets-and-costs) |
| `feature_disabled` | `403` | The project has this feature flag off. The body names `feature`. | [Projects](/docs/project#feature-flags) |
| `feature_operator_only` | `403` | Only Kortix (a platform admin) may change this flag, for example `apps`. | [Projects](/docs/project#feature-flags) |
| `account_mfa_required` | `403` | The account requires two-factor, and this sign-in did not use it. | [Sign-in, SSO & API keys](/docs/accounts/identity#two-factor-and-mfa-enforcement) |
| `expiry_required`, `expiry_too_far` | `400` | A new key breaks the account's key rules. | [Sign-in, SSO & API keys](/docs/accounts/identity#api-keys-and-tokens) |
| `INVALID_IDEMPOTENCY_KEY` | `400` | The `Idempotency-Key` header is not 1 to 255 characters of `A-Z a-z 0-9 . _ : + / = -`. | [Retries and idempotency](#retries-and-idempotency) |
| `IDEMPOTENCY_KEY_CONFLICT`, `IDEMPOTENCY_*_CONFLICT` | `409` | The key is in use for another request, or for this one with different `connector_bindings`, `secrets`, `runtime_context`, or `provider_secret_pools`. | [Retries and idempotency](#retries-and-idempotency) |
| `IDEMPOTENCY_KEY_SESSION_DELETED` | `409` | The key belongs to a session that was deleted. | Use a new key. |
| `INVALID_SESSION_RUNTIME_CONTEXT`, `INVALID_SESSION_CONNECTOR_BINDINGS`, `INVALID_SESSION_SECRETS` | `400` | A session create field is malformed. | [Sessions from code](/docs/sdk/sessions#session-errors) |
| `project_archived` | `404` | The project was deleted. | [Projects](/docs/project#delete-a-project) |
| `request_deadline` | `503` | A request ran past the API's 25-second processing limit. | Retry the read. Check the state before you repeat a write. |
| `git_mirror_unavailable` | `503` | The project repository is temporarily unreachable. | Retry after `Retry-After`. [Git](/docs/git) |
| `repository_push_rejected` | `409` | The git host refused a push because of branch protection or repository rules. | [Git](/docs/git#who-can-clone-and-push) |
| `CR_HEAD_NOT_AHEAD` | `422` | The change request branch has no commits ahead of its base. | [Git](/docs/git#change-requests) |
| `CR_SELF_MERGE_REFUSED` | `403` | A session tried to merge its own change request without a `project.gitops.merge` grant. | [Git](/docs/git#change-requests) |
| `MANIFEST_INVALID` | `422` | The change request makes `kortix.yaml` invalid. The merge is blocked. `issues` lists the errors. | [kortix.yaml reference](/docs/project/manifest) |
| `PERSONAL_SECRET_REQUIRES_PRIVATE_SESSION` | `409` | You tried to share a session that holds a secret shared only with you. | [Secrets](/docs/project/secrets#errors) |
| `project_secret_write_limit` | `429` | The project wrote secrets more than 100 times in one hour. | [Secrets](/docs/project/secrets#errors) |
| `app_config_invalid` | `400` | An App release config is not valid. `issues` lists the errors. | [Apps](/docs/apps) |
| `app_auth_required` | `401` | An App URL needs a viewer that may open it. | [Apps](/docs/apps#access) |

A connector call reports its outcome in `reason`, not `code`, inside
`{ ok: false, status: "denied" | "error", reason }`. The reasons, such as
`connector_not_connected` and `policy_block`, are on
[Connectors](/docs/connect/connectors#errors). LLM provider and model failures
are on [Models](/docs/project/models#errors).

## Retries and idempotency

The SDK retries reads, and a `DELETE` that got no response. It never repeats
any other write. In your own client, decide per response:

| Response | Retry |
|---|---|
| `GET` or `HEAD` with `502`, `503`, or `504` | Yes, after `Retry-After` when it is present. The SDK retries 2 times, after 250 ms and 500 ms. |
| A write with `502`, `503`, or `504` | Read the state first. |
| `429` | Yes, after `Retry-After` seconds. The body repeats the wait as `retry_after_seconds`. |
| A write with no response (network failure) | Read the state first, or resend with the same `Idempotency-Key` where the route takes one. |
| `400`, `401`, `402`, `403`, `404`, `409`, `422` | No. Change the request or the state first. |
| `500` | No. Report it with `X-Request-Id`. |

### Idempotency-Key

Two create routes deduplicate a retried request:

| Route | Where the key goes | Format |
|---|---|---|
| `POST /v1/projects/<project_id>/sessions` (start a session) | `Idempotency-Key` header | 1 to 255 characters of `A-Z a-z 0-9 . _ : + / = -` |
| `POST /v1/projects/provision` (create a project with a new repository) | `idempotency_key` body field | Up to 200 characters of `A-Z a-z 0-9 . _ : -` |

Send a new random value, such as a UUID, for each intent. Send the same value
again when you retry that intent. A repeated session create returns the original
session instead of starting a second sandbox. Session keys are unique across
all of Kortix. A key that another project or account already used returns
`409` `IDEMPOTENCY_KEY_CONFLICT`.

### Pagination

A list route that pages takes `limit` and `cursor`. Pass the next cursor back as
`cursor`. No next cursor means you have the last page.

| List | Next cursor | `limit` |
|---|---|---|
| `GET /v1/projects/<project_id>/sessions` | `X-Next-Cursor` response header | Default 50, maximum 200 |
| `GET /v1/accounts/<account_id>/audit` | `next_cursor` in the body | Default 50, maximum 200 |

The session list also returns an `ETag`. Send it back as `If-None-Match` and an
unchanged list answers `304` with no body.

### Rate limits

A rate-limited route returns `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
`X-RateLimit-Reset` (seconds until the window resets). When the limit is
reached it returns `429` with `Retry-After` and this body:

```json
{ "error": "rate_limit_exceeded", "message": "Rate limit exceeded. Please retry shortly.", "retry_after_seconds": 12 }
```

| Route | Limit |
|---|---|
| Headless sign-in routes under `/v1/auth` | 30 requests per minute per IP address. The `429` body is `{ "error": "over_request_rate_limit", "error_description": "…" }` with `Retry-After`, and no `X-RateLimit-*` headers. |
| The sandbox proxy, `/v1/p/<external_id>/<port>/*`: previews and the session runtime | 600 requests per minute per sandbox |
| Secret writes in one project | 100 per hour, `code: "project_secret_write_limit"` |

## What the reference does not cover

The reference does not list the session runtime routes. The surfaces below also
take a different credential or protocol than a plain bearer call:

| Surface | Use instead |
|---|---|
| The session runtime under `/v1/p/<external_id>/8000/kortix/runtime/*`: prompts, the event stream, files, and the transcript of a running session. The path takes the sandbox's external id at the provider, not the `session_id`. Read it from `sandbox_url` in `kortix sessions info <session_id> --json` (after `/v1/p/`), or from `sandboxId` in the SDK's `ensureReady()`. | The SDK session handle, `kortix.session(projectId, sessionId)`. See [Sessions from code](/docs/sdk/sessions). |
| SCIM 2.0 under `/scim/v2/accounts/<account_id>`. It takes a `kortix_scim_` token, not a personal key. | [SCIM provisioning](/docs/accounts/identity#scim-provisioning) |
| Inbound trigger webhooks, `POST /v1/webhooks/projects/<project_id>/<slug>`. They authenticate with the trigger secret, not a Kortix token: send an HMAC signature (`X-Kortix-Signature` or `X-Hub-Signature-256`), `X-Kortix-Token`, or `Authorization: Bearer <secret>`. | [Webhooks](/docs/connect/triggers#webhooks) |
| The LLM gateway's OpenAI- and Anthropic-compatible endpoints. They take a `kortix_gw_` key. | [Models](/docs/project/models#use-the-gateway-from-your-own-tools) |
| The project git origin, `https://api.kortix.com/v1/git/<project_id>.git`. | [Git](/docs/git) |

## Machine-readable docs

These docs are also published as plain Markdown for coding agents and LLM
tools:

| URL | Content |
|---|---|
| `https://kortix.com/llms.txt` | An index of every public page, docs included, with a link to its Markdown |
| `https://kortix.com/llms-full.txt` | All public pages in one file |
| `https://kortix.com/markdown/docs/<path>.md` | One page. For example, this page is `/markdown/docs/api.md`. |
| Any `/docs/<path>` URL, requested with `Accept: text/markdown` | The same page as Markdown |

Point a coding agent at `https://kortix.com/llms.txt` for the concepts and at
`https://api.kortix.com/v1/openapi.json` for the exact routes. An MCP client that uses the
[Kortix MCP server](/docs/connect/mcp) reads the route reference itself through
`search_api` and `describe_api`.
