> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oao.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP API

> Use the project and organization endpoints, pagination, idempotency, and error contracts.

The local API base URL is `http://127.0.0.1:3000/v1`. Hosted clients should use
their deployment's HTTPS URL. All tenant resources below are scoped by the
project in the path and by the authenticated principal resolved from the
credential.

## Common headers

```http theme={null}
Authorization: Bearer oao_your_secret
Accept: application/json
Content-Type: application/json
Idempotency-Key: unique-key-for-this-logical-write
```

`Idempotency-Key` is required for operations that change application state.

An OAO API key authenticates against any project of its organization: the
project addressed in the URL path selects the acting project, and requests
without a project in the path act in the organization's earliest-created
project. See [Authentication](/reference/authentication) for how per-project
principals are resolved.

## TypeScript SDK helpers

The workspace SDK exposes the same context and run-settling operations used by
the guided setup:

```ts theme={null}
import { OaoClient } from "@oao/sdk-js";

const oao = new OaoClient({
  baseUrl: "http://127.0.0.1:3000/v1",
  apiKey: process.env.OAO_API_KEY,
});

const context = await oao.getContext();
const run = await oao.waitForRunSettled(context.project.id, runId, {
  timeoutMs: 300_000,
  pollIntervalMs: 1_000,
});
```

`waitForRunSettled` polls `GET /projects/{projectId}/runs/{runId}` until the run
is `completed`, `failed`, `cancelled`, or `timed_out`. It throws
`run_wait_timeout` when the configured deadline expires; it does not cancel the
durable run.

## Browser authentication

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/auth/login` | Start hosted authentication and return an HTTPS redirect. |
| `GET` | `/auth/callback` | Validate one-time state and set HTTP-only session cookies. |
| `POST` | `/auth/refresh` | Refresh a cookie-authenticated browser session. |
| `POST` | `/auth/logout` | Clear OAO cookies and end the provider session. |
| `POST` | `/auth/switch-project` | Set the session's active project cookie. |
| `GET` | `/context` | Return the active tenant, project, and public principal. |

`GET /context` returns `principal` with `id`, `organizationId`, `projectId`,
`kind`, internal `subject`, `scopes`, and optional `displayName`, plus the
current organization, project, available tenant choices, model presets, and
`authProvider`. In IAP mode, `isIapDefaultProject` identifies the project that
anchors console authentication; its memberships cannot be removed.
`displayName` is presentation metadata from the active identity provider;
authorization continues to use the PostgreSQL principal and scopes.

`POST /auth/logout` requires the current cookie session, clears both session and
refresh cookies, and returns `{}` or `{ "redirectUrl": "https://..." }`. The
console follows the provider redirect when present so the hosted WorkOS session
is ended as well. Cookie-authenticated writes require an allowed `Origin` and an
`Idempotency-Key`.

## Organizations, projects, and members

Organization and project reads expose the PostgreSQL-authoritative tenant.
`GET /projects` lists every project of the authenticated organization,
projects can be created and deleted through the API, and a signed-in session
switches its active project with `POST /auth/switch-project`.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/organizations` | List the active principal's organization. |
| `GET` | `/organizations/{organizationId}` | Read organization identity and creation time. |
| `GET` | `/projects` | List every project of the organization. |
| `POST` | `/projects` | Create a project in the organization. |
| `DELETE` | `/projects/{projectId}` | Hard-delete a project and all of its data. |
| `GET` | `/projects/{projectId}` | Read the active project and organization metadata. |
| `GET` | `/projects/{projectId}/members` | List project principals, roles, and scopes. |
| `POST` | `/projects/{projectId}/members` | Create or update a project principal and membership. |
| `PATCH` | `/projects/{projectId}/members/{memberId}` | Change a project member's role. |
| `DELETE` | `/projects/{projectId}/members/{memberId}` | Remove project membership. |
| `DELETE` | `/projects/{projectId}/members/{memberId}/project-access` | IAP only: remove access to this project while retaining organization access. |

Member responses additionally include the persisted `organizationRole`;
`role` remains the project role. `GET /context` includes `principal.organizationRole`
and `principal.projectRole` (nullable when membership does not exist), independently
of scopes. The SDK preserves these fields.

For IAP, `PATCH /projects/{projectId}/members/{memberId}` with `{ role }` changes
the person's **organization role**, scopes, and existing IAP project memberships
in one audited transaction. Owner grants use `*`; Admin uses a fixed explicit
profile, not `*`. Owners can appoint/change other Owners; Owners/Admins can grant
Admin/Member/Viewer access. Admins cannot change Owners. Self changes and loss of
the last Owner are rejected. Only authenticated IAP humans qualify; API keys do
not. Lowering access revokes keys created by the target identity and any keys derived from them. Key issuance is serialized with role changes so an in-flight request cannot retain revoked authority. `DELETE` removes
organization and existing project memberships and revokes those keys, retaining
identity records as the rejoining guard. In IAP mode, `POST /members` accepts
`{ email }` for a user who has already completed a verified IAP login. It copies
that immutable identity into the current project and inherits the person's
stored organization role and scope profile. It never invites a new Google user
or changes their organization role. `DELETE .../project-access` removes only the
current project membership; the organization membership, other projects, and
organization-scoped API keys remain unchanged. Other authentication modes retain
the existing project-role and explicitly supplied scope behavior.

All routes require `project:admin`. Member and project writes also require an
`Idempotency-Key`. Non-IAP member create input is `{ subject, role, scopes }`;
IAP member create input is `{ email }`. Roles are `owner`, `admin`, `member`, or
`viewer`. A member response includes the
stable principal `subject`, safe scopes, role, and optional identity-provider
`displayName` and `email`. Provider metadata is presentational and does not
grant access.

### Create a project

`POST /projects` takes `{ "slug": "...", "name": "..." }`. The slug is 1-80
characters matching `^[a-z0-9][a-z0-9-]*$` and must be unique inside the
organization. The caller needs organization `owner` or `admin` role (an API
key qualifies through its scopes: `*` or `project:admin`). A human creator is
provisioned into the new project in the same transaction — a principal copy,
an `owner` membership, identity rows, and tenant links — so the identity
provider resolves into the new project immediately. The response is the
project object.

### Delete a project

`DELETE /projects/{projectId}` permanently removes the project and every row
it owns: agents, Skills, toolsets, presets, sessions, runs, events, and the
audit trail, plus the Flue conversation state of its threads. Two rules apply:
the project the caller is acting in cannot delete itself, and the last
remaining project of an organization cannot be deleted. In IAP deployments,
the configured default project also cannot be deleted because it anchors
console authentication. Organization-shared resources (API keys, model,
storage, sandbox, and MCP connections) are untouched.
Workspace archives and run files in object storage are not removed
automatically; operators clean the project's `workspace-backups/` and
`run-files/` prefixes separately.

Adding a member does not send a WorkOS invitation or create a provider mapping.
To let that person sign in through WorkOS, provision the existing OAO principal
with the operator workflow described in [Authentication](/reference/authentication).
The active principal cannot remove itself.

## Model providers, presets, and catalog

A published agent version names an immutable model preset. A hosted preset
references an OpenRouter, OpenAI, Anthropic, or xAI provider connection in the
same organization. Provider connections are organization-shared: every project
uses the same pool, addressed through any project path. Model presets remain
project-scoped.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/model-providers` | List safe provider and credential metadata. |
| `POST` | `/projects/{projectId}/model-providers` | Add a provider and encrypted API key. |
| `PUT` | `/projects/{projectId}/model-providers/{id}/credential` | Rotate the write-only API key. |
| `DELETE` | `/projects/{projectId}/model-providers/{id}` | Remove a connection (wipes the key). |
| `GET` | `/projects/{projectId}/model-catalog?providerId={id}` | List the matching live provider catalog. |
| `GET` | `/projects/{projectId}/model-presets` | List local, project, and unavailable legacy presets. |
| `POST` | `/projects/{projectId}/model-presets` | Add an immutable preset linked to a provider. |
| `DELETE` | `/projects/{projectId}/model-presets/{id}` | Archive a project preset. |

Reads require `agent:read`; writes require `project:admin`. Every write requires
an `Idempotency-Key`.

Create a provider with `{ key, displayName, providerType, apiKey }`, where
`providerType` is `openrouter`, `openai`, `anthropic`, or `xai`. Responses contain
`credentialConfigured`, a 12-character `credentialFingerprint`, and an
incrementing `credentialVersion`; they never contain the API key or encryption
material. Rotate a key with `{ "apiKey": "..." }`.

`DELETE /model-presets/{id}` archives a project preset and returns
`{ "id", "key", "archived": true }`. Presets stay append-only: the row, key,
model, routing, and settings are never changed or deleted, because published
agent versions pin the key. An archived preset leaves the list, can no longer
be named by a new agent version (`400`), and its key stays reserved (`409` on
reuse). Sessions of already published versions keep resolving it. Deployment
presets cannot be archived. The audit log records `model_preset.archived`.

`DELETE /model-providers/{id}` removes a connection. It is refused with `409`
while any live preset still routes through it; archive those presets first.
Removal wipes the encrypted key, leaves the list, releases the provider key for
a new connection, and returns `{ "id", "removed": true }`. Rotating or building
presets on a removed connection returns `404`, and runs of agents pinned to its
archived presets fail with a clear provider-removed error. The audit log records
`model_provider.removed`.

`GET /model-catalog` requires `providerId`, accepts `search` and `limit`, and
returns `providerId`, `providerType`, and safe catalog entries. Each entry
includes `providerType`, prefixed `model`, `catalogId`, display name, context
window, output limit, and reasoning support. Anthropic and supported xAI
reasoning entries may additionally include `adaptiveThinking`,
`thinkingCanBeDisabled`, and `effortLevels`.

Create a preset with:

```json theme={null}
{
  "key": "claude-sonnet-4-6-zdr-v1",
  "displayName": "Claude Sonnet 4.6 (zero retention)",
  "providerId": "55555555-5555-4555-8555-555555555555",
  "model": "openrouter/anthropic/claude-sonnet-4.6",
  "routing": {
    "zeroDataRetention": true,
    "dataCollection": "deny",
    "allowFallbacks": false,
    "providerAllowlist": ["anthropic"]
  }
}
```

The model must belong to the selected provider catalog. OpenRouter catalog
responses include live OpenRouter models and saved presets as
`openrouter/@preset/<slug>` entries. OpenRouter supports the routing policy
below. OpenAI catalog responses include the live models available to the
selected key in supported Responses text/reasoning families, including new
GPT-4.1, GPT-5-and-later, and o3-and-later IDs absent from the pinned catalog.
Specialized non-agent variants are excluded. Unknown context/output metadata
is returned as `null`; catalog requests fetch the account list afresh.
Entries without verified capabilities and pricing set `runtimeSupported: false`
and cannot be used to create presets (`400`). Omitted or `true` means supported.
Discovery limitations are described in
[Adding models](/models/adding-models#a-model-is-not-in-the-catalog).
OpenAI presets require an empty routing object and accept the immutable OpenAI
`settings` object below. GPT-6 Astra entries set `thinkingCanBeDisabled: false`
and list `low`, `medium`, `high`, `xhigh`, and `max` in `effortLevels`; creating
an Astra preset with `effort: "none"` returns `400`.
Anthropic catalog responses likewise include the live models available to the
selected key that OAO's native Messages runtime supports. Anthropic presets
require empty routing and accept the immutable Claude settings below. If an
older client omits `settings`, the API stores the provider's documented OAO
defaults.

xAI catalog responses come from the selected key's live
`GET /v1/language-models` result and include text-output Grok models as
`xai/<catalog-id>`. xAI presets require empty routing. Documented reasoning
models accept the immutable xAI settings below; if an older client omits them,
the API stores high effort and text format. Newly listed language-model IDs do
not require a hard-coded OAO catalog update, but reasoning controls appear only
after OAO can identify the model's supported effort levels.

### OpenAI model settings

| Field | Type | Default |
| - | - | - |
| `textFormat` | `"text"` | `"text"` |
| `mode` | `"standard"` \| `"pro"` | `"standard"` |
| `effort` | `"none"` \| `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | `"medium"` |
| `verbosity` | `"low"` \| `"medium"` \| `"high"` | `"medium"` |
| `summary` | `"auto"` \| `"concise"` \| `"detailed"` | `"auto"` |

The runtime translates these provider-neutral names to `text.format`,
`text.verbosity`, `reasoning.mode`, `reasoning.effort`, and
`reasoning.summary` in OpenAI Responses requests. Direct settings are rejected
for OpenRouter presets.

### Anthropic model settings

| Field | Type | Default |
| - | - | - |
| `thinking` | `"disabled"` \| `"adaptive"` | `"adaptive"` |
| `maxTokens` | integer from 1 through 300,000 | `20000` |
| `effort` | `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` \| `"max"` | `"high"` |

The selected live model must support the saved thinking mode and effort, and
`maxTokens` cannot exceed its advertised output limit. The runtime translates
these values to `thinking.type`, `max_tokens`, and `output_config.effort` in
Anthropic Messages requests. Anthropic's `max_tokens` is the combined ceiling
for thinking tokens and the visible response, not a separate thinking budget.
Models without live effort support are omitted from the OAO catalog. Manual
`thinking.type: "enabled"` and `budget_tokens` are not accepted by this preset
contract.

### xAI model settings

| Field | Type | Default |
| - | - | - |
| `textFormat` | `"text"` | `"text"` |
| `effort` | `"low"` \| `"medium"` \| `"high"` \| `"xhigh"` | `"high"` |

The selected catalog model must support the requested effort. OAO maps these
values to `text.format` and `reasoning.effort` in xAI Responses requests.
Reasoning cannot be disabled on supported Grok reasoning models. Tools and the
system prompt remain part of the immutable agent version rather than the model
preset.

### OpenRouter routing policy

| Field | Type | Meaning |
| - | - | - |
| `allowFallbacks` | boolean | Allow the provider to fall back to another route |
| `requireParameters` | boolean | Route only to upstreams honoring every parameter |
| `dataCollection` | `"deny"` \| `"allow"` | Requested training/data-collection policy |
| `zeroDataRetention` | boolean | Request zero-retention routing |
| `providerOrder` | string array, 1–16 unique slugs | Preference order |
| `providerAllowlist` | string array, 1–16 unique slugs | Only these upstream providers |
| `providerDenylist` | string array, 1–16 unique slugs | Never these upstream providers |
| `sort` | `"price"` \| `"throughput"` \| `"latency"` | Route preference |
| `maxPromptPriceUsdPerMillion` | number, 0–1,000,000 | Prompt price cap in USD per million tokens |
| `maxCompletionPriceUsdPerMillion` | number, 0–1,000,000 | Completion price cap in USD per million tokens |

### Failure modes

| Status | Cause |
| - | - |
| `400` | Malformed input, catalog/provider mismatch, or settings unsupported by the provider |
| `403` | Principal lacks the required scope, or the path project is outside its tenant scope |
| `404` | Provider connection does not exist in this organization |
| `409` | Provider or preset key already exists |
| `500` | Platform credential encryption is not configured |

<Warning>
  Provider create and rotation requests accept an API key as write-only secret
  input. No response, list route, event, audit detail, log, or trace returns it.
</Warning>

## Sandbox providers

Sandbox provider connections are organization-shared — every project uses
the same pool, addressed through any project path — and require
`project:admin` for writes. List reads use `agent:read`. `daytona` is the only supported
`providerType` today; the connection and agent contracts keep provider type
and provider key explicit so more adapters can be added later.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/sandbox-providers` | List safe connection and credential metadata |
| `GET` | `/projects/{projectId}/sandbox-providers/{id}/snapshots` | List all safe Daytona snapshot metadata |
| `POST` | `/projects/{projectId}/sandbox-providers` | Add an encrypted provider credential |
| `PUT` | `/projects/{projectId}/sandbox-providers/{id}/credential` | Rotate the write-only key |
| `PUT` | `/projects/{projectId}/sandbox-providers/{id}/configuration` | Change target and restricted-egress allowlist |

Create a connection with:

```json theme={null}
{
  "key": "daytona-primary",
  "displayName": "Daytona primary",
  "providerType": "daytona",
  "apiKey": "write-only-value",
  "target": null,
  "restrictedEgress": {
    "allowedDomains": ["api.example.com", "*.example.net"],
    "allowedCidrs": ["203.0.113.0/24"]
  }
}
```

Responses expose only `credentialConfigured`, a 12-character fingerprint,
credential version, target, and allowlists. Rotate with `{ "apiKey": "..." }`.
Update configuration with `{ target, restrictedEgress }`. All writes require
an `Idempotency-Key`; no read route returns credential or encryption material.

Snapshot discovery decrypts the selected connection only inside the API and
uses it to query Daytona. The response returns every snapshot visible to that
connection with safe identity, state, image, resource, region, class, and
timestamp metadata. It excludes credentials, build logs, build details, and
provider error payloads. `available` is true only when Daytona reports the
snapshot as `active`.

An agent version's sandbox object requires `enabled`, `provider`, `network`, and
`capabilities`. When `enabled` is true, it also requires an active snapshot UUID
as `snapshotId`.
Capabilities are unique values from `filesystem_read`,
`filesystem_write`, `shell`, and `browser`. Publishing an enabled sandbox
rejects an unknown project provider, missing encryption setup, a snapshot that
is not active for the selected Daytona connection, or a restricted network
policy whose selected connection has no allowlist.

## Object storage providers

S3-compatible storage connections are organization-shared: every project uses
the same pool of connections and the same organization-wide default. Writes
require `project:admin`; list reads use `agent:read`. Credentials are encrypted
with the platform credential key and are accepted only as write-only input.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/storage-providers` | List safe connection and credential metadata |
| `POST` | `/projects/{projectId}/storage-providers` | Add an encrypted S3-compatible connection |
| `PUT` | `/projects/{projectId}/storage-providers/{id}/credential` | Rotate write-only S3 credentials |
| `PUT` | `/projects/{projectId}/storage-providers/{id}/default` | Make the connection the organization default |
| `GET` | `/projects/{projectId}/storage-providers/{id}/objects` | List one folder level of stored objects |

Create input contains `key`, `displayName`, `providerType: "s3"`, nullable
HTTP(S) `endpoint`, `region`, `bucket`, nullable safe relative `prefix`,
`forcePathStyle`, optional `setDefault`, `accessKeyId`, `secretAccessKey`, and
optional `sessionToken`. The organization's first connection becomes the
default even when `setDefault` is false. All writes require an
`Idempotency-Key`.

Responses contain connection coordinates, default state, a 12-character
credential fingerprint, and credential version. They never contain S3
credentials or encryption material. Provider configuration is immutable;
credentials can rotate and the default can change. Each uploaded attachment and
each thread with an existing backup stays bound to the provider selected when
that object was first written.

The `objects` read lists one folder level under the project's tenant-scoped
storage root and uses `agent:read`. Optional query parameters are a safe
relative folder `prefix`, an opaque `cursor` from a previous truncated page,
and a `limit` from 1 through 1000. The response contains the normalized
`prefix`, `folders` (common prefixes ending in `/`), `objects` with `key`,
`sizeBytes`, and optional `lastModifiedAt`, plus `truncated` and, when
truncated, the next `cursor`. Keys are logical: they exclude the bucket,
configured provider prefix, and tenant path segments.

See [Send files to an agent](/integrations/files) and [Workspace
backups](/storage/workspace-backups) for object keys, checksum verification,
replacement-sandbox restore, and failure contracts.

## MCP servers, credentials, and toolsets

Remote MCP servers, credentials, and credential policies are
organization-shared and immutable/versioned: every project uses the same
connection pool through any project path. Toolsets remain project-scoped —
each project selects its own restricted tool sets from the shared servers. All
writes require `Idempotency-Key`. Secret values are accepted only on credential
create or rotation and never appear in a response.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/mcp-servers` | List servers and discovered tool snapshots. |
| `POST` | `/projects/{projectId}/mcp-servers` | Create a server and immutable version 1. |
| `POST` | `/projects/{projectId}/mcp-servers/{id}/discover` | Discover and snapshot remote tools. |
| `GET` | `/projects/{projectId}/mcp-credentials` | List redacted credential metadata. |
| `POST` | `/projects/{projectId}/mcp-credentials` | Store an encrypted write-only credential. |
| `POST` | `/projects/{projectId}/mcp-credentials/{id}/rotate` | Rotate the active encrypted version. |
| `DELETE` | `/projects/{projectId}/mcp-credentials/{id}` | Revoke the active credential. |
| `GET` | `/projects/{projectId}/mcp-credential-policies` | List exact-origin policy versions. |
| `POST` | `/projects/{projectId}/mcp-credential-policies` | Publish a credential policy version. |
| `GET` | `/projects/{projectId}/mcp-toolsets` | List restricted toolsets. |
| `POST` | `/projects/{projectId}/mcp-toolsets` | Publish selected discovered tools. |

Create a server with `{ key, displayName, endpointUrl, transport }`, where the
endpoint is HTTPS and transport is `streamable_http` or `legacy_sse`. Discover
with optional `{ credentialPolicyVersionId }`; authenticated discovery requires
an active policy whose exact origin and path prefix contain the endpoint.
Repeated discovery returns the current immutable version while metadata is
unchanged. Tool metadata drift publishes and returns the next server version;
existing toolsets remain pinned to their original server version.

Create a credential with `{ key, displayName, kind, headerName, secret }`.
`static_bearer` requires `headerName: null`; `api_key_header` requires a safe
header name. Responses contain only the fingerprint, version, lifecycle, and
`credentialConfigured: true`. Rotate with `{ secret }`.

Create a policy with `{ key, displayName, credentialId, exactOrigin,
pathPrefix, timeoutMs, maximumResponseBytes }`. Create a toolset with `{ key,
displayName, serverVersionId, tools: [{ remoteToolName, approval }] }`.
Discovery is capped at 256 tools; a toolset may select 1–64 tools.

Agent publication adds `mcpBindings`, each containing exact
`toolsetVersionId`, `credentialPolicyVersionId`, and a unique lowercase
`namespace`. Publication requires `mcp:bind`. See [Remote MCP
servers](/integrations/mcp) for runtime and egress behavior.

## Skills

Skills are PostgreSQL-authoritative, immutable procedural packages. Reads use
`skill:read`, package writes use `skill:write`, agent publication additionally
uses `skill:bind`, and lifecycle changes use `skill:revoke`.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/skill-drafts` | List editable package drafts. |
| `POST` | `/projects/{projectId}/skill-drafts` | Create a blank draft or clone a Skill version. |
| `GET` | `/projects/{projectId}/skill-drafts/{draftId}` | Read draft metadata, folders, and file bytes. |
| `PATCH` | `/projects/{projectId}/skill-drafts/{draftId}` | Save draft metadata and instructions. |
| `DELETE` | `/projects/{projectId}/skill-drafts/{draftId}` | Discard an editable draft. |
| `POST` | `/projects/{projectId}/skill-drafts/{draftId}/directories` | Create a package folder and missing parents. |
| `PUT` | `/projects/{projectId}/skill-drafts/{draftId}/files` | Create or replace one Markdown file. |
| `DELETE` | `/projects/{projectId}/skill-drafts/{draftId}/entries` | Remove one file or folder tree. |
| `POST` | `/projects/{projectId}/skill-drafts/{draftId}/validate` | Validate the complete staged package. |
| `POST` | `/projects/{projectId}/skill-drafts/{draftId}/publish` | Atomically publish an immutable version. |
| `GET` | `/projects/{projectId}/skills` | List Skills and their latest versions. |
| `POST` | `/projects/{projectId}/skills` | Create a Skill and immutable version 1. |
| `GET` | `/projects/{projectId}/skills/{skillId}` | Read all immutable versions and manifests. |
| `PATCH` | `/projects/{projectId}/skills/{skillId}` | Disable or enable a Skill. |
| `DELETE` | `/projects/{projectId}/skills/{skillId}` | Remove (archive) a Skill. |
| `POST` | `/projects/{projectId}/skills/{skillId}/versions` | Publish the next immutable version. |
| `GET` | `/projects/{projectId}/skills/{skillId}/versions/{versionId}/export` | Export exact instructions and resource bytes. |
| `PATCH` | `/projects/{projectId}/skills/{skillId}/versions/{versionId}/lifecycle` | Deprecate or revoke one version. |

Create and version bodies contain `name`, `description`, `instructions`,
optional `license`, `compatibility`, string-valued `metadata`, informational
`allowedTools`, and `files`. Creating a Skill also accepts `key` and
`displayName`. Each file is `{ path, contentType, dataBase64 }`. See
[Versioned Skills](/concepts/skills) for validation, package hashing,
progressive disclosure, lifecycle, and script-execution boundaries.

`PATCH /skills/{skillId}` requires `skill:revoke`, an `Idempotency-Key`, and
the body `{ "enabled": false }` or `{ "enabled": true }`. It returns
`{ "id", "key", "enabled", "disabledAt" }`. Disabling is reversible and never
touches bindings: a disabled Skill cannot be attached to a new agent version
(`400`) and stays in the list with a non-null `disabledAt`, while published
agent versions that pin it keep running with it. Enabling makes it attachable
again. Repeating the current state is a no-op that still returns `200`. The
audit log and product events record `skill.disabled` and `skill.enabled` only
when the state changed.

`DELETE /skills/{skillId}` requires `skill:revoke` and an `Idempotency-Key`. It
returns `{ "id", "key", "deleted": true }` and archives the Skill rather than
removing rows: versions, package files, agent-version bindings, and session
bindings are immutable and stay stored, so published agent versions and their
sessions keep running with it. After removal the Skill returns `404` from reads
and version publication, disappears from the list, cannot be attached to a new
agent version, and its open drafts are discarded. Its `key` becomes free for a
new Skill. Removal cannot be undone. To also stop existing sessions from using
one of its versions, revoke that version. The audit log and product events
record `skill.deleted`. Removing an already removed Skill returns `404`.

Create a blank draft with `{}`. To edit an existing package, pass exact
`skillId` and `sourceSkillVersionId` values; OAO copies that version's metadata
and files into the mutable draft. Directory bodies are `{ "path":
"references/customers" }`. File bodies use the same
`{ path, contentType, dataBase64 }` representation as direct publication and
currently require UTF-8 `.md` content with `text/markdown`. Entry deletion uses
`?path=references/customers&recursive=true` for a non-empty folder.

Draft paths are package-relative. They never address the API host filesystem or
a Daytona workspace. Published versions continue to use the existing direct
create/version endpoints, so API clients do not need to migrate to drafts.

Lifecycle accepts `{ "status": "deprecated" }` or
`{ "status": "revoked" }`. A deprecated version can later be revoked; a
revoked version cannot be restored. Package contents and bindings have no
update or delete endpoint.

## Agents and versions

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/agents` | List agent definitions. |
| `POST` | `/projects/{projectId}/agents` | Create an agent and publish version 1. |
| `GET` | `/projects/{projectId}/agents/{agentId}` | Read the definition and all versions. |
| `DELETE` | `/projects/{projectId}/agents/{agentId}` | Delete (archive) an agent. |
| `GET` | `/projects/{projectId}/agents/{agentId}/versions` | List immutable versions. |
| `POST` | `/projects/{projectId}/agents/{agentId}/versions` | Publish the next version. |

Creating an agent accepts `name`, optional `key` and `description`, and requires
`initialConfig` (or its `config` alias).
Agent list and detail responses include the latest immutable `sandbox` policy
so operators can determine delegate compatibility before publication.
See [Create an agent](/agents/create-agent) for the exact configuration shape.

`DELETE /agents/{agentId}` requires `agent:write` and an `Idempotency-Key`. It
returns `{ "id": "...", "deleted": true }` and archives the agent rather than
removing rows: sessions, runs, and delegate bindings keep their references to
the immutable versions, and existing session history stays readable. After
deletion the agent returns `404` from reads, disappears from the list, cannot
publish new versions, cannot start sessions (by `agentId` or by
`agentVersionId`), and cannot be picked as a new delegate. Its `key` becomes
free for a new agent. Coordinators that already pin one of its versions keep
working. The audit log records `agent.deleted`. Deleting an already deleted
agent returns `404`.
Set `initialConfig.sandbox.enabled`, `provider`, `snapshotId`, `network`, and
`capabilities`
to make the same sandbox selection available in API-driven creation as in the
console. `provider` must be a key returned by the sandbox provider list route;
there is no public fake-sandbox provider.

Set `config.skillVersionIds` to a unique array of active, exact Skill version
UUIDs from the same project whose Skills are enabled and not removed. Names must also be unique within one agent
version. The server records immutable agent-version bindings; the array is not
a reference to each Skill's moving latest version.

Set `config.harnessOperations` to up to 32 immutable entries with `{ key,
description, instructions, resultSchema, timeoutMs }`. Keys are 1–64
characters matching `^[a-z][a-z0-9_-]*$` and must be unique across the complete
mounted tool namespace. Descriptions are 1–2,000 characters, instructions are
1–100,000 characters, timeouts are 1,000–300,000 milliseconds, and
`resultSchema` is a required supported top-level object schema. The invocation
schema's serialized JSON is limited to 65,536 bytes. The invocation input is
fixed by OAO as `{ task: string }`, where `task` is 1–100,000
characters; clients do not publish an input schema.

Operations inherit the parent Agent version's model, rendered instructions,
tools, complete Skill catalog, and live session sandbox. They do not accept a
model, capability policy, sandbox, or `skillVersionId`. See [Harness
Operations](/concepts/harness-operations) for lifetime, shared-document,
structured-output, and sequential/batch behavior.

Set `config.delegates` to a unique roster of `{ key, description,
agentVersionId, maxParallel }`. Each UUID names an exact version of another
agent in the same project. The maximum is 32 bindings and `maxParallel` is 1–8.
The reserved platform tools `delegate_agent` and `message_agent` are supplied
by OAO and must not appear in `config.tools`. Coordinator and child versions
must have the same sandbox enabled state, provider, snapshot, and network
policy. An incompatible publication returns a `bad_request` that identifies the
child and directs the caller to publish a compatible child version first.

`config.modelPreset` must be an available deployment preset key or a preset
this project approved against a provider connection; the check runs inside
the same tenant transaction as the write. An unapproved or unavailable key
returns `400`. Linking a different model always means publishing a new version,
and existing sessions stay on the version they were created with.

## Sessions and runs

Create a session and its first run atomically:

```http theme={null}
POST /v1/projects/{projectId}/sessions

{
  "agentId": "d1111111-1111-4111-8111-111111111111",
  "title": "Repository review",
  "initialMessage": "Review the attached authentication boundary.",
  "files": [{
    "name": "auth.ts",
    "contentType": "application/typescript",
    "dataBase64": "ZXhwb3J0IGNvbnN0IGF1dGggPSB0cnVlOwo="
  }]
}
```

Use `agentVersionId` instead of `agentId` to pin an exact version yourself. If
you send `agentId`, OAO resolves its latest published version during the same
transaction. The response contains the session, `latestRunId`, and first queued
`run`. OAO copies the selected agent version's Skill bindings into immutable
session bindings in that transaction. Session create and follow-up bodies do
not accept a Skill list.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/sessions` | List sessions for the console or an integration. |
| `GET` | `/projects/{projectId}/sessions/{sessionId}` | Get transcript, runs, timing, pending work, and safe debug records. |
| `GET` | `/projects/{projectId}/sessions/{sessionId}/files` | List files in the latest persisted workspace backup. |
| `GET` | `/projects/{projectId}/sessions/{sessionId}/files/{path}` | Download one persisted workspace file. |
| `POST` | `/projects/{projectId}/sessions/{sessionId}/runs` | Submit a later message after the latest run settles. |
| `GET` | `/projects/{projectId}/runs/{runId}` | Read one run. |
| `POST` | `/projects/{projectId}/runs/{runId}/cancel` | Record cancellation intent and drain safely. |
| `GET` | `/projects/{projectId}/runs/{runId}/messages` | Page through public messages for a run. |
| `GET` | `/projects/{projectId}/runs/{runId}/timeline` | Page through the safe timing waterfall. |

Each session-list item created by delegation includes `parentSessionId` and
`delegateKey`. Both fields are `null` for root sessions. This lets clients group a
persistent child session beneath its coordinator without fetching every
session detail record.

Session list and detail responses report cumulative model usage as
`inputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`.
`cacheReadTokens` counts input tokens reused from a provider prompt cache;
`cacheWriteTokens` counts input tokens newly written to that cache. Providers
that do not report either value contribute zero. The detailed response repeats
these fields per model call under `debug.modelInvocations`.

A later turn accepts either `{ "message": "..." }` or the SDK-compatible
`{ "redactedInput": "..." }`, plus the same optional `files` array used during
session creation. Message text is limited to 100,000 characters and raw file
bytes to 10 MiB per file and 20 MiB per turn. A text field may be
omitted only when `files` is non-empty. File submissions require a
sandbox-enabled agent with read tooling, plus a default project storage
provider; binary formats require `shell`. The API uploads the original bytes to
that S3-compatible provider and stores only the safe run manifest and object
binding in PostgreSQL. See
[Send files to an agent](/integrations/files) for accepted media types, base64
encoding, and per-turn limits. Only one run can be active in a session thread;
submitting while its latest run is unsettled returns a conflict.

Session transcript messages and run-message pages include safe file metadata:
ID, filename, content type, byte size, SHA-256, and creation time. Stored bytes
and base64 are never stored in PostgreSQL or returned from those endpoints. The
runtime downloads and verifies the bound object, then copies the original bytes to
`.oao/attachments/{runId}/{filename}` under the sandbox's writable working
directory before the first model turn;
it does not extract file content into the prompt.

### Retrieve persisted workspace files

The session Files API exposes every regular file recorded in the latest
workspace-backup manifest. Both routes require `session:read` and the normal
authenticated project boundary. An API key can address a project only inside
its organization, and a signed-in user must resolve to that project; the
session and workspace-owner lookup remains tenant-filtered in PostgreSQL.

`GET /projects/{projectId}/sessions/{sessionId}/files` returns:

```json theme={null}
{
  "data": [
    { "name": "result.csv", "path": "output/result.csv", "sizeBytes": 42 }
  ],
  "generation": 3,
  "backedUpAt": "2026-09-01T10:00:00.000Z",
  "lastRunId": "b1111111-1111-4111-8111-111111111111"
}
```

A session without a completed backup returns an empty `data` array and `null`
backup fields. To download a file, append its exact relative `path`, encoding
each path segment independently:

```bash theme={null}
curl --fail-with-body \
  --header "Authorization: Bearer $OAO_API_KEY" \
  --output result.csv \
  "$OAO_API_URL/v1/projects/$OAO_PROJECT_ID/sessions/$OAO_SESSION_ID/files/output/result.csv"
```

The response is `application/octet-stream` with `Content-Disposition:
attachment`, `Content-Length`, `Cache-Control: no-store`, and
`X-Content-Type-Options: nosniff`. The API resolves delegated sessions through
their shared workspace owner, downloads the archive and manifest from the
workspace's bound persistent storage provider, verifies the recorded archive
length and SHA-256, validates the complete tar stream, and streams only the
requested regular file without writing it to the API host filesystem. It does
not read from Daytona. Missing sessions or paths return `404`; missing,
changed, or invalid persistent backup data returns `409` and no bytes.

The SDK exposes the same contract through `listSessionFiles(projectId,
sessionId)` and `downloadSessionFile(projectId, sessionId, path)`. A model can
therefore mention a relative path such as `output/result.csv`; the caller
combines that path with the known session ID without giving the model a storage
credential or provider URL.

The session detail response includes `debug.sandboxCommands`. Each item returns
the recorded `safeCommand` and `safeResult`, including model-facing arguments,
file/edit text, shell input, browser values, and tool output. The transcript
renders each call as a collapsed activity row; expanding it shows arguments
and responses directly as formatted, highlighted JSON, without a second payload
inspector. Caller tool calls from `debug.toolCalls` expand the same way and
show their recorded `safeArguments` and immutable `safeResult` once submitted;
successful envelopes display the response `value`, while failed envelopes
display the safe `error`. Tool events that carry only other safe metadata (for
example Skill activations) expand to that metadata instead.
Credential-bearing fields and obvious authorization values remain masked.
When a connector returns a JSON object or array encoded as a string, the console
decodes that string for display and unwraps a sole `{ "result": "{...}" }`
wrapper. This presentation step does not mutate the immutable stored result.

The same response includes top-level `skills` metadata with `skillId`, exact
`skillVersionId`, version number, name, description, content hash, and current
lifecycle status. It never includes Skill instructions or resource bytes.

It also includes top-level `tools`: the tool list of the pinned agent version
as `name`, `description`, `owner`, and `approval` per tool, in authored order
and without the input or output schemas. The console session sidebar lists
these next to the bound Skills and marks how many times each tool was called
during the session.

`debug.productEvents` and the project event stream include model progress before
the response completes:

| Event kind | Public payload |
| - | - |
| `model.invocation_started` | `turnId`, `model`, `provider`, `timeoutMs` (300000) |
| `model.retry_scheduled` | `retry` (1–3), `maximumRetries` (3), `delayMs`, `timeoutMs` (300000) |

Each agent model attempt has a five-minute deadline. Timeouts and transient
provider failures are retried up to three times after the initial attempt,
subject to the run deadline and model-turn budget. These progress events do not
indicate completion; use invocation outcomes and the run state for that. The
Console labels them **Model call started** and **Model retry scheduled**. See
[Model timeouts and retries](/models/adding-models#model-timeouts-and-retries)
for cancellation, compaction, and usage limitations.

If an early start observation could not be recorded, its outcome restores the
start event with the model's start time. Start events are deduplicated per turn,
including replay; a restored event can therefore arrive after the call finishes.

`debug.modelInvocations` includes each invocation's actual start/completion
window, OAO's normalized `finishReason`, and any provider thinking text returned
with the response. A failed invocation also includes the provider's exact
bounded `providerFinishReason` when available and a safe `errorExplanation`
describing why that finish condition is an error. The Console event inspector
labels these as **Provider finish reason** and **Why this is an error**.

The transcript renders each invocation as a collapsed **Reasoning** row.
Expanding it shows the full duration, token counts, and thinking text without
duplicating it in a payload tab. Provider reasoning signatures, raw provider
error messages, and partial response content are not returned. Invocation
records created before these diagnostics were projected can omit
`providerFinishReason` and `errorExplanation`. Older invocations whose runtime
projection recorded a zero-length window use the preceding message or tool
completion to reconstruct their visible duration.

Every transcript message and activity row, including tool calls, displays its
recorded event time in the browser's local timezone with millisecond precision.
Durations remain separate. Hover over a timestamp to see the full local date
and the exact UTC timestamp for comparison with server logs. The Debug timeline
and Harness Operation internal steps display the same timestamps.

## Delegations

The session detail response includes a top-level `delegations` array for
coordinator and child sessions. It contains the delegate key, exact child
version, child thread/session, shared workspace ID, relation state, and latest
child run/state.

| Method | Path | Scope |
| - | - | - |
| `GET` | `/projects/{projectId}/delegations/{delegationId}` | `delegation:read` |
| `POST` | `/projects/{projectId}/delegations/{delegationId}/messages` | `delegation:message` and `run:create` |
| `POST` | `/projects/{projectId}/delegations/{delegationId}/cancel` | `delegation:cancel` and `run:cancel` |

The message body is `{ "message": "..." }`. It creates a new run on the
same persistent child session only after that child's latest run settles and
returns `202` with `delegationId`, `childSessionId`, `childRunId`, and
`status: "queued"`. Both mutations require a fresh `Idempotency-Key`.

Cancelling closes the relation to future follow-ups and requests cancellation
of its latest unsettled child run. Raw prompts are not returned in delegation
events, audit detail, or tool-call safe arguments. See
[Multi-agent orchestration](/concepts/multi-agent-orchestration).

## Caller tools and approvals

New approval requests expire 24 hours after creation. The returned `expiresAt`
remains authoritative; existing and replayed gates retain their original expiry.
Approval waiting pauses the configured run execution deadline, including across
worker recovery, for the gated run and its requesting delegation ancestors.
Concurrent approval waits count once. Acceptance resumes the remaining budget
even when a caller has not yet submitted its tool result. See
[Approval waiting window](/agents/add-tools#approval-waiting-window) for the
independent submission ceiling.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/tool-calls?runId={runId}` | List tool work, optionally for one run. |
| `POST` | `/projects/{projectId}/tool-calls/{id}/claim` | Claim a caller tool for `leaseMs`. |
| `POST` | `/projects/{projectId}/tool-calls/{id}/renew` | Renew using the current fence. |
| `POST` | `/projects/{projectId}/tool-calls/{id}/release` | Release using the current fence. |
| `POST` | `/projects/{projectId}/tool-calls/{id}/result` | Submit one immutable safe result. |
| `GET` | `/projects/{projectId}/approvals?runId={runId}` | List approval requests. |
| `POST` | `/projects/{projectId}/approvals/{id}/decision` | Approve or deny a request. |
| `GET` | `/projects/{projectId}/pending-work` | List unresolved tool and approval work. |

The claim response includes a fence encoded as a positive integer string. The
same fence must accompany renew, release, and result operations.

Successful tool results use
`{ "version": 1, "status": "success", "value": { ... } }`. OAO validates
`value` against the immutable agent version's output schema before persistence.
If validation fails, the API stores a normalized `invalid_tool_result` failure,
returns `202` with `normalizedFailure.code` and `normalizedFailure.path`, and
resumes the agent. The raw invalid value is neither persisted nor sent to the
model. Retryable failures permit two automatic agent retries after the initial
attempt; further calls receive `tool_retry_exhausted`. Approval and cancellation
failures are never retried.

Tool input and output schema property names are part of the public caller-tool
contract because their values can appear in safe arguments, results, events,
and session diagnostics. Names classified as sensitive, such as `token`,
`apiKey`, `authorization`, `password`, or `secret`, are therefore rejected when
the agent is published. Use a domain-specific public name such as
`challengeCode` or `reference`; keep credentials in provider or MCP credential
storage. The API returns `400 bad_request` with the exact rejected path in
`error.details.path`.

## Event webhooks

Event webhooks push product events to a customer HTTPS endpoint in ordered,
signed batches. They are project-scoped and every route requires
`project:admin`. See [Event webhooks](/integrations/event-webhooks) for the
delivery format, signature verification, and a Convex receiver.

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/projects/{projectId}/event-webhooks` | List webhooks with delivery status |
| `POST` | `/projects/{projectId}/event-webhooks` | Create a webhook with a write-only signing secret |
| `GET` | `/projects/{projectId}/event-webhooks/{webhookId}` | Read one webhook and its delivery status |
| `PATCH` | `/projects/{projectId}/event-webhooks/{webhookId}` | Change name, URL, filter, message content, or enable |
| `PUT` | `/projects/{projectId}/event-webhooks/{webhookId}/credential` | Rotate the signing secret |
| `DELETE` | `/projects/{projectId}/event-webhooks/{webhookId}` | Delete the webhook and its encrypted secret |

Create a webhook with:

```json theme={null}
{
  "displayName": "Convex",
  "endpointUrl": "https://happy-animal-123.convex.site/oao/events",
  "signingSecret": "whsec_...",
  "eventKinds": ["run.*", "message.created"],
  "includeMessageContent": false,
  "deliverFrom": "now",
  "enabled": true
}
```

`signingSecret` is a Standard Webhooks secret: `whsec_` followed by base64
encoding 24 to 64 random bytes. `eventKinds` accepts exact kinds and family
wildcards such as `run.*`; `null` delivers every event. `deliverFrom` is `now`
(default) or `beginning`. The endpoint must use HTTPS and a public host.

The list response also reports `credentialEncryptionConfigured` and
`privateNetworkEndpointsAllowed`. The second is `true` only on development
servers that accept `http://` and private-network endpoints.

Responses expose `credentialConfigured`, a 12-character fingerprint, the
credential version, and delivery state: `status`, `deliveredPosition`,
`cursor`, `pendingEvents`, `consecutiveFailures`, `nextAttemptAt`, the last
attempt, success, and failure times, `lastResponseStatus`, and
`lastErrorCode`. `PATCH` accepts any subset of `displayName`, `endpointUrl`,
`eventKinds`, `includeMessageContent`, and `enabled`. Rotate with
`{ "signingSecret": "...", "previousCredentialTtlSeconds": 86400 }`; the
previous secret keeps signing for that many seconds, from `0` to `604800`.
`DELETE` returns `{ "id": "...", "deleted": true }`. All writes require an
`Idempotency-Key`, and a project can have at most 20 webhooks.

## Pagination

List endpoints accept `limit` and an opaque `cursor`. Responses use:

```json theme={null}
{
  "data": [],
  "pageInfo": {
    "hasMore": false,
    "nextCursor": null
  }
}
```

Never parse or synthesize a cursor. Pass `nextCursor` back unchanged.

## Errors

Non-success responses use a redacted envelope and include the request ID:

```json theme={null}
{
  "error": {
    "code": "conflict",
    "message": "Only a settled session can accept another run",
    "requestId": "..."
  }
}
```

Log the request ID and safe error code. Do not log credentials or resend a
failed write under a new idempotency key until you know whether it was accepted.

### Agent model-turn limits

Agent creation and version publication accept `config.limits.maxTurns` as a
required integer from `1` through `256`, for example
`"limits": { "maxTurns": 128, "timeoutMs": 300000 }`. Console and CLI creation
default to `32`; existing configurations with `32` remain valid. The bound of
256 limits runaway work while allowing longer imports. Invalid values return
`400`; timeout validation and enforcement remain independent.

The limit is stored in the immutable agent version and runtime snapshot. Existing
sessions do not adopt newly published values. Model turns, including scratch
calls, reserve a durable per-run budget before invoking the provider. Parallel
calls share the budget; compaction and worker recovery cannot reset it. Replaying
the same Flue turn ID reuses its reservation. Provider-internal retries do not
reserve another turn. Delegated runs have their own version and budget.

When exhausted, `model.invocation_failed` public payloads and model invocation
`safeResponse` include `errorCode: "model_turn_limit_exceeded"` and a safe
`errorExplanation` containing the configured maximum and next steps. No raw
provider error or credentials are exposed. Historical failures are not backfilled.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.