HTTP API
Everything your sandbox does, it does over one HTTP API on hardware you own. The browser is only its first client: an extension, a script or a CI job calls the same routes with the same credential.
There is no intentic API in the cloud that reaches your code. The platform stores your identity and your sandbox's address and nothing else; the API on this page is served by your daemon, inside your container, and the only way in is a credential you minted.
This page is long and you probably want one part of it:
| Section | What it covers |
|---|---|
| Where it is | Your sandbox's hostname, the loopback name, and the one open route. |
| How a call is authorized | Sessions for a person, control tokens for a program, and what each scope reaches. |
| The shape of a call | Where input rides, what comes back, and the one convention that covers every route. |
| Drive an agent | Start a turn, follow it, and pick the conversation it lands in. |
| The route surface | Every route group and what it's for: the map of the whole API. |
| Streams | The server-sent event feeds, and the frames each one emits. |
| Routes that aren't JSON | File bytes, archives and images, and how to ask for them. |
| Failures | Every status this API returns and what it actually means. |
| Open on purpose | The routes that take no credential, and why each one is safe. |
| Old daemon, new client | What happens when the two ends are different versions. |
Where it is
Each sandbox answers on its own hostname, https://sandbox-<id>.<zone>, over its private Cloudflare tunnel. The app shows the exact URL on Sandbox → Overview. One route stays open: /health, the "is a daemon there" probe every launch script and readiness loop makes.
curl https://sandbox-a1b2c3d4e5f6.intentic.dev/health
{ "ok": true, "sandboxId": "a1b2c3d4e5f6", "boot": { … }, "announce": { … } }The sandbox id in that answer is how a caller proves it reached this daemon rather than something else on the same port. On the machine running the sandbox there is also https://local-<id>.<zone>, a public DNS name that resolves to 127.0.0.1: a real certificate on a loopback address, so a browser on that machine skips the round trip to Cloudflare and back.
How a call is authorized
Two kinds of caller, two credentials, and the difference is whether a person is present.
A person, in a browser
The sandbox authenticates the end user directly against Google: the browser presents a Google ID token, and the daemon verifies its signature against Google's published keys. The platform never holds or signs this credential, so a platform compromise cannot command your sandbox. The first verified identity to arrive becomes the owner; further people are granted access by the owner, with a role that decides how far they reach.
POST /system/session exchanges that token for a daemon-minted session, which is what every steady-state call presents as authorization: Bearer …. Calling it again with a still-valid session renews it.
A program, anywhere
Anything outside a browser presents a control token in the x-intentic-control header instead. A control token is shown once when minted. The daemon keeps only its hash, and you can revoke it at any time.
curl -X POST "$SANDBOX/system/control/tokens" \
-H "authorization: Bearer $OWNER_SESSION" \
-H "content-type: application/json" \
-d '{"label":"nightly CI","scope":"read"}'
{ "id": "6f1c…", "token": "ict_9wQ…" }The app mints one for you on Sandbox → Computers → Editor bridge, at editor scope, together with the settings snippet an ACP editor needs. Any other scope is minted through the route above, which requires an owner session.
What a scope reaches
| Scope | Reaches |
|---|---|
editor | One conversation: run a turn, answer a card it parked on, read transcripts, search the tree. What an ACP editor bridge holds. It cannot see the fleet or land work. |
read | Observation only: the fleet, past sessions, workspace search, listening ports. The limited scope at the bottom of the ladder. |
drive | Everything read sees, plus making an agent work: start, answer, steer and stop a turn. Stops short of anything that moves code into the main tree. |
land | Everything drive does, plus merging a conversation's worktree into the main tree, and discarding one. Separate because the usual arrangement is a program that works and a person who decides. |
Worth saying plainly: at drive and above, a stolen token is the agent's reach, because driving an agent means editing files and running commands in this sandbox. read has a smaller scope, which is why it exists separately rather than as a politeness. A token presented on a route outside its scope gets a 403 that says so, rather than a confusing 401.
The shape of a call
The API is described by a typed contract that the daemon implements and the browser is built against, served over OpenAPI. That has one consequence worth internalising: the contract's path is the URL, there is no envelope, and the body you send and the body you get back are the declared shapes.
| Convention | Detail |
|---|---|
| Input on GET | The query string. |
| Input on everything else | A JSON body: including DELETE, which sends its parameters in the body rather than the query. |
| Path parameters | Written {repo}, {id} in the contract, URL-encoded in the request. root is the workspace repo itself. |
| Output | JSON, except the byte routes and the streams below. |
SANDBOX=https://sandbox-a1b2c3d4e5f6.intentic.dev
# GET: input rides the query string
curl "$SANDBOX/workspace/file?path=README.md" \
-H "x-intentic-control: $INTENTIC_TOKEN"
# Everything else: input rides the JSON body, including DELETE
curl -X POST "$SANDBOX/git/root/commit" \
-H "x-intentic-control: $INTENTIC_TOKEN" \
-H "content-type: application/json" \
-d '{"message":"fix the flaky test"}'Drive an agent
The reason most scripts are here. A turn executes as a detached daemon-side run: starting it acks with a run id, and any number of clients render it by attaching. So a reload, a second window, another device and your script all watch the same work identically, and nothing is lost if the caller goes away.
# Start a turn. It answers as soon as the run exists: the work is detached.
curl -X POST "$SANDBOX/agent" \
-H "x-intentic-control: $INTENTIC_TOKEN" \
-H "content-type: application/json" \
-d '{"prompt":"Update the changelog for the last five commits.",
"conversationId":"nightly-changelog",
"isolated":true}'
{ "run": "run_8c2f…" }
# Watch it: server-sent events, from the start or from a cursor you kept.
curl -N -X POST "$SANDBOX/agent/attach" \
-H "x-intentic-control: $INTENTIC_TOKEN" \
-H "content-type: application/json" \
-d '{"conversationId":"nightly-changelog","after":0}'conversationId is yours to choose and is what makes a run resumable and addressable later. isolated: true runs the turn in that conversation's own git worktree instead of the shared tree, which is what lets several agents work at once without colliding. The work reaches your main tree only when something calls land.
The route surface
Around 230 routes in the groups below. Rather than list them here, where they would date, ask the daemon: the hello frame on /events advertises every route name this particular daemon implements.
| Group | What it covers |
|---|---|
agent | One conversation: run a turn, attach to it, reply, steer, stop, rewind. |
agents | The fleet: every registered conversation, its cumulative diff, and landing or discarding its work. |
sessions | Past conversations and their transcripts. |
workspace | The /work tree: read and write files, search, health, classification, cloning, dependency setup. |
git | Per-repo git for every repo under /work: status, log, stage, commit, branch, stash, rebase, push. |
history | Daemon-owned snapshots of the workspace, with diff and restore. |
system | Identity, the live event stream, terminals, browsers, subagents, presence, sessions. |
capabilities | The connected integrations, and the streamed apply that provisions one. |
secrets | Env-var secrets: set, remove, list keys, inventory across every store, owner-only reveal. |
settings | Per-sandbox agent settings, token-savings reporting, the built-in system prompts. |
extensions | Installed extensions, their approved manifests, and their settings. |
automations | Scheduled and event-driven wake-ups, plus the owner's approval queue. |
workflows | Multi-session designs: create, edit, delete, run. |
loops | Run a conversation again until its goal is met: start, stop, list. |
chores | What every repo currently measures, and what has been done about it. |
prepush | The pre-push check: start it, poll its state, cancel it. |
ci | Pipelines on the workspace repos' remotes: runs, rerun, cancel, and open an agent on a failure. |
panels | Per-repository dev servers, and the content facts extensions detect on. |
ports | Listening TCP ports, and forwarding one through the preview proxy. |
public | The workspace outbox: publish a file to a public address, or withdraw it. |
memory | The agent's persistent memory notes: read, write, delete. |
drafts | Agent-proposed posts awaiting approval. |
activity | The append-only audit feed of what the agent did. |
usage | The durable spend ledger, grouped by day, provider, account and model. |
logs | Daemon-owned debug logs: terminal captures, CLI run logs, daemon.log. |
push | Web-push notification subscriptions for this sandbox. |
inventory | The deploy config's managed hosts and services. |
intentic | Run the in-sandbox intentic CLI and stream its output. |
vpn | Dial, drop and inspect the links a vpn capability configured. |
komodo | A connected Komodo orchestrator: stacks, deployments, servers, builds, logs. |
claude | Claude subscription OAuth, and the Claude accounts this sandbox holds. |
grok | xAI subscription OAuth and its connected account. |
translator | The routed subscriptions: Codex, Gemini, Grok and Kimi under the Claude Code harness. |
codex, gemini, kimi, endpoints | A model-catalog route each, for the model picker. |
Streams
Long-lived routes emit server-sent events: blank-line separated frames, each carrying one data: line of JSON.
curl -N "$SANDBOX/events" -H "authorization: Bearer $SESSION"
data: {"kind":"hello","workspaceId":"…","routes":["agent.run","git.status", …],"build":"…"}
data: {"kind":"heartbeat"}
data: {"kind":"workspaceChanged","paths":["src/app.ts","README.md"]}
data: {"kind":"refsChanged","repos":["root"]}| Stream | Carries |
|---|---|
GET /events | The sandbox's liveness and everything that changed out of band: a hello identity frame, heartbeats, boot progress, batches of changed file paths, the repo set, which repos' git refs moved, the presence roster and the fleet. |
POST /agent/attach | One turn's frames, replayed from your cursor and then live. |
POST /capabilities, /intentic/apply/events | Progress lines while a capability or a deployment applies. |
GET /system/terminal | A WebSocket carrying a tmux session's bytes. A WebSocket cannot set headers, so it authorizes a short-lived ticket from the query string instead. |
Those change pushes are not a nicety. An intentic workspace is file-first: the agent edits files and moves git refs with its own tools, out of band from every HTTP route. Without them a client is only ever as fresh as the last thing someone clicked.
Routes that aren't JSON
| Route | Why it's different |
|---|---|
GET /workspace/raw | A file's bytes with a content type by extension: how images and PDFs are previewed, where the text route would corrupt them. |
GET /workspace/media | A streamed byte range for audio and video. A media element cannot send a header, so this takes a path-scoped ticket minted by POST /workspace/media-ticket. |
POST /workspace/upload | Raw bytes to ?path=, in parts via ?offset= so a large file stays under the edge's body cap. |
GET /extensions/{id}/bundle | An installed extension's ESM bundle, as bytes. |
Failures
| Status | Means |
|---|---|
400 | The input didn't match the declared shape, or a path escaped the workspace. |
401 | No credential, or one that doesn't verify. Indistinguishable, on purpose. |
403 | A real credential that may not go there: an identity that isn't a member, a member below the route's role floor (the answer names the tier), or a control token outside its scope. |
404 | No such route on this daemon, or no such thing. Also the answer for the daemon's own credential files, which are not readable through the file API. |
413 | Too large: the file routes are bounded so one open log can't stall the daemon. |
Open on purpose
A handful of routes check no bearer, because the caller provably cannot present one. They are /health, the web-chat widget's routes, an automation's webhook fire, a CI provider's webhook, a workflow gate, and the enrollment routes a new computer redeems a one-time pairing token on. Each carries its own narrower check instead.
Old daemon, new client
Your browser is routinely newer than the daemon it talks to: a released app plane serves every sandbox, whatever image each user last pulled. That stays supported, and it is why hello advertises route names. A client compares that list against the contract it was built with, so a route the daemon predates surfaces as a named gap instead of a 404 nobody can attribute. Pin nothing; ask.
Related pages
- Host API: the same daemon from inside the app, typed, for an extension's views.
- Manifest reference: how an extension declares which of these routes it may call.