HTTP mode only: read setup and transport selection first. These Managed examples are separate from the MCP tool schemas; consult current operation docs for deployment-specific fields.
- Agents
- Authentication
- Models
- Environments
- Sessions
- Events
- Files
- Memory Stores
- Skills
- Vaults
- Deployments
- Common Response Codes
All endpoints are under https://api.qoder.com/api/v1/cloud.
Every request requires:
Authorization: Bearer <PAT or SAT>
Add Content-Type: application/json only when the request body is JSON. Multipart uploads (POST /files, POST /skills) must use multipart/form-data with the client-generated boundary. Bodyless GET/DELETE requests do not need a Content-Type header.
Two token types are accepted — see Authentication.
Pagination: most list endpoints are cursor-based. Prefer page (pass the previous response's next_page value); after_id / before_id remain as compatibility cursors. The three cursor params are mutually exclusive — sending more than one returns 400. Response shape: {data: [...], next_page, first_id, last_id, has_more}. GET /models is the explicit non-paginated exception and returns {data, has_more}.
Resource ID prefixes: agent_, sess_, env_, evt_, file_, memstore_, mem_, skill_, dep_, sthr_, vault_.
| Token | Identity | Use case | How to get it |
|---|---|---|---|
PAT (pt- prefix) |
A user | Personal development and testing | Created in the Qoder console (see guide.md § Authentication) |
| SAT (JWT) | A Service Account | Server-side integrations, CI, automation | Exchange a Service Account API Key (SA Key) for a short-lived token |
Both go in the same header. Docs use QODER_ACCESS_TOKEN as the neutral variable name for "whichever token you hold".
An org admin supplies an SA Key. Request scopes at token exchange, within the key's permissions. Use matching region/environment endpoints; the Global production pair is https://openapi.qoder.sh for exchange and https://api.qoder.com for business APIs. Never mix exchange and business endpoints from different regions.
: "${QODER_SA_KEY:?Configure the SA Key securely}"
: "${QODER_OPENAPI_BASE_URL:?Set the matching regional exchange origin}"
SAT_RESPONSE=$(curl --fail-with-body -sS "$QODER_OPENAPI_BASE_URL/api/v1/serviceToken/exchange" \
-H "Authorization: Bearer $QODER_SA_KEY" \
-H 'Content-Type: application/json' \
-d '{"grant_type":"client_credentials","audience":"qoder","scope":"qca.access","ttl_seconds":3600}')
QODER_ACCESS_TOKEN=$(printf '%s' "$SAT_RESPONSE" | jq -er '.access_token | select(type == "string" and length > 0)')
export QODER_ACCESS_TOKEN
unset SAT_RESPONSEStop on exchange/parsing failure. qca.access covers Managed; request qca.access forward.access only when both API families are needed and permitted. An SAT expires in at most 43200 seconds; exchange again rather than refresh it. Never use the SA Key as the business bearer token or print token responses.
For current authentication details, use Qoder authentication.
| Method | Path | Description |
|---|---|---|
| POST | /agents |
Create agent |
| GET | /agents |
List agents (excludes archived) |
| GET | /agents/{id} |
Get agent (?version=N returns a version snapshot) |
| GET | /agents/{id}/versions |
List version snapshots |
| POST | /agents/{id} |
Update agent (OCC, requires version) |
| POST | /agents/{id}/archive |
Archive agent |
| DELETE | /agents/{id} |
Delete agent (archive optional) |
{
"name": "my-agent",
"description": "Optional description",
"system": "You are a helpful assistant.",
"model": "ultimate",
"tools": [{"type": "agent_toolset_20260401"}]
}modelaccepts a string ID or an object{"id": "ultimate", "effort": "high", "context_window": 200000}. Discover valid values via GET /models.toolsis a union bytype:agent_toolset_20260401(built-in tools, optionalenabled_toolsallowlist /disallowed_tools/ per-toolconfigs[].permission_policy),browser_toolset_20260714(Browser Use, Beta — see below),mcp_toolset(requiresmcp_server_namereferencing a top-levelmcp_servers[].name), andcustom(client-side tool:name,description,input_schema).mcp_serversis a top-level agent field:[{"name": "my-tools", "type": "url", "url": "https://..."}]. MCP auth goes through Vaults (static_bearercredential +vault_idson the session), not inline tokens — seetools-and-resources.md§ MCP Tools.skillsbinds uploaded skills:[{"type": "custom", "skill_id": "skill_xxx"}](optionalversionstring).
On success returns the agent object with type: "agent", version: 1, archived_at, multiagent, metadata fields.
browser_toolset_20260714 enables the platform-managed browser_* tools plus session live preview. It is a separate tools entry, not a member of agent_toolset_20260401.enabled_tools, and accepts only the type field (no per-tool selection):
{"tools": [{"type": "agent_toolset_20260401"}, {"type": "browser_toolset_20260714"}]}Any request that writes this into tools — agent create and agent update — must carry the Beta header:
x-qoder-beta: browser-use-2026-07-14
Omitting it on those writes returns 400 (tools containing browser_toolset_20260714 require the 'x-qoder-beta: browser-use-2026-07-14' header.); 503 feature_not_available means Browser Use is temporarily off. Creating a session against an agent that already has the toolset does not need the header (the toolset lives in the agent snapshot; session create doesn't re-validate it). Being Beta, its behavior and limits may change. Updating an agent affects only sessions created afterwards — existing sessions keep their agent snapshot.
Update is POST to the agent path.
Partial update with optimistic concurrency control — version is required and must match the current value (fetch via GET first). Stale version returns 409 Version conflict. On success version increments.
{
"name": "updated-name",
"version": 1
}Semantics per field: scalar fields (name, system, description, model) — omitted fields are preserved. Array fields (tools, mcp_servers, skills) are full replacements of the stored list. metadata is a patch: string values upsert keys, null values delete keys.
No body. Returns the agent with archived_at set.
No body. Can be called directly without archiving first.
| Method | Path | Description |
|---|---|---|
| GET | /models |
List models enabled for the account |
Response (not paginated, has_more always false; values below are illustrative — query live for current models/windows):
{
"data": [
{
"id": "ultimate",
"type": "model",
"display_name": "Ultimate",
"is_enabled": true,
"efforts": ["low", "medium", "high", "xhigh", "max"],
"default_effort": "high",
"default_context_window": 200000,
"available_context_windows": [200000, 400000, 1000000]
}
],
"has_more": false
}Use id as the agent's model, and efforts / available_context_windows to fill the object form's effort / context_window. Note: some models (e.g. auto) expose no efforts/context_windows — they only accept the string form.
| Method | Path | Description |
|---|---|---|
| POST | /environments |
Create environment |
| GET | /environments |
List environments |
| GET | /environments/{id} |
Get environment |
| POST | /environments/{id} |
Update environment |
| POST | /environments/{id}/archive |
Archive environment |
| DELETE | /environments/{id} |
Delete environment only when no session references it |
{
"name": "my-env",
"config": {
"type": "cloud",
"networking": {"type": "unrestricted"},
"packages": {"apt": ["curl"], "npm": [], "pip": []},
"setup_script": "set -euo pipefail\n[ -d /workspace/.git ] || git clone https://github.com/me/repo /workspace"
}
}Always set
networkingexplicitly.configis optional — omit it and you get a default cloud environment — but whennetworkingis absent it resolves tolimited(no package managers, no MCP egress), and that choice is silent until the agent later fails to install packages or reach the network. A present-but-emptyconfig: {}orconfig: nullis rejected; either omit the key entirely or give it a valid object withtype.
config.type:"cloud"or"self_hosted". The other config fields are only valid forcloud.networking.type:limited(default when omitted) orunrestricted.packages: maps package managers (apt,npm,pip) to arrays of package spec strings.setup_script: shell script run via/bin/bash -lcduring sandbox preparation, afterpackagesinstall. Non-zero exit aborts session startup (error includes exit code + stderr excerpt). Make it idempotent — it may run on every sandbox provision.
A new environment is immediately usable — there is no status field on the environment object. Actual container provisioning (package install, setup_script) happens when a session starts, which is part of why first turns are slower.
Environment deletion is stricter than session deletion: DELETE /environments/{id} succeeds only when no session references the environment. Deleted sessions may retain that reference, so an environment used by a test session should normally be archived with POST /environments/{id}/archive instead of deleted.
| Method | Path | Description |
|---|---|---|
| POST | /sessions |
Create session |
| GET | /sessions |
List sessions |
| GET | /sessions/{id} |
Get session |
| POST | /sessions/{id} |
Update session (title/metadata) |
| POST | /sessions/{id}/archive |
Archive session |
| DELETE | /sessions/{id} |
Delete session |
| POST | /sessions/{id}/cancel |
Cancel running turn |
| POST | /sessions/{id}/resources |
Add a file resource mid-session |
| GET | /sessions/{id}/resources |
List session resources |
| GET | /sessions/{id}/threads |
List threads (managed-agent sessions) |
{
"agent": "agent_xxx",
"environment_id": "env_xxx",
"title": "Optional title",
"environment_variables": "FEATURE_FLAG=on;LOG_LEVEL=debug",
"resources": [
{"type": "file", "file_id": "file_xxx", "mount_path": "/data/inputs/data.csv"},
{"type": "github_repository", "url": "https://github.com/org/repo", "authorization_token": "ghp_xxx", "mount_path": "/app/repo"},
{"type": "memory_store", "memory_store_id": "memstore_xxx", "access": "read_write"}
],
"vault_ids": []
}Critical: field name is
agent, NOTagent_id. It accepts a string ID or an object{"id": "agent_xxx", "type": "agent", "version": 2}to pin an agent version (object form must includetype: "agent").
environment_idis required; the environment must exist and not be archived.- If the agent declares
mcp_servers, the platform performs MCP tool discovery during this call — an unreachable or unauthorized MCP server fails session creation. Attach the credential vault viavault_idsfor authenticated servers. resources[]entries carry atypediscriminator:file(optionalmount_path, defaults to/mnt/session/uploads/<file_id>),github_repository(requiresurl+authorization_token; cloned atmount_path),memory_store(optionalaccess:read_only/read_write, optionalinstructions).environment_variablesis a string ofKEY=VALUEpairs separated by;or newlines. Names must match[A-Za-z_][A-Za-z0-9_]*; reserved names (SERVER_ENDPOINT,USER_ID,WORK_DIR, prefixesCAW_/QODER_) are rejected. The response echoes it as a JSON object. Note: the variables are exported to login shells — the agent's Bash tool runs non-login commands by default, so have the agent usebash -lc '...'when it needs to read them.- Legacy fields
environment,vaults,memory_store_idsare not supported. - Self-hosted environments do not support session resources or environment variables.
On success returns the session object with type: "session", status (idle initially), embedded agent snapshot, resources, stats, usage, deployment_id. Legacy fields agent_id, turn_status are gone.
Session status values: rescheduling, running, idle, canceling (transient), terminated.
usage is a cumulative credits snapshot for the session: {"total_credits": 5.94} (0 on a fresh session, field omitted when no usage data exists). Values are floored to at most 2 decimals (7.6681 → 7.66), and JSON drops trailing zeroes (1.20 → 1.2). Treat it as a snapshot — overwrite your local value per session ID, never accumulate it yourself.
If the agent uses
browser_toolset_20260714, that toolset is captured in the agent snapshot at agent create/update time — session create needs no extra header.
No body. Acknowledged immediately and safe to call even when the session is already idle. The session passes through the transient canceling status and settles asynchronously — watch the stream for session.status_idle or poll GET /sessions/{id}.
| Method | Path | Description |
|---|---|---|
| POST | /sessions/{id}/events |
Send event(s) |
| GET | /sessions/{id}/events |
List events (paginated, `order=asc |
| GET | /sessions/{id}/events/stream |
SSE stream |
{
"events": [
{
"type": "user.message",
"content": [{"type": "text", "text": "Hello, what can you do?"}]
}
]
}Critical ×2: (1) the body must wrap events in an
eventsarray — a bare event object returns 400. (2)contentmust be a non-empty array of content blocks — a plain string returns 400.
Accepted client event types:
| Type | Required fields | Notes |
|---|---|---|
user.message |
content |
array of content blocks |
user.interrupt |
— | optional session_thread_id |
user.tool_confirmation |
tool_use_id, result |
result: allow / deny; optional deny_message |
user.tool_result |
tool_use_id |
self-hosted worker tool results |
user.custom_tool_result |
custom_tool_use_id |
client-side custom tool results |
user.define_outcome |
description, rubric |
rubric: {"type":"text","content":"..."} or {"type":"file","file_id":"..."} |
system.message |
content |
max one per request, must be the final event |
Accepted asynchronously; the response body is {data: [...]} containing the accepted event objects.
SSE endpoint. See events.md for event types, event_deltas[] incremental streaming, and Last-Event-ID reconnection.
GET /sessions/{id}/events/stream
Authorization: Bearer <PAT>
Accept: text/event-stream
Last-Event-ID: evt_xxx # optional, resume after this event
Optional query param event_deltas[] (repeatable; values agent.message, agent.thinking) turns on incremental output for this connection.
Query params: page / after_id / before_id (mutually exclusive), limit (default 20, max 100), order (asc/desc).
| Method | Path | Description |
|---|---|---|
| POST | /files |
Upload file (multipart) |
| GET | /files |
List files |
| GET | /files/{id} |
Get file metadata |
| GET | /files/{id}/content |
Get a presigned download URL |
| DELETE | /files/{id} |
Delete file |
Multipart form upload — text-based files only:
Content-Type: multipart/form-data
- file: <binary> # required
- name: "my-data.csv" # optional, defaults to uploaded filename
- metadata: '{"k":"v"}' # optional JSON string, max 8 KB
The legacy
purposeform field has been removed — do not send it.
Response 200:
{
"id": "file_xxx",
"type": "file",
"filename": "my-data.csv",
"mime_type": "text/csv",
"size_bytes": 1024,
"downloadable": false,
"scope": null,
"metadata": {},
"created_at": "2026-01-01T00:00:00Z"
}The ID field is
id(notfile_id). To let an agent read the file, attach it as a session resource:{"type": "file", "file_id": "file_xxx"}onPOST /sessions(orPOST /sessions/{id}/resourcesmid-session). Default mount path is/mnt/session/uploads/<file_id>; passmount_pathto control it.
Only works when the file object's downloadable is true (agent-delivered artifacts are; your own uploads generally are not). Returns a short-lived presigned URL:
{"url": "https://...", "expires_at": "2026-01-01T01:00:00Z"}Fetch with curl -L "$url" — no Authorization header needed. Mint a fresh URL by calling the endpoint again after expiry.
Agents deliver output files via the built-in DeliverArtifacts tool (enabled by default; must be listed explicitly if you set a non-empty enabled_tools allowlist). Each delivery emits an agent.artifact_delivered event carrying file_id, original_filename, content_type, and size — that is the signal to consume. Delivered artifacts are File objects with downloadable: true and scope set to the session; download via GET /files/{id}/content.
| Method | Path | Description |
|---|---|---|
| POST | /memory_stores |
Create memory store |
| GET | /memory_stores |
List memory stores |
| GET | /memory_stores/{id} |
Get memory store |
| POST | /memory_stores/{id} |
Update store (name/description/metadata) |
| POST | /memory_stores/{id}/archive |
Archive store |
| DELETE | /memory_stores/{id} |
Delete store |
| POST | /memory_stores/{id}/memories |
Create memory entry |
| GET | /memory_stores/{id}/memories |
List memories |
| GET | /memory_stores/{id}/memories/{mem_id} |
Get memory |
| POST | /memory_stores/{id}/memories/{mem_id} |
Update memory |
| DELETE | /memory_stores/{id}/memories/{mem_id} |
Delete memory |
ID prefix is memstore_ for stores, mem_ for entries.
{
"name": "project-knowledge",
"description": "Included in the agent's system prompt when attached",
"metadata": {"team": "backend"}
}Entries are path-addressed — path is required and unique within the store (duplicate path returns 409):
{
"path": "infra/deployment-target.md",
"content": "The deployment target is us-west-2.",
"metadata": {"category": "infra"}
}Attach a store via session resources: {"type": "memory_store", "memory_store_id": "memstore_xxx", "access": "read_write"}. Only the store's description is injected into the system prompt — entry contents are not auto-loaded into context; the agent reads/writes them through its own tools at runtime. So set a description/instructions that tells the agent what's in the store and when to consult it, and prompt it to check its memory in the turn itself — otherwise a bare "what do you remember?" can come back empty. See tools-and-resources.md § Memory Stores.
| Method | Path | Description |
|---|---|---|
| POST | /skills |
Upload skill (multipart/form-data) |
| GET | /skills |
List skills |
| GET | /skills/{id} |
Get skill |
| GET | /skills/{id}/versions |
List skill versions |
| PUT | /skills/{id} |
Update skill (JSON) |
| DELETE | /skills/{id} |
Delete skill (no archive needed) |
Multipart form upload — a .zip (max 50 MiB) with SKILL.md at the zip root (or one directory below):
Content-Type: multipart/form-data
- file: <skill.zip>
- type: "custom" (optional, default)
- metadata: '{"k":"v"}' (optional JSON string)
name / description form fields are accepted but overridden by the SKILL.md YAML frontmatter (name, description, version).
Returns the created skill object (use its id for binding and deletion).
JSON body (this one IS a PUT, unlike agents): name, description, metadata, and optionally new content — plain text, or a base64-encoded zip with content_encoding: "base64".
No body. Immediate — no archive step.
Bind via agent create or update (POST /agents/{id}, requires version):
{
"version": 1,
"skills": [{"type": "custom", "skill_id": "skill_xxx"}]
}
skillsis a full-replacement array on update. The agent object now returnsskillson GET, so you can verify the binding by reading the agent.
Vaults hold credentials the runtime injects — most importantly auth for MCP servers (the agent's mcp_servers never carry inline tokens). Attach a vault to a session with vault_ids: ["vault_xxx"].
| Method | Path | Description |
|---|---|---|
| POST | /vaults |
Create vault |
| GET | /vaults |
List vaults |
| GET | /vaults/{id} |
Get vault |
| POST | /vaults/{id}/archive |
Archive vault |
| DELETE | /vaults/{id} |
Delete vault |
| POST | /vaults/{id}/credentials |
Add a credential |
| GET | /vaults/{id}/credentials |
List credentials |
| DELETE | /vaults/{id}/credentials/{cred_id} |
Delete a credential |
ID prefixes: vault_ for vaults, vcred_ for credentials.
{"display_name": "mcp-creds", "metadata": {"team": "docs"}}Returns the vault with type: "vault", credentials: [], archived_at, timestamps.
The credential is a wrapper around an auth object; the auth.type decides the shape:
{"auth": {"type": "static_bearer", "mcp_server_url": "https://my-mcp.example.com/mcp", "token": "<secret>"}}auth.type |
Required fields | Use |
|---|---|---|
static_bearer |
mcp_server_url, token |
A fixed bearer token for an MCP server. mcp_server_url must match the agent's mcp_servers[].url. |
environment_variable |
secret_name, secret_value |
Inject a secret as an env var into the container (secret_name must match [A-Za-z_][A-Za-z0-9_]*). |
Secrets are write-only. The response and both list/get echo the credential shape (
id,auth.type,auth.mcp_server_url,secret_name) but never thetoken/secret_value. To rotate a secret, delete the credential and add a new one; there is no in-place update.
- Create the vault, add a
static_bearercredential whosemcp_server_urlequals the agent's MCP server URL. - Create the session with
vault_ids: ["vault_xxx"]. - On session create the platform runs MCP tool discovery using that credential — a missing/mismatched credential surfaces as an upstream
Unauthorizedand fails session creation (seetools-and-resources.md§ MCP Tools).
Run an agent on a cron schedule (or manually) — each run creates a session and delivers initial_events to it.
| Method | Path | Description |
|---|---|---|
| POST | /deployments |
Create deployment |
| GET | /deployments |
List deployments |
| GET | /deployments/{id} |
Get deployment |
| POST | /deployments/{id} |
Update deployment |
| POST | /deployments/{id}/pause |
Pause schedule |
| POST | /deployments/{id}/unpause |
Resume schedule |
| POST | /deployments/{id}/run |
Trigger a run now |
| GET | /deployments/{id}/runs |
List runs of a deployment |
| GET | /deployment_runs |
List runs across all deployments (top-level path) |
| POST | /deployments/{id}/archive |
Archive deployment |
{
"name": "daily-report",
"agent": "agent_xxx",
"environment_id": "env_xxx",
"schedule": {"type": "cron", "expression": "0 9 * * *", "timezone": "Asia/Shanghai"},
"initial_events": [
{"type": "user.message", "content": [{"type": "text", "text": "Generate today's status report"}]}
],
"resources": [],
"vault_ids": []
}scheduleomitted ⇒ manual-only deployment (trigger withPOST /deployments/{id}/run).initial_events: 1-50 events, typesuser.message/user.define_outcome/system.message, content-block format.environment_variables(string, same format/validation as sessions) andmetadataare also supported.- ID prefix
dep_. Newly created deployments areactiveand fire immediately per schedule.
Each run creates a session with deployment_id set — find run output by streaming/listing that session's events.
| Code | Meaning |
|---|---|
| 200 / 201 | Success (create, get, update, archive, delete) |
| 202 | Accepted (async operations) |
| 400 | Bad request (missing/invalid fields, plain-string content, multiple cursors) |
| 401 | Unauthorized (invalid/missing PAT) |
| 404 | Resource not found |
| 409 | Conflict (version mismatch on agent update, duplicate memory path) |
| 429 | Rate limited — honor Retry-After, back off |
| 500 | Internal server error |