DOCSThe API
DOCS

The API

The isolance API lets you automate sandboxes, MCP servers, connectors, your users' logins and billing with an API key. It's a JSON API over HTTPS at https://api.caas.sh/v1, the caas CLI (The CLI) maps its commands onto it one to one, and the TypeScript SDK (The TypeScript SDK) offers it as typed methods.

This guide covers authentication, the conventions every route follows, and the routes for each resource. The other guides explain what each resource does.

Authenticate

Create an API key

  1. In the console, open Settings.

  2. In the API keys card, enter a Key name, such as ci, and choose Create key. If you're an owner, you can also check Billing access, which lets the key read the balance and add funds.

  3. Copy the key. It starts with ck_, and it isn't shown again.

Owners and operators can create keys; viewers can't. A key from the console doesn't expire: it works until you choose Revoke beside it. Keys, members and invitations are managed only in the console, never with an API key.

Send the key

Send the key as a bearer token:

shell
export CAAS_API_KEY=<api-key>

curl https://api.caas.sh/v1/sandboxes \
  -H "Authorization: Bearer $CAAS_API_KEY"

A key belongs to one workspace, and every request it sends acts in that workspace: no path names a workspace. A sandbox, server or connector of another workspace, by ID or name, answers 404, as one that doesn't exist does. To work in another workspace, use a key created there.

Scopes

A key's scopes say which routes it may call. A key from the console has every scope but the two billing ones, and Billing access adds those.

ScopeWhat it allows
sandbox:readRead sandboxes, their execs, logs and snapshots; connectors, your users and their usage; the workspace's settings and audit log
sandbox:writeCreate, change, start, stop and delete sandboxes; take, restore and delete snapshots; change the workspace's settings; make connect links, get and see your users' sign-in codes, disconnect and remove your users; change connectors and their workspace accounts (owners only); add, change and delete schedules, and run them now (with sandbox:execute)
sandbox:executeRun and cancel execs; read, write and delete files
mcp:readRead MCP servers, their client tokens, traces and stats
mcp:writeCreate, change and delete MCP servers (with sandbox:write, which a delete that leaves the server's sandbox doesn't need), and mint and revoke client tokens
registry:readPull images and list tags at registry.caas.sh
registry:writePush and delete images at registry.caas.sh, and pull them
billing:readRead the balance, usage and auto-recharge, and see billing records in the audit log
billing:writeAdd funds and change auto-recharge

Roles

A key can do what its scopes allow, within the role its creator has in the workspace now:

RoleScopes the role allows
OwnerAll of them
OperatorAll but billing:read and billing:write
Viewersandbox:read, mcp:read, registry:read and billing:read

So if an operator who created a key becomes a viewer, the key can only read, and if they leave the workspace, the key gets 403. Adding, changing and removing connectors, and connecting or disconnecting their workspace accounts, needs an owner's key.

A revoked key, or a changed role, takes effect within a minute. Requests that grant access check the key at once instead: creating a sandbox without a user, changing a sandbox's labels, minting MCP client tokens, creating an MCP server, every change of a connector or its workspace account, and adding funds or changing auto-recharge.

Requests and responses

  • Send request bodies as JSON, with Content-Type: application/json, except for files. A body larger than its route takes gets 413.

  • Objects are flat: what you set and what isolance reports sit side by side. An unknown field is refused, naming it.

  • A path's <sandbox> is a sandbox's ID or its name, and <server> an MCP server's ID or name.

  • Times are milliseconds since the Unix epoch. Money is in micro-USD: 1,000,000 is $1.

  • A list answers { "<resource>": [...] }, and a paged one its nextCursor beside it: pass it back as ?cursor= for the next page. It's null on the last page.

  • Deleting a sandbox or a snapshot answers { "id": "…", "deleted": true }. An MCP server's answer adds "sandboxDeleted", whether its sandbox went with it; a connector's adds how many users' connections and whether an account went with it; a file answers { "path": "…", "deleted": true }. Revoking a client token answers the token.

Errors

Every error has the same JSON body:

json
{ "error": { "code": "invalid_request", "message": "ports[0]: port must be from 1024 to 65535", "field": "ports" } }

field names the request field a validation error is about; a nested one is dotted, as in sandbox.size. Other fields some errors carry sit inside error: a billing refusal's billingUrl, a 5xx's requestId.

codeStatusMeaning
invalid_request400A field, query or header the API refuses, or a body that isn't JSON
unauthorized401No API key, or one that's unknown or revoked
payment_method_required, out_of_funds, spend_cap_reached402The workspace can't run sandboxes: no card on file, a balance of $0 or below, or this month's spend cap reached
forbidden403The key's scopes or its creator's role don't allow the request
tenant_suspended403The workspace is suspended. Billing still works, so an owner can pay.
not_found404No such route, sandbox, server, exec, file, snapshot or connector
method_not_allowed405The route doesn't take this method; the Allow header lists the ones it does
conflict409The request conflicts with the resource's state, such as restoring a snapshot into a running sandbox, or an idempotency key reused with another body
name_taken409The name belongs to another sandbox or server
not_running409An exec or a file transfer for a sandbox you stopped yourself
too_large413The body is too large
unsupported_media_type415A body whose Content-Type the route doesn't take
rate_limited429A rate limit was reached; see Rate limits
unavailable502, 503A temporary failure. Try again shortly.
timeout504A file transfer or snapshot is still under way. Check again shortly.

Every 402 has a billingUrl: the console page where an owner adds funds or a card. See Billing and usage.

Request IDs

Every response has an x-caas-request-id header that names the request. isolance logs everything the request did under that ID, and the audit log records it with each change. Quote it when you report a problem.

Rate limits

RequestsLimit
To the API, from one client address600 a minute
To the API, with one API key600 a minute
To one MCP endpoint, with a valid client token1,200 a minute
To the MCP gateway without a valid client token, from one client address600 a minute

A request over a limit gets 429 with "code": "rate_limited" and a Retry-After header. Limits are counted at each edge location separately.

Idempotency keys

Requests that create something you mustn't create twice take an Idempotency-Key header of 8 to 128 letters, digits, _ and -. If you get no answer, or a 5xx, send the request again with the same key and body. If the first one went through, you get its result, and nothing is created twice.

RequestA repeat with the same keyWith another body
POST /v1/sandboxes200 with the same sandbox409
POST /v1/sandboxes/<sandbox>/execThat exec; the key is its ID409
POST /v1/mcp-servers200 with the server, without its tokens' secrets409
POST /v1/billing/top-ups (required)The same top-up409

Without the header, each request creates something new.

Sandboxes

See Sandboxes for every field and what it does.

Create a sandbox

shell
curl https://api.caas.sh/v1/sandboxes \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: build-agent-0001" \
  -d '{
    "name": "build-agent",
    "image": "default",
    "size": "l",
    "env": { "MODE": "ci" },
    "ports": [{ "port": 3000, "public": true }],
    "egress": ["api.github.com", "*.npmjs.org"],
    "labels": { "team": "ci" }
  }'

{ "image": "default" } alone is a whole request. The answer is 201 Created with the sandbox, which starts on its own:

json
{
  "id": "7c9e4a2b-1f3d-4b6e-8a5c-3d2f1e0b9a87",
  "name": "build-agent",
  "image": "default",
  "command": null,
  "env": { "MODE": "ci" },
  "workdir": "/workspace",
  "size": "l",
  "volumes": [{ "path": "/workspace" }],
  "ports": [{ "port": 3000, "public": true }],
  "egress": ["api.github.com", "*.npmjs.org"],
  "user": null,
  "idleStopMinutes": 15,
  "labels": { "team": "ci" },
  "connectors": [{ "id": "github", "name": "GitHub", "hosts": ["api.githubcopilot.com", "api.github.com", "github.com"], "account": "workspace" }],
  "provider": "cloudflare",
  "providerId": null,
  "state": "creating",
  "stateMessage": null,
  "urls": [],
  "pendingRestart": [],
  "createdAt": 1791797400000,
  "startedAt": null,
  "stoppedAt": null,
  "usage": { "startedAt": null, "costPerHourMicroUsd": 0, "today": { "microUsd": 0, "egressBytes": 0 }, "lifetimeMicroUsd": 0 },
  "signIn": null
}

state moves through creating, starting and running; read the sandbox until it runs. urls list the provider's own URL of each port it gives one for, while the sandbox runs (see URLs). A failed start leaves it failed, with the reason in stateMessage. signIn is null unless the sandbox is one of your users' and needs a sign-in they make with a code, such as Codex (ChatGPT)'s, which they haven't made: then it's the code they enter, which …/start and …/restart answer too (see Sign users in with a code).

Sandbox routes

RouteScopeWhat it does
POST /v1/sandboxessandbox:writeCreates a sandbox
GET /v1/sandboxessandbox:readLists sandboxes, newest first: { sandboxes, nextCursor }. Filter with ?label=key=value (repeat it) and ?provider=; ?limit= 1 to 100, 50 by default.
GET /v1/sandboxes/<sandbox>sandbox:readReads one
PATCH /v1/sandboxes/<sandbox>sandbox:writeReplaces the fields it names; pendingRestart lists what waits for the next start
DELETE /v1/sandboxes/<sandbox>sandbox:writeDeletes it, with its volumes and snapshots
POST …/start, …/stop, …/restartsandbox:writeStarts, stops, or stops and starts it: 200 once done within about 25 seconds, else 202 while it changes
POST …/execsandbox:executeRuns a command; see Execs
GET …/exec/<exec-id>sandbox:readReads an exec
DELETE …/exec/<exec-id>sandbox:executeCancels a running exec
GET …/logssandbox:readIts log; see Logs
GET, PUT, DELETE …/files/<path>sandbox:executeIts files; see Files
GET …/snapshots, POST …/snapshotssandbox:read, sandbox:writeLists snapshots, or takes one; see Snapshots
DELETE …/snapshots/<snapshot-id>sandbox:writeDeletes a snapshot
POST …/restoresandbox:writeRestores a snapshot into a stopped sandbox

… is /v1/sandboxes/<sandbox>.

Execs

shell
curl https://api.caas.sh/v1/sandboxes/build-agent/exec \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-tests-0001" \
  -d '{ "command": ["npm", "test"], "env": { "CI": "1" }, "workdir": "/workspace/app", "timeoutSeconds": 1800 }'
FieldValue
commandRequired. 1 to 32 arguments of up to 512 characters; no shell is added
stdinText sent to the command, up to 64 KiB
envVariables over the sandbox's
workdirIts directory; the sandbox's by default
timeoutSeconds1 to 7,200; 600 by default. Up to 86,400 when detached
detachtrue answers at once while it runs

The answer is 200 once the command ended (within about five minutes), or 202 while it runs:

json
{ "execId": "run-tests-0001", "state": "succeeded", "detach": false, "createdAt": 1791797400000, "endedAt": 1791797412000,
  "exitCode": 0, "signal": null, "stdout": "…", "stderr": "", "stdoutTruncated": false }

state is submitting, running, succeeded, failed, timed_out, canceled or lost; stdout and stderr are null until it ends, then up to 1 MiB each. A command sent to a sandbox that stopped while idle starts it first.

Logs

GET /v1/sandboxes/<sandbox>/logs answers { "entries": [{ "at", "stream", "message", "kind" }] }: the newest 1,000 entries at or after ?since= (milliseconds or an ISO 8601 time), oldest first. stream is system for what isolance recorded, with its kind, or stdout and stderr for what the sandbox's command wrote. With ?follow=true, the answer is newline-delimited JSON (application/x-ndjson): those entries, then each new one as it comes, for up to 10 minutes; follow on with since.

Files

RouteWhat it does
GET …/files/<path>A file's bytes as application/octet-stream (?tail=N for its last N bytes), or a directory's listing as JSON: { "path", "entries": [{ "name", "type", "size" }] }, up to 1,000 entries
PUT …/files/<path>Writes the body as the file, up to 100 MiB, creating its directories: { "path", "bytes" }. With ?extract=true, the body is a gzipped tar unpacked into the directory <path>.
DELETE …/files/<path>Deletes the file; a directory needs ?recursive=true

<path> is the absolute path with each segment percent-encoded, as in /v1/sandboxes/build-agent/files/workspace/my%20file.txt; …/files/ is /, which can only be read. Paths under /proc, /sys, /dev and /workspace/.caas are refused.

shell
curl -X PUT "https://api.caas.sh/v1/sandboxes/build-agent/files/workspace?extract=true" \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/gzip" \
  --data-binary @project.tar.gz

Snapshots

GET …/snapshots answers { "snapshots": [{ "id", "name", "kind", "createdAt", "size", "current" }], "storedBytes" }, newest first. POST …/snapshots with an optional { "name": "before-upgrade" } takes one of a running sandbox: { "snapshot" }. POST …/restore with { "snapshot": "<id or manual snapshot name>" } restores one into a stopped or failed sandbox and starts it, answering the sandbox and restoredSnapshotId.

MCP servers

RouteScopeWhat it does
POST /v1/mcp-serversmcp:write, sandbox:writeCreates a server, in a new sandbox or one you have
GET /v1/mcp-serversmcp:readLists servers: { mcpServers, nextCursor }
GET /v1/mcp-servers/<server>mcp:readReads one
PATCH /v1/mcp-servers/<server>mcp:write, sandbox:writeRenames it, or changes its port and path
DELETE /v1/mcp-servers/<server>mcp:write; sandbox:write too when it deletes the server's sandboxDeletes it, and the sandbox its create made unless ?keepSandbox=true
GET /v1/mcp-servers/<server>/tokensmcp:readLists its client tokens, never a secret
POST /v1/mcp-servers/<server>/tokensmcp:writeMints a token: { "label", "expiresAt", "allowedTools" }; the answer has its secret, once
DELETE /v1/mcp-servers/<server>/tokens/<token-id>mcp:writeRevokes a token
GET /v1/mcp-servers/<server>/tracesmcp:readIts newest 100 requests
GET /v1/mcp-servers/<server>/statsmcp:readIts gateway's last 24 hours
shell
curl https://api.caas.sh/v1/mcp-servers \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "search-mcp",
    "sandbox": { "image": "registry.caas.sh/<tenant-id>/search-mcp@sha256:…", "egress": ["api.search.example.com"] },
    "transport": { "http": { "port": 8080 } },
    "tokens": [{ "label": "laptop-claude", "allowedTools": ["search.query"] }]
  }'
FieldValue
nameRequired. 1 to 63 lowercase letters, digits and hyphens, unique among the workspace's servers
sandboxA new sandbox, exactly as POST /v1/sandboxes takes it
sandboxIdInstead of sandbox: an existing sandbox, by ID or name. It hosts at most one server.
transport{ "http": { "port": 3000, "path": "/mcp" } }: the port the sandbox's server serves MCP over HTTP on, and its path (/mcp by default)
tokensThe client tokens to mint, up to 20; one Initial client token without it

The answer, 201, is the server: its id, its endpoint url (https://mcp.caa.sh/<id>), the sandboxId and sandboxName it runs in, transport, state, stats, and tokens, each minted token with its secret. See MCP servers for the endpoint, tokens and traces.

Connectors

RouteScopeWhat it does
GET /v1/connector-catalogsandbox:readLists the catalog's tools, each with available, reason and added
POST /v1/connectors/discoversandbox:write, ownerSays how an MCP server's users would connect: { "mcpUrl" }. Nothing is kept.
POST /v1/connectorssandbox:write, ownerAdds a connector: { "catalog" }, { "mcpUrl", "auth"?, "app"? } or { "api": { "hosts" }, "app"? }, with id, name, selector, envVar and, for GitHub, githubApp optional
GET /v1/connectors, GET /v1/connectors/<id>sandbox:readLists connectors, or reads one, with its workspace account
PATCH /v1/connectors/<id>sandbox:write, ownerChanges its name, selector, envVar, app or githubApp
DELETE /v1/connectors/<id>sandbox:write, ownerRemoves it with its workspace account and its users' connections
POST /v1/connectors/<id>/accountsandbox:write, ownerConnects its workspace account: { "method": "key", "key" }, or sign_in or install, whose next step is in action, or device, whose code is in signIn
POST /v1/connectors/<id>/account/callbacksandbox:write, ownerFinishes a sign-in that came back to the console: { "session", "confirm" }
DELETE /v1/connectors/<id>/accountsandbox:write, ownerDisconnects its workspace account
GET /v1/connectors/<id>/connectionssandbox:readIts users' connections: { connections, next }
GET /v1/userssandbox:readYour users, with their connections' statuses, their logins and this month's usage: { users, next }
GET /v1/users/<user>, DELETE /v1/users/<user>sandbox:read, sandbox:writeReads a user with their connections, or removes them with their connections, logins and usage
GET /v1/users/<user>/usagesandbox:readTheir sandbox time, browser time and requests per UTC day: ?from= and ?to=, YYYY-MM-DD
POST /v1/users/<user>/connectionssandbox:writeA connect link: { "connector", "returnUrl"? }, or the user's key: { "connector", "key" }
POST /v1/users/<user>/connections/<connector>/sign-insandbox:writeThe user's code for Codex (ChatGPT): { connection, signIn }, the code under way while it still works, else a new one; for a user who's signed in, a sign-in again
GET /v1/users/<user>/connections/<connector>, DELETE …sandbox:read, sandbox:writeReads one connection, with its code's signIn, or disconnects the user
GET, POST /v1/users/<user>/logins, GET, PATCH, DELETE …/logins/<login>sandbox:read, sandbox:writeThe user's logins: list ({ logins, nextCursor }), add, read, change and remove one
…/logins/<login>/passkeys/<passkey>, …/session, …/live, …/live/done, …/checksandbox:read, sandbox:writeRename or remove a passkey; read, upload or remove a saved sign-in; open, finish or end a live view; check whether the site is still signed in
POST /v1/users/<user>/logins/<login>/link, POST /v1/users/<user>/logins/linksandbox:writeA link for the user to sign in to a site: { login, connectUrl }
/v1/connectors/<id>/account/logins…sandbox:read; changes an ownerYour workspace's logins, with the same routes as a user's but the link

<user> is your ID for the user, URL-encoded: user_123, or auth0%7Cabc for auth0|abc. The two lists take ?limit= (1 to 100, 50 by default) and ?after=, from the answer's next.

shell
curl https://api.caas.sh/v1/users/user_123/connections \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "connector": "linear", "returnUrl": "https://app.acme.com/settings" }'

The answer's connectUrl is the link to send the user. A sandbox created with "user": "user_123" then uses their accounts for the connectors that reach it, and their logins in its browsers (see Logins). Codex (ChatGPT)'s users sign in with a code instead, which a Codex sandbox's start answers when it's needed; see Sign users in with a code.

shell
curl https://api.caas.sh/v1/connectors \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "catalog": "openai", "selector": { "team": "ci" } }'

curl https://api.caas.sh/v1/connectors/openai/account \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "method": "key", "key": "sk-..." }'

A sandbox reaches a connector when it has every label of the connector's selector ({}, every sandbox). Sandboxes without a user then use the workspace's account; a sandbox's connectors lists what it got. See Connectors.

Billing

Billing routes need a key with Billing access, which only an owner can create. They work while the workspace is suspended.

RouteScopeWhat it does
GET /v1/billing/balancebilling:read{ balanceMicroUsd, autoRecharge, card, topUps, entries, nextCursor }: the balance, auto-recharge, the card on file, recent top-ups with their receipts, and the balance's history, 60 entries a page
GET /v1/billing/usagebilling:readA month's usage report: ?month=2026-10, the current UTC month by default. Add &format=csv for the month as a CSV file.
GET /v1/billing/auto-rechargebilling:read{ enabled, minimumMicroUsd, targetMicroUsd, active, failure }
PUT /v1/billing/auto-rechargebilling:writeChanges any of enabled, minimumMicroUsd ($5 to $1,000) and targetMicroUsd (at least $5 more, up to $2,000)
POST /v1/billing/top-upsbilling:writeAdds funds, { "amountMicroUsd": 25000000 } ($5 to $2,000 of credits, in whole cents), with a required idempotency key. The card is charged the credits; the top-up's feeMicroUsd is 0 (only a top-up made from October 20, 2026 until the fee's removal can carry a fee)
shell
curl "https://api.caas.sh/v1/billing/usage?month=2026-10" \
  -H "Authorization: Bearer $CAAS_API_KEY"

The usage report has a lines entry for each meter, with its quantity, unit and amountMicroUsd, the month's totalMicroUsd, a total for each UTC day in daily, and, while the month is under way, a projection for the whole month. The CSV has one row per UTC day and meter: day,meter,quantity,unit,rate_usd,amount_usd.

Adding funds charges the card on file and answers { "topUp": { … }, "url": null }; the balance goes up once the payment is confirmed. Without a card, or with "checkout": true, url is a Stripe Checkout page to finish the payment in. A declined card gets 402 with the failed topUp. See Billing and usage, and pricing for the rates.

Settings and the audit log

RouteScopeWhat it does
GET /v1/settingssandbox:read{ "timeZone": "UTC" }
PATCH /v1/settingssandbox:writeSets the time zone, as an IANA name: { "timeZone": "Europe/Berlin" }
GET /v1/audit-logsandbox:readThe audit log, newest first: { records, nextCursor, retentionDays }

GET /v1/audit-log takes these filters, each at most once:

ParameterValue
actionAn action, such as sandbox.stop, or a group, such as sandbox, command or mcp_server
actorA user's or API key's ID, a sandbox's ID (what its AI did), one of your users' IDs (what they did through a link), operator or system
targetAn ID, such as a sandbox's
outcomesuccess or failure
since, untilTimes in milliseconds since the Unix epoch
limit1 to 100 records; 50 by default
cursorThe previous page's nextCursor

Each record has its time (at), actor, action, target, outcome, HTTP status, requestId and a short detail, never a secret, a token or a command's text. Records are kept for 365 days. An API key never sees records about API keys, members or invitations, sees billing records only with billing:read, and sees a user who made a change by ID, without their email.