HTTP API
isb serve answers on two kinds of listener, and every route on them leads
to the same tools through the same authorizer. Use this
page when you are writing a client: an agent's MCP configuration, a script
calling REST, something following the event stream.
- The unix socket (
$ISB_SERVE_SOCKET, else$XDG_RUNTIME_DIR/isb/serve.sock; 0600 in a 0700 directory) for the localisbCLI. Its callers are the daemon's own user, trusted with everything. - HTTP listeners (
--listen): loopback addresses, reached through a tunnel or reverse proxy, and tailnet addresses (Reach isb serve remotely). Each org with a workspace also gets an org-bound listener on its own bridge (below).
Routes#
| Path | What |
|---|---|
POST /mcp |
MCP (Streamable HTTP); every tool takes an org argument |
POST /orgs/<org>/mcp |
MCP bound to one org: org is filled in, and any other value is refused; a superadmin or platform admin acts as an admin of that org only |
POST /api/v1/tools/<tool>, POST /orgs/<org>/api/v1/tools/<tool> |
REST: one tool call, the arguments as a JSON body |
GET /api/v1/tools |
the tools this listener offers, with their schemas and annotations |
GET /api/v1/openapi.json |
an OpenAPI 3.1 document of every route on this page (below), without authentication |
GET /api/v1/events |
server-sent events: deploys, rollouts, health and restarts in the caller's orgs |
GET /api/v1/audit/stream |
server-sent events: new audit entries the caller may read |
GET /api/v1/history/stream |
server-sent events: new history items the caller may read |
GET /orgs/<org>/api/v1/terminal?app=NAME (or ?instance=NAME) |
a websocket to a shell (below) |
GET /orgs/<org>/api/v1/ssh?instance=NAME |
a websocket carrying SSH, for isb ssh-proxy (below) |
/orgs/<org>/api/v1/workspace[/ACTION] |
the org's workspace as a REST resource (below) |
POST /api/v1/webhooks/<org>/<app> |
an app's push and pull request webhooks (Deploy apps) |
GET /api/v1/templates/<catalog>/<id>/logo |
a template's logo, from isb's cached copy (Templates) |
/api/v1/auth/* |
identity: sign-in, sessions, invitations, tokens, keys (Identity API) |
GET /healthz |
{"ok": true, "isb": VERSION, "stacks": [{name, converged}]}, without authentication |
any request for <port>-<workspace>-<org>.<preview domain> |
a published workspace port's preview, proxied to the workspace; taken by Host ahead of everything above (Previews through isb) |
every other GET |
the web UI, embedded in the binary; it never answers an API path |
Signing in#
Every HTTP caller is an isb user, or a superadmin:
- API tokens:
Authorization: Bearer isb_tok_...(fromisb token createor the web UI). How agents and scripts sign in. A workspace's token (isb_ws_...) is a bearer token too. - Sessions: the
isb_sessioncookie from the sign-in endpoints. How the web UI signs in. Cookie-authenticated writes must carryX-Isb-Csrf: 1. - Superadmin sources: a superadmin token (
isb_sa_...), a tailnet identity on a tailnet listener, or a verified Cloudflare Access identity on the allow lists. Tailnet and Access identities ride along like cookies, so their writes needX-Isb-Csrf: 1, their/mcpcallsContent-Type: application/json, anOrigin, when sent, must name the request'sHost, and theHostmust be one of this server's names. - Cloudflare Access, when configured (
CF_ACCESS_TEAM_DOMAIN,CF_ACCESS_AUD), guards the loopback listeners: every request needs a validCf-Access-Jwt-Assertion, and isb's own sign-in applies behind it. An Access identity whose email is an isb user's acts as that user. The webhook path and/healthzare served ahead of Access. - Org-bound paths (
/orgs/<org>/...): a superadmin or platform admin is scoped down to an admin of that org. Host, superadmin and platform tools are not listed and are refused there, the remote-spec policy applies, and the audit row recordsscope: org <org>beside the real identity. The unbound paths and the unix socket keep their full reach (Access). - No credential: refused (anonymous calls work only with
--allow-unauthenticated, for local testing). A request with a credential that does not check out is a 401, even when a valid session cookie rides along.
What each caller may then do is in Who may call what and Users, roles and superadmins.
MCP#
The daemon speaks MCP's Streamable HTTP transport with plain JSON responses, statelessly:
POSTonly:GET /mcp(no SSE stream) and other methods are 405.- No session id;
tools/callworks without a priorinitialize. - Methods:
initialize(protocol versions2025-11-25,2025-06-18,2025-03-26; an unknown one is answered with the newest),ping,tools/list(never paginated),tools/call. Notifications are accepted and answered with 202. JSON-RPC batches (arrays) work. - A refused or failed tool call is a tool result with
isError: trueandstructuredContent: {code, message, data}.
curl -s https://isb.example.com/orgs/acme/mcp \
-H "Authorization: Bearer $ISB_TOKEN" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
REST tools#
POST /api/v1/tools/<tool> (or /orgs/<org>/api/v1/tools/<tool>) with the
arguments as a JSON object body (an empty body is {}):
curl -s https://isb.example.com/orgs/acme/api/v1/tools/app_deploy \
-H "Authorization: Bearer $ISB_TOKEN" -H 'Content-Type: application/json' \
-d '{"name": "web", "wait": true}'
Success is 200 {"result": ...}. Failure is {"error": CODE, "message": TEXT, "data": ...} with a status from the code:
| Status | Codes |
|---|---|
| 400 | invalid, parse, interpolation, bad_request; a body that is not a JSON object |
| 401 | unauthorized: a credential that does not check out |
| 403 | forbidden: not allowed, or a CSRF/Origin check failed |
| 404 | not_found, and an unknown or hidden tool |
| 409 | already_exists |
| 504 | request_timeout, operation_timeout, exec_timeout, not_ready |
| 500 | everything else |
The OpenAPI document#
GET /api/v1/openapi.json is an OpenAPI 3.1 document of the whole HTTP
surface, generated from what serves it rather than written by hand:
- one
POST /api/v1/tools/<tool>per tool the listener offers, from the tool registry (so--allow-tools/--deny-toolsshape it), taggedtools; - the workspace resource, from the table its
router dispatches by, each body the tool's schema (
workspace); - the identity endpoints, from the identity
router's route table, with request and answer schemas (
identity); - MCP, the event, audit and history streams (
text/event-stream), the terminal and SSH websockets (101), webhooks, template logos and/healthz(surface).
Each operation says where agents get the same capability: x-isb-tool
names the tool, and x-isb-browser-only says why there is none. The web UI's typed client is generated from the
snapshot in web/openapi.json, and the tool calls and identity calls both
go through it.
Events#
GET /api/v1/events is a stream of server-sent events: deploys, rollouts,
health changes, restarts, failures, backups, jobs, certificates, and each
deployment log line (level log), in the caller's orgs only. Each event has
id: <seq>, event: <level> and the event as JSON data. It resumes from
Last-Event-ID or ?since=SEQ, and sends a keepalive comment when quiet.
The events tool is the same feed for clients that poll.
GET /api/v1/audit/stream[?org=ORG][&after=ID] streams new audit entries
(event: audit, the entry's id as id) from after, Last-Event-ID or
now, and GET /api/v1/history/stream[?org=ORG] new history items
(event: history; the event id is the cursor to resume from). Both need a
session or a token, show only what the caller may read, and send keepalives
every 15 s. See The audit log and
The history.
The web terminal#
GET /orgs/<org>/api/v1/terminal?app=NAME[&slot=N][&cols=C&rows=R]
upgrades to a websocket bridged to a login shell (bash, else sh) in one of
the app's running replicas (slot, or one in rotation), with a
pseudo-terminal. With ?instance=NAME instead of app, the shell is in
that instance of the org (a workspace, as its user; a sandbox, as root).
&session=NAME (percent-encoded, up to 40 characters) on the workspace
attaches to that herdr session instead, made if new, which outlives the
socket (The web terminal).
The web UI's Terminal tabs use it.
- Who: the caller signs in as for any tool and is admitted as if calling
sandbox_execin the org: its members, admins and owners (not viewers, nor tokens whose scopes leave outsandbox_exec), platform admins, and nobody when--deny-toolscoverssandbox_exec. Only the org's own apps and instances are reachable. - Cross-site: a session cookie rides along on a websocket from any
site, so a cookie-authenticated upgrade must carry an
Originnaming the request'sHost. A bearer token needs noOrigin(a browser never adds one by itself). - Wire: binary frames are terminal bytes both ways. Text frames are
JSON:
{"type": "resize", "cols", "rows"}from the client;{"type": "exit", "code"}and{"type": "error", "message"}from the server before it closes. What fails after the upgrade (no such app, a replica that is not running, an image without a shell) arrives as an error frame. - Bounds: 16 terminals at once per daemon, closed after 30 minutes without a byte either way and after 8 hours in all, messages up to 64 KiB. Closing the websocket kills the shell. Each opening is an event on the app's service, with the caller, and the audit log records the opening (or its refusal) and the closing with how long it ran, never the keystrokes.
The SSH websocket#
GET /orgs/<org>/api/v1/ssh?instance=NAME[&as=EMAIL] upgrades to a
websocket whose binary frames are an SSH connection's bytes; the daemon runs
sshd -i in the instance through incus exec and bridges the two. It is
admitted exactly like the web terminal. as names whose isb SSH keys to let
in, for the unix socket and superadmin tokens, which have no account of
their own; anyone else may only name themselves. 64 sessions per daemon,
closed after 2 hours idle and 24 hours in all. isb ssh-proxy is its
client; see SSH and herdr.
The workspace resource#
The org's workspace as REST, over the workspace_* tools
(Workspaces):
Method and path (/orgs/<org>/api/v1/...) |
Tool |
|---|---|
GET workspace[?name=] |
workspace_get |
POST workspace |
workspace_create |
PATCH workspace |
workspace_update |
DELETE workspace (body {"confirm": true}) |
workspace_delete |
POST workspace/start, /stop, /restart, /rebuild, /token/rotate |
the action |
GET, PATCH workspace/settings |
workspace_settings |
Bodies and answers are as for the REST tools.
The org-bridge listener#
For each org with a workspace, the daemon serves the org-bound surface on
the org bridge's gateway address, http://<gateway>:8481
(--workspace-mcp-port). That is $ISB_URL inside the workspace.
- It answers only peers in the org's own subnet (others get 403).
- It serves only
/orgs/<org>/...(MCP, the REST tools, the workspace resource, the terminal and SSH websockets) and/healthz; every call is pinned to the org. - It takes bearer tokens only (the workspace's, or an org API token): no session cookies, no Access assertions, no superadmin tokens.
Agents' listener (servers)#
A server's agent (isb serve --agent) has no public API: its mTLS listener
admits only the control plane's client certificate, and each request carries
the control plane's assertion of the caller (Authorization: IsbAssert <base64url JSON>). It serves the org-scoped routes above for the orgs
placed on it, plus internal routes the control plane uses
(/internal/v1/heartbeat, /internal/v1/orgs, /internal/v1/cert). See
Servers and dedicated VMs.
Audit#
Every call that changes something, every refusal, every secret read,
sign-in, webhook delivery and terminal or SSH session is recorded in the
audit log, with the surface it came through
(mcp, rest, web, cli, webhook). Every call is also logged to the
daemon's stderr with the caller, the tool, its duration and whether it
failed, never its arguments.