Skip to content

API

Every public route lives under /v1. The beta header OpenAI-Beta: agents=v1 is accepted and ignored.

Unknown JSON keys return invalid_request with code unknown_field. Known OpenAI fields we have not implemented return not_implemented. They are not stored and they are not ignored. Extra JSON keys are rejected because request bodies use strict models. The comparison matrix is OpenAI compatibility.

Auth is Authorization: Bearer on every request except /health and /metrics. The gateway does not mint or store the auth bearer. A callback maps the bearer to key_id and tenant_id, or rejects with a status, code, and message. Invalid keys are 401 with code unauthorized. An auth plugin may return 429 for a rate limit or quota. See auth. Every query is tenant-scoped. An id that belongs to another tenant returns 404, not 403.

Clients send a bearer and talk to /v1.

Agents

An agent is saved config, not a running process. A new install has no agents until you create one.

Method Path
POST /v1/agents
GET /v1/agents
GET /v1/agents/{agent_id}
POST /v1/agents/{agent_id}
DELETE /v1/agents/{agent_id}

Fields: id, name, model, instructions, idle_ttl, metadata, tools (function, mcp HTTP, mcp stdio), created_at, updated_at. idle_ttl is an ApiPi extension: a duration such as 30m or 1h, or 0 to turn the idle timer off. Omit it to keep the environment default. See config.

Rejected: multi_agent, tool_search, programmatic_tool_calling.

A session may pass agent_id or an inline agent. You must provide exactly one of those. Inline config is used for that session only. It is not saved unless you POST /v1/agents. A live turn needs agent.model. That id must exist on OPENAI_BASE_URL. Missing model is 400 with code model_required. Unknown model is 400 with code model_not_found. Inline model and instructions are kept on the session for follow-up turns. Saved agents keep reading the agent row. The gateway appends a platform prompt, then agent.instructions when those are set. A system prompt may replace Pi's harness default first. See Concepts and config.

vault_ids on session create attaches vaults for HTTP MCP. The gateway matches mcp_server_url and injects the bearer on the host broker. Tokens are encrypted at rest. GET of a vault or credential never returns the token.

Vaults

Method Path
POST /v1/agents/vaults
GET /v1/agents/vaults
GET /v1/agents/vaults/{vault_id}
POST /v1/agents/vaults/{vault_id}
DELETE /v1/agents/vaults/{vault_id}
POST /v1/agents/vaults/{vault_id}/credentials
GET /v1/agents/vaults/{vault_id}/credentials
GET /v1/agents/vaults/{vault_id}/credentials/{id}
POST /v1/agents/vaults/{vault_id}/credentials/{id}
DELETE /v1/agents/vaults/{vault_id}/credentials/{id}

Create a vault with name and metadata. Add a credential with auth.type static_bearer, mcp_server_url, and token. The store keeps the token as AES-256-GCM ciphertext (APIPI_VAULT_MASTER_KEY). List and get omit token. auth.type mcp_oauth is not_implemented. Every query is tenant-scoped. A vault from another tenant is 404.

Files

Method Path
POST /v1/files
GET /v1/files
GET /v1/files/{file_id}
GET /v1/files/{file_id}/content
DELETE /v1/files/{file_id}

Upload is multipart form data with file and purpose. Accepted purposes are user_data and assistants. Other purposes return not_implemented. The object is { id, object: "file", bytes, created_at, filename, purpose, status }. created_at is a Unix timestamp. Ids look like file- plus hex. Bytes live in the same object store as artifacts (APIPI_ARTIFACT_STORE). Metadata is in Postgres. The upload cap is APIPI_MAX_FILE_BYTES (default 50 MiB). A larger body returns 413 with code payload_too_large. A file from another tenant is 404. Attach an uploaded file on session create with environment.files { "type": "file_id", "file_id": "…", "path": "/workspace/…" }.

Browser and BFF uploads that must not proxy bytes through the gateway use presigned uploads instead of this multipart route. GET /v1/files/{id}/content still streams through the gateway. The response uses Content-Disposition: attachment with the stored file name, including an RFC 5987 filename* when the name is not ASCII, and X-Content-Type-Options: nosniff. POST /v1/files/{id}/download returns a short-lived GET URL when the artifact store is S3.

Uploads

S3-compatible object storage only (APIPI_ARTIFACT_STORE=s3). Local store returns 400 with code presign_unsupported.

Method Path
POST /v1/uploads
POST /v1/uploads/{upload_id}/complete
POST /v1/files/{file_id}/download
POST /v1/skills/{skill_id}/download
POST /v1/agents/sessions/{session_id}/artifacts/{artifact_id}/download

Create takes purpose (file, attachment, or skill), filename, bytes, and optional content_type. attachment is the same store as file (chat attachments reuse Files). The response is a PUT URL and headers. PUT the bytes to object storage, then complete. Complete checks the object with HeadObject, enforces APIPI_MAX_FILE_BYTES, and writes Files or Skills metadata. Complete before PUT is 400 with code upload_incomplete. Wrong tenant is 404. The Pi harness session cache is not exposed this way.

A presigned GET forces a download. The URL sets Content-Disposition: attachment to the original file name. A name that is not plain ASCII also gets an RFC 5987 filename* parameter. The URL sets Content-Type from the stored type. HTML, SVG, XML, and JavaScript are signed as application/octet-stream so a browser does not render them from the bucket domain. The same attachment header is used when the gateway streams /content.

Do not put the ApiPi API key in the browser. Do not log the presigned URL.

Skills

Method Path
POST /v1/skills
GET /v1/skills
GET /v1/skills/{skill_id}
DELETE /v1/skills/{skill_id}

Upload is multipart form data with field files (a zip). The zip must contain exactly one SKILL.md. The object is { id, object: "skill", name, bytes, created_at }. Ids look like skill- plus hex. Bytes live in the shared object store. The upload cap is APIPI_MAX_FILE_BYTES. A skill from another tenant is 404. Attach on session create with environment.skills { "type": "skill_reference", "skill_id": "…" }. ApiPi unpacks under .agents/skills/. At most 32 skills per create. There are no version endpoints.

Models

Method Path
GET /v1/models

When APIPI_FORWARD_MODELS is on (the default), this route proxies to {OPENAI_BASE_URL}/models on the model host. The JSON body is the host's list, unchanged. Auth is the usual bearer. The host call uses OPENAI_API_KEY_OVERWRITE when that is set, otherwise the request bearer: the same key Pi uses.

A host 401 or 403 is 401 with code model_host_unauthorized. If the host is unreachable, the response is 400 with code model_host_unreachable. When APIPI_FORWARD_MODELS is off, the route returns 400 with type not_implemented and code forward_models.

Chat

GEKI-native chat sessions. Not OpenAI /v1/chat/completions. The store is the same session and event log as Agents. Clients never set or see environment. The gateway stores environment.type=none and metadata.apipi.session_kind=chat so placement uses chat workers. Fleet layout and placement footguns are in chat fleets.

To attach a computer later, create a new Agents session. Chat sessions do not upgrade in place.

Method Path
POST /v1/chat/sessions
GET /v1/chat/sessions
GET /v1/chat/sessions/{session_id}
POST /v1/chat/sessions/{session_id}
DELETE /v1/chat/sessions/{session_id}
POST /v1/chat/sessions/{session_id}/events
GET /v1/chat/sessions/{session_id}/events
GET /v1/chat/sessions/{session_id}/turns
GET /v1/chat/sessions/{session_id}/turns/{turn_id}
GET /v1/chat/sessions/{session_id}/items
GET /v1/chat/sessions/{session_id}/export

Create accepts agent or agent_id, input, metadata, vault_ids, and stream. stream: true returns SSE as soon as the session exists. An environment field is 400 with code unknown_field. List returns only chat sessions. An Agents session id on a chat path is 404. Event types match Agents so one frontend can read both.

Chat tools are an allowlist: function tools and HTTP MCP. Stdio MCP, Playwright auto-inject, workspace skills, and computer environments are rejected. A disallowed tool is 400 with code chat_tool. Saved agents used as chat profiles should set metadata.apipi.session_kind to chat; create and update then apply the same allowlist.

Sessions

Method Path
POST /v1/agents/sessions
GET /v1/agents/sessions
GET /v1/agents/sessions/{session_id}
POST /v1/agents/sessions/{session_id}
DELETE /v1/agents/sessions/{session_id}

Create accepts agent or agent_id, environment (including capability_directories), input, metadata, and stream. If environment is omitted, the type is openai_hosted: a local session directory next to Pi, not OpenAI's cloud. hosted is an alias for that same directory; the session stores and returns openai_hosted. input may be a string or an object with content or text. A non-empty input starts the first turn. Non-stream create waits for that turn. If it fails, the response is 502 with the turn error code and session_id on the error object. stream: true returns SSE as soon as the session row exists; turn events follow while Pi runs. If that first turn fails, including when no worker can take it, the stream includes agent.session.error with a code and then agent.session.failed. The stream ends on agent.session.failed, so a client does not wait for a later event.

Status: idle | in_progress | requires_action | failed.

required_actions: function_call, environment_connection.

POST /v1/agents/sessions/{session_id} updates metadata only. DELETE stops the live guest on the worker that holds the lease, removes stored artifact bytes, then removes the session row. It returns {"id": "…", "deleted": true}. The guest does not stay up until the worker drains.

Session create may set idle_ttl to the same duration. That value wins over the agent field. Stock clients can set metadata["apipi.idle_ttl"] instead of the top-level field. See config.

metadata is a JSON object. Keys that start with apipi. are reserved. The gateway interprets apipi.sandbox_size, apipi.session_kind, apipi.thinking, apipi.system_prompt, apipi.idle_ttl, apipi.title, and apipi.title_status. It stores apipi.actor_type, apipi.schedule_id, and apipi.source and does not branch on them. There is no top-level actor_type field. See reserved metadata.

Events

Method Path
POST /v1/agents/sessions/{session_id}/events
GET /v1/agents/sessions/{session_id}/events

POST accepts two bodies with the same meaning. The OpenAI Agents shape is {"events":[{...}]} with exactly one event (what official SDK helpers send). The flat shape is {type, text|content, …}. Send one shape or the other, not both.

A message event starts a turn. Nested form: type agent.session.input.message and input with a user message whose content has input_text. Flat form: type agent.session.input.message and content or text. Follow-up messages work the same way after the session is idle. A message while the session is in_progress cancels that turn (or fails it if the process no longer owns it) and starts a new turn, so a hung Pi cannot block the next command. GET of a session that is in_progress with no live turn on this process does the same fail-and-idle recovery. A message while the session is requires_action is rejected; send a tool result instead.

Tool result: agent.session.input.tool_result with turn_id, call_id, success, and output (on success) or error (on failure), either nested in events or flat.

Cancel: agent.session.input.cancel on a session in in_progress. The gateway persists agent.session.turn.cancelled then agent.session.idle.

GET returns {"data": […]}. GET ?stream=true is SSE. The stream stays open across idle and ends after agent.session.failed. It sends SSE comment keepalives (: ping) without a blank line, so clients that parse every dispatched event as JSON do not see an empty payload. Reconnect and replay from the store with after_seq. Stored public events are written before SSE. output_text.delta is live SSE only and is not stored; reconnect and export skip those fragments. Full assistant text is on output_text.done and the assistant item. Thinking is stored as a short preview, not as live deltas. Behind more than one gateway process, the stream and the next turn must hit the node that owns Pi. See multiple nodes.

Only these event types are public. Anything else from Pi is an internal log line.

Type Meaning
agent.session.created Session exists
agent.session.in_progress Turn running
agent.session.idle No turn
agent.session.requires_action Waiting on tool or environment
agent.session.failed Terminal failure
agent.session.error Error
agent.session.turn.created Turn id
agent.session.turn.in_progress Work started
agent.session.turn.completed Done; may include usage (tokens only)
agent.session.turn.failed Failed (including a model host error)
agent.session.turn.cancelled Cancelled
agent.session.turn.output_text.delta Assistant text fragment (live SSE only; not stored)
agent.session.turn.output_text.done Text finished
agent.session.turn.item.added New item
agent.session.turn.item.done Item finished
agent.session.turn.thinking.started Thinking block started. Stored. item_id, content_index.
agent.session.turn.thinking.completed Thinking block finished. Stored. Preview only, not the full text.
agent.session.turn.thinking.summary.completed Short summary of that block. Stored. item_id, summary, summary_status=done.
agent.session.turn.thinking.summary.failed Summary was not produced. Stored. item_id, summary_status=failed. No summary text.
agent.session.turn.compaction.started Pi started compaction. Stored. reason when Pi sent one (manual, threshold, or overflow).
agent.session.turn.compaction.completed Pi finished compaction. Stored. reason, aborted, will_retry, tokens_before, tokens_after, and a short error when present. The summary text is not stored.
agent.session.title.updated metadata["apipi.title"] was set or the title job failed. Stored.
agent.session.environment.pending Waiting for a computer
agent.session.environment.connected Computer ready
agent.session.environment.disconnected Computer gone
agent.session.environment.failed Could not attach, or hosted setup failed

Item types: message, function_call, mcp_call, command_execution. Thinking is not an item. GET /items does not list it.

agent.session.turn.thinking.completed carries item_id, content_index, duration_ms, reasoning_tokens, preview, and preview_truncated. preview is the first 100 Unicode code points. preview_truncated is true when the block was longer. duration_ms is null when the start event was missed. reasoning_tokens is Pi's reasoning count at the end of the block, or null when the host did not report one. The full thinking text is not on these events, not in items, and not in logs. There is no admin API that returns it. Pi's session cache may still hold the full text. That cache is not the public transcript. Thinking deltas are not sent to clients. Enable thinking with APIPI_PI_THINKING, or override it per session with metadata["apipi.thinking"]. See Pi.

Compaction events are optional. A Pi build that does not emit compaction_start or compaction_end does not fail the turn. The summary text is not a public event.

A thinking summary is optional and arrives later on agent.session.turn.thinking.summary.completed. It does not replace the preview. Clients can show summary when that event has arrived, and the preview otherwise. A failed summary does not fail the turn. The platform flag and the auth callback must both allow it. See configuration and auth.

metadata["apipi.title"] is the automatic session title when that feature is on. metadata["apipi.title_status"] is pending, done, or failed. A metadata update that omits those keys keeps the stored values. The gateway does not replace an existing title.

Turns, items, artifacts

Method Path
GET /v1/agents/sessions/{session_id}/turns
GET /v1/agents/sessions/{session_id}/turns/{turn_id}
GET /v1/agents/sessions/{session_id}/items
GET /v1/agents/sessions/{session_id}/artifacts
GET /v1/agents/sessions/{session_id}/artifacts/{id}/content
DELETE /v1/agents/sessions/{session_id}/artifacts/{id}

When a turn completes, files under outputs/ on the computer are copied into the artifact store. Copies are immutable and include turn_id. A later turn that writes the same path publishes another artifact. Rows already stored with a path under artifacts/ stay readable; new publishes use outputs/. GET content works as soon as the turn has completed, even if Pi is still alive. Harvest on Pi stop is a safety net for files written after the last completed turn. 410 if nothing was published. DELETE removes the metadata and the stored bytes. The live file on the computer stays. Local disk is the default store. S3 is optional. See run modes and config.

GET turn may include usage (prompt, completion, cache read/write, total). Tokens only. See usage.

Export

Method Path
GET /v1/agents/sessions/{session_id}/export

JSON of the transcript from the store: public events, turns, and items. Same shapes as the list endpoints. Does not read Pi files. Wrong tenant is 404. A session export is enough to leave: the customer keeps the thread if the gateway disappears.

Usage

Method Path
GET /v1/usage

Tenant-scoped totals from hot usage data (turn log and/or daily rollups). Filter by exactly one of session_id, turn_id, or day. Tokens and turn counts, not USD. See usage.

Request ids

Every public request except /health has an id. The gateway echoes x-request-id. It generates a UUID if that header is missing. It honors X-Client-Request-Id when present (ASCII, at most 512 characters). That client value becomes the request id. When APIPI_INSTANCE_ID is set, responses also include X-ApiPi-Instance. After a successful bearer, responses include X-Tenant-Id (auth tenant_id) and X-User-Id (auth key_id). Those request headers are not used for auth. When a trace is known (traceparent, or an active OpenTelemetry span), responses include X-Trace-Id. /health omits these. See multiple nodes.

Environments

environment.type on create:

Type Behaviour
openai_hosted Default. Session directory next to Pi. Not OpenAI's cloud.
hosted Alias for openai_hosted. Stored and returned as openai_hosted.
none No computer. MCP and chat only.
self_hosted Wait for an external runner. Create returns environment_id and a one-time key. Runner WebSocket: /v1/environments/{environment_id}.

environment.capability_directories: paths on the computer that contain SKILL.md trees. See tools.

On openai_hosted (and the hosted alias), create also accepts packages, setup_commands, env, files, skills, and network. packages is an object with optional python, system, and npm lists of package names (pin versions when you need to, such as pandas==2.2.3). setup_commands is an ordered list of { "command": "…", "cwd": "…" } objects. cwd is optional and defaults to the session workspace. env is an object of string environment variables for that session. files entries are { "type": "inline", "path": "/workspace/…", "data": "<base64>" } or { "type": "file_id", "file_id": "file-…", "path": "/workspace/…" }. file_id must be a Files API object owned by this tenant. Other files types return not_implemented. At most 50 files per create. skills entries are { "type": "skill_reference", "skill_id": "…" }. Other skill types return not_implemented. At most 32 skills per create. A missing or foreign skill_id is 404. network is { "access": "enabled"|"disabled"|"restricted", "allowed_domains": ["api.example.com"] }. allowed_domains is required for restricted (1–100 exact hostnames, no wildcards, schemes, paths, or ports) and is otherwise rejected. Files are written first, then packages install, then setup commands run, before the first agent turn. A nonzero install or setup exit emits agent.session.environment.failed and fails the session; Pi does not start. packages, setup_commands, env, files, and skills on none or self_hosted return 400. network on self_hosted returns 400. On environment type none it is ignored. Isolation none cannot enforce disabled or restricted and fails the environment instead. Session network cannot open hosts that [sandbox.network] forbids. environment_template_id and plugins return 400 with type not_implemented. Reserved env names (PATH, HOME, OPENAI_API_KEY, OPENAI_BASE_URL, DATABASE_URL, PI_CODING_AGENT_DIR, and APIPI_ / CODEX_ / PI_ prefixes) return 400. Decoded files must fit APIPI_MAX_WORKSPACE_BYTES. A missing or foreign file_id is 404.

environment.sandbox_size is an ApiPi extension: S, M, or L. Unknown values return 400. A top-level sandbox_size on the session body is still unknown_field. Stock OpenAI clients can set metadata["apipi.sandbox_size"] instead. Agent metadata with that key is a default for later sessions. The gateway default is APIPI_SANDBOX_DEFAULT_SIZE (S unless you change it). The resolved size is stored on the session environment and does not change if you later PATCH metadata. Isolation none accepts the field and ignores RAM and rootfs. Isolation microvm uses it for guest RAM and image: S/M boot the default rootfs, L boots the browser rootfs. L without that rootfs fails clearly (combined create returns 400; API-only fails when the worker spawns). On microvm, L also injects Playwright MCP unless the agent already has it or auto-inject is off. See environments.

Compatibility

The official client surface, backend differences, and error matrix are on OpenAI compatibility. The OpenAI Python example is examples/sessions/openai_sdk.py. Create-and-stream steps are in Using the API.

Errors

{ "error": { "type": "not_implemented", "code": "...", "message": "...", "session_id": "..." } }

session_id is set when create already stored a session and the first turn failed. The session stays idle so a follow-up message works.

Missing or invalid bearer is 401 with code unauthorized. An auth plugin may return 429 with a plugin code such as rate_limited or quota. A new turn that would pass APIPI_MAX_SESSIONS live Pi processes returns 429 with code capacity. A known sandbox_image that no live worker has returns 503 with code image_unavailable. Workers that have the image but are full still return 429 capacity. An unknown image id is 400. A tenant that would pass APIPI_MAX_SESSIONS_PER_TENANT returns 429 with code capacity_tenant. A request body larger than APIPI_MAX_REQUEST_BYTES returns 413 with code payload_too_large. An openai_hosted directory over APIPI_MAX_WORKSPACE_BYTES emits agent.session.error with code workspace_too_large. Publishing artifacts that would pass APIPI_MAX_ARTIFACT_BYTES emits agent.session.error with code artifact_too_large. If the artifact store cannot be written (OSError, including a permission error on the local .artifacts tree, or an S3/botocore error such as AccessDenied, NoSuchBucket, or a connection failure), the turn fails with agent.session.turn.failed and agent.session.error with code artifact_store. A missing object (NoSuchKey) is not that error. If a Pi session cache is stored and reading it fails, the turn fails the same way and does not continue without the cache. Session create that cannot read a hosted file or skill returns 503 with code artifact_store. A later turn that cannot read those bytes fails the environment with the same code. An HTTP read, upload, or download that hits the same store error also returns 503 with code artifact_store, not 500. Settings and defaults are in config.