> ## Documentation Index
> Fetch the complete documentation index at: https://opensandbox-docs-ask-context.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Management API

> Create sessions, send turns, read the event log and manage projects over HTTP

The management API is the HTTP surface behind the CLI, the dashboard, the
SDK helpers and the React hook's proxy routes. Use it from trusted server
code to run agents from your own application.

## Base URL and authentication

```text theme={null}
https://app.opencomputer.dev/api/managed-agents
```

Every request carries an API key in the `x-api-key` header. A key belongs to
an organization and reaches every project, agent and session in it. Keep it
on the server; the [React integration](/agents/react) shows how a browser
attaches through your own routes without seeing the key.

Your server must authenticate callers and enforce which projects,
environments, agents and sessions they may access. A list filter or session
label is not an authorization boundary.

Request and response bodies are JSON. Paths below are relative to the base
URL; `<p>` is a project ID and `<r>` a memory resource ID. The
[TypeScript client](/reference/typescript-sdk/agents) on
`@opencomputer/sdk/agents` mirrors these routes one to one.

## Errors

Error bodies are `{ error: { code, message } }`. The `code` is stable and is
what to branch on; the `message` is short and safe to show.

| Status | Meaning |
| - | - |
| `400` | Invalid body, query or header |
| `401` | Missing or invalid API key |
| `402` | `insufficient_credits`: prepaid credits are exhausted |
| `403` | The key may not perform this operation |
| `404` | The route or the target does not exist |
| `409` | The request conflicts with current state, for example a reused `Idempotency-Key` |
| `429` | Too many requests; retry after `Retry-After` |
| `5xx` | Temporary failure; retry with the same idempotency key |

A missing key and an unknown route return `{ "error": "<text>" }` without a
code.

## Sessions

A session is a durable conversation with one deployed agent. It keeps the
deployment it was created on; see [Sessions and turns](/agents/sessions).

| Operation | Method and path | Success |
| - | - | - |
| Create | `POST /sessions` | `201` with the session, `200` when the key had already created it |
| Get | `GET /sessions/<id>` | `200` with the session |
| List | `GET /sessions` | `200` with `{ sessions, nextCursor }`: one page of rows |
| Labels | `PATCH /sessions/<id>/labels` | `200` with the session |
| End | `POST /sessions/<id>/end` | `200` with the ended session |
| Interrupt | `POST /sessions/<id>/interrupt` | `200` with the session |
| Dismiss a question | `POST /sessions/<id>/questions/<questionId>/dismiss` | `200`; see [Questions](#questions) |

### Create

```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/managed-agents/sessions' \
  -H 'x-api-key: <api-key>' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: topic/workshop/1' \
  -d '{
    "agentId": "<agent-id>@development",
    "labels": { "topic": "workshop", "requested_by": "u-42" },
    "memory": {
      "notes": { "scope": "document", "id": "workshop", "access": "read-write" }
    }
  }'
```

| Field | Meaning |
| - | - |
| `agentId` | `<agent-id>@development` or `<agent-id>@production`. The alias selects the environment and the deployment active in it. A bare agent ID means `production`. This is how applications address an agent; which deployment runs the session is the platform's choice, recorded on the session. |
| `deploymentId` | Advanced: pin one deployment instead of resolving an alias, for reproducibility or testing. Send `environment` with it; it may be omitted only when the deployment is promoted to exactly one environment. |
| `environment` | `development` or `production` for legacy projects; `default` for single-environment projects. Optional with `agentId`; it must agree with the alias. |
| `memory` | Bindings keyed by resource ID, at most eight. Needs a deployed agent and an environment. Shapes on [Document memory](/agents/document-memory#session-bindings). |
| `source` | `api` (default), `playground` or `webhook`. The dashboard groups sessions by it. |
| `labels` | Your own metadata on the session, an object of strings; see [Labels](#labels). Applied at creation and ignored on a replay of the same `Idempotency-Key`. |
| `externalReference` | Your own reference for the session, such as the id of the record it works for: a string of 1 to 256 bytes of UTF-8 without control characters. Stored as given and never interpreted; it is not shown to the agent, grants nothing and does not replace the session id or the `Idempotency-Key`. Returned on the session, its list row and every `session.*` event; see [External references](#external-references). |

The response is `{ session: { id, executionMode, status, createdAt, externalReference? }, deployment }`.
The session starts without a turn; send one with the turns route.

`Idempotency-Key` (at most 256 characters) makes a retry safe. The key
identifies the session within your organization:

* The same key with the same request, meaning the same `agentId` (or the
  same pinned `deploymentId`), environment and memory bindings, returns the
  existing session with `200`, together with the deployment it pinned when
  it was first created. That holds after a redeploy or a rollback: the
  session keeps the deployment it started with, and your retry does not
  need to know which one that was. A new session under a new key gets the
  deployment active at that time.
* Anything else under that key is `409 idempotency_conflict`: another
  agent, environment or pinned deployment, different memory bindings, or a
  different `externalReference` (including adding or dropping one).
  Pinning the deployment the session already holds counts as the same
  request.
* Two identical requests under one key that arrive together create one
  session; both answers name it, one with `201` and the other with `200`.
* Sessions created before this rule was recorded keep the earlier one: a
  replay through an alias succeeds only while the alias still resolves to
  the deployment the session pinned.
* `503 memory_admission_unconfirmed` means the memory grants were not
  confirmed in time. Retry with the same key; the retry resumes the wait.
* `503 session_publication_unconfirmed` means the session exists but its
  row in the list was not confirmed in time; `error.sessionId` names it.
  Retry with the same key and the same body until the answer is `201` or
  `200`. A success means [`GET /sessions`](#get-and-list) reflects the
  session; the platform keeps publishing the row in the background either
  way.

Other codes: `400 invalid_environment`, `400 invalid_memory_binding`,
`400 invalid_labels`, `400 invalid_external_reference`, `404
deployment_not_found`, `409 deployment_not_promoted` (a pinned deployment is
not active in the named environment), `402 insufficient_credits`.

### Get and list

`GET /sessions/<id>` returns the session:

| Field | Meaning |
| - | - |
| `id`, `agentId`, `deploymentId` | The session and the deployment it runs on, chosen when it was created |
| `projectId` | The project the agent belongs to |
| `environment` | `development` or `production`, when the request named one |
| `status` | `new`, `connecting`, `idle`, `running`, `stopping`, `waiting_runtime`, `suspending`, `suspended`, `resuming`, `failed` or `ended` |
| `source` | The `source` given at creation |
| `labels`, `labelsUpdatedAt` | Your metadata, and when it last changed; see [Labels](#labels) |
| `externalReference` | The reference given at creation; absent when none was given |
| `memory` | Bindings with `resource`, `scope`, `id`, `access` and `writable`; present when the session was created with bindings |
| `turns` | Every turn: `id`, `input`, `mode`, `status`, `createdAt`, `updatedAt`; `outcome: "question"` on a completed turn that ended by asking; and `deliveries` when an [event subscription](#event-subscriptions) selected its outcome |
| `result` | The latest committed output of the agent's [result tool](/agents/tools#the-session-result), or `null`: `{ turnId, callId, reportedAt, data }`. `turnId` and `callId` name the call that reported `data`, so a reader can tell which turn it came from and whether a later turn changed things without reporting. A failed or cancelled turn never clears it. |
| `question` | The open [question](/agents/sessions#questions) the agent asked with `ask`, or `null`: `{ id, text, options, context?, askedAt }`, `options` an array of `{ label, value }`, empty for free text, `context` the Markdown the agent gave to help decide, when it gave one. Independent of `result`. |
| `revision` | A counter that increases with every change to the session's status, turns, labels or result |
| `createdAt`, `updatedAt` | Timestamps |

`GET /sessions` returns one page of rows, `{ sessions, nextCursor }`, filtered
by the query:

```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/sessions?project=<project-id>&environment=development&label.topic=workshop&limit=50' \
  -H 'x-api-key: <api-key>'
```

| Parameter | Meaning |
| - | - |
| `project` or `projectId` | A project id or slug |
| `environment` | `development` or `production` |
| `agent` or `agentId` | An agent id |
| `status` | One session status |
| `deploymentId` | A deployment id |
| `externalReference` | Sessions created with exactly this [external reference](#external-references) |
| `createdAfter`, `createdBefore` | RFC 3339 timestamps: sessions with `createdAt >= createdAfter` and `createdAt < createdBefore` |
| `updatedAfter` | An RFC 3339 timestamp: sessions with `updatedAt >= updatedAfter` |
| `label.<key>=<value>` | Sessions whose label `<key>` equals `<value>` exactly; at most three |
| `limit` | Rows per page, 1 to 100; default 50 |
| `cursor` | The `nextCursor` of the previous page |

Every filter is an exact match; there is no prefix, substring or wildcard
search. Filters combine with AND. Naming both spellings of a parameter
(`project` and `projectId`, `agent` and `agentId`), any other parameter or a
malformed value is `400 invalid_query`.

Filters select among the sessions the API key can already see: an
organization key sees the organization's sessions, a project-scoped key
only its project's, and a filter naming another project is `403
project_scope_violation`. A `development` filter never returns Production
sessions, and a reference created in one project is not found from another.

A row is `id`, `projectId`, `agentId`, `deploymentId`, `environment` (`null`
when the session has none), `source`, `status`, `labels`,
`externalReference` (when one was given), `createdAt`, `updatedAt`,
`revision`, `activity` and `result`. `activity` is
`{ activeTurnId, queued, lastSettledTurn }`: the running turn's id or `null`,
how many turns wait behind it, and the most recently finished turn as
`{ id, status, at }` or `null`. Rows carry no `turns`, no `memory` and no
prompt or message text; read `GET /sessions/<id>` for those.

Rows are ordered by `createdAt`, newest first, then by `id`, so sessions
created in the same millisecond have a fixed order and none is skipped.
Updates do not move rows. `nextCursor` is an opaque string, valid only with
the filters of the page that returned it: sending it with different filters
is `400 invalid_cursor`, and so is a cursor that cannot be read. `limit` may
change between pages. `null` means there are no more matches after that
cursor.

Pagination is not a snapshot: each request applies the filters to the
current rows. New sessions and sessions that newly match a status or label
filter can fall before your cursor. Refresh from the first page to see
them. Sorting fetched rows by `updatedAt` orders only those rows, not all
matching sessions. A successful create response confirms the session is
listed.

### External references

`externalReference` is one opaque string your application chooses when it
creates a session, for example the id of the order, ticket or job the
session works for. It is 1 to 256 bytes of UTF-8 with no control characters;
anything else is `400 invalid_external_reference`. Omitting it, or sending
`null`, creates a session without one; it cannot be changed afterwards, and
it is not required to be unique.

The platform stores the reference as given. It is not sent to the agent or
the model, does not grant access, and does not replace the session id or the
`Idempotency-Key`. Keep your own mapping from reference to session id as the
authoritative record; the reference is there for recovery and audit when
that record is incomplete:

* If a create response was lost, retry with the same `Idempotency-Key`
  first; it returns the same session with `200`. The reference makes
  a second recovery path available: `GET /sessions?externalReference=<ref>`
  finds the session however many unrelated sessions were created since.
* A retry of the key with a different reference is `409
  idempotency_conflict`, so two records can never share one session by
  accident.
* The reference is returned on the session, on its list row and in the
  `data` of every `session.*` [event](/agents/events), so a consumer of the
  event log can route each event to the record it belongs to without
  another lookup.

A reference is metadata, not a credential: do not put secrets, tokens or
signed URLs in it. It is visible to every API key that can read the
session.

### Labels

Labels are your own metadata on a session: at most 16 keys, each matching
`^[a-z][a-z0-9_.-]{0,63}$`, each value at most 256 characters, 4 KB in all.
They filter the list and appear on the session and its row. They are not
permissions, are not searched beyond equality, and are never shown to the
agent; give the agent its context in the turn input.

`PATCH /sessions/<id>/labels` changes them and returns the session:

```bash theme={null}
curl -X PATCH 'https://app.opencomputer.dev/api/managed-agents/sessions/<session-id>/labels' \
  -H 'x-api-key: <api-key>' \
  -H 'Content-Type: application/json' \
  -d '{ "set": { "outcome": "merged" }, "unset": ["requested_by"] }'
```

`unset` removes keys, then `set` writes keys; each key takes the last write.
The session's `labelsUpdatedAt` records the change. A patch that would exceed
the bounds, or a key or value that does not fit them, is `400 invalid_labels`
and changes nothing. Labels can be changed on an ended session.

A `200` means the list reflects the change: the session's row carries the
new labels and the filters see them. `503 session_publication_unconfirmed`
(with `error.sessionId`) means the labels are recorded on the session but
the row was not confirmed in time; retry the same patch until it answers
`200`. The platform keeps publishing the row in the background either way,
and repeating the patch is safe because each key takes the last write.

### End and interrupt

`POST /sessions/<id>/end` ends the session. Queued and running turns are
marked `cancelled` with reason `session_ended`, and no further turns are
accepted. A `200` confirms memory write access is revoked.
`503 session_end_unconfirmed` means the session ended but memory revocation
was not acknowledged; retry the same end request, or
[freeze the document](/agents/document-memory#disable-agent-writes).
Repeating end confirms any pending memory revocation without ending the
session again.

The `session.ended` event is recorded before memory revocation is confirmed.
Neither that event nor the end response waits for commands to stop; remote
cleanup continues in the background. Interrupt provides command-settlement
confirmation for the current turn, subject to the compatibility note below;
it leaves queued turns available to run.

`POST /sessions/<id>/interrupt` stops the running turn without spending a
model turn. The response acknowledges the stop request; it does not wait
for completion. While stopping, the session is `stopping` and the turn
remains `running`. Once the model loop and its commands have stopped, the
turn becomes `cancelled` with reason `interrupted`, the session becomes
`idle`, and the next queued turn can start.

If a command cannot be confirmed stopped, OpenComputer terminates its
computer before settling the turn. The next command uses a fresh computer
with the session's workspace. The cancellation event records the
[settlement details](/agents/events#turns). An idle session is returned
unchanged. Stopping does not undo files already written or external effects.

<Note>
  Sessions created with `executionMode: "microvm"` cancel immediately on
  interrupt, without waiting for their commands to stop. They do not enter
  `stopping`, and their cancellation events have no settlement fields.
  Check the creation response when this guarantee matters to your app.
</Note>

## Turns

```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/managed-agents/sessions/<session-id>/turns' \
  -H 'x-api-key: <api-key>' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: message-42' \
  -d '{
    "input": "Plan the workshop.",
    "mode": "queue",
    "payload": { "workshopId": "ws_812", "audience": "backend" }
  }'
```

`Idempotency-Key` is the one rule on both routes: it identifies this request,
a repeat returns the existing turn, and a different request under the same
key is refused (below). Without one every request starts a turn. The body
field `idempotencyKey` carries the same key for a send that a browser makes
through your own server, where the header is the server's to set; when both
are present they must be equal, else `400 invalid_turn`.

| Field | Meaning |
| - | - |
| `input` | The user text for this turn. Required, not empty. |
| `idempotencyKey` | Optional. The key in body form, for a proxied browser send; equal to the header when both are present. |
| `mode` | `queue` (default): run after earlier turns. `steer`: deliver into the running turn when the runtime supports it, otherwise queue. `interrupt`: interrupt running turns and run next, with the [same settlement behavior](#end-and-interrupt) as the interrupt route. |
| `payload` | Optional. A JSON value for the agent, at most 32 KB encoded. It reaches the agent as [`useInput().payload`](/agents/inputs) with `source: "user"`, next to the text, and is recorded on the turn's `message.received` event. The text stays the message; the payload is structured context the agent reads. |
| `answers` | Optional. The id of the session's open [question](/agents/sessions#questions). The input answers it, and the agent reads it as [`useInput().answer`](/agents/inputs#answers-and-steering). |

The response is `202 { turnId, status, duplicate }` for a new turn and
`200` with `duplicate: true` when the key had already created one. `status`
is the turn's status: `queued` or `running` for a new turn, and for a
repeated key whatever the existing turn has reached, `completed`, `failed`
or `cancelled` once it has settled. Follow the turn in the event log; the
response does not wait for it.

The key identifies the request, not only the call. A repeat with the same
`input`, `payload` and `mode` is the existing turn: key order inside the
payload does not matter, an omitted `mode` means `queue`, and whitespace in
the text counts. The same key with anything else is `409
idempotency_conflict` and admits nothing. A retry after a lost reply is
therefore safe with the same body, and a different request never lands on
another request's key.

Codes: `400 invalid_turn`, `400 invalid_payload` (over 32 KB),
`402 insufficient_credits`, `409 idempotency_conflict`,
`409 question_stale`,
`409 memory_admission_pending` (retry the session creation with its key
first), `409 memory_admission_rejected` (the session is ended; create a new
one).

### Questions

While the session has an open [question](/agents/sessions#questions), a turn
sent without `answers` is held rather than run: the response is
`202 { status: "held", questionId, duplicate }` with no `turnId`, and `200`
for a repeated key. The held input is delivered with the answer, in order, as
the answering turn's `steering`, up to a bounded number and 64 KiB; the rest
run as ordinary turns after it. A repeat of its key after delivery returns the
receipt of the turn that carried it; after a [Linear](/agents/linear#stop)
stop, it returns `status: "discarded"`. A turn already queued when the
question was asked keeps its `turnId` and settles as `cancelled` with
reason `held` and the `questionId`.

A turn with `answers` resolves the question and runs as the answer, in one
step, idempotent on its key; the same key with a different `answers` is
`409 idempotency_conflict`. Naming a question that is not the open one, or
answering when none is open, is `409 question_stale`; read the session and
answer its current question.

`POST /sessions/<id>/questions/<questionId>/dismiss` closes the open question
with reason `dismissed`; its held inputs run as ordinary turns, in order.
Ending the session closes it with `ended`. Interrupt does not close it.

### Start work from an application

An application that turns a form submission into agent work makes two calls,
each with its own key, and keeps what it sent until the second one is
acknowledged:

1. `POST /sessions` with `Idempotency-Key: <submission-id>` and
   `agentId: <agent-id>@<environment>`. The platform chooses the deployment
   active in that environment and records it on the session; a retry under
   the same key returns that session, even if the alias has moved since.
   The application never discovers, verifies or stores a deployment id.
2. `POST /sessions/<id>/turns` with `Idempotency-Key: <submission-id>/start`,
   the text and the `payload`.

Keep the submission (its id, the text and the payload) until the turns call
answers, with `duplicate: false` or `true`.
A lost reply at either step is retried with the same body and the same key:
the first step returns the same session, the second the same turn. A
session that exists without a turn is work that was never admitted; only the
holder of the submission can retry it. A `409 idempotency_conflict` from
either call means a different request already used that id; show it, keep
the draft, and do not append the submission to a session it did not create.
Each later message is its own submission with its own key.

## Events

```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/sessions/<session-id>/events?after=0' \
  -H 'x-api-key: <api-key>'
```

`GET /sessions/<id>/events?after=<seq>` returns `{ events }`: up to 500
events with `seq` greater than `after`, in ascending order. Each event is
`{ id, seq, timestamp, sessionId, turnId, type, data }`; `turnId` is absent
on session-level events.

To read a whole log, start at `after=0` and repeat with the last `seq` you
received until a page is empty. A full page means more may follow; read on
without waiting. To follow a live session, keep polling from the last
`seq`; the CLI's `sessions tail` and `useAgent` do exactly this. The log is
durable, so a consumer that stops can resume from its cursor and miss nothing.
Event types and their `data` are listed on [Session events](/agents/events).

## Memory

Document memory belongs to a project and an environment. Every route takes
`?environment=development` or `?environment=production`. Bodies, conditional
headers, the document object and the error codes are on
[Document memory](/agents/document-memory#management-api).

| Operation | Method and path | Success |
| - | - | - |
| Resource inventory | `GET /projects/<p>/memory` | `200` with `{ resources }` |
| List documents | `GET /projects/<p>/memory/<r>/documents` | `200` with `{ documents, nextCursor }` |
| Read | `GET /projects/<p>/memory/<r>/documents/<id>` | `200` with the document and `ETag` |
| Create | `PUT /projects/<p>/memory/<r>/documents/<id>` with `If-None-Match: *` | `201` |
| Replace text or summary | `PUT /projects/<p>/memory/<r>/documents/<id>` with `If-Match` | `200` |
| Change title or policy | `PATCH /projects/<p>/memory/<r>/documents/<id>` with `If-Match` | `200` |
| Delete | `DELETE /projects/<p>/memory/<r>/documents/<id>` with `If-Match` | `204` |

```bash theme={null}
curl -X PUT 'https://app.opencomputer.dev/api/managed-agents/projects/<project-id>/memory/notes/documents/workshop?environment=development' \
  -H 'x-api-key: <api-key>' \
  -H 'Content-Type: application/json' \
  -H 'If-None-Match: *' \
  -d '{ "title": "Workshop notes" }'
```

`oc.sessions.startOnDocument` on the
[TypeScript client](/reference/typescript-sdk/agents) does this
create and the session create in one call.

## Webhooks

Webhook configuration lives under the project; invocation uses the separate
`/api/agent-webhooks/<id>/<token>` URL described on
[Agent webhooks](/agents/webhooks).

| Operation | Method and path | Body or query | Success |
| - | - | - | - |
| List | `GET /projects/<p>/webhooks` | Optional `?environment=` and `?agentId=` | `200` with `{ webhooks }`, URLs without tokens |
| Create | `POST /projects/<p>/webhooks` | `{ name, agentId, environment, identity? }` | `201` with `{ webhook }`; `token` and the full `invocationUrl` appear once |
| Update | `PATCH /projects/<p>/webhooks/<id>` | Any of `name`, `enabled`, `identity` (`null` removes it) | `200` with `{ webhook }` |
| Rotate token | `POST /projects/<p>/webhooks/<id>/rotate-token` | None | `200` with `{ webhook }` carrying the new token |
| Delete | `DELETE /projects/<p>/webhooks/<id>` | None | `204` |
| Request ledger | `GET /projects/<p>/webhooks/<id>/requests` | None | `200` with `{ requests }` |

`identity` is `header:<name>` or `body:<json-pointer>`. Creating a webhook
for an agent that is not deployed in the environment is
`409 webhook_target_unavailable`.

```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/projects/<project-id>/webhooks/<webhook-id>/requests' \
  -H 'x-api-key: <api-key>'
```

## Event subscriptions

An event subscription delivers the recorded outcome of turns run by agents
in a project to a session in the same project, as a new turn of that
session. It is how one agent learns that another finished: a coordinator
subscribes to its workers' outcomes and reasons about them when they arrive.
Destinations are sessions only; there is no public HTTPS destination, and
this is not the [outbound webhooks](/agents/webhooks) feature.

| Operation | Method and path | Success |
| - | - | - |
| Create | `POST /projects/<p>/event-subscriptions` | `201` with `{ subscription }` |
| List | `GET /projects/<p>/event-subscriptions` | `200` with `{ subscriptions }` |
| Get | `GET /projects/<p>/event-subscriptions/<id>` | `200` with `{ subscription }` |
| Delete | `DELETE /projects/<p>/event-subscriptions/<id>` | `204`; pending deliveries stop |

```bash theme={null}
curl -X POST 'https://app.opencomputer.dev/api/managed-agents/projects/<project-id>/event-subscriptions' \
  -H 'x-api-key: <api-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "agentId": "worker",
    "events": ["turn.completed", "turn.failed"],
    "destination": { "type": "session", "sessionId": "<coordinator-session-id>" },
    "environment": "development"
  }'
```

| Field | Meaning |
| - | - |
| `agentId` | Optional. Outcomes of this agent only; omitted, every agent in the project. |
| `events` | One or more of `turn.completed`, `turn.failed`, `turn.cancelled`. Turn outcomes, not session ends. |
| `destination` | `{ type: "session", sessionId }`: a session of an agent in this project that can still take turns. |
| `environment` | `development` or `production`. A subscription is scoped to one environment: it selects outcomes recorded there and its destination session runs there. |

A subscription is immutable; `{ subscription }` carries `id`, `projectId`,
the fields above and `createdAt`. Codes: `400 invalid_event_subscription`,
`403 project_scope_violation` (the agent is outside the project),
`404 destination_session_not_found`, `409 destination_session_ended`,
`404 event_subscription_not_found`.

### Delivery and receipts

When a selected turn settles, the source session records one delivery per
matching subscription and starts a turn on the destination session with
`source: "event"` input; the receiving agent reads it with `useInput()`
([Event input](/agents/inputs#event-input)). The destination turn's
idempotency key is `<subscription-id>:<event-id>`, so a retried delivery
never starts a second turn. Deliveries are retried with backoff and stop
when the subscription is deleted or the destination has ended.

`GET /sessions/<id>` on the source session lists each turn's `deliveries`:

| Field | Meaning |
| - | - |
| `id` | `<subscription-id>:<event-id>` |
| `subscriptionId`, `eventId`, `eventType` | What was delivered, to which subscription |
| `destination` | The subscription's destination |
| `status` | `pending`, `delivered` or `failed` |
| `attempt` | Delivery attempts so far |
| `receipt` | `{ sessionId, turnId }`: the turn the destination admitted, once delivered |
| `nextAttemptAt` | When a pending delivery is retried |
| `error` | `subscription_unavailable`, `target_missing`, `target_ended`, or `delivery_failed` |

The delivered event names identifiers and the outcome; a completed turn's
final message is included, bounded to 16 KB. The receiving agent should
treat that text as data about another agent's work, not as instructions.

## Linear connections

A [Linear connection](/agents/linear) binds a Linear app in your workspace to
one agent in one environment. The dashboard drives these routes. A
project-scoped key may list its own project's connections; every other route
needs an organization API key and is `403 forbidden` otherwise.

| Operation | Method and path | Body or query | Success |
| - | - | - | - |
| Create | `POST /projects/<p>/linear/connections` | `{ name, environment, agentId }` | `201` with `{ connectionId, webhookUrl, createAppUrl, connection }`; `200` when a pending connection for that binding already exists |
| List | `GET /projects/<p>/linear/connections` | Optional `?environment=` | `200` with `{ connections }` |
| Set credentials | `PUT /linear/connections/<id>/credentials` | `{ clientId, clientSecret, signingSecret }`, as copied from the Linear app | `200` with `{ connection }` |
| Authorize | `POST /linear/connections/<id>/authorize` | None | `200` with `{ authorizeUrl, expiresAt }` |
| Disconnect | `DELETE /linear/connections/<id>` | None | `200` with `{ connection, revoked }` |

`createAppUrl` opens Linear's create-app page prefilled with the name, the
callback URL and `webhookUrl`. Creating again for a pending binding resets it
with the new name and a new `webhookUrl`, so an earlier `createAppUrl` stops
working. `webhookUrl` contains the connection's secret;
treat it as a credential. `authorizeUrl` is Linear's consent page for the app;
a workspace admin completes it, and Linear returns to OpenComputer's callback,
which stores the tokens and redirects to the project's Connections tab.
`revoked` on a disconnect says whether Linear confirmed the token revocation.

A connection is `{ id, projectId, environment, agentId, name, status,
clientId, appUserId, organizationId, webhookUrl, createAppUrl, verifiedAt,
verificationError, lastEventAt, teams, health, revision, createdAt,
updatedAt }`. It never carries a secret or a token. `status` is `pending`,
`connected`, `disconnected` or `revoked`. `health` is `{ state, message,
lastEventAt? }`, with `state` one of `awaiting_credentials`,
`awaiting_authorization`, `waiting_for_first_delegation`, `receiving` or
`revoked`. `teams` is `{ allPublic, teamIds }`, the teams the app can see.

| Code | Meaning |
| - | - |
| `400 invalid_environment`, `400 invalid_agent` | `environment` must be `development` or `production`; `agentId` must name an agent of the project |
| `400 invalid_linear_app_name` | The name must be 1 to 64 characters |
| `400 linear_app_name_reserved` | Linear does not allow app names that contain "Linear" |
| `400 invalid_linear_credentials` | `clientId`, `clientSecret` and `signingSecret` are all required |
| `403 forbidden` | A project-scoped key on a route other than List |
| `404 project_not_found` | No such project for this key |
| `404 linear_connection_not_found` | No such connection, or it was disconnected |
| `409 project_archived` | Restore the project before creating a connection |
| `409 linear_already_connected` | The agent already has a connection in this environment; `error.connectionId` names it |
| `409 linear_app_already_connected` | That Linear app is connected to another agent or environment; create a separate app |
| `409 linear_connection_connected` | The connection is authorized with a different Linear app; disconnect it first |
| `409 linear_credentials_required` | Set the credentials before authorizing |
| `409 linear_connection_changed` | Another request changed the connection; list the connections and continue; `error.connectionId` names it |
| `503 linear_connector_unavailable` | Linear connections are not available right now |

## Projects, agents and deployments

| Operation | Method and path | Success |
| - | - | - |
| List projects | `GET /projects` | `200` with `{ projects }` |
| Create project | `POST /projects` with `{ name, slug?, environmentMode? }` | `201` with the project. `environmentMode` is `single` (one `default` environment) or `legacy` (Development and Production) |
| Get project | `GET /projects/<p>` | `200` with `{ project, deployments, sessions, connections, schedules }` |
| List agents | `GET /agents` | `200` with `{ agents }`: `id`, `name`, `activeAlias`, `activeDeploymentId`, `deploymentCount`; `activeAlias` and `activeDeploymentId` are `null` while the agent has no active deployment |
| List deployments | `GET /deployments?agentId=<agent-id>` | `200` with `{ deployments }` |
| Get deployment | `GET /deployments/<id>` | `200` with `id`, `agentId`, `alias`, `memory` declarations, `createdAt` |
| List GitHub repositories | `GET /projects/<p>/github/repositories?environment=<env>` | `200` with `{ repositories, nextCursor }`; see [GitHub App connections](/agents/github#list-the-repositories-an-environment-can-reach) |

A project is `{ id, slug, name, environmentMode, environments, agents, createdAt, updatedAt }`;
`agents` carries each agent's `id` and `name`. Single-environment projects have one
environment named `default`; requests that name `development` resolve to it,
and requests that name `production` fail with `409 single_environment_project`. See
[Projects and agents](/agents/projects) for how projects are created and
linked from the CLI.

```bash theme={null}
curl 'https://app.opencomputer.dev/api/managed-agents/projects' \
  -H 'x-api-key: <api-key>'
```

Secrets, runtime variables, schedules and logs are managed with the CLI and
the dashboard; see their pages under Concepts.


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