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

# Conductor API

> HTTP API exposed by the Orca Conductor, the stateless control plane used by the dashboard and external clients.

## Base URL

```bash theme={null}
https://api.orcapods.ai
```

For self-hosted deployments, the base URL is `http://localhost:8080`.

JSON endpoints return `application/json`. Errors use:

```json theme={null}
{ "error": "message" }
```

***

## Tenancy

Most conductor routes are tenant-scoped by the caller's `ao_` bearer API key (`Authorization: Bearer ao_...`); the tenant is derived from the key, and the server strips any client-supplied `X-Tenant-ID`. Tenant admin routes are operator-only: they require Postgres, `AGENT_ORC_ADMIN_TENANT`, and a verified caller whose tenant matches that admin tenant. Non-admin tenants receive `404 Not Found` for `/api/tenants*` routes so the surface is not enumerable.

When `POSTGRES_DSN` or the `POSTGRES_*` component variables are configured, the conductor verifies that the connecting role is not a `SUPERUSER` and does not have `BYPASSRLS`. A privileged role bypasses the row-level security policies that scope tenanted reads by `app.tenant_id`, so the process fails closed at boot. Use a non-superuser `NOBYPASSRLS` application role; see [Publishing an Agent](/guides/publishing#postgres-role-requirement) for the role grants.

### List Tenants

<ParamField path="GET /api/tenants" />

Query params:

| Query              | Description                  |
| ------------------ | ---------------------------- |
| `includeDeleted=1` | Include soft-deleted tenants |

Returns `{ "total": 1, "tenants": [...] }`.

### Create Tenant

<ParamField path="POST /api/tenants" />

```json theme={null}
{
  "id": "acme",
  "name": "Acme",
  "metadata": {}
}
```

### Get Tenant

<ParamField path="GET /api/tenants/{id}" />

### Update Tenant

<ParamField path="PUT /api/tenants/{id}" />

### Delete Tenant

<ParamField path="DELETE /api/tenants/{id}" />

Soft-deletes the tenant. The `default` tenant cannot be deleted.

***

## Profiles

### List Profiles

<ParamField path="GET /api/profiles" />

Returns an array of profiles registered on the connected runner or runner pool.
When `limit` is present, returns a page using `limit` and `offset` and includes
`X-Total-Count`; without `limit`, returns the full legacy list.

```bash theme={null}
curl https://api.orcapods.ai/api/profiles
```

```json theme={null}
[
  {
    "id": "agent-4f8a2e1b",
    "name": "general",
    "runtime": "general",
    "model": "anthropic:claude-haiku-4-5",
    "systemPrompt": "You are a general-purpose assistant...",
    "tools": ["@default", "web_search", "web_extract"],
    "mcpServers": []
  }
]
```

### Get Profile

<ParamField path="GET /api/profiles/{name}" />

Returns one stored profile by name. Unknown names return `404 Not Found`.

```bash theme={null}
curl https://api.orcapods.ai/api/profiles/general
```

**Response `200 OK`:** the stored profile.

### Create Profile

<ParamField path="POST /api/profiles" />

Creates a profile and broadcasts it to all runners. `id` is assigned when omitted.

| Field             | Type      | Required | Description                                                                                            |
| ----------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------ |
| `name`            | string    | Yes      | Unique profile name                                                                                    |
| `runtime`         | string    | Yes      | `claude`, `codex`, or `general`                                                                        |
| `model`           | string    | No       | Runtime-specific model identifier                                                                      |
| `systemPrompt`    | string    | No       | Agent instructions                                                                                     |
| `skills`          | string\[] | No       | Explicit skill names to resolve and inject into runs; confirmed unknown names return `400 Bad Request` |
| `tools`           | string\[] | No       | Tool selectors; empty means the default safe toolkit                                                   |
| `mcpServers`      | object\[] | No       | External MCP server specs                                                                              |
| `fs`              | object    | No       | Extra filesystem policy layered on the profile home                                                    |
| `workerMode`      | string    | No       | `static` (default) uses a fixed sidecar; `sandbox` launches one worker per session                     |
| `workerSubstrate` | string    | No       | Explicit sandbox-worker substrate: `e2b`, `daytona`, `docker`, or `process`                            |
| `workerImage`     | string    | No       | Image override for the selected substrate: E2B template, Daytona snapshot, or Docker image             |

Profile list and get responses may include `workerPlacement` for sandbox-worker
profiles when the Conductor can resolve the live runner fleet. It reports the
resolved `substrate`, `source`, and `servable` state, with `requested` and
`detail` when the requested placement cannot be honoured or the answer is
ambiguous. The field is computed on read, ignored on create or update, and
never persisted. See [Worker Placement](/concepts/profiles#worker-placement).

```bash theme={null}
curl -X POST https://api.orcapods.ai/api/profiles \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "researcher",
    "runtime": "general",
    "model": "anthropic:claude-sonnet-4-6",
    "systemPrompt": "You are a research assistant.",
    "tools": ["@default", "web_search", "web_extract"]
  }'
```

**Response `201 Created`:** the stored profile.

### Update Profile

<ParamField path="PUT /api/profiles/{name}" />

Replaces an existing profile and broadcasts the new definition to all runners. The request body uses the same fields as create. Skill validation uses the same source as `GET /api/skills`: the tenant-scoped Postgres skill store when DB-backed skills are wired, otherwise the in-memory local catalog. Unknown skill names return `400 Bad Request`.

**Response `200 OK`:** the stored profile.

### Delete Profile

<ParamField path="DELETE /api/profiles/{name}" />

Deletes a profile from every runner. Existing sessions keep their snapshot.

***

## Publishing Profiles

Publishing turns a conductor profile into a public chat-gateway route. The publish endpoints require Postgres. Key issuance also requires `AGENT_API_KEY_PEPPER`; plaintext API keys are returned exactly once and are stored only as HMAC hashes.

When pay-first billing is enabled, publishing uses the same billing gate as run creation: the tenant must be registered with Polar and have prepaid credit remaining.

### List Published Agents

<ParamField path="GET /api/published" />

Returns all active published agents in the current tenant:

```json theme={null}
{
  "total": 1,
  "publishedAgents": [
    {
      "id": "pub_abcd1234",
      "tenantId": "acme",
      "profileName": "support",
      "slug": "support-bot",
      "visibility": "private",
      "authMode": "api_key",
      "allowedOrigins": [],
      "rateLimitRpm": 60,
      "syncMaxDurationSeconds": 60,
      "exposeToolEvents": false,
      "enabled": true,
      "publicUrl": "https://agents.example.com/v1/chat/acme/support-bot"
    }
  ]
}
```

`publicUrl` is present only when `CHAT_GATEWAY_PUBLIC_HOST` is set.

### Get Published Agent

<ParamField path="GET /api/profiles/{name}/published" />

Returns the active published row for a profile, or `404 not_published`.

### Get Published Agent Metrics

<ParamField path="GET /api/profiles/{name}/metrics" />

Returns request, run, conversation, cost, and token metrics for the active published agent behind a profile. The response is scoped to one published agent; profiles that are not published return `404 not_published`.

Query params:

| Query    | Description                                                             |
| -------- | ----------------------------------------------------------------------- |
| `window` | Look-back window, such as `1h`, `24h`, `7d`, or `30d`; defaults to `1h` |

```json theme={null}
{
  "window": "7d",
  "since": "2026-07-01T12:00:00Z",
  "until": "2026-07-08T12:00:00Z",
  "bucket": "3h30m0s",
  "totals": {
    "requests": 431,
    "runs": 427,
    "conversations": 82,
    "costCents": 96,
    "tokens": {
      "inputTokens": 120000,
      "outputTokens": 28000,
      "cacheReadTokens": 0,
      "cacheCreateTokens": 0
    }
  },
  "buckets": [
    {
      "start": "2026-07-08T08:30:00Z",
      "end": "2026-07-08T12:00:00Z",
      "runs": 18,
      "failedRuns": 0,
      "runningRuns": 0,
      "tokens": {
        "inputTokens": 5400,
        "outputTokens": 1200,
        "cacheReadTokens": 0,
        "cacheCreateTokens": 0
      },
      "costCents": 4,
      "sandboxSeconds": 0,
      "ingressRequests": 18
    }
  ]
}
```

`buckets` uses the same shape as `GET /api/stats/timeseries` so dashboard charts can render the scoped view directly. `sandboxSeconds` and `failedRuns` are always `0` in this per-published-agent response because those values are not attributable to one published agent from `usage_records`.

### Publish Profile

<ParamField path="POST /api/profiles/{name}/publish" />

All fields are optional; omitted values default server-side.

| Field                    | Type           | Default                  | Description                                                        |
| ------------------------ | -------------- | ------------------------ | ------------------------------------------------------------------ |
| `slug`                   | string         | kebab-cased profile name | Public URL slug                                                    |
| `visibility`             | string         | `private`                | `private`, `org`, or `public`                                      |
| `authMode`               | string         | `api_key`                | `api_key` or `jwt`                                                 |
| `allowedOrigins`         | string\[]      | `[]`                     | Browser origins allowed by the chat gateway; `*` allows any origin |
| `rateLimitRpm`           | number         | `60`                     | Per-key request limit; `0` disables limiting                       |
| `syncMaxDurationSeconds` | number         | `60`                     | Sync chat timeout, bounded to `1..270`                             |
| `conversationTtlDays`    | number or null | null                     | Conversation retention; null keeps history forever                 |
| `exposeToolEvents`       | boolean        | `false`                  | Whether public streams include tool events                         |

### Update Published Agent

<ParamField path="PATCH /api/profiles/{name}/published" />

Partially updates the active published row. `slug` is immutable; unpublish and publish again to change it. The patch also accepts `enabled`.

### Unpublish Profile

<ParamField path="DELETE /api/profiles/{name}/published" />

Soft-deletes the active published row and returns `204 No Content`.

### List Agent Keys

<ParamField path="GET /api/profiles/{name}/keys" />

Returns active and revoked key metadata for the active published row. The key hash and plaintext token are never returned.

### Issue Agent Key

<ParamField path="POST /api/profiles/{name}/keys" />

```json theme={null}
{ "label": "production website", "expiresAt": "2026-12-31T00:00:00Z" }
```

Returns `201 Created` with key metadata plus one-time plaintext:

```json theme={null}
{
  "id": "key_abcd1234",
  "publishedId": "pub_abcd1234",
  "label": "production website",
  "token": "ao_prod_abcd..."
}
```

### Revoke Agent Key

<ParamField path="DELETE /api/profiles/{name}/keys/{id}" />

Revokes one key and returns `204 No Content`.

***

## Tenant API Keys

Tenant API keys are durable control-plane bearer credentials for SDKs, CI, and scripts calling `/api/*`. They are separate from published-agent keys under `/api/profiles/{name}/keys`: published-agent keys authenticate public chat-gateway traffic, while tenant API keys authenticate the conductor API directly.

These routes require Postgres. Key issuance also requires `AGENT_API_KEY_PEPPER`; plaintext tokens are returned exactly once and are stored only as HMAC hashes. A key inherits the minter's RBAC role, and route-level RBAC still applies.

### List Tenant API Keys

<ParamField path="GET /api/api-keys" />

Returns key metadata for the current tenant. Admins and owners see every key in the tenant; members see only keys they created. Tokens and hashes are never returned.

### Issue Tenant API Key

<ParamField path="POST /api/api-keys" />

```json theme={null}
{ "name": "ci-pipeline", "expiresAt": "2026-12-31T00:00:00Z" }
```

Returns `201 Created` with key metadata plus the one-time plaintext token:

```json theme={null}
{
  "id": "key_abcd1234",
  "tenantId": "default",
  "name": "ci-pipeline",
  "role": "admin",
  "createdBy": "user_123",
  "createdAt": "2026-06-19T10:00:00Z",
  "expiresAt": "2026-12-31T00:00:00Z",
  "token": "ao_prod_abcd..."
}
```

Send the token as `Authorization: Bearer <token>` on future conductor API requests. The conductor only verifies these keys when it has Postgres and a decodable `AGENT_API_KEY_PEPPER`.

### Revoke Tenant API Key

<ParamField path="DELETE /api/api-keys/{id}" />

Revokes one key and returns `204 No Content`. Members can revoke only their own keys; admins and owners can revoke any key in the tenant.

***

## Agent Pools

### List Pools

<ParamField path="GET /api/pools" />

Returns an array of named agent pools. When `limit` is present, returns a page
using `limit` and `offset` and includes `X-Total-Count`; without `limit`,
returns the full legacy list.

### Create Pool

<ParamField path="POST /api/pools" />

Creates a pool and broadcasts it to all runners.

```bash theme={null}
curl -X POST https://api.orcapods.ai/api/pools \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "research-team",
    "description": "Shared workspace for research agents",
    "members": [
      { "profile": "researcher", "role": "lead" },
      { "profile": "reviewer", "role": "member" }
    ],
    "fs": {
      "read": ["/datasets/**"],
      "write": ["/pools/{pool}/sot/**"]
    }
  }'
```

**Response `201 Created`:** the stored pool with an assigned `id`.

### Delete Pool

<ParamField path="DELETE /api/pools/{name}" />

***

## Sessions

### List Sessions

<ParamField path="GET /api/sessions" />

Returns all live session records visible to the conductor. When `limit` is
present, returns a page using `limit` and `offset` and includes
`X-Total-Count`; paged requests can also pass `q` to filter by session ID or
profile substring.

```json theme={null}
[
  {
    "id": "sess-a1b2c3d4-e5f6a7b8",
    "profile": "researcher",
    "runtime": "general",
    "status": "idle",
    "createdAt": "2026-04-25T10:00:00Z",
    "lastUsedAt": "2026-04-25T10:05:32Z",
    "lastPrompt": "Summarize...",
    "lastRunStatus": "ok",
    "runCount": 3
  }
]
```

Session statuses are `idle`, `running`, `errored`, and `shutdown`.

### Get Session

<ParamField path="GET /api/sessions/{sessionId}" />

Returns one session DTO with the same shape as list entries.

### List VFS Leases

<ParamField path="GET /api/vfs/leases" />

Returns active per-session VirtualFS leases visible to the conductor. In pooled deployments the conductor fans out to runners and merges the results; if no VFS manager is wired the response is `[]`.

```json theme={null}
[
  {
    "sessionId": "sess-a1b2c3d4-e5f6a7b8",
    "allowedMounts": ["/agents", "/pools"],
    "allocatedAt": "2026-04-25T10:00:00Z"
  }
]
```

An empty `allowedMounts` array means the VirtualFS session is unrestricted.

### Get Session VFS Lease

<ParamField path="GET /api/sessions/{sessionId}/vfs" />

Returns the VirtualFS lease for one session, or `404` with `{ "error": "no vfs session" }` when no lease exists.

### Delete Session

<ParamField path="DELETE /api/sessions/{sessionId}" />

Terminates a session on its owning runner.

### List Session Runs

<ParamField path="GET /api/sessions/{sessionId}/runs" />

Returns run summaries whose `subTask.sessionId` matches the session.

***

## Runs

### Create Run

<ParamField path="POST /api/runs" />

Submits a `SubTask`. The conductor creates a session when `sessionId` is omitted and returns immediately. Unknown profiles return `400` with an `unknown_profile: <name>` error.

Client-supplied `id` and `sessionId` values must be 1-128 characters and contain only letters, numbers, underscores, or hyphens. Invalid values return `400` with `invalid run id` or `invalid session id`. Leave either field empty or omit it when you want the conductor to mint a safe identifier.

When `sessionId` is supplied in a Postgres-backed deployment, the conductor first reuses the live runner session if it is still in memory. If the runner registry no longer knows the session after a conductor, runner, or sidecar restart, the conductor hydrates it from the durable session row and any saved state bundle before dispatching the run. A missing or shut-down durable session returns `404` instead of creating a new session under the caller-supplied id. Hydration infrastructure failures, such as a database read failure or runner transport error, return `503 Service Unavailable` so clients can retry instead of treating the session as gone.

When pay-first billing is enabled, run creation is blocked before session or sandbox allocation unless the tenant is registered with Polar and has prepaid credit remaining. Billing failures return `402 Payment Required` with a machine-readable `error` such as `registration_pending`, `credits_exhausted`, or `billing_unavailable`, plus `checkout_path: "/api/billing/checkout"`.

| Field       | Type      | Required | Description                                                                                         |
| ----------- | --------- | -------- | --------------------------------------------------------------------------------------------------- |
| `profile`   | string    | Yes      | Profile name                                                                                        |
| `sessionId` | string    | No       | Existing session to reuse; same identifier rules as `id`                                            |
| `id`        | string    | No       | Run ID; generated when omitted; only letters, numbers, underscores, and hyphens, max 128 characters |
| `parentId`  | string    | No       | Parent run for delegated work                                                                       |
| `title`     | string    | No       | Short label                                                                                         |
| `prompt`    | string    | No       | Task prompt                                                                                         |
| `files`     | string\[] | No       | File references                                                                                     |

```bash theme={null}
curl -X POST https://api.orcapods.ai/api/runs \
  -H 'Content-Type: application/json' \
  -d '{
    "profile": "researcher",
    "title": "Q1 analysis",
    "prompt": "Summarize the top AI trends in Q1 2026."
  }'
```

**Response `202 Accepted`:**

```json theme={null}
{
  "runId": "run-f3a9b72c",
  "sessionId": "sess-a1b2c3d4-e5f6a7b8"
}
```

### List Runs

<ParamField path="GET /api/runs" />

Returns newest-first run summaries.

```json theme={null}
[
  {
    "id": "run-f3a9b72c",
    "subTask": {
      "id": "run-f3a9b72c",
      "profile": "researcher",
      "sessionId": "sess-a1b2c3d4-e5f6a7b8",
      "title": "Q1 analysis",
      "prompt": "Summarize..."
    },
    "status": "ok",
    "startedAt": "2026-04-25T10:00:00Z",
    "finishedAt": "2026-04-25T10:00:42Z"
  }
]
```

Run statuses are `running`, `ok`, `error`, `cancelled`, and `interrupted`. `interrupted` marks runs orphaned by a conductor restart and closed by the boot reconciliation sweep.

### List Profile Runs

<ParamField path="GET /api/profiles/{name}/runs" />

Returns run summaries whose `subTask.profile` matches the profile.

### Get Run

<ParamField path="GET /api/runs/{runId}" />

Returns a run summary plus the buffered event log.

### Cancel Run

<ParamField path="DELETE /api/runs/{runId}" />

Cooperatively cancels a running run and returns `204 No Content`. The operation is idempotent: already-finished runs remain in their terminal status. An unknown run in the current conductor's in-memory registry returns `404 Not Found`.

### Force-Terminate Run

<ParamField path="POST /api/runs/{runId}/terminate" />

Force-stops a run when cooperative cancellation does not complete. The conductor independently attempts to cancel its local run context, wait up to two seconds for the session's run claim to drain, and change a still-running durable row to `cancelled`. If the claim does not drain, the conductor force-releases it. Once the durable row is `cancelled`, a late run completion cannot overwrite that status.

The endpoint is idempotent and returns `200 OK` even when the run is absent from the current conductor's in-memory registry, allowing it to recover runs stranded by a restart. Inspect every result field and `warnings` rather than treating the status code alone as proof that all steps completed.

```json theme={null}
{
  "runId": "run-f3a9b72c",
  "cancelled": true,
  "sessionReleased": true,
  "claimStolen": false,
  "statusWritten": true,
  "sessionId": "sess-a1b2c3d4-e5f6a7b8",
  "warnings": []
}
```

| Field             | Description                                                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `cancelled`       | This conductor found the run's cancel handle and invoked it                                                                               |
| `sessionReleased` | The session's run claim is no longer held, whether it drained normally or was stolen                                                      |
| `claimStolen`     | The claim did not drain in time and had to be forcibly taken; `true` indicates a genuinely wedged run                                     |
| `statusWritten`   | This request changed the durable run row from `running` to `cancelled`                                                                    |
| `sessionId`       | Session recovered from the in-memory run or durable row, when available                                                                   |
| `warnings`        | Stable codes for failed best-effort steps: `run_not_found`, `force_release_failed`, `force_release_unsupported`, or `status_write_failed` |

### Stream Run Events

<ParamField path="GET /api/runs/{runId}/stream" />

Opens a Server-Sent Events stream. Subscribers receive replayed buffered events first, then live events.

```
data: {"type":"assistant","message":"I'll research this now...","ts":"2026-04-25T10:00:00Z"}

data: {"type":"session_init","ts":"2026-04-25T10:00:01Z"}

data: {"type":"tool_call","toolCallId":"call_1","toolName":"web_search","input":{"query":"..."}}

data: {"type":"result","message":"Final answer...","ts":"2026-04-25T10:00:42Z"}
```

Event types are `progress`, `session_init`, `assistant`, `tool_call`, `tool_result`, `usage`, `result`, and `error`. The runtime session id is not on this SSE surface; read it from the raw `GET /api/runs/{id}/events` JSONL stream, which carries the underlying SDK/thread id per event, or from the session row's metadata.

***

## Sandbox Provider Events

### Invalidate Sandbox Worker State

<ParamField path="POST /api/sandbox-events/{provider}" />

Receives provider notifications that a sandbox may have changed state. `daytona` is currently the registered provider decoder. The route is public and deliberately does not require Orca authentication: a delivery can only invalidate a cached worker-lease status so the next read asks the provider again. It cannot set state, terminate a session, or destroy a sandbox.

Recognized sandbox events return `202 Accepted` with the number queued for asynchronous invalidation:

```json theme={null}
{ "accepted": 1 }
```

Unknown providers, malformed or irrelevant events, and events without a sandbox ID also return `202 Accepted` with `{ "ignored": true }` so provider retries do not turn ignored deliveries into an outage. An unreadable or larger-than-64-KiB body returns `400 Bad Request`.

The public ingress has a dedicated per-client-IP tier configured by `AGENT_ORC_RATELIMIT_SANDBOX_EVENTS_RPM` and defaulting to 600 requests per minute; exceeding it returns `429 Too Many Requests`. In conductor mode, accepted events are broadcast best-effort to every runner because provider sandbox IDs do not identify the owning runner. At most 32 invalidation fan-outs run concurrently. Further deliveries still receive `202`, but their fan-out is shed and `sandbox_event.fanout_shed` records that cached liveness remains stale until the next provider probe.

***

## Billing

Billing routes are admin-gated. In dev, when the conductor is not wired to the billing service, wallet and checkout calls return `503 Service Unavailable`. When `AGENT_ORC_ENV` is any non-`dev` value, the conductor instead refuses to start without a valid billing client so runs cannot silently bypass the credit gate.

### Get Billing Wallet

<ParamField path="GET /api/billing/wallet" />

Returns the tenant's live credit balance from Polar plus the purchasable top-up packs.

```json theme={null}
{
  "configured": true,
  "balanceMicroUSD": 16750000,
  "balanceUSD": 16.75,
  "packs": [
    { "cents": 2000, "usd": 20 }
  ]
}
```

### Create Billing Checkout

<ParamField path="POST /api/billing/checkout" />

Creates a Polar hosted checkout URL for a one-time prepaid credit pack. The `cents` body field must match one of the packs returned by `GET /api/billing/wallet`.

```bash theme={null}
curl -X POST https://api.orcapods.ai/api/billing/checkout \
  -H 'Content-Type: application/json' \
  -d '{ "cents": 2000 }'
```

**Response `200 OK`:**

```json theme={null}
{ "url": "https://sandbox.polar.sh/checkout/..." }
```

***

## Workflow Runs

<Note>
  Workflow run snapshots currently encode `status` and node `status` as integer enums. Create/start responses expose string status labels.
</Note>

Workflow run status mapping: `0=pending`, `1=running`, `2=paused`, `3=completed`, `4=failed`, `5=cancelled`.

Node status mapping: `0=pending`, `1=ready`, `2=running`, `3=ok`, `4=error`, `5=skipped`, `6=cancelled`.

### Create Workflow Run

<ParamField path="POST /api/workflows/runs" />

Creates a DAG workflow run. `autoStart` defaults to `true`.

```bash theme={null}
curl -X POST https://api.orcapods.ai/api/workflows/runs \
  -H 'Content-Type: application/json' \
  -d '{
    "userPrompt": "Compare LangGraph and AutoGen",
    "nodes": [
      {
        "id": "research-langgraph",
        "title": "Research LangGraph",
        "profile": "researcher",
        "promptTemplate": "Summarize LangGraph strengths."
      },
      {
        "id": "compare",
        "title": "Compare findings",
        "profile": "analyst",
        "promptTemplate": "Compare using {{research-langgraph.output}}",
        "dependsOn": ["research-langgraph"]
      }
    ]
  }'
```

**Response `202 Accepted`:**

```json theme={null}
{ "workflowRunId": "workflow-8b2a9c4f", "status": "pending" }
```

### List Workflow Runs

<ParamField path="GET /api/workflows/runs" />

Optional filters:

| Query               | Description                                                                              |
| ------------------- | ---------------------------------------------------------------------------------------- |
| `orchestratorRunId` | Only runs created by a parent run                                                        |
| `status`            | String status filter: `pending`, `running`, `paused`, `completed`, `failed`, `cancelled` |
| `since`             | RFC3339 timestamp                                                                        |
| `q`                 | Search workflow run ID, user prompt, or orchestrator session ID                          |
| `limit`             | Return a page and include `X-Total-Count`                                                |
| `offset`            | Page offset; used with `limit`                                                           |

### Get Workflow Run

<ParamField path="GET /api/workflows/runs/{workflowRunId}" />

Returns the full workflow run snapshot.

### Start Workflow Run

<ParamField path="POST /api/workflows/runs/{workflowRunId}/start" />

Submits a pending workflow run to the engine. Idempotent for non-pending runs.

### Cancel Workflow Run

<ParamField path="POST /api/workflows/runs/{workflowRunId}/cancel" />

Cancels pending and running nodes where possible.

### Repair Workflow Run

<ParamField path="POST /api/workflows/runs/{workflowRunId}/repair" />

Applies a repair action to a paused workflow run.

```json theme={null}
{ "type": "retry_node", "nodeId": "research-langgraph" }
```

Supported action types are `retry_node`, `replace_node`, `add_dependency`, and `abort`.

### Update Node Status

<ParamField path="POST /api/workflows/runs/{workflowRunId}/nodes/{nodeId}/status" />

Used by orchestrator-driven execution to mark a pending node terminal.

```json theme={null}
{ "status": "ok", "output": "Node result" }
```

Accepted statuses are `ok`, `error`, `skipped`, and `cancelled`.

### Stream Workflow Run

<ParamField path="GET /api/workflows/runs/{workflowRunId}/stream" />

SSE stream with a `snapshot` event followed by `plan_status` events. Each frame's payload is keyed by `workflowRun`:

```
event: snapshot
data: {"type":"snapshot","workflowRun":{...}}

event: plan_status
data: {"type":"plan_status","workflowRun":{...}}
```

### Create Workflow Definition

<ParamField path="POST /api/workflows/definitions" />

Creates a reusable workflow graph template. `name` and a non-empty `nodes` array are required.

```json theme={null}
{
  "name": "Review PR",
  "description": "Research and review a pull request",
  "userPrompt": "Review the requested PR",
  "nodes": [
    {
      "id": "review",
      "title": "Review",
      "profile": "reviewer",
      "promptTemplate": "Review the PR."
    }
  ],
  "defaults": {},
  "inputSchema": {},
  "metadata": {}
}
```

### List Workflow Definitions

<ParamField path="GET /api/workflows/definitions" />

Returns all in-memory workflow definitions.

### Get Workflow Definition

<ParamField path="GET /api/workflows/definitions/{id}" />

Returns one workflow definition.

### Update Workflow Definition

<ParamField path="PATCH /api/workflows/definitions/{id}" />

Updates supplied fields on an existing workflow definition.

### Delete Workflow Definition

<ParamField path="DELETE /api/workflows/definitions/{id}" />

Deletes one workflow definition. Returns `204 No Content` on success.

### Create Workflow Schedule

<ParamField path="POST /api/workflows/schedules" />

Creates an active schedule for an existing workflow definition. `workflowDefinitionId` and `cron` are required.

```json theme={null}
{
  "workflowDefinitionId": "wfdef-8b2a9c4f",
  "name": "Daily review",
  "cron": "0 9 * * *",
  "timezone": "UTC",
  "dedupeKeyTemplate": "daily-{{date}}",
  "input": {}
}
```

### List Workflow Schedules

<ParamField path="GET /api/workflows/schedules" />

Returns all in-memory workflow schedules.

### Get Workflow Schedule

<ParamField path="GET /api/workflows/schedules/{id}" />

Returns one workflow schedule.

### Pause Workflow Schedule

<ParamField path="POST /api/workflows/schedules/{id}/pause" />

Marks a workflow schedule as `paused`.

### Resume Workflow Schedule

<ParamField path="POST /api/workflows/schedules/{id}/resume" />

Marks a workflow schedule as `active`.

### Delete Workflow Schedule

<ParamField path="DELETE /api/workflows/schedules/{id}" />

Deletes one workflow schedule. Returns `204 No Content` on success.

***

## Skills Catalog

Skills are named instruction bodies that profiles can attach by name. Responses include `source`, which is `user`, `imported`, or `platform`, plus Agent Skills metadata such as `license`, `compatibility`, `allowedTools`, `metadata`, `resources`, and `requiresSandbox`. Platform skills are seeded `using-*` guidance entries tied to capability bundles and may be auto-attached from profile `tools`.

| Method   | Path                                  | Description                                                                                               |
| -------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `GET`    | `/api/skills`                         | List the full skill catalog, or a page with `limit` / `offset`; paged responses include `X-Total-Count`   |
| `POST`   | `/api/skills/import-package?dryRun=1` | Validate and stage an uploaded Agent Skills folder as repeated multipart `files` parts                    |
| `POST`   | `/api/skills/import-package/commit`   | Register a staged skill package by `stagingId`; pass `force: true` to replace an existing skill           |
| `POST`   | `/api/skills/import`                  | Legacy conductor-filesystem import; disabled unless `AGENT_ORC_ALLOW_FS_SKILL_IMPORT=1`                   |
| `POST`   | `/api/skills`                         | Create a user skill                                                                                       |
| `GET`    | `/api/skills/{name}`                  | Retrieve one skill                                                                                        |
| `PUT`    | `/api/skills/{name}`                  | Update or rename one skill                                                                                |
| `DELETE` | `/api/skills/{name}`                  | Delete one skill                                                                                          |
| `GET`    | `/api/skills/{name}/resources/{path}` | Fetch one supporting resource file; `path` may contain multiple segments                                  |
| `PUT`    | `/api/skills/{name}/resources/{path}` | Upload one supporting resource file; `path` may contain multiple segments; requires DB and object storage |
| `DELETE` | `/api/skills/{name}/resources/{path}` | Delete one supporting resource file; `path` may contain multiple segments; requires DB and object storage |

Folder import is a staged flow. First upload exactly one skill folder containing `SKILL.md`; each multipart `files` part must use the package-relative filename, such as `my-skill/SKILL.md` or `my-skill/scripts/run.sh`. The dry-run response returns validation details, resource metadata (`contentType`, `sha256`, `executable`), `requiresSandbox`, `totalBytes`, and a short-lived `stagingId`. Commit that `stagingId` to register the package.

Deleting a platform-seeded skill records a tombstone so later seed reconciliation does not restore that deleted `using-*` skill. Filesystem deployments store tombstones in `platform_skills_state.json`; Postgres deployments store them per tenant and reconcile platform skills lazily on the tenant's first skills-list or run-dispatch request.

***

## MCP Server Catalog

### List Catalog Entries

<ParamField path="GET /api/mcp-servers" />

Returns an array of catalog entries.

### Get Catalog Entry

<ParamField path="GET /api/mcp-servers/{name}" />

### Create Catalog Entry

<ParamField path="POST /api/mcp-servers" />

```bash theme={null}
curl -X POST https://api.orcapods.ai/api/mcp-servers \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "company-db",
    "transport": "http",
    "url": "https://db.internal/mcp",
    "description": "Internal warehouse tools",
    "headers": { "Authorization": "Bearer ${DB_API_KEY}" }
  }'
```

### Update Catalog Entry

<ParamField path="PUT /api/mcp-servers/{name}" />

`managedBy` is server-owned. Both create and update ignore a client-supplied value; provider-managed entries are created only through the Connected Apps flow.

### Delete Catalog Entry

<ParamField path="DELETE /api/mcp-servers/{name}" />

***

## Connected App Tool Facade

These tenant-scoped endpoints are available only for provider-managed toolkits that are active in the MCP catalog. They proxy discovery and execution through the internal MCP bridge so provider credentials do not enter profiles or worker run envelopes.

### List Connected App Tools

<ParamField path="GET /api/connected-apps/providers/{provider}/toolkits/{slug}/tools" />

Returns tool names, descriptions, and input schemas. Returns `403 Forbidden` when the toolkit is not active for the tenant.

### Call Connected App Tool

<ParamField path="POST /api/connected-apps/providers/{provider}/toolkits/{slug}/call" />

```json theme={null}
{
  "name": "GMAIL_FETCH_EMAILS",
  "arguments": {}
}
```

`name` is required. The response is the provider MCP tool result; bridge failures return `502 Bad Gateway`.

***

## Topology

### Get Runner Topology

<ParamField path="GET /api/topology" />

Available whenever the conductor has a runner pool, including a pool of one
runner. Returns an array in registry order. In addition to point-in-time health
and session data, each entry reports membership state:

* `live` — eligible for new sessions
* `draining` — existing sessions remain routable, but new sessions avoid the runner
* `unreachable` — the last runner probe failed; new sessions avoid the runner

`capabilitiesKnown` is `false` until `/runner/info` has answered successfully;
such a runner is not eligible for new sessions. A successful probe with an
empty `capabilities` list retains the backward-compatible meaning “supports all
runtimes.” Fields such as `process`, `sessionsByRuntime`, and `sidecars` are
`null` when the runner cannot provide that detail.

This tenant-facing response omits runner base URLs, sidecar endpoints, PIDs,
and Node versions. Sidecar `instanceId` values contain only the distinguishing
suffix of the underlying process identity; host prefixes are removed. Probe
and run errors also redact the associated infrastructure address. The full
shape remains available to operator tooling on the internal topology plane.

The nested details are intentionally observation-based:

* `process` is the runner's Go-process footprint (`allocBytes`, `sysBytes`,
  `heapInuseBytes`, `numGoroutine`, `numCPU`, and `uptimeSeconds`).
* `sessionsByRuntime` breaks down live sessions on that runner across
  runtimes.
* `sidecars` lists runtime slots. A slot can be an in-process `stub` or a
  `sidecar` with state `live`, `cold`, `degraded`, or `unreachable`; `cold`
  means configured but quiet and is not a fault.
* Each sidecar's `observedInstances` is a lower bound, not a replica count.
  Connection pooling can hide idle replicas, and slots sharing a sidecar
  process can report the same process.
* Instance and process statistics can be `null` until a health probe reaches
  that process. Null means not measured and must not be rendered as zero.

The topology fan-out is cached for 30 seconds and shared across concurrent
callers. When per-tenant rate limiting is enabled, `GET /api/topology` also has
its own tier, configurable with `AGENT_ORC_RATELIMIT_TOPOLOGY_RPM` and defaulting
to 30 requests per minute.

The dashboard's [Runtime](/dashboard/runtime) page uses this endpoint with a
manual Refresh action. It does not poll; a read can trigger a probe when the
shared cache has expired.

```json theme={null}
[
  {
    "hash": "a1b2c3d4",
    "healthy": true,
    "latencyMs": 8,
    "activeSessions": 3,
    "state": "live",
    "capabilitiesKnown": true,
    "capabilities": ["claude", "general"]
  }
]
```

### Manage Runner Membership (Internal)

These machine-facing routes are served on the conductor's `/internal/` service
plane, not on the tenant-scoped `/api/*` surface. They require the internal
signing-plane authentication described under [Resolve Secret](#resolve-secret).

<ParamField path="POST /internal/topology/reconcile" />

Re-probes the supplied runner URLs and adds or refreshes members. The body is
optional; an empty body re-reads `RUNNER_URLS` (or the legacy `RUNNER_URL`)
from the conductor environment. If a URL was previously represented only by an
unreachable placeholder, a successful probe replaces that placeholder with the
runner's advertised hash rather than leaving a duplicate topology entry.
Reconcile is otherwise additive: URLs no longer listed are not removed because
they may still own live sessions.

```json theme={null}
{ "runnerUrls": ["http://runner-1:7070", "http://runner-2:7070"] }
```

<ParamField path="POST /internal/topology/drain" />

<ParamField path="POST /internal/topology/undrain" />

<ParamField path="POST /internal/topology/remove" />

These routes accept `{ "hash": "a1b2c3d4" }`. Drain stops new sessions while
keeping existing sessions routable; undrain returns a member to service when
its last probe permits it. Remove deletes the member from this registry and
can make its remaining sessions unroutable, so drain first and wait for its
session count to reach zero.

Membership is per conductor process. In a horizontally replicated conductor,
each replica must reconcile or receive the lifecycle action; these routes do
not provide fleet-wide membership storage.

***

## Stats

All stats endpoints accept `window`, a Go duration string such as `5m`, `1h`, `24h`, or `168h`. The window is capped at 30 days.

| Method | Path                    | Description                                                                                                |
| ------ | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GET`  | `/api/stats/summary`    | Fleet totals, run/session counts, tokens, p95 duration                                                     |
| `GET`  | `/api/stats/agents`     | Per-profile stats; supports `limit`, `offset`, and `sort`                                                  |
| `GET`  | `/api/stats/timeseries` | Bucketed runs, tokens, spend, sandbox compute, and published-agent ingress requests; supports `bucket`     |
| `GET`  | `/api/stats/hotspots`   | Token consumers, failing agents, busy runners, long sessions                                               |
| `GET`  | `/api/usage`            | Tool-call, sandbox-compute, and published-agent ingress meters; supports `window`, `profiles`, and `limit` |

Agent sort values: `last_activity_desc`, `tokens_desc`, `failures_desc`, `runs_desc`, `sessions_desc`, `name_asc`.

### Stats Timeseries

<ParamField path="GET /api/stats/timeseries" />

Returns buckets on one shared time grid. Each bucket includes run counts, token usage, `costCents`, `sandboxSeconds`, and `ingressRequests`. `costCents`, `sandboxSeconds`, and `ingressRequests` are zero when the usage store is not configured. Sandbox seconds are attributed by the sandbox meter row's `recorded_at` timestamp, so a bucket contains sessions whose sandbox meter last refreshed inside that bucket rather than exact in-bucket compute.

```json theme={null}
{
  "window": "24h",
  "bucket": "1h0m0s",
  "buckets": [
    {
      "start": "2026-07-08T09:00:00Z",
      "end": "2026-07-08T10:00:00Z",
      "runs": 3,
      "failedRuns": 1,
      "runningRuns": 0,
      "tokens": {
        "inputTokens": 1200,
        "outputTokens": 420,
        "cacheReadTokens": 0,
        "cacheCreateTokens": 0
      },
      "costCents": 18,
      "sandboxSeconds": 42.75,
      "ingressRequests": 31
    }
  ]
}
```

### Usage Meters

<ParamField path="GET /api/usage" />

Returns the tool-call, sandbox-compute, and published-agent ingress meter totals recorded in `usage_records`. `window` uses the same values and 30-day cap as the stats endpoints. `totals` is always the tenant-wide rollup for the window.

Query params:

| Query      | Description                                                                                                                                                                                                                                                                                 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `window`   | Look-back window, such as `1h`, `24h`, `7d`, or `30d`                                                                                                                                                                                                                                       |
| `profiles` | Optional comma-separated profile names. When present, `byProfile` returns meter rows for exactly those profiles, which lets a paginated UI join meters onto the current `/api/stats/agents` page. Empty, duplicate, and whitespace-only names are ignored; the list is capped at 500 names. |
| `limit`    | Fallback max rows for the top profiles by tool calls when `profiles` is absent; default 50, max 500                                                                                                                                                                                         |

```json theme={null}
{
  "window": "7d",
  "totals": {
    "toolCalls": 128,
    "sandboxSeconds": 42.75,
    "ingressRequests": 431
  },
  "byProfile": [
    {
      "profile": "orchestrator",
      "toolCalls": 64,
      "sandboxSeconds": 21.5
    }
  ],
  "byPublishedAgent": [
    {
      "publishedId": "pub_abcd1234",
      "slug": "support-bot",
      "requests": 431
    }
  ]
}
```

***

## Storage

Storage is backed by the optional S3-compatible artifact client configured from `AWS_*` / `S3_*` environment variables.

### Storage Info

<ParamField path="GET /api/storage/info" />

Always returns `200 OK`. When storage is not configured, `configured` is false.

```json theme={null}
{
  "configured": true,
  "bucket": "agent-artifacts",
  "usedBytes": 12400,
  "objectCount": 8,
  "capacityBytes": 1073741824,
  "breakdown": [{ "prefix": "agents", "bytes": 9200, "count": 6 }]
}
```

### List Objects

<ParamField path="GET /api/storage/objects" />

Query params:

| Query    | Description                        |
| -------- | ---------------------------------- |
| `prefix` | Key prefix filter                  |
| `limit`  | Max objects to return; default 100 |

### Get Object

<ParamField path="GET /api/storage/objects/{key}" />

Returns inline object content. `key` may contain multiple path segments. Non-UTF-8 content is base64 encoded.

### Upload Object

<ParamField path="PUT /api/storage/objects/{key}" />

Stores the raw request body at `key`. The `Content-Type` header is preserved. Inline payloads are capped at 8 MiB; oversize requests return `413 Payload Too Large`.

```bash theme={null}
curl -X PUT https://api.orcapods.ai/api/storage/objects/agents/general/notes.md \
  -H 'Content-Type: text/markdown' \
  --data-binary @notes.md
```

```json theme={null}
{
  "key": "agents/general/notes.md",
  "contentType": "text/markdown",
  "size": 2048,
  "etag": "\"abc123\""
}
```

### Delete Object

<ParamField path="DELETE /api/storage/objects/{key}" />

Removes one object. When `key` ends with `/`, the path is treated as a folder prefix and every object under it is deleted (capped at 1000 entries — one List page).

```bash theme={null}
curl -X DELETE https://api.orcapods.ai/api/storage/objects/agents/general/notes.md
curl -X DELETE https://api.orcapods.ai/api/storage/objects/agents/general/scratch/
```

```json theme={null}
{ "key": "agents/general/scratch/", "deleted": 14 }
```

***

## Secrets

Secrets are tenant-scoped and envelope-encrypted. These routes return `503 Service Unavailable` unless both Postgres and `AGENT_ORC_MASTER_KEY` are configured. Plaintext is required on writes and is never returned by metadata endpoints.

### List Secrets

<ParamField path="GET /api/secrets" />

Returns metadata only:

```json theme={null}
{
  "total": 1,
  "secrets": [
    {
      "name": "r2-access-key",
      "key": "AWS_ACCESS_KEY_ID",
      "description": "R2 access key",
      "algorithm": "xchacha20poly1305"
    }
  ]
}
```

### Create Secret

<ParamField path="POST /api/secrets" />

```json theme={null}
{
  "name": "r2-access-key",
  "key": "AWS_ACCESS_KEY_ID",
  "plaintext": "secret value",
  "description": "R2 access key"
}
```

`key` is optional and names the canonical environment variable or credential slot the value is meant to populate.

### Update Secret

<ParamField path="PUT /api/secrets/{name}" />

Uses the same body as create. If `name` is present in the body, it must match the path.

### Delete Secret

<ParamField path="DELETE /api/secrets/{name}" />

Deletes the secret metadata and ciphertext.

### Resolve Secret

<ParamField path="POST /internal/secrets/resolve" />

Internal-plane endpoint for sibling services, such as VirtualFS, that need to dereference a `secret://<name>` ref into plaintext under the same tenant boundary. The route is served on the conductor's `INTERNAL_PORT` listener under the `/internal/` sub-mux and is **not** exposed on the public `/api/*` surface.

Authentication: chatsig HMAC using the `VFS_INTERNAL_SIGNING_KEY_CURRENT` key family (distinct from the chat-gateway key family). Required request headers:

* `X-Chat-Gateway-Signature`
* `X-Chat-Gateway-Timestamp`
* `X-Chat-Gateway-Nonce`
* `X-Chat-Gateway-Key-ID` (e.g. `CURRENT`)
* `X-Chat-Gateway-Tenant` (the tenant the resolution is scoped to; stamped onto the request context by the verifier)

Request body:

```json theme={null}
{ "ref": "secret://r2-access-key" }
```

Response:

```json theme={null}
{ "value": "...", "key": "AWS_ACCESS_KEY_ID" }
```

Literal values and `${ENV_VAR}` are rejected; `ref` must start with `secret://`. A signed request whose `X-Chat-Gateway-Tenant` does not own the named secret returns 404 (non-enumerating). Missing, malformed, or wrong-key-family signatures return 401.

***

## VirtualFS Proxy

When `VFS_BASE_URL` is set on the conductor, it mounts a reverse proxy at `/api/vfs/` for the standalone VirtualFS server. The proxy strips the `/api` prefix before forwarding, so `GET /api/vfs/mounts` reaches upstream `GET /vfs/mounts`.

`GET /api/vfs/leases` is the conductor's runtime lease endpoint documented in [Sessions](#sessions), not a proxied standalone VirtualFS route.

| Environment      | Description                                                                                                                         |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `VFS_BASE_URL`   | Base URL for the standalone VirtualFS service. When unset or invalid, the conductor does not mount `/api/vfs/`.                     |
| `VFS_AUTH_TOKEN` | Optional bearer token injected by the conductor when forwarding to VirtualFS. Client-supplied `Authorization` headers are stripped. |

Upstream connection failures return `502 Bad Gateway` with `vfs upstream unavailable`.

```bash theme={null}
curl https://api.orcapods.ai/api/vfs/mounts
```

***

## Internal Chat-Gateway Listener

When `INTERNAL_PORT` is set, the conductor also starts a private listener for the public chat gateway. The listener serves only:

| Method | Path                    |
| ------ | ----------------------- |
| `GET`  | `/healthz`              |
| `POST` | `/api/sessions`         |
| `POST` | `/api/runs`             |
| `GET`  | `/api/runs/{id}`        |
| `GET`  | `/api/runs/{id}/stream` |

All `/api/*` requests on this listener must carry the signed chat-gateway headers. The verified `X-Chat-Gateway-Tenant` drives tenant scoping; inbound `X-Tenant-ID` is ignored on this private surface.

| Environment                         | Description                                            |
| ----------------------------------- | ------------------------------------------------------ |
| `INTERNAL_PORT`                     | Enables the second listener                            |
| `CHAT_GATEWAY_SIGNING_KEY_CURRENT`  | Base64 HMAC key that matches the gateway               |
| `CHAT_GATEWAY_SIGNING_KEY_PREVIOUS` | Optional previous key during rotation                  |
| `REDIS_URL`                         | Nonce replay store; falls back to in-memory when unset |

***

## Memory Bank

Per-profile long-lived memory plus a global admin view. See [Memory Bank](/concepts/memory-bank) for the data model, ranking, and prompt injection.

All endpoints return `503 Service Unavailable` when the bank is not wired.

### List Profile Memories

<ParamField path="GET /api/profiles/{name}/memories" />

Query: `limit` (default `100`), `offset` (default `0`).

```json theme={null}
{
  "profile": "researcher",
  "total": 12,
  "limit": 100,
  "offset": 0,
  "memories": [
    {
      "id": "mem-3a8f1c20",
      "profileName": "researcher",
      "rawInput": "I prefer to return structured JSON when the caller provides a schema",
      "processedContent": "The agent prefers returning structured JSON whenever the caller supplies a schema.",
      "summary": "Prefers JSON when schema given",
      "category": "preference",
      "source": "explicit",
      "confidence": 1.0,
      "createdAt": "2026-04-25T10:00:00Z",
      "lastAccessedAt": "2026-04-28T09:00:00Z",
      "accessCount": 4,
      "stalenessScore": 0.04,
      "isActive": true,
      "version": 1
    }
  ]
}
```

### Create Profile Memory

<ParamField path="POST /api/profiles/{name}/memories" />

Body fields (`MemoryCreateRequest`):

| Field              | Type   | Required | Description                                                                        |
| ------------------ | ------ | -------- | ---------------------------------------------------------------------------------- |
| `rawInput`         | string | one of   | Free text — LLM extracts structure when set                                        |
| `processedContent` | string | one of   | Pre-structured statement — skips the LLM                                           |
| `summary`          | string | —        | Short label (auto-derived when blank)                                              |
| `category`         | string | —        | `preference` \| `fact` \| `behavior` \| `context` \| `general` (default `general`) |
| `source`           | string | —        | `explicit` \| `inferred` (default `explicit`)                                      |
| `confidence`       | number | —        | `[0, 1]` (default `1.0`)                                                           |

Either `rawInput` or `processedContent` is required. Returns `201 Created` with the stored memory. Unknown profile returns `404`.

### Search Profile Memories

<ParamField path="GET /api/profiles/{name}/memories/search" />

Query: `q` (search string, empty matches all), `limit` (default `8`), `minScore` (default `0.05`).

```json theme={null}
{
  "profile": "researcher",
  "query": "json schema",
  "count": 1,
  "results": [
    {
      "memory": { "id": "mem-3a8f1c20", "summary": "Prefers JSON when schema given", "...": "..." },
      "relevance": { "score": 0.82, "recency": 0.95, "usage": 0.68, "topic": 0.83 }
    }
  ]
}
```

### Get Profile Memory

<ParamField path="GET /api/profiles/{name}/memories/{id}" />

### Delete Profile Memory

<ParamField path="DELETE /api/profiles/{name}/memories/{id}" />

```json theme={null}
{ "id": "mem-3a8f1c20", "deleted": true }
```

### Get Memory Bank

<ParamField path="GET /api/memory-bank" />

Global admin view used by the dashboard's Memory Bank page. Without `limit`,
returns the legacy grouped response. With `limit`, returns a flattened page
using `limit` and `offset`; `q` searches memory content, summary, profile name,
or ID, and `category` filters by memory category. Paged responses include
`total`, `profilesWithMemory`, `limit`, `offset`, and `items`.

```json theme={null}
{
  "total": 27,
  "profilesWithMemory": 3,
  "limit": 25,
  "offset": 0,
  "items": [{ "id": "mem-3a8f1c20", "profileName": "researcher", "...": "..." }]
}
```

### Get Memory Bank Stats

<ParamField path="GET /api/memory-bank/stats" />

```json theme={null}
{
  "totalMemories": 27,
  "profilesWithMemory": 3,
  "perProfile": { "researcher": 12, "analyst": 9, "general": 6 }
}
```

***

## Health & Metrics

### Health Check

<ParamField path="GET /healthz" />

Returns text/plain `ok`.

### Prometheus Metrics

<ParamField path="GET /metrics" />

Returns Prometheus text format metrics when the process metrics handler is mounted.
