Base URL and authentication
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 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 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.
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.Create
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 pinneddeploymentId), environment and memory bindings, returns the existing session with200, 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 differentexternalReference(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
201and the other with200. - 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_unconfirmedmeans the memory grants were not confirmed in time. Retry with the same key; the retry resumes the wait.503 session_publication_unconfirmedmeans the session exists but its row in the list was not confirmed in time;error.sessionIdnames it. Retry with the same key and the same body until the answer is201or200. A success meansGET /sessionsreflects the session; the platform keeps publishing the row in the background either way.
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:
GET /sessions returns one page of rows, { sessions, nextCursor }, filtered
by the query:
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-Keyfirst; it returns the same session with200. 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
dataof everysession.*event, so a consumer of the event log can route each event to the record it belongs to without another lookup.
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:
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.
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. An idle session is returned
unchanged. Stopping does not undo files already written or external effects.
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.Turns
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.
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, a turn sent withoutanswers 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
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:POST /sessionswithIdempotency-Key: <submission-id>andagentId: <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.POST /sessions/<id>/turnswithIdempotency-Key: <submission-id>/start, the text and thepayload.
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
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.
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.
oc.sessions.startOnDocument on the
TypeScript client 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.
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.
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 feature.
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 withsource: "event" input; the receiving agent reads it with useInput()
(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:
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 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 is403 forbidden otherwise.
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.
Projects, agents and deployments
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 for how projects are created and
linked from the CLI.