Agent API Reference

Run the CodingFleet agent from your own code. You send a task; the API runs the model loop, executes tools in a Linux sandbox, and streams back every tool call and the answer. You write no agent loop, run no container, and hold no provider key.

Get an API key

Overview

Every endpoint lives under https://codingfleet.com/v1. Requests and responses are JSON, except a run, which streams server-sent events. All timestamps are ISO 8601 in UTC. All ids are opaque strings — do not parse them.

Authentication

Send your key as a bearer token on every request. Create and revoke keys on the Agent API page. A revoked key stays on that page for 7 days, then it is deleted.

curl https://codingfleet.com/v1/credits \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

A key spends your credits. It reaches your models and carries your plan's run limit. Only a hash of it is stored, so a lost key cannot be recovered — create another and revoke the old one. Give each integration its own key.

Two kinds of key

A key for code (scripts, CI, integrations) runs the agent: sessions, runs, files and models. It cannot change your account: changing settings, your own provider keys and billing answer 403 desktop_key_required. It reaches the sandboxes, snapshots and specs of API sessions, never the web chat's: a snapshot of the web chat's sandbox, the spec pinned on the web, and a spec the desktop app's sessions use also need a desktop key (see sandboxes). So a key that leaks from a CI log costs at most the credits it can spend. A desktop key, made by the desktop app's browser sign-in, can do those too, and is priced like the web chat.

Expiry

When you create a key you can make it expire in 7, 30, 90 or 365 days, or never. An expired key answers 401 token_expired; create a new one. GET /v1/credits shows the key's expires_at (null: never).

Quickstart

Two calls: open a session, give it a task.

# 1. Open a session. It owns a sandbox and remembers the conversation.
SESSION=$(curl -s https://codingfleet.com/v1/sessions \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"system_message": "You are a coding agent. Use your tools."}' \
  | python -c "import json,sys; print(json.load(sys.stdin)['id'])")

# 2. Give it a task. The response is a stream of events.
curl -N https://codingfleet.com/v1/sessions/$SESSION/runs \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Write fizzbuzz.py, run it, and show me the output."}'

# Send more runs to the same session whenever you like. Its sandbox stops by
# itself when idle, and the next run starts it again with the same files.

How it fits together

ObjectWhat it is
Session A conversation plus the machine it works on. Holds the message history, the provider's prompt cache and its own sandbox container. Sessions are not limited and never end on their own: a session lasts until you delete it.
Run One turn inside a session: your message, the model's tool calls, and its answer. Runs are billed; sessions are not.
Sandbox A Linux container belonging to one session. Created on the first run that uses a tool, not when the session is created. When it is idle it is stopped, after a snapshot of its files, and the next run that uses a tool starts it again from that snapshot. Two sessions never share one, and API snapshots and web app snapshots never see each other.

Why sessions are worth it. The second run in a session reuses the warm container and the cached prompt prefix, so it is faster and cheaper than the first. A run per session pays a cold start every time.

Sessions

POST /v1/sessions

Open a session. Returns 201.

Body parameters

NameTypeDefaultDescription
modelstring your default Default model for runs in this session, from GET /v1/models. Omit it to use the default model in your settings (Auto-Select when you have none). A run can override it.
system_messagestring coding agent The agent's standing instructions. Omit it to use a short built-in coding-agent prompt.
capabilitiesobject see below Which tool groups the agent may use. Keys: code_execution (default true — the sandbox, files and shell), web_access (false), image_generation (false), parallel_agents (false — sub-agents: the agent can delegate tasks to helpers that run at the same time; see Sub-agents), memory (false — your CodingFleet memory in the prompt, plus tools to search your past conversations, web chats and API sessions alike, and to update that memory). An unknown key gets 400 unknown_field.
git_repositorystring null Id of a repository you connected in the web app. The sandbox is started with it cloned. Forces code_execution on, since the repo only exists inside the sandbox.
snapshot_sandboxboolean true Save a snapshot of the sandbox filesystem before the sandbox is stopped. The next run on this session restores it, and so does restore_from on a new session. Set it to false for a throwaway sandbox that starts empty each time. Ignored for executor: "client".
restore_fromstring null Id of an earlier session. The new session starts with an empty history, but its sandbox starts from that session's last snapshot. That session's sandbox must have been stopped at least once with snapshot_sandbox on.
sandbox_specstring null Id of a sandbox spec: the session's sandbox is built from it (your Dockerfile, a Docker Hub image such as golang:1.23, or a snapshot) instead of the standard image or the spec pinned in your sandbox settings. When the session has a snapshot of its own (its sandbox was stopped) or restore_from, that snapshot is restored first. Needs code_execution.
executorstring "sandbox" Where tools run. "sandbox": a CodingFleet sandbox. "client": your machine. The run sends each coding tool call to you and waits for the result. See client execution. On the Free plan only "client" can run tools: a cloud sandbox needs a paid plan.

Request

curl https://codingfleet.com/v1/sessions \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "inclusionai/ling-3.0-flash",
    "system_message": "You are a coding agent with a Linux sandbox. Use your tools.",
    "capabilities": {"code_execution": true, "web_access": true}
  }'

Response 201

{
  "id": "bTaQBvzPGHZrSlA6",
  "title": null,
  "pinned": false,
  "executor": "sandbox",
  "model": "inclusionai/ling-3.0-flash",
  "snapshot_sandbox": true,
  "created_at": "2026-09-13T09:14:02.117441+00:00",
  "last_used_at": "2026-09-13T09:14:02.117441+00:00",
  "capabilities": {
    "code_execution": true,
    "web_access": true,
    "image_generation": false,
    "parallel_agents": false,
    "memory": false
  },
  "git_repository": null,
  "restore_from": null,
  "sandbox_spec": null,
  "sandbox": null
}

sandbox is null until the first run that uses a tool builds one.

Only the fields above are accepted. Any other field, such as image, dockerfile or env, gets 400 unknown_field. To choose the image and env vars, save a sandbox spec and send its id as sandbox_spec.

Errors

400unknown_field A field this endpoint does not take, or an unknown key in capabilities. fields names them.
400executor_conflict executor: "client" with an option that needs a sandbox: git_repository, restore_from or sandbox_spec.
400invalid_body A field of the wrong type, or sandbox_spec with code_execution off.
404spec_not_found sandbox_spec names no spec of yours.
403desktop_key_required A key for code, and sandbox_spec is the pinned spec, a spec the desktop app's sessions use, or a spec built from a snapshot of the web chat's sandbox.
400invalid_executor Unknown executor value.
403paid_plan_required Free plan: a session that would start a cloud sandbox (executor: "sandbox" with code_execution, git_repository or restore_from). Use executor: "client".
404repository_not_found No repository with that id on your account.
404session_not_found restore_from names no session of yours.
409snapshot_not_found The restore_from session has no snapshot.
GET /v1/sessions

List your sessions, newest first, with how many runs you have going.

Query parameters

NameTypeDefaultDescription
limitinteger25 How many to return. Capped at 100.

Request

curl "https://codingfleet.com/v1/sessions?limit=10" \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{
  "sessions": [
    { "id": "bTaQBvzPGHZrSlA6", "title": "FizzBuzz script", "pinned": false, "executor": "sandbox", "...": "..." }
  ],
  "concurrency": { "limit": 5, "active": 1 }
}
GET /v1/sessions/{session_id}

One session, including its sandbox if it has one.

Request

curl https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6 \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{
  "id": "bTaQBvzPGHZrSlA6",
  "title": "FizzBuzz script",
  "pinned": false,
  "executor": "sandbox",
  "model": "inclusionai/ling-3.0-flash",
  "snapshot_sandbox": true,
  "created_at": "2026-09-13T09:14:02.117441+00:00",
  "last_used_at": "2026-09-13T09:21:44.902130+00:00",
  "capabilities": { "code_execution": true, "web_access": false,
                    "image_generation": false, "parallel_agents": false,
                    "memory": false },
  "git_repository": null,
  "restore_from": null,
  "has_snapshot": false,
  "sandbox": {
    "id": "sb-GMQ0w9pCd78gjARaUZ7E48",
    "created_at": "2026-09-13T09:15:25.300000+00:00",
    "expires_at": "2026-09-14T09:15:25.299502+00:00",
    "snapshot_on_reap": true
  }
}

Returns 404 session_not_found for an id that doesn't exist.

GET /v1/sessions/{session_id}/messages

The session's conversation so far, to show a past session again.

Request

curl https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/messages \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{
  "session_id": "bTaQBvzPGHZrSlA6",
  "messages": [
    { "role": "user", "text": "Run the tests", "files": [], "steers": [], "created_at": "2026-09-13T09:13:52+00:00" },
    { "role": "assistant", "text": "Running the tests.

All 3 tests pass.", "model": "DeepSeek V4 Flash",
      "tools": [ { "name": "run_command", "arguments": { "command": "pytest" }, "ok": true,
                   "text_offset": 18 } ],
      "usage": { "credits": 0.5, "prompt_tokens": 2410, "cache_read_input_tokens": 1800,
                 "cache_creation_input_tokens": 0, "completion_tokens": 96,
                 "reasoning_tokens": 0, "time_taken": 12.4 },
      "created_at": "2026-09-13T09:14:05+00:00" }
  ]
}

One user and one assistant message per finished run, oldest first. A user message's created_at is when its run started; an assistant message's is when the run ended, and its usage is what that run cost. steers are the messages you sent into that run with /steer. tools lists the tools the run used, in order; a string argument longer than 2,000 characters is cut. A run still going is not in the list until it ends. Returns 404 session_not_found for an id that doesn't exist.

compactions lists each time the conversation was compacted, oldest first: at, before_tokens, after_tokens, and manual (true for compact, false when a run did it). One that a run did falls between that run's user and assistant messages.

text_offset is where in text the tool was called: the text before that offset came before the call, the rest after it. Tools with the same offset were called together, with no text between them. To show the turn as it ran, cut text at each offset and put the tools there. Runs finished before offsets were kept, and a few internal steps, have no text_offset.

A spawn_agent entry in tools carries subagent (id, role, model, summary, credits, error). The sub-agent's own tool calls are not listed with the turn; read the sub-agent for its work.

A user message's files are the uploads it carried, as file objects with deleted added.

model is the model's display name. usage.credits is the run's exact price, which can be a fraction such as 0.5; runs finished before exact prices were kept show whole credits.

A session is also a chat on the website, and the list holds every turn, including ones sent from the web chat. To notice those, compare the session's last_message_at with the last one you showed, and check busy on the open session: it is true while a turn is going in any client. The web chat shows an API run's progress live, tool by tool, as it does for its own runs.

GET /v1/sessions/{session_id}/agents/{agent_id}

What one sub-agent of the session was asked and wrote, to show it after its run.

Request

curl https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/agents/4f1c2a9be07d3316 \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{ "id": "4f1c2a9be07d3316", "role": "Test runner", "model": "DeepSeek V4 Flash",
  "task": "Run the test suite and report failures.",
  "text": "All 42 tests pass.",
  "summary": "2 tool calls: run_command ×2", "credits": 1, "error": false }

agent_id comes from subagent.started, from a spawn_agent result, or from /messages. Returns 404 agent_not_found when the session has no such sub-agent.

PATCH /v1/sessions/{session_id}

Rename a session, or pin it. The web chat shows the same title and pin.

Body parameters

NameTypeDescription
titlestring The new name, at most 200 characters.
pinnedboolean Pin or unpin the session. Send at least one of the two.

Request

curl -X PATCH https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6 \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Auth refactor", "pinned": true}'

Returns the session. 400 invalid_body for a blank or too long title; 409 session_broken for a session whose chat was deleted in the web app.

POST /v1/sessions/{session_id}/duplicate

A new session with the same settings and the same conversation so far. Returns 201 and the new session.

The copy is free: no turn is copied or billed again, and from here on the two sessions answer independently. The sandbox is not copied; the copy's first sandbox starts from the original's snapshot when it has one. The title starts with "Copy of".

curl -X POST https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/duplicate \
  -H "Authorization: Bearer $CODINGFLEET_KEY"
DELETE /v1/sessions/{session_id}

Delete a session for good: its conversation, its runs and its sandbox. This cannot be undone.

Request

curl -X DELETE https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6 \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{ "id": "bTaQBvzPGHZrSlA6", "deleted": true }

409 run_in_progress while a run is going: cancel it first. A duplicate of the session keeps the conversation it copied.

Sessions never end on their own, so you never need to close one. What ends is the sandbox: it is stopped when idle for your plan's timeout (5 minutes on Free; 10 on Pro; 20 on Unlimited; 30 on Elite; 60 on Ultimate and Ultimate Max), and when it reaches 24 hours. Its files are snapshotted first (unless the session set snapshot_sandbox to false), and the next run starts it again from that snapshot.

Commands can keep running in the sandbox after their call: dev servers, watchers, long builds. The agent starts one with background: true on run_command or execute_code, optionally with stop_after (seconds, at most a day) to stop it after that long, and a command still running at its timeout stays running too. That call's tool.result output starts with a background object, {"id": "bg_1a2b3c4d", "status": "running", "origin", "note"}, before the output so far. origin is model, user (your client moved it) or timeout, and note says the same to the agent. The agent then reads what the command printed with read_background (id, wait) and ends it with stop_background (id), across later runs of the session. A background command ends with the sandbox. Your client can list, read and stop them too, and move a waiting one there.

DELETE /v1/sessions/{session_id}/sandbox

Stop the session's sandbox now, instead of when it goes idle. The session stays; its next run that uses a tool starts the sandbox again.

curl -X DELETE https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/sandbox \
  -H "Authorization: Bearer $CODINGFLEET_KEY"
{ "id": "bTaQBvzPGHZrSlA6", "stopped": true, "snapshot": true }

stopped is false when no sandbox was running. snapshot says whether a snapshot was taken first. 409 run_in_progress while a run is going.

GET /v1/sessions/{session_id}/background

The commands left running in the session's sandbox, newest first: dev servers, watchers, long builds. Only for a session with a sandbox.

curl "https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/background?refresh=1"   -H "Authorization: Bearer $CODINGFLEET_KEY"
{ "jobs": [
  { "id": "bg_1a2b3c4d", "command": "npm run dev", "state": "running", "exit_code": null,
    "started": 1760000000.5, "ended": null, "reason": "", "origin": "model",
    "last": "Local: http://localhost:3000/", "port": 3000,
    "url": "https://ta-0123-3000.w.modal.host" }
] }

state is running, exited, stopped or lost (its sandbox shut down; reason says why). origin: model (started with background: true), user (moved while it ran) or timeout. last is the last line it printed and port the local port it said it listens on; url is that port's public tunnel, given with refresh=1. Without refresh=1 the sandbox is not asked, only whether it still runs. Finished commands stay in the list for a while.

GET /v1/sessions/{session_id}/background/{job_id}

The end of what the command printed: {"id", "state", "exit_code", "stdout", "stderr", "cut", "url"}, the last 20,000 bytes of each stream (cut is true when there was more). Reading it here does not count as the agent reading it. 404 job_not_found.

POST /v1/sessions/{session_id}/background/{job_id}/stop

Stop the command and every process it started: {"id", "status", "exit_code"}. A command that already ended is left as it is. 409 no_sandbox for a session whose tools run on the client.

POST /v1/sessions/{session_id}/compact

Summarize the session's older messages now, to free context. The newest messages stay as they are; the next run works from the summary. A run does this on its own when the conversation nears the context limit.

curl -X POST https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/compact   -H "Authorization: Bearer $CODINGFLEET_KEY"
{
  "id": "bTaQBvzPGHZrSlA6",
  "compaction": { "at": "2026-09-18T18:17:12+00:00", "before_tokens": 48210,
                  "after_tokens": 9120, "manual": true },
  "context": { "tokens": 9120, "limit": 100000 }
}

Costs no credits. 409 nothing_to_compact when the conversation is under 20,000 tokens or has nothing saved yet; 409 run_in_progress while a run is going. Every compaction, by this call or during a run, is listed in the session's messages as compactions.

GET /v1/sessions/{session_id}/files

List what is on the session's sandbox filesystem.

Query parameters

NameTypeDefaultDescription
pathstring"." Directory to list, inside the sandbox.

Request

curl "https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/files?path=/workspace" \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{
  "sandbox": "sb-GMQ0w9pCd78gjARaUZ7E48",
  "path": "/workspace",
  "listing": "total 12\ndrwxr-xr-x 2 root root 4096 Sep 13 09:20 .\n-rw-r--r-- 1 root root  61 Sep 13 09:20 fizzbuzz.py\n"
}

Before any sandbox exists you get {"files": [], "sandbox": null}. 409 no_sandbox if the session does not run one; 502 sandbox_error if the container cannot be reached.

Runs

POST /v1/sessions/{session_id}/runs

Give the agent a task. Streams events as text/event-stream, or returns 202 immediately when detached.

Body parameters

NameTypeDefaultDescription
messagestring required The task. No character limit: the whole conversation must fit your plan's input token limit, as in the web app.
modelstring session's Override the session's model for this turn. With neither, the run uses the default model in your settings, or Auto-Select.
filesstring[] none Up to 10 uploaded file ids to attach to this message: server uploads, or this session's sandbox uploads. Images go to the model to look at; other files are listed for it, and it reads them with its file tools. An image needs a model with supports_vision, or Auto-Select, which then picks one.
detachedboolean false Do not stream. Returns a run id at once and keeps working. Read the result with GET /v1/runs/{id} or attach with /events. Use this from CI, where nothing should hold a connection open for ten minutes.
mcp_serversobject none Remote MCP servers the agent may use in this run. See MCP servers and local tools.
client_toolsobject[] none Tools your client runs itself, such as local MCP servers. Client sessions only. See MCP servers and local tools.
computer_useboolean false Offer the computer tool: the agent uses the mouse, the keyboard and other apps' windows. Client sessions only, and only the CodingFleet desktop app runs it (see the client tools).
model_fallbackboolean true When the model cannot answer (a usage limit, a daily limit, a rate limit or an outage, or it declines the request), the run moves to another model and says so: see model switches. Set it to false for runs whose result must come from the model they started on (automated review, benchmarks): the run then fails instead. Before it starts, that is 409 model_unavailable; once it streams, it ends with error.

Any other field gets 400 unknown_field.

A stopped sandbox comes back. When the session's sandbox was stopped, the run's first tool call starts it again from the session's snapshot. Without a snapshot (snapshot_sandbox: false) it starts empty: the run begins with a notice event saying so, and the model is told the same.

Request — streaming

curl -N https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/runs \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Write fizzbuzz.py, run it, show the output."}'

Response 200 · text/event-stream

event: run.started
data: {"run_id": "LXDMO5UQo5DXr0UV", "session_id": "bTaQBvzPGHZrSlA6", "model": "inclusionai/ling-3.0-flash"}

event: run.meta
data: {"model": "Ling 3.0 Flash", "model_unique_name": "inclusionai/ling-3.0-flash", "auto_routed": false, "tools": {"use_code_execution": true}}

event: tool.call
data: {"id": "0613ecb00755", "name": "fs_write", "arguments": {"path": "/fizzbuzz.py", "content": "for i in range(1, 16):\n    ..."}}

event: tool.result
data: {"id": "0613ecb00755", "name": "fs_write", "ok": true, "output": {"stdout": "File created.", "stderr": null}, "truncated": false}

event: text.delta
data: {"text": "Done. The script prints "}

event: usage
data: {"credits": 1, "credits_remaining": 480, "model": "Ling 3.0 Flash", "requested_model": "inclusionai/ling-3.0-flash", "model_id": "inclusionai/ling-3.0-flash", "model_switches": [], "prompt_tokens": 334, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "completion_tokens": 26, "reasoning_tokens": 0, "time_taken": 3.7, "context_tokens": 412}

event: run.ended
data: {"run_id": "LXDMO5UQo5DXr0UV", "reason": "completed", "finish_reason": "stop"}

Lines beginning : are keepalive comments; ignore them. See the event protocol for every event.

Request — detached

curl https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/runs \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Refactor the auth module and run the tests.", "detached": true}'

Response 202

{
  "id": "LXDMO5UQo5DXr0UV",
  "session_id": "bTaQBvzPGHZrSlA6",
  "status": "running",
  "reason": null,
  "detached": true,
  "model": "inclusionai/ling-3.0-flash",
  "requested_model": "inclusionai/ling-3.0-flash",
  "model_id": null,
  "model_switches": [],
  "model_fallback": true,
  "credits_used": 0.0,
  "prompt_tokens": 0,
  "completion_tokens": 0,
  "error": null,
  "rounds": [],
  "created_at": "2026-09-13T09:22:41.465732+00:00",
  "ended_at": null
}

Errors

400missing_message message absent or blank.
400unknown_model model is not an id from GET /v1/models.
400file_not_found A files id is not one of your uploads, or was deleted.
400too_many_files More than 10 files.
400file_not_in_session A files id was uploaded into another session's sandbox.
400vision_unsupported An image is attached and model cannot read images.
400invalid_body A malformed mcp_servers or client_tools, a computer_use, detached or model_fallback that is not a boolean, or an MCP server URL that is not public https.
400unknown_field A field this endpoint does not take, or an unknown field in an mcp_servers entry or a client_tools entry.
400executor_conflict client_tools or computer_use in a session that does not run tools on the client.
403private_session_mode Private session mode is on in your settings. It promises that nothing new is stored, and a session is its stored history, so runs are refused until you turn it off.
409session_broken The session's chat was deleted in the web app.
409run_in_progress A session runs one turn at a time. Wait for the current run to end, or cancel it; the body carries its run_id.
409model_unavailable model_fallback: false, and the model cannot take this run (the weekly usage limit or the 5-hour limit on Unlimited models). The message says which limit. Nothing was billed.
402rejected Out of credits. link points to the page that fixes it.
403rejected The model is not on your plan, or another gate refused the turn. link, when present, points to the page that fixes it.
403model_disabled The model was retired. Pick one from GET /v1/models.
404session_not_found—
429run_limit_reached Your account already has as many runs going as your plan allows at the same time. The body carries limit and active. Wait for one to end or cancel it; see the limits.
GET /v1/runs/{run_id}

The state and cost of a run. This is how you poll a detached run.

Request

curl https://codingfleet.com/v1/runs/LXDMO5UQo5DXr0UV \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{
  "id": "LXDMO5UQo5DXr0UV",
  "session_id": "bTaQBvzPGHZrSlA6",
  "status": "completed",
  "reason": "completed",
  "detached": true,
  "model": "Ling 3.0 Flash",
  "requested_model": "inclusionai/ling-3.0-flash",
  "model_id": "inclusionai/ling-3.0-flash",
  "model_switches": [],
  "model_fallback": true,
  "credits_used": 1.0,
  "prompt_tokens": 402,
  "completion_tokens": 171,
  "error": null,
  "rounds": [
    {"index": 1, "model": "inclusionai/ling-3.0-flash",
     "started_at": "2026-09-13T09:22:41.512000+00:00", "ended_at": "2026-09-13T09:22:43.870000+00:00",
     "duration": 2.36, "prompt_tokens": 201, "completion_tokens": 120, "cache_read_input_tokens": 0,
     "cache_creation_input_tokens": 0, "reasoning_tokens": 0, "credits": 0.6, "tools": ["0613ecb00755"]},
    {"index": 2, "model": "inclusionai/ling-3.0-flash",
     "started_at": "2026-09-13T09:22:43.870000+00:00", "ended_at": "2026-09-13T09:22:44.990000+00:00",
     "duration": 1.12, "prompt_tokens": 201, "completion_tokens": 51, "cache_read_input_tokens": 0,
     "cache_creation_input_tokens": 0, "reasoning_tokens": 0, "credits": 0.4, "tools": []}
  ],
  "client_executor": null,
  "created_at": "2026-09-13T09:22:41.465732+00:00",
  "ended_at": "2026-09-13T09:22:45.006246+00:00"
}

status is running, completed, cancelled or error. reason adds the detail — see object shapes.

Model switches

requested_model is the model id the run asked for ("auto" for Auto-Select). model_id is the id of the model that answered, from the end of the run. When they differ for a reason other than Auto-Select, model_switches lists each move as {"from", "to", "reason"} (model ids). reason is one of:

quotaYour weekly usage limit is reached, so the model's fallback answered.
pool_capThe 5-hour limit on Unlimited models (desktop app keys).
daily_limitThe model's daily request limit on your account.
unavailableThe model was rate-limited or down before it wrote anything.
refusedThe model declined the request. Only the model that answered is charged.

The stream says the same in a notice with code model_switched, model and model_switches, and in the usage event. Send model_fallback: false when starting the run to make it fail instead of switching.

Rounds: where the time and credits went

rounds has one entry for each model request of the run: one per tool round, and the last one writes the answer. Each has index, model (id), started_at, ended_at, duration (seconds, the model request and the tools it called), the round's own prompt_tokens, completion_tokens, cache_read_input_tokens, cache_creation_input_tokens and reasoning_tokens, credits, and tools: the ids of the main agent's tool.call events in that round. A sub-agent's work is in its spawn_agent call's round.

The round credits add up to credits_used: each round gets the credits of the tools it ran (web search, image generation) and a share of the rest in proportion to its billed tokens. While the run is going, the rounds so far are listed with credits: null, and the first one's started_at is null. Some models report their tokens only for the whole run: their last round then carries all of them. Runs from before October 2026 have no rounds.

progress.tools gives the tool side of the time: each call has called_at, and ended_at once its result came back.

Is the client there?

For a run whose tools run on your machine (executor: "client"), client_executor says what the server knows of your client. null for a sandbox session.

"client_executor": {
  "connected": false,
  "last_heartbeat_at": "2026-09-13T09:31:02.114000+00:00",
  "lost": false,
  "waiting_calls": [
    {"id": "b3c1e0f2a4d54b8f9e6a7c1d2e3f4a5b", "name": "run_command",
     "called_at": "2026-09-13T09:30:40.020000+00:00"}
  ]
}
connectedboolean | null A heartbeat came in the last 45 seconds. null when the server cannot tell right now. Starting a run sets it back to false until the first heartbeat of that run.
last_heartbeat_atstring | null When the last heartbeat for this session came, kept for 7 days.
lostboolean | null A tool call waited too long for your client, so the server gave up on it: tool calls in this session fail at once until your client starts a run or returns a result.
waiting_callsobject[] Tool calls sent to your client that have no result yet: id, name, called_at, and agent_id for a sub-agent's call. Empty when the run is not going.

A run with waiting_calls and connected: false is stuck on your machine. About 10 minutes after the last heartbeat, the server stops it with reason interrupted. A call that gets no result for 30 minutes while heartbeats still come stops it the same way.

GET /v1/runs/{run_id}/events

Attach to a run that is going, or replay one that just finished. Same event stream as starting a run.

Everything the run has already emitted is replayed from the start, then you follow it live. A dropped connection during a long run costs you nothing — reconnect here. The buffer lives for one hour.

Request

curl -N https://codingfleet.com/v1/runs/LXDMO5UQo5DXr0UV/events \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

409 no_stream if that run has no replayable stream. Only the latest run in a session keeps its events: for an earlier run this returns 409 events_unavailable, and GET /v1/runs/{run_id} still has its result.

POST /v1/runs/{run_id}/heartbeat

Keep a client executor connected while its tools or approvals are pending.

Send an authenticated POST with an empty JSON object every 15 seconds, independently of the event stream. Returns {"status": "connected"}. Tool calls can keep waiting while heartbeats arrive. If heartbeats stop, a ten-minute reconnect grace period applies after the heartbeat lease expires (45 seconds). Watching a run in the web chat does not send executor heartbeats. 409 not_running means the run ended; 409 not_client means its tools run in a cloud sandbox. Retry network failures and 503 responses with backoff. GET /v1/runs/{run_id} shows whether the heartbeats arrive (client_executor).

POST /v1/runs/{run_id}/cancel

Stop a run that is still going. No body.

Request

curl -X POST https://codingfleet.com/v1/runs/LXDMO5UQo5DXr0UV/cancel \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{ "id": "LXDMO5UQo5DXr0UV", "status": "cancelling" }

A run that already finished returns its row instead, with 200. 409 not_running means it is not currently streaming — a turn needs a moment after it starts before it can be cancelled.

Work already done is still billed. Cancelling stops the turn; it does not refund it.

POST /v1/runs/{run_id}/steer

Send a correction into a run that is still going, without restarting it.

Body parameters

NameTypeDefaultDescription
messagestring required Up to 4,000 characters. Delivered at the next tool-call boundary, as a user message after that round's tool results.
filesstring[] none Up to 3 uploaded images to send with the message. Other files wait for the next run (unsupported_file_type).

Request

curl -X POST https://codingfleet.com/v1/runs/LXDMO5UQo5DXr0UV/steer \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Actually use Postgres, not SQLite."}'

Response 200

{ "status": "accepted", "steer_id": "5f2c9d1e84b74b0f9a1f0c2d3e4b5a67" }

At most 10 steers per turn, 4,000 characters each. 409 not_running if the run has finished; 409 steer_refused with a reason if the turn cannot take another.

POST /v1/runs/{run_id}/tool_calls/{call_id}/background

Stop waiting for a run_command or execute_code command that the run is waiting for in the sandbox, and leave it running. Use the id of its tool.call event.

{ "status": "requested", "call_id": "4f1c9a7b2e0d" }

The call then returns within a second, with what the command printed so far and a background object, as if the agent had asked for background: true; the command shows in the session's background commands. 409 call_not_waiting when no sandbox command of yours is waiting with that id (it finished, or it is not a command); 409 not_running when the run has ended. For a command on your own machine (executor: "client") your client does this itself.

POST /v1/runs/{run_id}/tool_results

Return the output of a tool call that a run sent to your machine. Only for sessions created with executor: "client".

In a client session the agent's coding tools run where your client runs, and no CodingFleet sandbox is started. When the agent calls one, the stream sends a tool.call event with "executor": "client" and the run waits. Run the tool, then send its output here with the call's id. The run continues when the output arrives. Other tools, such as web search and memory, still run on CodingFleet and send tool.call without executor.

Tools sent to the client

ToolArgumentsWhat to do and send back
run_commandcommand, timeout, background, stop_after Run the shell command. Send {"stdout", "stderr", "exit_code"}. With background: true (only offered to a client that runs background commands, see below), start it, return at once with {"id", "pid", "status": "running"}, and keep it running across later runs of the session. With stop_after (seconds; only offered to the desktop app from version 1.2), stop it after that long.
execute_codecode, filename, command, timeout Write code to filename (skip if code is empty), then run command (skip if empty). Send {"stdout", "stderr", "exit_code"}.
fs_readpath, offset, limit, with_line_numbers Send the text, each line prefixed with its number unless with_line_numbers is false.
fs_writepath, content Create missing directories and write the file. Send a short confirmation.
fs_editpath, old, new, all Replace old with new. Fail when old is missing, or occurs more than once and all is not true.
fs_globpat, path Send the matching paths, one per line.
view_imagesource Only when source is a file path; a http(s) source is fetched by CodingFleet and never reaches you. Read the image and send {"data": "<base64 of the file>"}. Keep it under about 900,000 base64 characters and 1568 pixels on its long side: resize or re-encode first. PNG, JPEG, GIF and WebP are accepted; the model then sees the image.
read_backgroundid, wait What the background command printed since it was last read, waiting up to wait seconds (at most 30) for something new. Send {"status": "running" | "exited", "exit_code", "output"}.
stop_backgroundid Stop the background command and every process it started. Send {"status", "exit_code"}.
take_screenshotwindow, screen Take a picture of the user's screen, or of the one window whose title contains window, and send {"data": "<base64 of a PNG or JPEG>"} — the same shape and the same limits as view_image. Ask the user first: a screen holds more than the project. Only for a model that can see images.
computeraction, x, y, element, … Use the computer: click, double_click, right_click, move, drag, scroll, type, key, wait, focus_window, and two that only read: elements (the front window's controls from the accessibility tree, numbered, with their centers in screenshot pixels) and windows. Send {"text": "what happened"}, with "data" (base64 PNG or JPEG of the screen afterwards, at most 1280 pixels on its long side) unless screenshot is false. Coordinates are pixels in the latest picture of that screen. Only offered when the run sets computer_use, to the main agent; ask the user before acting.

Which tools a client is offered. The six coding tools and view_image go to every client session. The others go only to a client that says it runs them, in its X-CodingFleet-Client header (app/version): take_screenshot to desktop/0.2.0 or later, background commands to desktop/0.3.0 or later, computer to desktop/1.0.0 or later when the run asks for it. A client that never heard of a tool would ignore its call and leave the run waiting, so an older one is never sent it. The header only chooses tools; what a run costs follows the key.

Relative paths resolve against your client's working directory. Tell the agent what that directory is, and which operating system it runs on, in system_message.

A client that draws markdown can show the images the agent makes. The CodingFleet desktop app does, so its runs are told to write ![a chart](out/plot.png) for an image in the working directory; the path in such a link is a local file, not a URL. Resolve it against your working directory and draw it, or leave the markdown as it is.

Body parameters

NameTypeDefaultDescription
call_idstring required The id of the tool.call event.
outputstring | object | array required What the tool produced. Up to 1,000,000 characters as JSON. The model gets it cut to your plan's tool-output length, like sandbox output.
is_errorboolean false True when the tool failed, or the user refused it. Say why in output.

Request

curl -X POST https://codingfleet.com/v1/runs/LXDMO5UQo5DXr0UV/tool_results \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "call_id": "9f1c0b6e2d4a4c8e8b7a6f5e4d3c2b1a",
    "output": {"stdout": "3 passed\n", "stderr": "", "exit_code": 0}
  }'

Response 200

{ "status": "accepted", "call_id": "9f1c0b6e2d4a4c8e8b7a6f5e4d3c2b1a", "name": "run_command" }

Send heartbeats to keep long-running tools and approvals alive. Without heartbeats, the run waits up to 10 minutes for each call. After that, or when the run is cancelled, the agent is told that the client did not answer, and a late result gets 409 call_not_pending. Each call takes one result, so a retry of a POST that already succeeded also gets call_not_pending. So does a call you see again when you re-attach with /events. Several calls can be open at the same time: answer each one by its id. Keep results locally and retry transient delivery failures with backoff. Never execute a tool again just because its result POST failed or its call was replayed.

The agent's commands run with your client's permissions. Show each call to the user, or limit what it may do, before you run it.

POST /v1/sessions/{session_id}/runs

MCP servers and local tools: extra tools for one run.

mcp_servers

MCP servers on the internet. CodingFleet connects to them during the run, the way the web chat's MCP integrations do, and their tools are named <server>_<tool>. Keys are server names (letters, digits, _, -; not starting with local); at most 10. The token is used for this run only and is never stored: send it with each run.

FieldTypeDescription
urlstring required The server's streamable HTTP endpoint. Must be https and resolve to a public address.
tokenstring Sent as Authorization: Bearer <token>.
auth_headerstring Send the token raw in this header instead, for servers that want one such as CONTEXT7_API_KEY.
toolsstring[] Only these of the server's tools. Omit for all of them.

client_tools

Tools your client runs, for example the tools of MCP servers on the user's own computer. Only in a session with executor: "client" and code_execution (Agent mode) on. The model sees them next to the coding tools; each call reaches you as a tool.call with executor: "client", and you return the result the same way. Declare them on every run that should have them; at most 64.

FieldTypeDescription
namestring required Starts with local_, then letters, digits, _ or -; 64 characters at most.
descriptionstring What the tool does, for the model. Cut at 2,000 characters.
input_schemaobject A JSON Schema object for the arguments, as an MCP server lists it. 16 KB at most.
curl -N https://codingfleet.com/v1/sessions/bTaQBvzPGHZrSlA6/runs \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Find the docs for FastAPI dependencies, then list my open issues.",
    "mcp_servers": {
      "context7": { "url": "https://mcp.context7.com/mcp" }
    },
    "client_tools": [
      { "name": "local_github_list_issues",
        "description": "List open issues in a GitHub repository.",
        "input_schema": { "type": "object",
                          "properties": { "repo": { "type": "string" } },
                          "required": ["repo"] } }
    ]
  }'

Files

Upload a file once, then attach its id to a run's files. Uploads are private to your account and live alongside your web chat uploads.

POST /v1/files

Upload one file as multipart form data, in a field named file. Add a session_id field to send it into that session's sandbox instead.

Request

curl -X POST https://codingfleet.com/v1/files \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -F "file=@screenshot.png"

Response 201

{ "id": "screenshot.png", "filename": "screenshot.png", "size": 48213,
  "content_type": "image/png", "kind": "image", "location": "server",
  "created_at": "2026-09-17T10:02:11+00:00" }

Into a session's sandbox

curl -X POST https://codingfleet.com/v1/files   -H "Authorization: Bearer $CODINGFLEET_KEY"   -F "file=@index.html" -F "session_id=bTaQBvzPGHZrSlA6"
{ "id": "index.html", "filename": "index.html", "size": 912, "content_type": "text/html",
  "kind": "file", "location": "sandbox", "path": "/uploads/index.html",
  "session_id": "bTaQBvzPGHZrSlA6", "created_at": "2026-09-17T10:04:40+00:00" }

With the id of a session that runs a sandbox (code_execution, Agent mode, on), the file is written to that sandbox's /uploads/ and can be attached only to that session's runs. A session whose tools run on your machine takes uploads on the server, as without session_id. The upload starts the sandbox if it is stopped. The file is on the sandbox's disk, so it is kept by the sandbox's snapshot.

id is what you attach. It is based on the file name and unique within your account; a second upload with the same name gets a different id. kind is image or file.

What can be uploaded

The type is decided by the file's content, not its name. Kept on the server: PNG, JPEG, GIF and WebP images; PDF, Word, Excel, PowerPoint and OpenDocument files; ZIP archives; JSON; and text files, source code included. HTML, SVG and XML are refused there whatever they are called, and so are executables. Into a sandbox almost any file goes, HTML, SVG and XML included; images must still be valid. At most 20 MB per file, 120 uploads an hour and 1 GB a day per account.

How long uploads are kept

An account keeps at most 2 GB of uploads on the Free plan and 10 GB on a paid plan; past that, an upload gets 413 storage_limit_reached. Delete files you no longer need with DELETE /v1/files/{id}. A file kept on the server that no run or steer attached within 7 days of its upload is deleted. Once attached, it is kept.

Errors

400missing_file No multipart field named file.
400invalid_image The image does not decode, or is too large to decode safely.
413file_too_large Over 20 MB.
415unsupported_file_type A type that is not accepted (see above).
429upload_limit_reached 120 uploads this hour, or 1 GB today. Try again later.
413storage_limit_reached Your uploads already use what your plan may keep (2 GB Free, 10 GB paid).
403paid_plan_required Free plan: uploads into a sandbox need a paid plan.
429sandbox_limit_reached The session has no sandbox running, and every sandbox your plan allows has a run going. Wait for a run to end.
409upload_failed The file could not be stored. Retry.
404session_not_found session_id is not one of your sessions.
502sandbox_error The sandbox could not take the file. Retry.
GET /v1/files/{file_id}

Read one upload's details. DELETE on the same path removes the file for good and answers {"id": "…", "deleted": true}; runs that used it keep their history. Both return 404 file_not_found for an id that is not one of your live uploads.

curl -X DELETE https://codingfleet.com/v1/files/screenshot.png \
  -H "Authorization: Bearer $CODINGFLEET_KEY"
GET /v1/files/{file_id}/content

Download an uploaded image, for example to show it again when a client reloads a session. The body is the image itself, with its real type (image/png, image/jpeg, image/gif or image/webp). Other files cannot be downloaded: they answer 404 not_an_image. An unknown id answers 404 file_not_found.

curl https://codingfleet.com/v1/files/screenshot.png/content \
  -H "Authorization: Bearer $CODINGFLEET_KEY" -o screenshot.png
POST /v1/audio/transcriptions

Turn a short voice recording into text, for a prompt spoken instead of typed. Send multipart form data: the recording in audio (webm, ogg, mp3, wav or m4a, at most 10 MB), and optionally language, a two-letter code such as en; without it the language is detected.

curl https://codingfleet.com/v1/audio/transcriptions   -H "Authorization: Bearer $CODINGFLEET_KEY"   -F audio=@voice.webm
{ "text": "Fix the login bug and add a test for it." }

Costs no credits. The recording is not kept. At most 60 recordings an hour and 500 a day per account (10 and 50 on the Free plan), counted together with voice input in the web app (429 transcription_limit_reached). 503 transcription_unavailable when the limit cannot be checked. 400 invalid_audio for a file that is not audio; 502 transcription_failed when the speech service fails — try again.

Sandboxes

A sandbox spec is a recipe for a session's sandbox, the same recipe as on the web's Sandboxes page: where the image comes from, whether CodingFleet's tooling is installed on top, and env vars. Save specs once, then name one when you create a session ("sandbox_spec"): a Go project gets Go, a C project gets the gcc it needs. A spec is built when a session first needs a sandbox; build it ahead with POST /v1/sandbox_specs/{spec_id}/build so that first run does not wait for the build, and so a broken Dockerfile shows up before a run does.

image_sourceReadsThe image
standard— Debian with git, curl, wget, Python 3, build tools, ripgrep and zip. What a session gets without a spec.
dockerhubregistry_image A public image, such as golang:1.23, gcc:13 or ghcr.io/org/app:1.2.
dockerfiledockerfile Your Dockerfile, up to 64 KB. There is no build context: fetch files inside the image (RUN git clone, curl) or COPY --from another stage. ONBUILD, STOPSIGNAL and VOLUME are not supported. An ENTRYPOINT must end in exec "$@".
snapshotsnapshot A snapshot you saved: a sandbox with everything you installed in it.

install_essentials (default true) installs CodingFleet's tooling on a Docker Hub or Dockerfile image (with apt, or apk on Alpine): the agent's file and git tools need it. Env vars are set in the sandbox when it starts, never baked into the image; their values are written and never read back. Your sandbox settings (network, account env vars) apply to every sandbox, and a spec's env vars win over the account's.

What a key for code reaches. A session can read its sandbox, so a key for code reaches only the sandboxes, snapshots and specs of API sessions. These need a desktop key, and a key for code gets 403 desktop_key_required: a spec built from a snapshot of the web chat's sandbox ("pool": "web"); renaming, deleting or saving a snapshot of the web chat's sandbox; changing, deleting or starting a session from the spec pinned on the web, or from a spec the desktop app's sessions use. Listing and reading them works with either key.

POST /v1/sandbox_specs

Save a spec. Returns 201 with the spec.

Body parameters

NameTypeDefaultDescription
namestringthe summary Up to 100 characters.
descriptionstring"" Up to 2,000 characters.
image_sourcestring"standard" standard, dockerhub, dockerfile or snapshot.
registry_imagestring— For dockerhub.
dockerfilestring— For dockerfile.
snapshotstring— A snapshot id, for snapshot.
install_essentialsbooleantrue Install CodingFleet's tooling on top of the image.
env_varsobject{} Names (letters, digits, _) to string values. Up to 50.

A field the image source does not read (a dockerfile with "dockerhub") is refused, not stored. An account keeps up to 50 specs.

Request

curl https://codingfleet.com/v1/sandbox_specs \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Go 1.23",
    "image_source": "dockerhub",
    "registry_image": "golang:1.23",
    "env_vars": {"GOFLAGS": "-mod=mod"}
  }'

Response 201

{
  "id": "q8Zr2mXc1VbN4tLp",
  "name": "Go 1.23",
  "description": "",
  "image_source": "dockerhub",
  "snapshot": null,
  "registry_image": "golang:1.23",
  "dockerfile": null,
  "install_essentials": true,
  "env_var_names": ["GOFLAGS"],
  "summary": "Docker Hub · golang:1.23",
  "pinned": false,
  "sessions": 0,
  "build": {"status": null, "error": null, "steps": [], "started_at": null, "updated_at": null},
  "created_at": "2026-10-10T08:00:00.000000+00:00",
  "updated_at": "2026-10-10T08:00:00.000000+00:00"
}

pinned: the spec every sandbox of the account is built from when nothing else says (see sandbox settings). sessions: how many sessions use it. build: see build a spec.

Errors

400invalid_body A field of the wrong type, a field the source does not read, a missing one, or a bad env var name.
400invalid_spec The Dockerfile cannot be built here; the message says why.
400unknown_field A field this endpoint does not take.
404snapshot_not_found snapshot names no snapshot of yours.
403desktop_key_required A key for code, and snapshot is a snapshot of the web chat's sandbox.
409snapshot_broken That snapshot failed to start a sandbox before.
409spec_limit_reached You already keep 50 specs.
GET /v1/sandbox_specs

Your specs, newest first: {"specs": [...], "pinned": id | null}. The spec you pinned on the web's Sandboxes page is listed too.

GET PATCH DELETE /v1/sandbox_specs/{spec_id}

Read a spec, change it, or delete it.

PATCH takes the create fields; what you leave out stays. env_vars updates the stored ones: a name with a string sets it, a name with null removes it, and names you leave out stay. Changing image_source needs the new source's field. A change to the recipe applies to the next sandbox of every session that uses the spec (a running sandbox keeps going), and clears build. 409 build_in_progress while the spec is being built.

DELETE answers {"id", "deleted": true, "unpinned"}. 409 spec_in_use, with sessions, while sessions use the spec: delete them first.

Changing or deleting the pinned spec, or one the desktop app's sessions use, needs a desktop key: 403 desktop_key_required.

curl -X PATCH https://codingfleet.com/v1/sandbox_specs/q8Zr2mXc1VbN4tLp \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"registry_image": "golang:1.24", "env_vars": {"GOFLAGS": null, "CGO_ENABLED": "0"}}'
GET /v1/sandbox_specs/options

What a spec can be, for a client that draws a form: image_sources (each with the fields it reads), fields (types, defaults and limits), dockerfile rules, the essentials packages, limits, and the sandbox_config fields.

POST /v1/sandbox_specs/validate

Check a spec body without saving or building it.

Same body as create. Answers {"valid": true, "summary", "dockerfile_steps": [{"keyword", "text"}]}, or {"valid": false, "error", "code"}. It checks what can be checked here; only a build shows whether a Dockerfile's commands work.

POST /v1/sandbox_specs/{spec_id}/build

Build the spec's image now. No body. Returns 202 with the spec, build.status "building".

The build goes on after the response; poll GET /v1/sandbox_specs/{spec_id}. A built image is kept, so the next sandbox from this spec starts without building. Nothing starts or bills a sandbox. build.status is null (never built, or the recipe changed since), building, built or failed with error. build.steps lists the layers: step, label, status (running, done, failed) and seconds. A Docker Hub image is pulled in a few seconds; a Dockerfile can take minutes.

"build": {
  "status": "built",
  "error": null,
  "steps": [
    {"step": "registry", "label": "Pulling golang:1.23", "status": "done", "seconds": 41.2},
    {"step": "essentials", "label": "Installing CodingFleet's tooling", "status": "done", "seconds": 23.8}
  ],
  "started_at": "2026-10-10T08:01:00.000000+00:00",
  "updated_at": "2026-10-10T08:02:05.000000+00:00"
}

One build runs at a time per account: 409 build_in_progress, with spec. An account starts at most 30 builds a day (UTC): 429 build_limit_reached; a spec is still built when a session first needs it. 403 paid_plan_required on the Free plan. A build whose server restarted reports failed; build again.

GET PATCH /v1/sandbox_config

Your account's sandbox settings: the web's Sandboxes → Configuration. They apply to every sandbox, the web chat's and your sessions'.

{
  "env_var_names": ["NPM_TOKEN"],
  "always_fresh": false,
  "snapshot_window_hours": 72,
  "block_network": false,
  "default_spec": null
}
env_varsobject Env vars for every sandbox. PATCH updates them like a spec's: a string sets, null removes. Read back as env_var_names.
always_freshboolean The web chat starts every sandbox empty instead of from its newest snapshot. A session still gets its own snapshot back.
snapshot_window_hoursinteger How old a web snapshot can be and still be restored: 1 to 720.
block_networkboolean No network in new sandboxes (and no open ports).
default_specstring | null The pinned spec: every new sandbox is built from it unless its session names its own. null unpins.

Changing them needs a desktop key (403 desktop_key_required), as for settings: they change the web chat too.

GET /v1/sandboxes

Your running sandboxes, your sessions' and the web chat's, and what holds a slot.

?refresh=1 asks each sandbox whether it still runs (slower); without it the list is what the server last knew.

{
  "sandboxes": [
    {
      "id": "sb-a1B2c3D4e5F6g7H8",
      "pool": "api",
      "session_id": "bTaQBvzPGHZrSlA6",
      "name": "Go 1.23 workspace",
      "status": "running",
      "image_source": "dockerhub",
      "image_ref": "golang:1.23",
      "sandbox_spec": "q8Zr2mXc1VbN4tLp",
      "busy": false,
      "snapshot_on_stop": true,
      "resources": {"cpu": {"reserved": 0.125, "limit": 4.0},
                    "memory_mb": {"reserved": 256, "limit": 8192}},
      "created_at": "2026-10-10T08:05:00.000000+00:00",
      "last_used_at": "2026-10-10T08:20:00.000000+00:00",
      "idle_stop_at": "2026-10-10T08:40:00.000000+00:00",
      "expires_at": "2026-10-11T08:05:00.000000+00:00"
    }
  ],
  "limits": {"api_sandboxes": 2, "api_active": 1, "idle_timeout_minutes": 20}
}

pool: api for a session's sandbox, web for the web chat's. busy: a run is using it (null for the web chat's). idle_stop_at: when it stops if nothing uses it before then (snapshotted first when snapshot_on_stop). resources: the CPU cores and memory your plan gives a sandbox. limits.api_sandboxes is how many session sandboxes may run at once (null: no limit); see the limits.

GET DELETE /v1/sandboxes/{sandbox_id}

Read one sandbox (it asks the sandbox whether it still runs), or stop it.

DELETE stops a session's sandbox like DELETE /v1/sessions/{session_id}/sandbox: snapshotted first unless the session opted out; the session's next run starts it again. Answers {"id", "stopped", "snapshot"}. 409 run_in_progress while its session has a run going; 409 web_sandbox for the web chat's, which you stop on the web. 404 sandbox_not_found.

POST /v1/sandboxes/{sandbox_id}/snapshot

Save a snapshot of a running sandbox now. Returns 201 with the snapshot.

Optional body: name (the time is added to it) and description. A saved snapshot is kept until you delete it; the automatic ones a stopped sandbox leaves are pruned (a session keeps its newest). Use it as a spec's image ("image_source": "snapshot") to start sessions from a sandbox you set up once. The sandbox keeps running.

curl https://codingfleet.com/v1/sandboxes/sb-a1B2c3D4e5F6g7H8/snapshot \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Go toolchain + deps", "description": "go mod download done"}'
403plan_upgrade_required Saving snapshots needs the Unlimited plan or higher, as on the web.
403desktop_key_required A key for code, and the sandbox is the web chat's.
409run_in_progress Its session has a run going: snapshot between runs.
409sandbox_stopped The sandbox is not running.
409snapshot_limit_reached You keep 50 saved snapshots: delete one.
502snapshot_failed The snapshot could not be taken. Try again.
GET /v1/snapshots

Your snapshots, newest first. ?pool=api or web, ?session_id=, ?limit= (1 to 200, default 100).

{
  "snapshots": [
    {
      "id": "im-Zk3pQ7rT9wX2yB5n",
      "name": "Go toolchain + deps · 2026-10-10 08:30 UTC",
      "description": "go mod download done",
      "pool": "api",
      "session_id": "bTaQBvzPGHZrSlA6",
      "manual": true,
      "broken": false,
      "size_mb": null,
      "sandbox_specs": [],
      "created_at": "2026-10-10T08:30:00.000000+00:00"
    }
  ]
}

manual: saved on request, kept until deleted. broken: it failed to start a sandbox, so nothing restores it. sandbox_specs: the specs built from it.

GET PATCH DELETE /v1/snapshots/{snapshot_id}

Read a snapshot, rename it (name, description), or delete it for good.

409 snapshot_in_use, with sandbox_specs, when specs are built from it. Deleting a session's automatic snapshot means its next sandbox starts without its files. 404 snapshot_not_found. Renaming or deleting a snapshot of the web chat's sandbox ("pool": "web") needs a desktop key: 403 desktop_key_required.

Account

GET /v1/models

Every model you can name in model, with its price. The list follows your settings, as the web model picker does: with privacy_models on it holds only models that do not use your data, with zero_data_retention only models whose providers keep nothing, and legacy models appear only with show_legacy_models on.

Query parameters

NameTypeDefaultDescription
includestring— Comma-separated. provider adds each model's provider and orders the list as the CodingFleet model picker does: by provider, then by model. family adds family and effort, so effort variants of one model (Low, Medium, High, ...) can be shown as one entry with an effort choice.

Request

curl "https://codingfleet.com/v1/models?include=provider,family" \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{
  "models": [
    {
      "id": "inclusionai/ling-3.0-flash",
      "name": "Ling 3.0 Flash",
      "legacy": false,
      "credits_per_20k_tokens": 1,
      "context_window": 262144,
      "supports_vision": false,
      "unlimited": false,
      "intelligence_index": 47,
      "output_tokens_per_second": 182,
      "provider": { "name": "inclusionAI", "logo_url": "https://codingfleet.com/media/logos/inclusionai.png" },
      "family": null,
      "effort": { "label": "Standard", "rank": 0 }
    }
  ]
}

id is what you pass as model. legacy marks an older model kept for accounts that asked to see them. intelligence_index is a 0–100 capability score, the one Auto-Select uses to choose models; null when unknown. output_tokens_per_second is the model's measured median output speed; null when unknown. credits_per_20k_tokens can be below 1 on cheap models — those are billed fractionally, not rounded up. provider is there only with include=provider; its logo_url can be null. With include=family, a model with effort variants has family {id, name, default}, shared by all its variants, where default is the variant to pick first (the lowest effort); other models have family: null. effort.rank orders variants from lowest to highest. unlimited marks a model that Unlimited-and-higher plans use for no credits in the web app and the desktop app; through the API it has its normal price.

POST /v1/diagnostics/crash

Send a crash report from your client, after asking its user. Stored for the CodingFleet team to read; nothing else is done with it.

NameTypeDefaultDescription
kindstring required main, renderer, child or unresponsive: which part of the client failed.
messagestring The error. Cut at 2,000 characters.
stackstring Its stack trace. Cut at 20,000 characters.
platformstring Operating system and version.
log_tailstring The last lines of the client's log. Cut at 64,000 characters. Never put a key or a file's contents in it.
happened_atstring When, as ISO 8601 with a time zone.

Answers 201 {"id", "status": "received"}. At most 20 reports a day per account; after that 429 rate_limited.

GET /v1/credits

Your balance and how many runs you have going. The cheapest way to check a key works.

Request

curl https://codingfleet.com/v1/credits \
  -H "Authorization: Bearer $CODINGFLEET_KEY"

Response 200

{ "plan": { "key": "ultimate", "name": "Ultimate",
            "unlimited_models": true, "byok_in_sandbox": true,
            "urls": { "plans": "https://codingfleet.com/pricing/",
                      "credits": "https://codingfleet.com/pricing/#one-time-purchase",
                      "billing": "https://codingfleet.com/billing/" } },
  "credits": 100, "weekly_remaining": 480, "weekly_total": 600, "quota_remaining": 580,
  "concurrency": { "limit": 5, "active": 1 },
  "account": { "username": "ada", "email": "ada@example.com" },
  "key": { "name": "CodingFleet Desktop", "hint": "cf_sk_…YzVI", "client": "desktop",
           "created_at": "2026-09-16T10:02:11+00:00", "expires_at": null } }

concurrency counts runs: active are going now, out of the limit your plan allows at the same time. plan names the account's plan and the pages that change it — a client cannot sell a plan, but it can open the right page. weekly_remaining and weekly_total describe the weekly allowance, which runs use before credits; both are null on plans without one. quota_remaining is the allowance left plus your credits. account and key say which account and which key made the call; key.client is "desktop" for a desktop app key and "api" for a key for code (see billing). plan.unlimited_models says whether the plan includes the models marked unlimited (in the web and desktop apps); plan.byok_in_sandbox whether your own provider keys also work in cloud sandbox sessions.

GET /v1/settings

Your privacy settings and preferences, the same ones as on the account page. PATCH on the same path changes them: send only what changes. They apply to every run, whichever client starts it.

Response 200

{
  "privacy": {
    "privacy_models": true,
    "zero_data_retention": false,
    "data_region": "",
    "data_region_available": false,
    "private_sessions": false
  },
  "preferences": {
    "default_model": "auto",
    "show_legacy_models": false,
    "credit_cap_per_message": 200,
    "economy_mode": true
  }
}
SettingTypeDescription
privacy.privacy_modelsboolean Only models that do not use your data for training, through providers that do not collect prompts. A run on another model is refused.
privacy.zero_data_retentionboolean With privacy_models: only providers that keep nothing.
privacy.data_regionstring "" (global), "eu" or "us". Only when data_region_available is true. "us" needs private session mode, so it can be set only on the account page.
privacy.private_sessionsboolean Read-only. Nothing new is stored on CodingFleet. Agent API runs are refused while it is on, because a session is its stored history, so it can be changed only on the account page.
preferences.default_modelstring | null A model id, "auto" for Auto-Select, or null for the web app's random pick (Auto-Select in the API). Runs with no model use it.
preferences.show_legacy_modelsboolean List and allow older models.
preferences.credit_cap_per_messageinteger The most credits one run may spend, tool rounds included. 0 is no cap; the lowest cap applied is 20. At most 100,000.
preferences.economy_modeboolean Compact long conversations earlier, so each tool round re-sends less.

Change a setting

curl -X PATCH https://codingfleet.com/v1/settings \
  -H "Authorization: Bearer $CODINGFLEET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"preferences": {"economy_mode": true, "credit_cap_per_message": 200}}'

Returns all settings. A value that breaks a rule answers 400 invalid_setting and changes nothing. Changing settings needs a desktop key; a key for code gets 403 desktop_key_required (see two kinds of key).

GET /v1/settings/keys
PUT /v1/settings/keys/{provider}
DELETE /v1/settings/keys/{provider}

Use your own API key for a provider (BYOK): runs on that provider's models go through your key and cost no credits. The same keys as the web app's API keys page.

curl -X PUT https://codingfleet.com/v1/settings/keys/anthropic   -H "Authorization: Bearer $CODINGFLEET_KEY" -H "Content-Type: application/json"   -d '{"api_key": "sk-ant-..."}'
{ "provider": "anthropic", "name": "Anthropic", "configured": true, "masked": "sk-a…9xQz",
  "key_url": "https://console.anthropic.com/settings/keys", "placeholder": "sk-ant-…",
  "last_used_at": null, "last_error": null }

provider is openrouter, openai, anthropic, deepseek or gemini. GET lists all five as {"keys": [...]}; a key is never returned, only masked. DELETE removes it. Where your key is used: in sessions whose tools run on your computer (executor: "client") on any plan, and in cloud sandbox sessions and the web app from the Unlimited plan up (plan.byok_in_sandbox in credits). Elsewhere runs use CodingFleet's keys and cost credits. last_error is the provider's last refusal of your key. 400 invalid_key for a value that is not a key; 404 unknown_provider. Saving or removing a key needs a desktop key; a key for code gets 403 desktop_key_required.

GET /v1/billing

Your subscriptions and credit orders, as on the website's billing page, plus plan as in credits. Buying something new happens on the website: open plan.urls.plans. Every billing endpoint needs a desktop key; a key for code gets 403 desktop_key_required.

{
  "subscriptions": [
    { "id": "sub_1Q...", "name": "Unlimited Monthly", "product": "CodingFleet", "provider": "stripe",
      "status": "active", "created_at": "2026-08-03T10:00:00+00:00",
      "renews_at": "2026-10-03T10:00:00+00:00", "ends_at": null, "trial_ends_at": null,
      "card": { "brand": "visa", "last4": "4242" }, "discount": null,
      "can_cancel": true, "can_resume": false, "needs_payment": false }
  ],
  "credit_orders": [
    { "id": "pi_3P...", "name": "500 credits", "product": "Credits", "status": "paid",
      "total_cents": 1500, "currency": "usd", "created_at": "2026-09-01T08:00:00+00:00",
      "pro_expires_at": null }
  ],
  "plan": { "key": "unlimited", "name": "Unlimited", "...": "..." }
}

discount is {percent, until} while a stay discount applies. What you can do with one of your subscriptions:

POST /v1/billing/subscriptions/{subscription_id}/{action}
ActionDoes
GET invoices{"invoices": [...]}: date, status, total_formatted, and invoice_url to open the invoice.
POST cancelCancels at the end of the paid period; a trial ends now (result is cancelled or trial_ended). Optional feedback (too_expensive, missing_features, unused, switched_service, low_quality, too_complex, customer_service, other) and comment. 409 not_cancellable.
POST resumeUndoes a cancellation before the period ends. 409 not_resumable when it is not cancelled, or its period has ended (can_resume is false).
GET offerWhether a stay discount is available: eligible, percent, interval, amount_due_cents, discounted_cents.
POST offerClaims it: the next invoice is discounted and the subscription stays. 409 offer_not_available.
POST portal{"url"}: a one-time link to the payment provider's portal, to change the card or the plan.

Only your own subscriptions: another id answers 404 subscription_not_found. A payment provider that does not answer gives 502 billing_unavailable. A change can take a few seconds to show in GET /v1/billing.

POST /v1/auth/device

Sign a desktop or command-line client in through the browser, instead of asking for a pasted key. These two calls carry no key: they are how one is obtained.

Request

curl -X POST https://codingfleet.com/v1/auth/device \
  -H "Content-Type: application/json" \
  -d '{"name": "my-laptop (desktop app)"}'

name is optional and names the key that is created.

Response 200

{ "device_code": "3pQ…", "user_code": "BCDF2345",
  "verify_url": "https://codingfleet.com/agent-api/authorize/?code=BCDF2345",
  "interval": 3, "expires_in": 600 }

Open verify_url in the user's browser and show user_code so they can check it matches the page. They approve there, signed in as themselves, and a key is issued for your client alone. The page shows name only as what the app calls itself, next to the address the request came from and how long ago. 30 sign-ins an hour can start from one address; more get 429 rate_limited.

Collecting the key

curl -X POST https://codingfleet.com/v1/auth/device/token \
  -H "Content-Type: application/json" \
  -d '{"device_code": "3pQ…"}'
{ "status": "approved", "api_key": "cf_sk_…" }

Poll every interval seconds until the status leaves pending. The others are approved (with api_key, handed over once and never again), denied and expired. Store the key the way you would a pasted one. A 503 unavailable means sign-in cannot be served at that moment: keep polling, or ask for a pasted key.

Sub-agents

With parallel_agents, the agent can hand independent tasks to sub-agents that run at the same time, each with its own model and tool loop. The main run's stream shows their work as it happens: subagent.started, then the sub-agent's subagent.text, subagent.status and its tool.call / tool.result events (each with agent_id), then subagent.done. Group them by agent_id, and attach a sub-agent to the spawn_agent call whose id is its call_id.

In a client session a sub-agent's coding tools run on your machine too. Several can wait for you at once: answer each by its own id. A sub-agent's credits are part of the run's usage.

Event protocol

A run streams these. Each frame is an event: line and a data: line of JSON, separated by a blank line. Ignore events you do not recognise — more may be added.

EventPayload
run.started run_id, session_id, model. Always first. Keep run_id — you need it to cancel or steer.
run.meta model (display name), model_unique_name, auto_routed, tools. Says which model actually answered, which matters when Auto-Select picked it.
notice code, message. Something to know before reading the answer, sent right after run.started. Today only sandbox_fresh: the session's sandbox was stopped without a snapshot, so its next one starts empty. A notice sent just before usage says another model answered: the one you picked hit a limit, failed, or declined the request. It has code model_switched, model (the id of the model that answered) and model_switches (see model switches). A declined request is not charged; when no other model can take it, the run ends with error. With a desktop key, a run moved by the 5-hour limit on Unlimited models has code pool_cap instead, upgrade (true when a higher plan has a larger limit) and resets_at (ISO 8601, when Unlimited models are back).
status kind, state (started or ended). What the agent is doing while no text or tool call is streaming: thinking, compacting (the conversation is being compacted, also in the middle of a run), writing_code, writing_file, editing_file, writing_query. For a progress indicator; nothing to store.
text.delta text — a piece of the answer. Concatenate them in order.
tool.call id, name, arguments (object). The agent is about to run a tool. In a client session, coding tools arrive with "executor": "client": run them and return the result. A call a sub-agent makes also carries agent_id; run it the same way.
tool.result id (matches the call), name, ok (boolean), output, truncated. output is the tool's own JSON when it returns JSON, otherwise a string. Very large outputs are cut at 200,000 characters with truncated: true — the model still saw its own full copy. agent_id when a sub-agent made the call. For spawn_agent, subagent describes the run: id, role, model, summary, credits, error.
context.compacted before_tokens, after_tokens. The conversation neared the context limit, so it was compacted: old tool outputs (and the long inputs of old tool calls) were cleared, and older messages were summarized when that was not enough. It happens before the model runs, between two tool rounds when a run passes 90% of the limit, or when the model's provider refuses a request as too long (the request is then sent again); the run then goes on. agent_id when it happened in a sub-agent.
subagent.started agent_id, call_id (the spawn_agent call that started it), role, task, model. A sub-agent began. Only with parallel_agents enabled.
subagent.text agent_id, text — a piece of that sub-agent's answer. Not part of the main answer.
subagent.status agent_id, kind, state — the sub-agent's progress, like status.
subagent.done agent_id, call_id, model, ok, cancelled, credits, summary (e.g. "3 tool calls: fs_read ×2, run_command ×1"). The sub-agent finished; its answer reaches the main agent as the spawn_agent result.
session.title title — a name generated for the session on its first run. Cosmetic.
usage credits, credits_charged, credits_remaining, quota_remaining, model (display name), requested_model and model_id (the id asked for and the id that answered), model_switches, prompt_tokens, cache_read_input_tokens, cache_creation_input_tokens, completion_tokens, reasoning_tokens, time_taken, context_tokens, and billing: why the run cost what it did — credits, free (a free model), plan (included in your plan) or byok (your own key). Sent once, just before the end.
run.ended run_id, reason, and finish_reason or error. Always last.

run.ended reasons

completedThe agent finished and answered.
budget The turn hit its spend ceiling and was told to wrap up with what it had. The answer is real, but the work may be unfinished. Treat this as a partial success, not a failure.
cancelledYou cancelled it.
interrupted A client session only: your client stopped answering (no heartbeat and no tool result for 10 minutes — the computer slept, was turned off or went offline), so the run was stopped instead of going on without its tools. A notice says so first. Everything up to that point is kept, including the call that never ran; send continue as the next message and the agent picks up from there. A sandbox session never stops for this: its tools run on CodingFleet.
errorThe turn failed; error says why. A turn the model could not finish — a rate limit that did not lift after about a minute of retries, a connection to the model that could not be restored — ends here too. Its earlier work is kept; send continue to resume it.
disconnected The stream ended without a result. Re-attach with /events, or read the run — it may have finished anyway.

Object shapes

Session

idstringSession id.
titlestring | nullA name generated after the first run. null until then.
pinnedbooleanWhether the session is pinned.
executorstringWhere tools run: "sandbox" or "client".
modelstring | nullDefault model; null means your default model from settings.
snapshot_sandboxbooleanWhether a snapshot is saved before the sandbox is stopped. Always false for a client session.
capabilitiesobjectThe five capability flags.
git_repositorystring | nullAttached repository id.
restore_fromstring | nullSession whose snapshot this one's sandbox started from.
sandbox_specstring | nullThe sandbox spec its sandbox is built from.
has_snapshotbooleanWhether this session saved a snapshot to resume or restore_from. Only on a single-session read.
sandboxobject | nullid, created_at, expires_at, snapshot_on_reap. Only on a single-session read.
contextobjecttokens: the size of the conversation after its last run. limit: your plan's context limit, capped by the context window of the model the session last ran. Near the limit, older messages are summarized automatically. On list and single-session reads.
last_modelstring | nullDisplay name of the model the latest run used. Run the next turn on the same model to keep the provider's prompt cache. null before the first run. On list and single-session reads.
last_message_atstring | nullWhen the conversation last got an answer, from any client, the web chat included. On list and single-session reads.
busyboolean | nullWhether a turn is going in this session's chat right now, from any client. null when it cannot be told at the moment. Only on a single-session read.
active_run_idstring | nullThe in-progress API run in this session, if any. Use its /events endpoint to reattach after a lost start response. Only on a single-session read.
created_atstringISO 8601, UTC.
last_used_atstringWhen this API last ran or opened the session. Drives the idle timeout.

Run

idstringRun id.
session_idstringIts session.
statusstringrunning, completed, cancelled, error.
reasonstring | nullThe run.ended reason, plus rejected (a gate refused it before it began) and lost (the worker died before reporting a result).
detachedbooleanHow it was started.
noticestring | nullThe notice event's message, if the run had one.
modelstringThe model that answered (display name). While the run is going, the id it asked for.
requested_modelstring | nullThe model id the run asked for; "auto" for Auto-Select.
model_idstring | nullThe id of the model that answered. null until the run ends.
model_switchesobject[]Each move to another model: from, to, reason. See model switches.
model_fallbackbooleanWhether the run may move to another model.
credits_usednumberMay be fractional.
prompt_tokensintegerInput tokens.
completion_tokensintegerOutput tokens.
errorstring | nullSet when it failed.
progressobjectDurable checkpoint with text generated so far, tools called and their results (with called_at and ended_at), and updated_at. It remains available if the worker stops before writing a final transformation.
roundsobject[]One entry per model request: time, tokens, credits and tool calls. See rounds.
client_executorobject | nullClient sessions: connected, last_heartbeat_at, lost, waiting_calls. See is the client there? Only on GET /v1/runs/{id}.
created_atstringISO 8601, UTC.
ended_atstring | nullnull while running.

Errors

Every error is JSON in one shape. Branch on code, show message to a human. A refusal that a page on the website can fix (out of credits, a model on a higher plan) also carries link: {"url": "https://codingfleet.com/pricing/", "label": "Upgrade to the Elite plan"}. The message is plain text; offer the link as a button or print its URL.

{
  "error": {
    "message": "You already have 2 runs going, the most your plan allows at the same time. Wait for one to end, or cancel one with POST /v1/runs/{id}/cancel.",
    "code": "run_limit_reached",
    "limit": 2,
    "active": 2
  }
}
StatusCodeMeaning
400invalid_bodyBody is not a JSON object, or a field has the wrong type.
400unknown_fieldA field the endpoint does not take. Every endpoint refuses fields it would not use, so nothing you send is ignored without a word. fields lists them. An endpoint that takes no body accepts an empty body or {}.
409model_unavailableA run with model_fallback: false whose model cannot take it.
400invalid_specA sandbox spec's Dockerfile cannot be built here.
403plan_upgrade_requiredSaving a snapshot needs the Unlimited plan or higher.
404spec_not_foundNo sandbox spec with that id on your account.
404sandbox_not_foundNo sandbox with that id on your account.
404snapshot_not_foundNo snapshot with that id on your account.
409spec_in_useSessions build their sandboxes from the spec; the body lists them.
409spec_limit_reachedThe account keeps 50 sandbox specs.
409build_in_progressA spec is being built; one build runs at a time.
429build_limit_reachedThe account started 30 spec builds today (UTC).
409snapshot_brokenThe snapshot failed to start a sandbox before.
409snapshot_in_useSandbox specs are built from the snapshot.
409snapshot_limit_reachedThe account keeps 50 saved snapshots.
409sandbox_stoppedThe sandbox is not running.
409web_sandboxThe web chat's sandbox: stop it on the web.
502snapshot_failedThe snapshot could not be taken. Try again.
404subscription_not_foundNo subscription with that id on your account.
409not_cancellableThe subscription is not active or on trial.
409not_resumableOnly a cancelled subscription whose paid period has not ended can be resumed.
409offer_not_availableThe stay discount does not apply to this subscription.
502billing_unavailableThe payment provider did not answer. Try again.
400invalid_keyThe value sent as a provider key does not look like one.
404unknown_providerBYOK keys are for openrouter, openai, anthropic, deepseek and gemini.
400invalid_audioThe file sent for transcription is not an audio recording.
400missing_messageA run needs a non-empty message.
400file_not_foundAn attached or requested file is not one of your live uploads (also 404 on the file endpoints).
400invalid_settingA PATCH /v1/settings value that breaks a rule.
403private_session_modeRuns are refused while private session mode is on.
404not_an_imageOnly uploaded images can be downloaded from /v1/files/{id}/content.
400too_many_filesMore files than a run (10) or a steer (3) takes.
400file_not_in_sessionA sandbox upload attached to a run of another session.
400vision_unsupportedAn image is attached to a model that cannot read images.
400missing_fileAn upload without a file field.
400invalid_imageAn uploaded image does not decode.
413file_too_largeAn upload over 20 MB.
415unsupported_file_typeAn upload of a type that is not accepted, or a non-image sent with a steer.
429upload_limit_reachedThe hourly or daily upload limit.
429sandbox_limit_reachedA new sandbox is needed, and every sandbox your plan allows has a run going.
403paid_plan_requiredFree plan: a session, run or upload that needs a cloud sandbox. Use executor: "client", or turn code_execution off.
409upload_failedAn upload could not be stored; retry.
400unknown_modelmodel is not an id from GET /v1/models.
400executor_conflictexecutor: "client" with an option that needs a sandbox.
400invalid_executorUnknown executor.
401missing_tokenNo Authorization header.
401invalid_tokenUnknown or revoked key.
401token_expiredThe key reached the expiry date chosen when it was made. Create a new one.
403desktop_key_requiredA key for code on settings, provider keys, billing, or the web chat's sandboxes, snapshots and pinned spec. Use the desktop app, or the website.
413storage_limit_reachedYour uploads already use what your plan may keep.
402rejectedOut of credits. Carries link.
403email_unconfirmedConfirm your email address first.
403rejectedThe model is not available on your plan. Carries link.
403model_disabledThe model was retired. Pick one from GET /v1/models.
404session_not_foundNo such session on your account.
404agent_not_foundNo sub-agent with that id in the session.
404run_not_foundNo such run on your account.
404repository_not_foundNo such connected repository.
409snapshot_not_foundrestore_from names a session with no snapshot.
409session_brokenThe session lost its history and cannot run.
409nothing_to_compactThe conversation is too small, or has nothing saved, to compact.
429transcription_limit_reachedToo many voice recordings this hour or today.
503transcription_unavailableThe voice limit could not be checked, so nothing was transcribed. Try again.
502transcription_failedThe speech service failed to transcribe the recording. Try again.
409not_runningCancel, steer or a tool result on a run that is not streaming.
409not_clientExecutor heartbeat on a run whose tools run in a cloud sandbox.
409call_not_pendingNo tool call with that id waits for a result: answered already, timed out, or cancelled.
413result_too_largeA tool result over 1,000,000 characters.
503delivery_failedA tool result could not be delivered. Send it again.
409steer_refusedToo many steers this turn, or the message is too long.
409no_sandboxThat session does not run a sandbox.
409no_streamThat run has no replayable stream.
409events_unavailableEvents are kept only for the latest run in a session.
409run_in_progressThe session already has a run going.
413request_too_largeThe request body is over the server's size limit.
429run_limit_reachedStarting the run would go over the number of runs your plan allows at the same time.
429rate_limitedToo many requests. Honour Retry-After. For browser sign-in: too many sign-ins started from one address in an hour.
500run_failedThe run could not be started. Nothing was billed; try again.
502sandbox_errorThe container could not be reached.

Limits

Concurrent runs and sandboxes

How many runs your account can have going at the same time, and how many session sandboxes it can have running, across all its sessions. Sessions themselves are not limited. A run over the limit gets 429 run_limit_reached: wait for one to end, or cancel one. The sandbox limits are the web app's own, except that API sessions on the Free plan get no cloud sandbox: they use executor: "client", which runs tools on your machine and costs only credits. When a session needs a new sandbox at the limit, the sandbox of another session with no run going is stopped for it, the longest idle first. Its files are snapshotted first, as when it goes idle, so that session gets them back on its next run. Only when every sandbox has a run going is the new one refused: the tool returns an error, and an upload into a sandbox gets 429 sandbox_limit_reached. A session always gets its own sandbox back.

PlanRunsSandboxes
Free1None (client only)
Pro22
Unlimited32
Elite55
Ultimate10No limit
Ultimate Max10No limit

Everything else

Requests 120 per 60 seconds, per key. Over it: 429 with Retry-After.
Input sizeNo character limit. The whole conversation must fit your plan's input token limit, the same as in the web app.
Steers10 per turn, 4,000 characters each.
Idle sandboxes Stopped after 5 minutes with no run or tool call on Free (10 on Pro, 20 on Unlimited, 30 on Elite, 60 on Ultimate and Ultimate Max), the same timeouts as sandboxes in the web app. The session stays; its next run starts the sandbox again from its snapshot.
Sandbox lifetime 24 hours from when the sandbox started, as in the web app. Then it is stopped the same way.
Sessions Not limited, and kept until you delete them.
Event replay A run's events can be re-read for 1 hour.
Keys10 active per account.
Sandbox specs50 per account, 30 builds a day (UTC) with POST /v1/sandbox_specs/{spec_id}/build.
Saved snapshots50 per account.

Billing

Runs are billed in the same credits as the web app. A session costs nothing by itself; every run reports what it cost in its usage event and on its run object.

Unlimited models are not unlimited here. On a plan with a weekly allowance, runs draw on that allowance first and then on your credits, as in the web app. But models that are unlimited in the web app are charged their normal price through the API. Example: 600 weekly credits plus 100 purchased credits give you 700 to spend through the API, on any model. The CodingFleet desktop app is priced like the web app: there, these models are included in Unlimited-and-higher plans, with the same fair-use limits.

Which price a run gets depends on the key, not on anything the request says. A key is a desktop key when the app's browser sign-in made it, or when you chose "The CodingFleet desktop app" as you created it on the keys page. Any other key is a key for code, and pays every model. GET /v1/credits shows it as key.client.

Prices are per 20,000 billed tokens, from GET /v1/models. Cheap models can cost a fraction of a credit per run. Your account is charged whole credits and the rest is carried to later runs — ten runs at 0.1 credits cost one credit, not ten. In usage, credits is a run's exact price and credits_charged is the whole credits it took.

Free models. A model that costs 0 credits stays free when its tools run on your machine (executor: "client"). A run on a free model that uses a CodingFleet sandbox costs a flat 1 credit, because the sandbox is not free to run.

Full examples

Python — stream a run

import json
import os
import requests

BASE = "https://codingfleet.com/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['CODINGFLEET_KEY']}",
    "Content-Type": "application/json",
}


def iter_events(response):
    """Yield (event_name, payload) from a server-sent event stream."""
    event, data = None, None
    for raw in response.iter_lines(decode_unicode=True):
        line = (raw or "").strip("\r")
        if line == "":                      # blank line ends a frame
            if event and data is not None:
                yield event, json.loads(data)
            event, data = None, None
        elif line.startswith(":"):          # keepalive comment
            continue
        elif line.startswith("event:"):
            event = line[6:].strip()
        elif line.startswith("data:"):
            data = line[5:].strip()


session = requests.post(f"{BASE}/sessions", headers=HEADERS, json={
    "system_message": "You are a coding agent with a Linux sandbox. Use your tools.",
    "capabilities": {"code_execution": True},
}).json()

try:
    response = requests.post(
        f"{BASE}/sessions/{session['id']}/runs",
        headers=HEADERS,
        json={"message": "Write fizzbuzz.py, run it, and show me the output."},
        stream=True, timeout=900,
    )
    response.raise_for_status()

    answer = []
    for name, payload in iter_events(response):
        if name == "text.delta":
            answer.append(payload["text"])
            print(payload["text"], end="", flush=True)
        elif name == "tool.call":
            print(f"\n[tool] {payload['name']}({payload['arguments']})")
        elif name == "tool.result":
            print(f"[tool] {payload['name']} -> {'ok' if payload['ok'] else 'FAILED'}")
        elif name == "usage":
            print(f"\n[cost] {payload['credits']} credits, "
                  f"{payload['completion_tokens']} output tokens")
        elif name == "run.ended":
            print(f"[done] {payload['reason']}")
finally:
    # Optional: stop the sandbox now rather than when it goes idle.
    requests.delete(f"{BASE}/sessions/{session['id']}/sandbox", headers=HEADERS)

Python — detached run, for CI

import os
import time
import requests

BASE = "https://codingfleet.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CODINGFLEET_KEY']}",
           "Content-Type": "application/json"}

session = requests.post(f"{BASE}/sessions", headers=HEADERS, json={
    "system_message": "You are a coding agent. Use your tools.",
}).json()

run = requests.post(f"{BASE}/sessions/{session['id']}/runs", headers=HEADERS, json={
    "message": "Run the test suite and fix whatever fails.",
    "detached": True,          # returns at once; the agent keeps working
}).json()

# Poll until it settles. Nothing holds an HTTP connection open meanwhile.
while True:
    time.sleep(5)
    run = requests.get(f"{BASE}/runs/{run['id']}", headers=HEADERS).json()
    if run["status"] != "running":
        break

print(run["status"], run["reason"], f"{run['credits_used']} credits")
requests.delete(f"{BASE}/sessions/{session['id']}", headers=HEADERS)

# 'budget' means the turn hit its spend ceiling and wrapped up early: the
# answer is real, but it may not have finished the job.
raise SystemExit(0 if run["reason"] in ("completed", "budget") else 1)

JavaScript — stream in the browser or Node

const BASE = "https://codingfleet.com/v1";
const HEADERS = {
  Authorization: `Bearer ${process.env.CODINGFLEET_KEY}`,
  "Content-Type": "application/json",
};

const session = await fetch(`${BASE}/sessions`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    system_message: "You are a coding agent with a Linux sandbox. Use your tools.",
  }),
}).then((r) => r.json());

const run = await fetch(`${BASE}/sessions/${session.id}/runs`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ message: "Write fizzbuzz.py and run it." }),
});

const reader = run.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;

  // Frames are separated by a blank line; keep the trailing partial one.
  const frames = buffer.split("\n\n");
  buffer = frames.pop();

  for (const frame of frames) {
    let name = "", data = "";
    for (const line of frame.split("\n")) {
      if (line.startsWith("event:")) name = line.slice(6).trim();
      else if (line.startsWith("data:")) data = line.slice(5).trim();
    }
    if (!name || !data) continue;               // keepalive comment
    const payload = JSON.parse(data);

    if (name === "text.delta") process.stdout.write(payload.text);
    else if (name === "tool.call") console.log(`\n[tool] ${payload.name}`);
    else if (name === "usage") console.log(`\n[cost] ${payload.credits} credits`);
    else if (name === "run.ended") console.log(`[done] ${payload.reason}`);
  }
}

await fetch(`${BASE}/sessions/${session.id}`, { method: "DELETE", headers: HEADERS });

There is a ready-made end-to-end test in the repository, agent_api_smoke.py, which exercises every endpoint on this page against your own key.