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.
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
| Object | What 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
Open a session. Returns 201.
Body parameters
| Name | Type | Default | Description |
|---|---|---|---|
| model | string | 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_message | string | coding agent | The agent's standing instructions. Omit it to use a short built-in coding-agent prompt. |
| capabilities | object | 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_repository | string | 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_sandbox | boolean | 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_from | string | 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_spec | string | 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. |
| executor | string | "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
| 400 | unknown_field | A field this endpoint does not take, or an unknown key in
capabilities. fields names them. |
| 400 | executor_conflict | executor: "client" with an option that needs a sandbox:
git_repository, restore_from or
sandbox_spec. |
| 400 | invalid_body | A field of the wrong type, or sandbox_spec with
code_execution off. |
| 404 | spec_not_found | sandbox_spec names no spec of yours. |
| 403 | desktop_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. |
| 400 | invalid_executor | Unknown executor value. |
| 403 | paid_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". |
| 404 | repository_not_found | No repository with that id on your account. |
| 404 | session_not_found | restore_from names no session of yours. |
| 409 | snapshot_not_found | The restore_from session has no snapshot. |
List your sessions, newest first, with how many runs you have going.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| limit | integer | 25 | 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 }
}
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.
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.
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.
Rename a session, or pin it. The web chat shows the same title and pin.
Body parameters
| Name | Type | Description |
|---|---|---|
| title | string | The new name, at most 200 characters. |
| pinned | boolean | 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.
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 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.
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.
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.
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.
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.
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.
List what is on the session's sandbox filesystem.
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
| path | string | "." | 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
Give the agent a task. Streams events as text/event-stream, or returns
202 immediately when detached.
Body parameters
| Name | Type | Default | Description |
|---|---|---|---|
| message | string | required | The task. No character limit: the whole conversation must fit your plan's input token limit, as in the web app. |
| model | string | session's | Override the session's model for this turn. With neither, the run uses the default model in your settings, or Auto-Select. |
| files | string[] | 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. |
| detached | boolean | 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_servers | object | none | Remote MCP servers the agent may use in this run. See MCP servers and local tools. |
| client_tools | object[] | none | Tools your client runs itself, such as local MCP servers. Client sessions only. See MCP servers and local tools. |
| computer_use | boolean | 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_fallback | boolean | 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
| 400 | missing_message | message absent or blank. |
| 400 | unknown_model | model is not an id from GET /v1/models. |
| 400 | file_not_found | A files id is not one of your uploads, or was deleted. |
| 400 | too_many_files | More than 10 files. |
| 400 | file_not_in_session | A files id was uploaded into another session's sandbox. |
| 400 | vision_unsupported | An image is attached and model cannot read images. |
| 400 | invalid_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. |
| 400 | unknown_field | A field this endpoint does not take, or an unknown field in an
mcp_servers entry or a client_tools entry. |
| 400 | executor_conflict | client_tools or computer_use in a session
that does not run tools on the client. |
| 403 | private_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. |
| 409 | session_broken | The session's chat was deleted in the web app. |
| 409 | run_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. |
| 409 | model_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. |
| 402 | rejected | Out of credits. link points to the page that fixes it. |
| 403 | rejected | The model is not on your plan, or another gate refused the turn.
link, when present, points to the page that fixes it. |
| 403 | model_disabled | The model was retired. Pick one from GET /v1/models. |
| 404 | session_not_found | — |
| 429 | run_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. |
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:
| quota | Your weekly usage limit is reached, so the model's fallback answered. |
| pool_cap | The 5-hour limit on Unlimited models (desktop app keys). |
| daily_limit | The model's daily request limit on your account. |
| unavailable | The model was rate-limited or down before it wrote anything. |
| refused | The 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"}
]
}
| connected | boolean | 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_at | string | null | When the last heartbeat for this session came, kept for 7 days. |
| lost | boolean | 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_calls | object[] | 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.
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.
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).
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.
Send a correction into a run that is still going, without restarting it.
Body parameters
| Name | Type | Default | Description |
|---|---|---|---|
| message | string | required | Up to 4,000 characters. Delivered at the next tool-call boundary, as a user message after that round's tool results. |
| files | string[] | 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.
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.
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
| Tool | Arguments | What to do and send back |
|---|---|---|
| run_command | command, 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_code | code, filename, command, timeout | Write code to filename (skip if code is empty),
then run command (skip if empty). Send {"stdout", "stderr", "exit_code"}. |
| fs_read | path, offset, limit, with_line_numbers | Send the text, each line prefixed with its number unless
with_line_numbers is false. |
| fs_write | path, content | Create missing directories and write the file. Send a short confirmation. |
| fs_edit | path, old, new, all | Replace old with new. Fail when old is
missing, or occurs more than once and all is not true. |
| fs_glob | pat, path | Send the matching paths, one per line. |
| view_image | source | 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_background | id, 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_background | id | Stop the background command and every process it started. Send
{"status", "exit_code"}. |
| take_screenshot | window, 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. |
| computer | action, 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
 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
| Name | Type | Default | Description |
|---|---|---|---|
| call_id | string | required | The id of the tool.call event. |
| output | string | 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_error | boolean | 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.
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.
| Field | Type | Description |
|---|---|---|
| url | string | required The server's streamable HTTP
endpoint. Must be https and resolve to a public address. |
| token | string | Sent as Authorization: Bearer <token>. |
| auth_header | string | Send the token raw in this header instead, for servers that want
one such as CONTEXT7_API_KEY. |
| tools | string[] | 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.
| Field | Type | Description |
|---|---|---|
| name | string | required Starts with local_,
then letters, digits, _ or -; 64 characters at most. |
| description | string | What the tool does, for the model. Cut at 2,000 characters. |
| input_schema | object | 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.
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
| 400 | missing_file | No multipart field named file. |
| 400 | invalid_image | The image does not decode, or is too large to decode safely. |
| 413 | file_too_large | Over 20 MB. |
| 415 | unsupported_file_type | A type that is not accepted (see above). |
| 429 | upload_limit_reached | 120 uploads this hour, or 1 GB today. Try again later. |
| 413 | storage_limit_reached | Your uploads already use what your plan may keep (2 GB Free, 10 GB paid). |
| 403 | paid_plan_required | Free plan: uploads into a sandbox need a paid plan. |
| 429 | sandbox_limit_reached | The session has no sandbox running, and every sandbox your plan allows has a run going. Wait for a run to end. |
| 409 | upload_failed | The file could not be stored. Retry. |
| 404 | session_not_found | session_id is not one of your sessions. |
| 502 | sandbox_error | The sandbox could not take the file. Retry. |
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"
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
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_source | Reads | The image |
|---|---|---|
| standard | — | Debian with git, curl, wget, Python 3, build tools, ripgrep and zip. What a session gets without a spec. |
| dockerhub | registry_image | A public image, such as golang:1.23,
gcc:13 or ghcr.io/org/app:1.2. |
| dockerfile | dockerfile | 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 "$@". |
| snapshot | snapshot | 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.
Save a spec. Returns 201 with the spec.
Body parameters
| Name | Type | Default | Description |
|---|---|---|---|
| name | string | the summary | Up to 100 characters. |
| description | string | "" | Up to 2,000 characters. |
| image_source | string | "standard" | standard, dockerhub,
dockerfile or snapshot. |
| registry_image | string | — | For dockerhub. |
| dockerfile | string | — | For dockerfile. |
| snapshot | string | — | A snapshot id, for snapshot. |
| install_essentials | boolean | true | Install CodingFleet's tooling on top of the image. |
| env_vars | object | {} | 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
| 400 | invalid_body | A field of the wrong type, a field the source does not read, a missing one, or a bad env var name. |
| 400 | invalid_spec | The Dockerfile cannot be built here; the message says why. |
| 400 | unknown_field | A field this endpoint does not take. |
| 404 | snapshot_not_found | snapshot names no snapshot of yours. |
| 403 | desktop_key_required | A key for code, and snapshot is a snapshot of the web
chat's sandbox. |
| 409 | snapshot_broken | That snapshot failed to start a sandbox before. |
| 409 | spec_limit_reached | You already keep 50 specs. |
Your specs, newest first: {"specs": [...], "pinned": id | null}.
The spec you pinned on the web's Sandboxes page is listed too.
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"}}'
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.
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.
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.
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_vars | object | Env vars for every sandbox. PATCH updates them like a spec's: a string
sets, null removes. Read back as env_var_names. |
| always_fresh | boolean | The web chat starts every sandbox empty instead of from its newest snapshot. A session still gets its own snapshot back. |
| snapshot_window_hours | integer | How old a web snapshot can be and still be restored: 1 to 720. |
| block_network | boolean | No network in new sandboxes (and no open ports). |
| default_spec | string | 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.
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.
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.
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"}'
| 403 | plan_upgrade_required | Saving snapshots needs the Unlimited plan or higher, as on the web. |
| 403 | desktop_key_required | A key for code, and the sandbox is the web chat's. |
| 409 | run_in_progress | Its session has a run going: snapshot between runs. |
| 409 | sandbox_stopped | The sandbox is not running. |
| 409 | snapshot_limit_reached | You keep 50 saved snapshots: delete one. |
| 502 | snapshot_failed | The snapshot could not be taken. Try again. |
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.
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
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
| Name | Type | Default | Description |
|---|---|---|---|
| include | string | — | 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.
Send a crash report from your client, after asking its user. Stored for the CodingFleet team to read; nothing else is done with it.
| Name | Type | Default | Description |
|---|---|---|---|
| kind | string | required | main, renderer, child or
unresponsive: which part of the client failed. |
| message | string | The error. Cut at 2,000 characters. | |
| stack | string | Its stack trace. Cut at 20,000 characters. | |
| platform | string | Operating system and version. | |
| log_tail | string | 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_at | string | 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.
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.
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
}
}
| Setting | Type | Description |
|---|---|---|
| privacy.privacy_models | boolean | 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_retention | boolean | With privacy_models: only providers that keep nothing. |
| privacy.data_region | string | "" (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_sessions | boolean | 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_model | string | 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_models | boolean | List and allow older models. |
| preferences.credit_cap_per_message | integer | 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_mode | boolean | 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).
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.
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:
| Action | Does |
|---|---|
| GET invoices | {"invoices": [...]}: date, status,
total_formatted, and invoice_url to open the invoice. |
| POST cancel | Cancels 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 resume | Undoes a cancellation before the period
ends. 409 not_resumable when it is not cancelled, or its period
has ended (can_resume is false). |
| GET offer | Whether a stay discount is available:
eligible, percent, interval,
amount_due_cents, discounted_cents. |
| POST offer | Claims 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.
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.
| Event | Payload |
|---|---|
| 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
| completed | The 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. |
| cancelled | You 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. |
| error | The 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
| id | string | Session id. |
| title | string | null | A name generated after the first run. null until then. |
| pinned | boolean | Whether the session is pinned. |
| executor | string | Where tools run: "sandbox" or "client". |
| model | string | null | Default model; null means your default model from settings. |
| snapshot_sandbox | boolean | Whether a snapshot is saved before the sandbox is stopped. Always false for a client session. |
| capabilities | object | The five capability flags. |
| git_repository | string | null | Attached repository id. |
| restore_from | string | null | Session whose snapshot this one's sandbox started from. |
| sandbox_spec | string | null | The sandbox spec its sandbox is built from. |
| has_snapshot | boolean | Whether this session saved a snapshot to resume or restore_from. Only on a single-session read. |
| sandbox | object | null | id, created_at, expires_at, snapshot_on_reap. Only on a single-session read. |
| context | object | tokens: 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_model | string | null | Display 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_at | string | null | When the conversation last got an answer, from any client, the web chat included. On list and single-session reads. |
| busy | boolean | null | Whether 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_id | string | null | The 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_at | string | ISO 8601, UTC. |
| last_used_at | string | When this API last ran or opened the session. Drives the idle timeout. |
Run
| id | string | Run id. |
| session_id | string | Its session. |
| status | string | running, completed, cancelled, error. |
| reason | string | null | The run.ended reason, plus rejected (a gate refused it before it began) and lost (the worker died before reporting a result). |
| detached | boolean | How it was started. |
| notice | string | null | The notice event's message, if the run had one. |
| model | string | The model that answered (display name). While the run is going, the id it asked for. |
| requested_model | string | null | The model id the run asked for; "auto" for Auto-Select. |
| model_id | string | null | The id of the model that answered. null until the run ends. |
| model_switches | object[] | Each move to another model: from, to, reason. See model switches. |
| model_fallback | boolean | Whether the run may move to another model. |
| credits_used | number | May be fractional. |
| prompt_tokens | integer | Input tokens. |
| completion_tokens | integer | Output tokens. |
| error | string | null | Set when it failed. |
| progress | object | Durable 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. |
| rounds | object[] | One entry per model request: time, tokens, credits and tool calls. See rounds. |
| client_executor | object | null | Client sessions: connected, last_heartbeat_at, lost, waiting_calls. See is the client there? Only on GET /v1/runs/{id}. |
| created_at | string | ISO 8601, UTC. |
| ended_at | string | null | null 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
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_body | Body is not a JSON object, or a field has the wrong type. |
| 400 | unknown_field | A 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 {}. |
| 409 | model_unavailable | A run with model_fallback: false whose model cannot take it. |
| 400 | invalid_spec | A sandbox spec's Dockerfile cannot be built here. |
| 403 | plan_upgrade_required | Saving a snapshot needs the Unlimited plan or higher. |
| 404 | spec_not_found | No sandbox spec with that id on your account. |
| 404 | sandbox_not_found | No sandbox with that id on your account. |
| 404 | snapshot_not_found | No snapshot with that id on your account. |
| 409 | spec_in_use | Sessions build their sandboxes from the spec; the body lists them. |
| 409 | spec_limit_reached | The account keeps 50 sandbox specs. |
| 409 | build_in_progress | A spec is being built; one build runs at a time. |
| 429 | build_limit_reached | The account started 30 spec builds today (UTC). |
| 409 | snapshot_broken | The snapshot failed to start a sandbox before. |
| 409 | snapshot_in_use | Sandbox specs are built from the snapshot. |
| 409 | snapshot_limit_reached | The account keeps 50 saved snapshots. |
| 409 | sandbox_stopped | The sandbox is not running. |
| 409 | web_sandbox | The web chat's sandbox: stop it on the web. |
| 502 | snapshot_failed | The snapshot could not be taken. Try again. |
| 404 | subscription_not_found | No subscription with that id on your account. |
| 409 | not_cancellable | The subscription is not active or on trial. |
| 409 | not_resumable | Only a cancelled subscription whose paid period has not ended can be resumed. |
| 409 | offer_not_available | The stay discount does not apply to this subscription. |
| 502 | billing_unavailable | The payment provider did not answer. Try again. |
| 400 | invalid_key | The value sent as a provider key does not look like one. |
| 404 | unknown_provider | BYOK keys are for openrouter, openai, anthropic, deepseek and gemini. |
| 400 | invalid_audio | The file sent for transcription is not an audio recording. |
| 400 | missing_message | A run needs a non-empty message. |
| 400 | file_not_found | An attached or requested file is not one of your live uploads (also 404 on the file endpoints). |
| 400 | invalid_setting | A PATCH /v1/settings value that breaks a rule. |
| 403 | private_session_mode | Runs are refused while private session mode is on. |
| 404 | not_an_image | Only uploaded images can be downloaded from /v1/files/{id}/content. |
| 400 | too_many_files | More files than a run (10) or a steer (3) takes. |
| 400 | file_not_in_session | A sandbox upload attached to a run of another session. |
| 400 | vision_unsupported | An image is attached to a model that cannot read images. |
| 400 | missing_file | An upload without a file field. |
| 400 | invalid_image | An uploaded image does not decode. |
| 413 | file_too_large | An upload over 20 MB. |
| 415 | unsupported_file_type | An upload of a type that is not accepted, or a non-image sent with a steer. |
| 429 | upload_limit_reached | The hourly or daily upload limit. |
| 429 | sandbox_limit_reached | A new sandbox is needed, and every sandbox your plan allows has a run going. |
| 403 | paid_plan_required | Free plan: a session, run or upload that needs a cloud sandbox. Use executor: "client", or turn code_execution off. |
| 409 | upload_failed | An upload could not be stored; retry. |
| 400 | unknown_model | model is not an id from GET /v1/models. |
| 400 | executor_conflict | executor: "client" with an option that needs a sandbox. |
| 400 | invalid_executor | Unknown executor. |
| 401 | missing_token | No Authorization header. |
| 401 | invalid_token | Unknown or revoked key. |
| 401 | token_expired | The key reached the expiry date chosen when it was made. Create a new one. |
| 403 | desktop_key_required | A 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. |
| 413 | storage_limit_reached | Your uploads already use what your plan may keep. |
| 402 | rejected | Out of credits. Carries link. |
| 403 | email_unconfirmed | Confirm your email address first. |
| 403 | rejected | The model is not available on your plan. Carries link. |
| 403 | model_disabled | The model was retired. Pick one from GET /v1/models. |
| 404 | session_not_found | No such session on your account. |
| 404 | agent_not_found | No sub-agent with that id in the session. |
| 404 | run_not_found | No such run on your account. |
| 404 | repository_not_found | No such connected repository. |
| 409 | snapshot_not_found | restore_from names a session with no snapshot. |
| 409 | session_broken | The session lost its history and cannot run. |
| 409 | nothing_to_compact | The conversation is too small, or has nothing saved, to compact. |
| 429 | transcription_limit_reached | Too many voice recordings this hour or today. |
| 503 | transcription_unavailable | The voice limit could not be checked, so nothing was transcribed. Try again. |
| 502 | transcription_failed | The speech service failed to transcribe the recording. Try again. |
| 409 | not_running | Cancel, steer or a tool result on a run that is not streaming. |
| 409 | not_client | Executor heartbeat on a run whose tools run in a cloud sandbox. |
| 409 | call_not_pending | No tool call with that id waits for a result: answered already, timed out, or cancelled. |
| 413 | result_too_large | A tool result over 1,000,000 characters. |
| 503 | delivery_failed | A tool result could not be delivered. Send it again. |
| 409 | steer_refused | Too many steers this turn, or the message is too long. |
| 409 | no_sandbox | That session does not run a sandbox. |
| 409 | no_stream | That run has no replayable stream. |
| 409 | events_unavailable | Events are kept only for the latest run in a session. |
| 409 | run_in_progress | The session already has a run going. |
| 413 | request_too_large | The request body is over the server's size limit. |
| 429 | run_limit_reached | Starting the run would go over the number of runs your plan allows at the same time. |
| 429 | rate_limited | Too many requests. Honour Retry-After. For browser sign-in: too many sign-ins started from one address in an hour. |
| 500 | run_failed | The run could not be started. Nothing was billed; try again. |
| 502 | sandbox_error | The 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.
| Plan | Runs | Sandboxes |
|---|---|---|
| Free | 1 | None (client only) |
| Pro | 2 | 2 |
| Unlimited | 3 | 2 |
| Elite | 5 | 5 |
| Ultimate | 10 | No limit |
| Ultimate Max | 10 | No limit |
Everything else
| Requests | 120 per 60 seconds, per key.
Over it: 429 with Retry-After. |
| Input size | No character limit. The whole conversation must fit your plan's input token limit, the same as in the web app. |
| Steers | 10 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. |
| Keys | 10 active per account. |
| Sandbox specs | 50 per account, 30 builds a day
(UTC) with POST /v1/sandbox_specs/{spec_id}/build. |
| Saved snapshots | 50 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.