T0 API documentation
Hosted, sandboxed agent sessions over HTTP. Create a session, push files into it, send it messages, and stream the agent working — in an isolated Linux workspace that is still there on the next message.
- Base URL
https://api.t0.sikasio.com/v1- Auth
Authorization: Bearer t0k_live_YOUR_KEY- Streaming
- Server-Sent Events, resumable via
Last-Event-ID, plus a bidirectional WebSocket - Models
core·max— you bring your own provider key- Free plan
- 300 credits/month (5 h of awake session time), no card required
- Status
- status.t0.sikasio.com
A session is an isolated sandbox, a persistent workspace and one long-lived agent process. A turn is one message in and one response out, streamed as ordered, sequence-numbered events. Files written by the agent, packages it installed and git state all survive between messages, so message two builds on message one.
T0 sells the runtime, not the model. Isolation, persistence, orchestration and latency are the product; each account supplies its own model provider key, so no token is resold and nothing on your bill is a marked-up token.
Getting your API key
- Create an account at /signup — email and password, no card.
- Verify your email with the code we send you.
- Copy your API key. It is shown once, right after verification. Store it somewhere safe; you can always issue a fresh one from the dashboard.
- Add your model provider key in the dashboard. T0 is bring-your-own-key: the agent runs against your provider account, and your key is encrypted at rest and injected into the sandbox only at the moment a container is claimed.
- Make your first call — see the quickstart below.
Already have an account? Sign in. Want to try the API without writing a client first? The playground is built on nothing but the public API and your own key.
Authentication
Every /v1 request carries your key as a bearer token:
Authorization: Bearer t0k_live_YOUR_KEYThe key is the tenant boundary: it identifies the account, authorizes the request, and is what usage is metered against. Every session, event and file belongs to the account behind the key, and a request for another account’s session is a 404, never a redirect or a hint.
Keys are stored as an indexed prefix plus an HMAC-SHA256 digest — never in plaintext — and compared in constant time. Revoking a key from the dashboard takes effect within seconds, everywhere.
Keep keys server-side. The API host sets no cookies and is a separate origin from this portal, so there is no session to borrow and no CSRF surface — but a key shipped in a browser bundle is a key you have given away. Call T0 from a server you control, or proxy it.
A missing, malformed, revoked or unknown key returns 401 invalid_key. A valid key used against another account’s resource returns 403 forbidden.
Plans & quotas
A credit is one minute of awake session time. Sleeping sessions cost nothing, so a session you leave open overnight and wake in the morning is billed for the minutes it worked, not the hours it waited.
| Free | Starter | Pro | |
|---|---|---|---|
| Price | $0 | $19/mo | $59/mo |
| Credits / monthone credit = one minute awake | 300 (5 h) | 3,600 (60 h) | 18,000 (300 h) |
| Daily capcredits per UTC day | 60 | 600 | 2,400 |
| Concurrent sessions | 1 | 3 | 10 |
| Workspace storagedurable, per account | 2 GB | 25 GB | 100 GB |
| Live disk per sessiontechnical ceiling | 5 GB | 10 GB | 20 GB |
| Retentionafter a session ends | 7 days | 30 days | 90 days |
| Sandboxmemory / CPU | 1 GB / 1 CPU | 2 GB / 1.5 CPU | 4 GB / 2 CPU |
Metered: awake session-seconds, peak workspace bytes and turns. Token counts are recorded and shown to you for your own visibility — under bring-your-own-key they are not billed by T0. Enforced: concurrent sessions, monthly and daily credits, workspace size, retention window and request rate.
Overage is a hard stop, never a surprise bill. When a quota is reached the API returns 429 quota_exceeded naming the limit and the next plan up. Sessions already running are not killed mid-turn.
Full breakdown on the pricing page. Live figures for your own account come from GET /v1/usage.
Quickstart
Create a session, put a file in it, send a message, and read the result back out. Session creation is a claim against a pool of pre-booted containers, so it is bookkeeping rather than a boot.
# 1. create a session — comes from the warm pool, ready in well under a second
SESSION=$(curl -s -X POST https://api.t0.sikasio.com/v1/sessions \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"quickstart","model":"core"}' | jq -r .id)
# 2. push a file into its workspace
curl -X PUT https://api.t0.sikasio.com/v1/sessions/$SESSION/files/notes.md \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
--data-binary @notes.md
# 3. stream the events, then send a message from another shell
curl -N https://api.t0.sikasio.com/v1/sessions/$SESSION/events \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Accept: text/event-stream" &
curl -X POST https://api.t0.sikasio.com/v1/sessions/$SESSION/messages \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"Summarise notes.md into summary.md, then run the tests."}'
# 4. take the result back out — works even after the session has gone to sleep
curl -s https://api.t0.sikasio.com/v1/sessions/$SESSION/files/summary.md \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-o summary.md// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
// 1. create a session
const session = await fetch(T0 + "/sessions", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ name: "quickstart", model: "core" }),
}).then((r) => r.json());
// 2. push a file into its workspace
await fetch(T0 + "/sessions/" + session.id + "/files/notes.md", {
method: "PUT",
headers: { ...auth, "Content-Type": "text/markdown" },
body: "# Notes\n\nShip the parser.",
});
// 3. open the event stream before sending the message
const events = await fetch(T0 + "/sessions/" + session.id + "/events", {
headers: { ...auth, Accept: "text/event-stream" },
});
const reader = events.body.pipeThrough(new TextDecoderStream()).getReader();
await fetch(T0 + "/sessions/" + session.id + "/messages", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ content: "Summarise notes.md into summary.md." }),
});
for (;;) {
const { value, done } = await reader.read();
if (done) break;
process.stdout.write(value);
}import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
# 1. create a session
session = requests.post(
T0 + "/sessions",
headers=auth,
json={"name": "quickstart", "model": "core"},
).json()
sid = session["id"]
# 2. push a file into its workspace
requests.put(
T0 + "/sessions/" + sid + "/files/notes.md",
headers={**auth, "Content-Type": "text/markdown"},
data=b"# Notes\n\nShip the parser.",
)
# 3. send a message, then read the stream
requests.post(
T0 + "/sessions/" + sid + "/messages",
headers=auth,
json={"content": "Summarise notes.md into summary.md."},
)
with requests.get(
T0 + "/sessions/" + sid + "/events",
headers={**auth, "Accept": "text/event-stream"},
stream=True,
) as stream:
for line in stream.iter_lines(decode_unicode=True):
if line:
print(line)Sessions
A session owns a container, a workspace volume and a live agent process. The process stays alive for the life of the session, which is what keeps follow-up turns fast.
States
| State | Meaning |
|---|---|
ready | Awake and idle. Send it a message. |
working | A turn is in flight; events are streaming. |
sleeping | Idle past idle_timeout: the container is stopped, the workspace is kept, and nothing is being metered. The next message wakes it. |
expired | Past its ttl or its plan’s retention window. The workspace has been deleted. |
failed | The sandbox could not be started or the agent died unrecoverably. |
Create a session
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Your own label, returned on every read. Purely for your bookkeeping. |
model | string | core | core for everyday work, max for the hardest tasks. Fixed for the life of the session. |
idle_timeout | integer (s) | 600 | Seconds of inactivity before the session sleeps. Sleeping is free, so a short timeout costs you nothing but a resume. |
ttl | integer (s) | plan retention | Hard lifetime. At expiry the workspace is deleted, whatever state the session is in. |
env | object | — | Environment variables for the workspace. Do not put your provider key here — T0 injects that for you. |
curl -X POST https://api.t0.sikasio.com/v1/sessions \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice-parser",
"model": "core",
"idle_timeout": 600,
"ttl": 86400,
"env": { "NODE_ENV": "test" }
}'// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
const session = await fetch(T0 + "/sessions", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
name: "invoice-parser",
model: "core",
idle_timeout: 600,
ttl: 86400,
env: { NODE_ENV: "test" },
}),
}).then((r) => r.json());
console.log(session.id, session.state, session.stream_url);import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
session = requests.post(
T0 + "/sessions",
headers=auth,
json={
"name": "invoice-parser",
"model": "core",
"idle_timeout": 600,
"ttl": 86400,
"env": {"NODE_ENV": "test"},
},
).json()
print(session["id"], session["state"], session["stream_url"])Response — 201
{
"id": "sess_7f3a9c2e",
"name": "invoice-parser",
"state": "ready",
"model": "core",
"stream_url": "https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/events",
"idle_timeout": 600,
"created_at": "2026-08-17T09:14:02.117Z",
"last_active_at": "2026-08-17T09:14:02.117Z",
"expires_at": "2026-08-18T09:14:02.117Z"
}List sessions
Cursor-paginated, newest first. limit defaults to 20; follow next_cursor until it comes back null.
curl -s "https://api.t0.sikasio.com/v1/sessions?limit=20" -H "Authorization: Bearer t0k_live_YOUR_KEY"
# next page
curl -s "https://api.t0.sikasio.com/v1/sessions?limit=20&cursor=c_9f21b0" -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
let cursor;
do {
const url = new URL(T0 + "/sessions");
url.searchParams.set("limit", "20");
if (cursor) url.searchParams.set("cursor", cursor);
const page = await fetch(url, { headers: auth }).then((r) => r.json());
for (const s of page.data) console.log(s.id, s.state, s.name);
cursor = page.next_cursor;
} while (cursor);import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
cursor = None
while True:
params = {"limit": 20}
if cursor:
params["cursor"] = cursor
page = requests.get(T0 + "/sessions", headers=auth, params=params).json()
for s in page["data"]:
print(s["id"], s["state"], s["name"])
cursor = page.get("next_cursor")
if not cursor:
break{
"data": [
{ "id": "sess_7f3a9c2e", "name": "invoice-parser", "state": "working", "workspace_bytes": 41235968 },
{ "id": "sess_2b8d41af", "name": "nightly-refactor", "state": "sleeping", "workspace_bytes": 8912384 }
],
"next_cursor": "c_9f21b0"
}Retrieve a session
Reading a sleeping session does not wake it — this is the cheap way to poll state and workspace size.
curl -s https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
const session = await fetch(T0 + "/sessions/sess_7f3a9c2e", { headers: auth }).then((r) => r.json());
if (session.state === "sleeping") {
console.log("asleep since", session.last_active_at, "— the next message wakes it");
}import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
session = requests.get(T0 + "/sessions/sess_7f3a9c2e", headers=auth).json()
if session["state"] == "sleeping":
print("asleep since", session["last_active_at"], "- the next message wakes it")Delete a session
Stops the container and deletes the workspace immediately, including its snapshot. Returns 204. Download anything you want to keep first — this is not reversible.
curl -X DELETE https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
await fetch(T0 + "/sessions/sess_7f3a9c2e", { method: "DELETE", headers: auth });import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
requests.delete(T0 + "/sessions/sess_7f3a9c2e", headers=auth)Interrupt a turn
Stops the turn in flight without killing the session: the agent process stays alive, the workspace keeps whatever was written, and the session returns to ready. Interrupting an idle session is a no-op.
curl -X POST https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/interrupt -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
await fetch(T0 + "/sessions/sess_7f3a9c2e/interrupt", { method: "POST", headers: auth });import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
requests.post(T0 + "/sessions/sess_7f3a9c2e/interrupt", headers=auth)Messages & streaming
Sending a message is fire-and-forget: it returns 202 with a turn_id as soon as the prompt is written into the running agent. All output arrives on the event stream, so a slow turn never holds an HTTP request open.
Send a message
| Field | Type | Default | Notes |
|---|---|---|---|
contentrequired | string | — | The message for the agent. |
stream | boolean | true | Emit text.delta events as the response is produced. With false you still get turn.completed, just no token-by-token output. |
curl -X POST https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/messages \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Add a failing test for the invoice date parser, then make it pass.",
"stream": true
}'// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
const turn = await fetch(T0 + "/sessions/sess_7f3a9c2e/messages", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
content: "Add a failing test for the invoice date parser, then make it pass.",
stream: true,
}),
}).then((r) => r.json());
console.log("turn", turn.turn_id, "accepted"); // 202 — output arrives on the streamimport os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
turn = requests.post(
T0 + "/sessions/sess_7f3a9c2e/messages",
headers=auth,
json={
"content": "Add a failing test for the invoice date parser, then make it pass.",
"stream": True,
},
).json()
print("turn", turn["turn_id"], "accepted") # 202 - output arrives on the stream{ "turn_id": "turn_04c7", "session_id": "sess_7f3a9c2e", "state": "working" }A session that is sleeping wakes on the message. A message sent while a turn is already running returns 409 session_not_ready — interrupt first, or wait for turn.completed.
Event stream (SSE)
Server-Sent Events, one ordered and sequence-numbered stream per session. The id of each frame is that sequence number.
id: 126
event: turn.started
data: {"turn_id":"turn_04c7","seq":126}
id: 127
event: text.delta
data: {"turn_id":"turn_04c7","seq":127,"text":"Reading src/parser.ts"}
id: 128
event: tool.started
data: {"turn_id":"turn_04c7","seq":128,"tool":"shell","summary":"npm test"}
id: 129
event: tool.output
data: {"turn_id":"turn_04c7","seq":129,"tool":"shell","chunk":"2 passing\n"}
id: 130
event: file.changed
data: {"seq":130,"path":"src/parser.ts","change":"modified","bytes":4192}
id: 131
event: turn.completed
data: {"turn_id":"turn_04c7","seq":131,"usage":{"session_seconds":38,"tokens_in":12480,"tokens_out":842,"cache_read":11200,"cache_write":1280}}| Event | Carries |
|---|---|
session.ready | The sandbox is claimed and the agent process is up. |
turn.started | turn_id — a message has been accepted and work has begun. |
text.delta | A fragment of the agent’s response, unbuffered. |
tool.started | The tool the agent reached for, and a one-line summary. |
tool.output | A chunk of that tool’s output — shell stdout, test results. |
file.changed | A workspace path that was created, modified or deleted, with its new size. |
turn.completed | The turn is done, and carries the usage it cost: session seconds, tokens in and out, cache reads and writes. |
session.idle | The session has slept. Metering stops here; the workspace is kept. |
session.expired | The workspace has been deleted. The stream ends. |
error | Something went wrong inside the session, in the same { code, message } shape as an HTTP error. |
Resuming a dropped stream
Reconnect with a Last-Event-ID header carrying the last sequence number you processed and the stream continues from the next event — nothing lost, nothing replayed twice. This is the standard SSE reconnect header, so a browser EventSource does it for you; a hand-rolled client should track the id itself, as below. Events are retained for a replay window generous enough to cover a reconnect, not as a permanent log — for a long turn, keep your own copy.
# -N disables curl's own buffering so deltas appear as they are produced
curl -N https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/events \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Accept: text/event-stream"
# reconnecting after a drop: resume from the last id you saw, nothing is lost
curl -N https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/events \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Accept: text/event-stream" \
-H "Last-Event-ID: 128"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
// EventSource cannot send an Authorization header, so read the stream with
// fetch. lastId is what makes a dropped connection resumable.
let lastId = null;
for (;;) {
const headers = { ...auth, Accept: "text/event-stream" };
if (lastId !== null) headers["Last-Event-ID"] = String(lastId);
const res = await fetch(T0 + "/sessions/sess_7f3a9c2e/events", { headers });
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const frames = buffer.split("\n\n");
buffer = frames.pop() ?? "";
for (const frame of frames) {
const id = frame.match(/^id: (.*)$/m)?.[1];
const event = frame.match(/^event: (.*)$/m)?.[1];
const data = frame.match(/^data: (.*)$/m)?.[1];
if (id) lastId = id;
if (event === "text.delta") process.stdout.write(JSON.parse(data).text);
if (event === "turn.completed") console.log("\nusage", JSON.parse(data).usage);
}
}
} catch {
// fall through and reconnect from lastId
}
}import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
import json
last_id = None
while True:
headers = {**auth, "Accept": "text/event-stream"}
if last_id is not None:
headers["Last-Event-ID"] = str(last_id)
with requests.get(
T0 + "/sessions/sess_7f3a9c2e/events", headers=headers, stream=True
) as stream:
event = None
for line in stream.iter_lines(decode_unicode=True):
if line.startswith("id: "):
last_id = line[4:]
elif line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
payload = json.loads(line[6:])
if event == "text.delta":
print(payload["text"], end="", flush=True)
elif event == "turn.completed":
print("\nusage", payload["usage"])WebSocket
The same events, plus the ability to push. Send { "type": "message", "content": "…" } to start a turn and { "type": "interrupt" } to stop one, on the same connection the events arrive on. Use it when you want one connection instead of two; use SSE when you want the simplest possible client and free reconnection.
# curl 8 speaks WebSocket; --no-buffer keeps frames unbuffered
curl --include --no-buffer \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: c2FtcGxlLWtleS0xMjM0NTY=" \
https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/socket// npm i ws — a browser cannot set an Authorization header on a WebSocket,
// so proxy the socket from your own server, or use the SSE stream in the browser.
import WebSocket from "ws";
const socket = new WebSocket("wss://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/socket", {
headers: { Authorization: "Bearer " + process.env.T0_API_KEY },
});
socket.on("open", () => {
socket.send(JSON.stringify({ type: "message", content: "Run the test suite." }));
});
socket.on("message", (raw) => {
const event = JSON.parse(raw.toString());
if (event.type === "text.delta") process.stdout.write(event.text);
if (event.type === "turn.completed") socket.send(JSON.stringify({ type: "interrupt" }));
});# pip install websockets
import asyncio, json, os, websockets
URL = "wss://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/socket"
HEADERS = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
async def main():
async with websockets.connect(URL, additional_headers=HEADERS) as socket:
await socket.send(json.dumps({"type": "message", "content": "Run the test suite."}))
async for raw in socket:
event = json.loads(raw)
if event["type"] == "text.delta":
print(event["text"], end="", flush=True)
elif event["type"] == "turn.completed":
await socket.send(json.dumps({"type": "interrupt"}))
asyncio.run(main())Files
Files move in and out at any time — before a message, during a turn, or long after the session has gone to sleep. Every path is resolved and confined to /workspace before it reaches the container; anything that escapes it is rejected with 400 invalid_path without ever being touched by the sandbox.
Two transfer paths, chosen by size. Files under 10 MB stream straight through the API into the live workspace — one hop, lowest latency. Larger or bulk transfers are handed a presigned object-storage URL so the bytes never pass through our servers, which is why the upload ceiling is generous.
Upload a file
Raw body, no multipart wrapper. Creates parent directories as needed and overwrites silently. Returns 201 with the stored path and byte count.
curl -X PUT https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/files/src/parser.ts \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @src/parser.ts// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
import { readFile } from "node:fs/promises";
await fetch(T0 + "/sessions/sess_7f3a9c2e/files/src/parser.ts", {
method: "PUT",
headers: { ...auth, "Content-Type": "application/octet-stream" },
body: await readFile("src/parser.ts"),
});import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
with open("src/parser.ts", "rb") as fh:
requests.put(
T0 + "/sessions/sess_7f3a9c2e/files/src/parser.ts",
headers={**auth, "Content-Type": "application/octet-stream"},
data=fh,
)Bulk upload
A tar.gz body, unpacked into /workspace. Every entry in the archive is path-checked the same way a single upload is, and an archive containing one bad entry is rejected whole. This is the right way to seed a repository into a fresh session.
# whole trees go up as one tar.gz — one request instead of a thousand
tar czf - src package.json | curl -X POST https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/files \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-H "Content-Type: application/gzip" \
--data-binary @-// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
import { createReadStream } from "node:fs";
// tar the tree however you like, then stream the archive up in one request
await fetch(T0 + "/sessions/sess_7f3a9c2e/files", {
method: "POST",
headers: { ...auth, "Content-Type": "application/gzip" },
body: createReadStream("project.tar.gz"),
duplex: "half",
});import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
import io, tarfile
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
tar.add("src")
tar.add("package.json")
requests.post(
T0 + "/sessions/sess_7f3a9c2e/files",
headers={**auth, "Content-Type": "application/gzip"},
data=buf.getvalue(),
)Download a file
The raw bytes, with the content type inferred from the extension. This works on a sleeping session too, served straight from the workspace snapshot — you never have to pay to wake a session just to fetch its results.
# works while the session is asleep — served from the snapshot, no wake needed
curl -s https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/files/dist/report.pdf \
-H "Authorization: Bearer t0k_live_YOUR_KEY" \
-o report.pdf// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
import { writeFile } from "node:fs/promises";
const res = await fetch(T0 + "/sessions/sess_7f3a9c2e/files/dist/report.pdf", { headers: auth });
await writeFile("report.pdf", Buffer.from(await res.arrayBuffer()));import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
res = requests.get(T0 + "/sessions/sess_7f3a9c2e/files/dist/report.pdf", headers=auth)
with open("report.pdf", "wb") as fh:
fh.write(res.content)List files
curl -s "https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/files?path=/&recursive=true" -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
const url = new URL(T0 + "/sessions/sess_7f3a9c2e/files");
url.searchParams.set("path", "/");
url.searchParams.set("recursive", "true");
const listing = await fetch(url, { headers: auth }).then((r) => r.json());
for (const entry of listing.files) console.log(entry.path, entry.bytes);import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
listing = requests.get(
T0 + "/sessions/sess_7f3a9c2e/files",
headers=auth,
params={"path": "/", "recursive": "true"},
).json()
for entry in listing["files"]:
print(entry["path"], entry["bytes"]){
"path": "/",
"files": [
{ "path": "package.json", "bytes": 412, "modified_at": "2026-08-17T09:15:44.002Z" },
{ "path": "src/parser.ts", "bytes": 4192, "modified_at": "2026-08-17T09:18:10.551Z" },
{ "path": "dist/report.pdf","bytes": 208133,"modified_at": "2026-08-17T09:19:02.884Z" }
],
"workspace_bytes": 41235968
}Delete a file
Removes a file or, for a directory path, the tree beneath it. Returns 204.
curl -X DELETE https://api.t0.sikasio.com/v1/sessions/sess_7f3a9c2e/files/tmp/scratch.log -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
await fetch(T0 + "/sessions/sess_7f3a9c2e/files/tmp/scratch.log", { method: "DELETE", headers: auth });import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
requests.delete(T0 + "/sessions/sess_7f3a9c2e/files/tmp/scratch.log", headers=auth)Account & health
Who am I
The account behind the key, its plan and limits, and whether a provider key is configured. The provider key itself is never returned — by any endpoint, ever.
curl -s https://api.t0.sikasio.com/v1/me -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
const me = await fetch(T0 + "/me", { headers: auth }).then((r) => r.json());
console.log(me.plan, me.limits.concurrent_sessions);import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
me = requests.get(T0 + "/me", headers=auth).json()
print(me["plan"], me["limits"]["concurrent_sessions"]){
"account_id": "acct_51f0",
"email": "you@example.com",
"plan": "starter",
"key": { "prefix": "t0_live_9f21", "name": "server", "last_used_at": "2026-08-17T09:14:02.117Z" },
"limits": {
"concurrent_sessions": 3,
"credits_per_month": 3600,
"credits_per_day": 600,
"storage_bytes": 26843545600,
"retention_days": 30
},
"provider_key": { "configured": true, "updated_at": "2026-08-02T11:03:19.000Z" }
}Usage
Credits used and remaining, awake session seconds, turns, peak storage and token counts, with a daily breakdown. from and to are inclusive dates; the default window is the current billing month.
curl -s "https://api.t0.sikasio.com/v1/usage?from=2026-08-01&to=2026-08-17" -H "Authorization: Bearer t0k_live_YOUR_KEY"// Node 18+ or any browser. Keep the key on a server you control.
const T0 = "https://api.t0.sikasio.com/v1";
const KEY = process.env.T0_API_KEY; // "t0k_live_YOUR_KEY"
const auth = { Authorization: "Bearer " + KEY };
const url = new URL(T0 + "/usage");
url.searchParams.set("from", "2026-08-01");
url.searchParams.set("to", "2026-08-17");
const usage = await fetch(url, { headers: auth }).then((r) => r.json());
console.log(usage.credits_used, "of", usage.credits_included, "credits used");import os, requests
T0 = "https://api.t0.sikasio.com/v1"
auth = {"Authorization": "Bearer " + os.environ["T0_API_KEY"]}
usage = requests.get(
T0 + "/usage",
headers=auth,
params={"from": "2026-08-01", "to": "2026-08-17"},
).json()
print(usage["credits_used"], "of", usage["credits_included"], "credits used"){
"from": "2026-08-01",
"to": "2026-08-17",
"credits_included": 3600,
"credits_used": 812,
"credits_remaining": 2788,
"session_seconds": 48702,
"turns": 1394,
"storage_bytes_peak": 5033164800,
"tokens": { "in": 8114203, "out": 402118, "cache_read": 7411002, "cache_write": 703201 },
"daily": [
{ "day": "2026-08-16", "session_seconds": 3120, "turns": 88, "credits": 52 },
{ "day": "2026-08-17", "session_seconds": 1980, "turns": 41, "credits": 33 }
]
}Health
Liveness only. Cheap enough to poll from a load balancer.
curl -s https://api.t0.sikasio.com/healthconst health = await fetch("https://api.t0.sikasio.com/health").then((r) => r.json());
console.log(health.ok); // true — no key requiredimport requests
health = requests.get("https://api.t0.sikasio.com/health").json()
print(health["ok"]) # True - no key requiredStatus
Public component health — this is what the status page polls. It lives on its own host so it survives an outage of the stack it reports on.
curl -s https://api.t0.sikasio.com/statusconst status = await fetch("https://api.t0.sikasio.com/status").then((r) => r.json());
for (const component of status.components) console.log(component.name, component.status);import requests
status = requests.get("https://api.t0.sikasio.com/status").json()
for component in status["components"]:
print(component["name"], component["status"]){
"components": [
{ "name": "gateway", "status": "operational", "p95_ms": 24 },
{ "name": "runner", "status": "operational", "pool_depth": 12, "queue_depth": 0 },
{ "name": "storage", "status": "operational" }
],
"error_rate_5m": 0.0009,
"updated_at": "2026-08-17T09:20:00.000Z"
}Parameters at a glance
| Base URL | https://api.t0.sikasio.com/v1 |
|---|---|
| Auth header | Authorization: Bearer t0k_live_YOUR_KEY |
| Models | core (default) · max |
| Session states | ready · working · sleeping · expired · failed |
| Events | session.ready · turn.started · text.delta · tool.started · tool.output · file.changed · turn.completed · session.idle · session.expired · error |
idle_timeout | Seconds; default 600. Sleeping is never metered. |
ttl | Seconds; defaults to the plan retention window (Free 7 d · Starter 30 d · Pro 90 d). |
| Direct upload | Up to 10 MB per request; larger transfers get a presigned URL. |
| Workspace root | /workspace — every path is confined to it. |
| Stream resume | Last-Event-ID: <seq> on the SSE endpoint. |
| Credit | One minute of awake session time. |
Errors
One shape, everywhere — including the error event on a stream. Codes are stable and machine-readable; messages are for humans and should never be parsed. The request_id is what support needs to find your request in the logs.
{
"error": {
"code": "quota_exceeded",
"message": "Daily credit cap reached for plan starter. Upgrade to pro for 2,400 credits per day.",
"request_id": "req_0c41a7d9"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_path | A file path escaped /workspace, or was otherwise unusable. Rejected before it reached the container. |
| 401 | invalid_key | Missing, malformed, unknown or revoked API key. |
| 403 | forbidden | A valid key for the wrong account, or an action your plan does not allow. |
| 404 | not_found | Unknown session, file or route — and what another account’s session looks like. |
| 409 | session_not_ready | The session is busy with a turn, waking, or in a state that cannot accept the request yet. |
| 412 | provider_key_required | The account has no provider key on file, so a session cannot be created. Add one in the dashboard; sessions run on your own key. |
| 413 | payload_too_large | The body is over the direct-upload ceiling, or the workspace would exceed your plan’s size limit. |
| 429 | rate_limited | Per-key request rate exceeded. Retry after the Retry-After header. |
| 429 | quota_exceeded | A plan quota is exhausted — credits, concurrent sessions or storage. The message names the limit and the next plan up. |
| 500 | internal | Our fault. The message is deliberately generic; quote the request_id and we can find it. |
| 503 | unavailable | A dependency is briefly down. Nothing is broken on your side — retry with backoff. |
Rate limits
Each key has a token bucket: short bursts are fine, sustained flooding is not. Exceeding it returns 429 rate_limited with a Retry-After header in seconds. Rate limits and quotas are both enforced at the front of the gateway, before any work is done — a rejected request never claims a container and never spends a credit.
Rate limiting is per key, so issuing a separate key per service keeps a runaway job from starving the rest of your account. Quotas, by contrast, are per account: concurrent sessions, credits, storage and retention are shared across every key you hold.
| Limit | Scope | On exceeding |
|---|---|---|
| Request rate | Per key | 429 rate_limited + Retry-After |
| Concurrent sessions | Per account | 429 quota_exceeded on session create |
| Daily and monthly credits | Per account | 429 quota_exceeded; running turns finish |
| Workspace storage | Per account | 413 payload_too_large on the write that would cross it |
FAQ
What is T0?
T0 sells hosted, sandboxed AI coding-agent sessions over a REST API. You create a session, push files into it, send it messages, and stream the agent working in real time. Each session is an isolated Linux workspace that persists between messages, so files the agent wrote, packages it installed and git state are all still there on the next message.
T0 sells the runtime, not the model: isolation, persistence, orchestration and latency are the product. Base URL: https://api.t0.sikasio.com/v1.
How do I get an API key?
Create an account at https://t0.sikasio.com/signup with an email and a password, verify the code we email you, and your key is shown once — copy it straight away. Send it as an Authorization: Bearer header on every /v1 request.
Keys are the tenant boundary: they identify the account, authorize the request, and are what usage is metered against. Revoke one from the dashboard and it stops working within seconds.
Do I need my own model provider key?
Yes. T0 is bring-your-own-key: you add your model provider key once in the dashboard and T0 runs the agent with it. We do not resell model tokens, so nothing on your bill is a marked-up token — you pay us for compute time and storage only.
Your provider key is encrypted at rest with AES-256-GCM, decrypted only at the moment a container is claimed, injected as an environment variable inside your sandbox, and never logged, never written to disk and never returned by any endpoint.
Which models can I use?
Two tiers, named by T0: core for everyday work and max for the hardest tasks. Set the tier when you create a session; core is the default. Because you bring your own key, the tier selects the class of model your key is used against.
What persists between messages?
Everything inside /workspace, plus the agent process itself. The process stays alive for the life of the session, so conversation context stays resident and follow-up turns do not re-read everything from scratch. That is the single largest latency win in the system.
When a session goes idle the container stops but its volume survives, so resuming is a start rather than a rebuild. Workspaces are also snapshotted to object storage, which is what makes a resume possible days later.
What happens when a session goes idle?
After idle_timeout (10 minutes by default) the session moves to sleeping: the container stops, the workspace is kept, and a session.idle event is emitted. Sleeping costs nothing — metering counts awake seconds only.
The next message wakes it. A resume inside the warm window is a container start; a colder resume restores the workspace from object storage first. Files can be downloaded from a sleeping session without waking it.
How is usage metered?
A credit is one minute of awake session time. Sleeping sessions cost nothing. T0 meters awake session-seconds, peak workspace bytes and turns; token counts are recorded and shown to you for your own visibility but, under bring-your-own-key, are not billed by T0.
Check live usage any time with GET https://api.t0.sikasio.com/v1/usage.
Is there a free plan?
Yes, permanently. Free is $0 for 300 credits a month (5 h of awake session time), 60 a day, 1 concurrent session, 2 GB of workspace storage and a 7-day retention window. No card required.
What are the plans and quotas?
Free — $0: 300 credits/month (5 h), 60/day, 1 concurrent session, 2 GB storage, 7-day retention, 1 GB / 1 CPU sandbox.
Starter — $19/month: 3,600 credits/month (60 h), 600/day, 3 concurrent sessions, 25 GB storage, 30-day retention, 2 GB / 1.5 CPU sandbox.
Pro — $59/month: 18,000 credits/month (300 h), 2,400/day, 10 concurrent sessions, 100 GB storage, 90-day retention, 4 GB / 2 CPU sandbox.
What happens when I hit a quota?
A hard stop with an upgrade prompt, never a surprise bill. Requests past a quota return 429 quota_exceeded naming the limit you hit and the next plan up; requests past the per-key rate limit return 429 rate_limited with a Retry-After header. Sessions already awake are not killed mid-turn.
How do I stream a response?
POST a message to https://api.t0.sikasio.com/v1/sessions/:id/messages — it returns 202 with a turn_id immediately — and read events from GET https://api.t0.sikasio.com/v1/sessions/:id/events, which is Server-Sent Events. Events are ordered and sequence-numbered per session, so if the connection drops you reconnect with a Last-Event-ID header carrying the last sequence you saw and the stream resumes from there with nothing lost or repeated.
If you would rather push and pull over one connection, https://api.t0.sikasio.com/v1/sessions/:id/socket is a WebSocket that carries the same events and accepts messages and interrupts.
How do I get files in and out?
Small files go straight to the API: PUT the raw body to /v1/sessions/:id/files/<path> and it is streamed into the live workspace. Bulk uploads go as a tar.gz to POST /v1/sessions/:id/files. Large transfers are handed a presigned object-storage URL so the bytes never pass through our servers.
Every path is resolved and confined to /workspace before it reaches the container; traversal is rejected with 400 invalid_path.
How isolated is a session?
Each session is its own container: read-only root filesystem, all Linux capabilities dropped, no-new-privileges, a seccomp profile, a pids limit, and hard memory and CPU ceilings set by your plan. It runs as a non-root user and never sees a Docker socket, a host mount or host networking.
Egress is default-deny through an allowlist proxy that refuses every private network range, so a session can reach the model provider and — depending on plan — package registries, and nothing else. The proxy fails closed: if it is down, the sandbox has no network at all.
Where do I see whether T0 is up?
The status page at https://status.t0.sikasio.com shows live component health — gateway, runner, warm-pool depth, queue depth and recent error rate — plus incident notes. It is served from its own host so it stays up when the stack it reports on does not.
What does a first request look like?
curl -X POST https://api.t0.sikasio.com/v1/sessions -H "Authorization: Bearer t0k_live_YOUR_KEY" -H "Content-Type: application/json" -d '{"name":"my-first-session"}' — that returns a session id and a stream URL, and the session is ready in under a second because it comes from a pool of pre-booted containers.
Get a key
The free plan is permanent: 300 credits a month — 5 h of awake session time — 2 GB of workspace storage and a 7-day retention window, with no card required. Bring your own provider key and you are running in a minute.
Questions, or something in these docs that does not match what the API did? support@t0.sikasio.com. Health at https://api.t0.sikasio.com/health, live component status at status.t0.sikasio.com.