DOCSThe API
DOCS

The API

The caas.sh API lets you automate sandboxes, MCP servers, workflow runs, credentials and billing with an API key. It's a JSON API over HTTPS at https://api.caas.sh, and the caas CLI (The CLI) is built on it.

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.

The Workspace card on the same page shows the workspace's Tenant ID, which every API path includes.

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_TENANT=<tenant-id>
export CAAS_API_KEY=<api-key>

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

Every route in this guide starts with /v1/tenants/<tenant-id>, written … from here on: GET …/sandboxes is GET https://api.caas.sh/v1/tenants/<tenant-id>/sandboxes. A key works only in the workspace it was created in. With another workspace's ID, it gets 401.

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 and MCP servers, their commands, snapshots, logs, events and outbound requests; workflow runs; credentials, egress rules and egress policies; the workspace's settings and audit log
sandbox:writeCreate, start, stop and delete sandboxes and MCP servers; upload source archives; take, restore and delete snapshots; set the nightly snapshot, the sandbox URL and the egress policy; change the workspace's settings; change credentials and egress rules (owners only)
sandbox:executeRun and cancel commands; read, write and delete files; create sandbox URL access tokens; pause and cancel workflow runs. Starting or resuming a run needs sandbox:write as well
mcp:readList an MCP server's client tokens
mcp:writeCreate 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. Changing credentials, OAuth and GitHub apps, and egress rules needs an owner's key.

A revoked key, or a changed role, takes effect within a minute. Some requests check the key at once instead: creating MCP client tokens and sandbox URL tokens, turning on or changing a sandbox URL, changing credentials, apps, egress rules and egress policies, starting and resuming workflow runs, and adding funds or changing auto-recharge.

Requests and responses

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

  • Sandbox, snapshot, run, client token and egress rule IDs are UUIDs.

  • Times are milliseconds since the Unix epoch.

  • Money is in micro-USD: 1,000,000 is $1.

  • A list that comes in pages has a cursor: nextCursor, or cursor in the audit log. Pass it back as ?cursor= for the next page. It's null on the last page.

Errors

An error has a JSON body with a readable error message. Some errors add a code you can branch on, and more fields:

json
{
  "error": "The workspace balance is empty; add funds in the console before running sandboxes",
  "code": "out_of_funds",
  "billingUrl": "https://console.caas.sh/usage"
}
StatusMeaning
400The request is invalid: a malformed ID, an unknown field, or a value outside its limits. error names the problem.
401No API key, or one that's unknown, revoked, expired or from another workspace
402The workspace can't run sandboxes; see code
403The key's scopes or its creator's role don't allow the request, or the workspace is suspended
404No such route, sandbox, command, snapshot or other resource
405The route doesn't take this method
409The request conflicts with the resource's state, such as restoring a snapshot into a running sandbox, or reusing an idempotency key with a different body
413The body is too large
415A source upload that isn't application/gzip
429A rate limit, the workspace's quota, or a count limit was reached
503A temporary failure. Try again shortly.
504The work is still in progress, such as a snapshot still being taken. Check again shortly.
codeStatusMeaning
payment_method_required402No card is on file.
out_of_funds402The balance is $0 or below.
spend_cap_reached402This month's spend cap is reached.
tenant_suspended403The workspace is suspended. Suspension also revokes the workspace's API keys.
quota_exceeded429The sandbox or workflow run would pass the workspace's vCPU or memory quota. The body has quota, used and requested.
rate_limited429A rate limit was reached; see Rate limits.

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. caas.sh logs everything the request did under that ID, and the audit log records it with each change. Quote it when you report a problem. When something fails on caas.sh's side, the 503 body has it too, as requestId.

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
To sandbox URLs, from one client address1,200 a minute
To one sandbox URL6,000 a minute

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

GET …/sandboxes returns the sandboxes with their status in one request, so poll it instead of reading each sandbox.

Idempotency keys

Requests that create something you mustn't create twice need an Idempotency-Key header of 8 to 128 characters. 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.

RequestKeyA repeat with a different body
POST …/sandboxesAny 8 to 128 characters, such as a UUID409
POST …/sandboxes/<sandbox-id>/commands8 to 128 letters, digits, - and _. It becomes the command's ID.409
POST …/workflow-runs8 to 128 letters, digits, - and _409
POST …/billing/top-upsAny 8 to 128 characters409
  • A repeated sandbox create answers 200 instead of 201. For an MCP server, it doesn't return the client tokens' secrets again; it has "clientTokenNotReturned": true instead.

  • A key whose sandbox you've since deleted gets 409.

  • Command IDs belong to their sandbox. A command's key can be used again once the command's record is gone.

shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "kind": "agent", "source": { "type": "stdio", "command": ["node", "--version"] } }'

To retry this safely, save the key in a variable and send the same one each time.

Sandboxes

Create a sandbox

shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: build-agent-0001" \
  -d '{
    "kind": "agent",
    "name": "build-agent",
    "size": "l",
    "source": { "type": "stdio", "command": ["node", "--version"] },
    "egressHosts": ["api.github.com"]
  }'

The answer is 201 Created with the new sandbox. It starts at once, and runs its command once it's running:

json
{
  "id": "7c9e4a2b-1f3d-4b6e-8a5c-3d2f1e0b9a87",
  "tenantId": "0b6f2c1e-4d5a-4e8b-9c3f-2a7d1e6b8f40",
  "kind": "agent",
  "name": "build-agent",
  "status": "starting",
  "endpointId": null,
  "deployment": {
    "source": { "type": "stdio", "command": ["node", "--version"] },
    "size": "l",
    "egressHosts": ["api.github.com"],
    "idleStopMinutes": 15,
    "wakeOnRequest": false,
    "persistent": true
  },
  "resources": { "vcpu": 1, "memoryMib": 6144, "diskGb": 12 },
  "url": null,
  "lastSnapshot": null,
  "snapshotSchedule": { "time": "02:00", "timeZone": "UTC", "nextAt": 1791856800000 },
  "egress": {
    "policy": [{ "host": "api.github.com", "action": "allow" }, { "host": "*", "action": "deny" }],
    "credentials": []
  },
  "stopReason": null,
  "agentExit": null,
  "createdAt": 1791797400000,
  "updatedAt": 1791797400000
}

The body takes these fields:

FieldValue
kind"agent" for a sandbox, or "mcp" for an MCP server (see MCP servers). Required.
sourceWhat it runs. Required. See below.
size"xs", "s", "m", "l", "xl" or "2xl", or a custom size such as { "vcpu": 2, "memoryGib": 8, "diskGb": 16 }. Defaults to "m". See Sizes.
nameUp to 63 lowercase letters, digits and hyphens, unique in the workspace. Optional.
egressHostsUp to 32 exact host names the sandbox may reach, such as api.github.com.
persistentfalse to start with a fresh workspace every time. Defaults to true.
idleStopMinutes5 to 240. Defaults to 15.
wakeOnRequesttrue to let requests start a sandbox you stopped. Defaults to false.
expose{ "port": 8080, "public": false } turns on the sandbox's URL.
snapshotTimeThe nightly snapshot's time as "HH:MM" in the workspace's time zone, or null for none. Defaults to "02:00" when the sandbox keeps state.

source is one of:

SourceWhat it runs
{ "type": "stdio", "command": ["node", "task.js"] }A command alone: 1 to 32 arguments, run without a shell. Add "uploadId" to unpack an uploaded archive first.
{ "type": "git", "url": "https://github.com/your-org/agent", "ref": "main", "command": ["node", "task.js"] }A public HTTPS Git repository, cloned into /workspace. ref is optional.
{ "type": "image", "reference": "registry.caas.sh/<tenant-id>/agent@sha256:…", "command": ["node", "agent.js"] }Your own image, by digest, with size "m" or larger. An agent sandbox needs a command.

Choose what it runs has the details of each source, and Images and the registry shows how to push an image.

Upload a source archive

Upload a gzipped tar of up to 250 MiB, then pass the uploadId it returns in a stdio source. An upload is kept for seven days.

shell
tar -czf source.tar.gz -C my-project .

curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/uploads \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/gzip" \
  --data-binary @source.tar.gz

The answer is 201 with { "uploadId": "…", "size": 18342 }. See An uploaded archive.

Sandbox routes

RouteScopeWhat it does
GET …/sandboxessandbox:readLists the workspace's sandboxes and MCP servers, newest first, up to 500: { "resources": [...] }. Each has its sandbox_id, kind, name and created_at, and, once created, its status, as the next route returns it.
GET …/sandboxes/<sandbox-id>sandbox:readThe sandbox, as in the create response, with its usage. An MCP server also has its endpointUrl, and its gateway figures for the last 24 hours.
GET …/sandboxes/<sandbox-id>/detailsandbox:readEverything the console's sandbox page shows, in one request: status, snapshots, events, logs, the newest outbound requests (egress), the first page of logArchives, and, with mcp:read, an MCP server's clientTokens
POST …/sandboxes/<sandbox-id>/startsandbox:writeStarts a stopped sandbox
POST …/sandboxes/<sandbox-id>/stopsandbox:writeStops a running sandbox
DELETE …/sandboxes/<sandbox-id>sandbox:writeDeletes the sandbox with its workspace, snapshots, logs and URL: { "id": "…", "status": "deleted" }
GET …/sandboxes/<sandbox-id>/eventssandbox:readIts newest events, up to 100: { "events": [...] }
GET …/sandboxes/<sandbox-id>/logssandbox:readIts recent runtime log: { "logs": [...] }
GET …/sandboxes/<sandbox-id>/log-archivessandbox:readIts runtime log archives, 100 a page: { "archives": [...], "nextCursor": … }
GET …/sandboxes/<sandbox-id>/log-archives/<archive-id>sandbox:readOne archive, as JSONL
GET …/sandboxes/<sandbox-id>/egresssandbox:readThe outbound requests it tried to make, newest first, and whether its policy allowed them: { "attempts": [...], "nextCursor": … }
PUT …/sandboxes/<sandbox-id>/urlsandbox:writeTurns on or changes its URL: { "port": 8080, "public": false }
DELETE …/sandboxes/<sandbox-id>/urlsandbox:writeTurns off its URL, ending every access token and session
POST …/sandboxes/<sandbox-id>/url-tokenssandbox:executeCreates an access token for a protected URL: { "ttlSeconds": 600 } (60 to 86,400; 3,600 by default) returns { token, expiresAt, url, sessionUrl }

A sandbox's status is starting, running, stopping, stopped, error, deleting or deleted. Start and stop wait up to 25 seconds for the sandbox to finish starting or stopping. Start then answers with the sandbox, and stop with its id and status. If it's still under way when the wait ends, the answer is 202 with the sandbox: read it until its status changes. Start also takes a sandbox in error. A start the workspace's balance doesn't allow gets 402.

Sandbox URLs explains ports, access tokens and sessions.

Commands

Send a command with an idempotency key, which becomes the command's ID:

shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/commands \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: say-hello-0001" \
  -d '{ "command": ["echo", "hello"], "timeoutMs": 60000 }'

The answer is 202 Accepted with the command's record. Then read it, waiting up to 25 seconds for it to end:

shell
curl "https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/commands/say-hello-0001?waitMs=25000" \
  -H "Authorization: Bearer $CAAS_API_KEY"
json
{
  "id": "say-hello-0001",
  "state": "succeeded",
  "exitCode": 0,
  "signal": null,
  "stdout": { "bytes": 6, "base64": "aGVsbG8K", "truncated": false },
  "stderr": { "bytes": 0, "base64": "", "truncated": false },
  "createdAt": 1791797462000
}
RouteScopeWhat it does
POST …/sandboxes/<sandbox-id>/commandssandbox:executeRuns { "command": [...] }, 1 to 32 arguments without a shell, or { "script": "…" }, a shell script of up to 131,071 bytes run by /bin/sh -c. timeoutMs is 1,000 to 7,200,000, and 600,000 by default.
GET …/sandboxes/<sandbox-id>/commands/<command-id>sandbox:readThe command's record. ?waitMs= (up to 25,000) holds the answer until the command ends or the wait passes.
GET …/sandboxes/<sandbox-id>/commands/<command-id>/stdoutsandbox:readThe whole of stdout, once the command has ended. Use /stderr for stderr.
DELETE …/sandboxes/<sandbox-id>/commands/<command-id>sandbox:executeCancels a running command, and returns its record

A command's state is submitting, running, succeeded, failed, timed_out, canceled or lost. When a stream is short, its base64 holds all of it; otherwise base64 is null, and you download the stream from /stdout or /stderr. Downloaded output bills as egress.

A command sent to a sandbox that stopped while idle starts it first. An agent sandbox's first command has the ID initial-agent-command. Records are kept for seven days after a command ends, at most 1,000 per sandbox. See Run commands.

Files

The file routes read, write and delete one file at a time under /workspace or /tmp, other than /workspace/.caas, which belongs to caas.sh. They need sandbox:execute, reads included, because they reach any file a command could.

RouteWhat it does
PUT …/sandboxes/<sandbox-id>/files?path=<path>Writes the body, up to 100 MiB, as the whole file, creating its directories: { "bytes": … }
GET …/sandboxes/<sandbox-id>/files?path=<path>Returns the file's bytes. Add &tail=4096 for only its last 4,096 bytes.
DELETE …/sandboxes/<sandbox-id>/files?path=<path>Deletes the file: { "deleted": true }, or false when there was none
shell
curl -X PUT "https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/files?path=/workspace/config.json" \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  --data-binary @config.json

A transfer starts a sandbox that stopped while idle. While the sandbox is still starting, you get 503; while a transfer is still running, 504. Try again shortly in both cases. The bytes you read bill as egress. See Read and write files.

Snapshots

RouteScopeWhat it does
GET …/sandboxes/<sandbox-id>/snapshotssandbox:read{ snapshots, storedBytes, schedule }, newest first. Each snapshot has its id, name, kind (manual, scheduled or automatic), scheduleTime, createdAt, size, and whether it's the current recovery point.
POST …/sandboxes/<sandbox-id>/snapshotsandbox:writeTakes a snapshot of a running sandbox, named with an optional { "name": "before-upgrade" }: { "snapshot": { … } }
DELETE …/sandboxes/<sandbox-id>/snapshots/<snapshot-id>sandbox:writeDeletes a snapshot: { "id": "…", "deleted": true }
POST …/sandboxes/<sandbox-id>/restoresandbox:writeStarts a stopped or failed sandbox from { "snapshotId": "…" }, and answers with the sandbox and its restoredSnapshotId
PUT …/sandboxes/<sandbox-id>/snapshot-schedulesandbox:writeMoves the nightly snapshot, { "time": "03:30", "timeZone": "Europe/Berlin" }, or turns it off, { "time": null }: { schedule }
shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/snapshot \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "before-upgrade" }'

Taking a snapshot of a large workspace can outlast the request. You then get 504, and the snapshot is listed once it's done. A snapshot of a sandbox that isn't running gets 409, and so does deleting the current recovery point of a sandbox that keeps state. See Snapshots.

MCP servers

Deploy a server

An MCP server is a sandbox with "kind": "mcp", created with POST …/sandboxes. Its command starts the server, which speaks MCP on stdio; a server on your own image can serve HTTP instead, with "transport": "http" and its "port" in the source. clientTokens declares up to 20 client tokens, each with a label, and optionally allowedTools and expiresAt:

shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: search-mcp-0001" \
  -d '{
    "kind": "mcp",
    "name": "search-mcp",
    "source": { "type": "stdio", "command": ["npx", "-y", "search-mcp"] },
    "egressHosts": ["registry.npmjs.org", "api.search.example.com"],
    "clientTokens": [
      { "label": "laptop-claude" },
      { "label": "ci-pipeline", "allowedTools": ["search.query"] }
    ]
  }'

The answer has the server's endpointUrl, https://mcp.caa.sh/<endpoint-id>, and clientTokens: each token's secret and its token record. Without clientTokens, the server gets one token, Initial client, whose secret is in clientToken. Every secret is returned only once, so save it.

Client tokens

RouteScopeWhat it does
GET …/sandboxes/<sandbox-id>/client-tokensmcp:read{ "tokens": [...] }: each token's id, label, createdAt, expiresAt, revokedAt, lastUsedAt, allowedTools, requestCount and errorCount, never its secret
POST …/sandboxes/<sandbox-id>/client-tokensmcp:writeCreates a token, { "label": "…", "allowedTools": [...], "expiresAt": … }, and returns 201 with its secret and token
POST …/sandboxes/<sandbox-id>/client-tokens/<token-id>/revokemcp:writeRevokes a token at once: { "token": { … } }
shell
curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/client-tokens \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "ci-pipeline", "allowedTools": ["search.query"] }'
  • label is 1 to 80 characters.

  • allowedTools lists up to 32 tool names the token may call. Leave it out to allow every tool; an empty list allows none.

  • expiresAt is a time more than a minute and at most a year ahead. Leave it out for a token that never expires.

  • A server can have 20 tokens in force at once. A new token beyond that gets 429.

Call the endpoint

MCP clients call the server at its endpointUrl with a client token, not an API key:

shell
curl https://mcp.caa.sh/<endpoint-id> \
  -H "Authorization: Bearer $CAAS_MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'

Connect a client covers client configuration and the gateway's limits, and Tool grants what a limited token can do.

Workflow runs

Start a run

Send the workflow file's text as file, with its inputs. This example uses jq to build the body from build-and-test.yaml, the file in Workflows:

shell
jq -n --rawfile file build-and-test.yaml '{
  file: $file,
  inputs: { branch: "release" },
  images: { "registry.caas.sh/ci:node24": "registry.caas.sh/<tenant-id>/ci@sha256:<digest>" }
}' | curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/workflow-runs \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @-
FieldValue
fileThe workflow file, YAML or JSON, up to 64 KiB. Required.
inputsA value, as text, for each input the file declares. An input you leave out keeps its default; one without a default is required.
imagesMaps each image as the file writes it to its digest in your workspace's registry, registry.caas.sh/<tenant-id>/NAME@sha256:…. The API doesn't resolve tags: an image the file writes in exactly that form needs no entry, and any other (a tag, no tag, or a digest without your tenant ID) does.

The answer is 201 with the run:

json
{
  "id": "5d2b7e1a-9c4f-4a3e-b8d6-1f0e2c3a4b5d",
  "name": "build-and-test",
  "number": 12,
  "status": "queued",
  "resumes": 0,
  "progress": { "total": 3, "done": 0, "states": ["pending", "pending", "pending"], "current": null, "failed": null },
  "createdAt": 1791797400000,
  "startedAt": null,
  "endedAt": null,
  "costMicroUsd": 0,
  "resumeFrom": null,
  "error": null
}

A file with an error gets 400, naming the line or field. So does a run whose steps name a secret that isn't connected and added by a workspace egress rule. The workspace may have 10 runs queued, running or paused at once; another gets 429.

Run routes

RouteScopeWhat it does
POST …/workflow-runssandbox:write and sandbox:executeStarts a run
GET …/workflow-runssandbox:readThe newest 100 runs: { "runs": [...] }
GET …/workflow-runs/<run-id>sandbox:readOne run with its inputs, steps, checkpoints, the files the newest checkpoint carried (carried), and its cost
POST …/workflow-runs/<run-id>/pausesandbox:executePauses a running run: the steps already running finish, and no more start
POST …/workflow-runs/<run-id>/cancelsandbox:executeCancels a queued, running or paused run
POST …/workflow-runs/<run-id>/resumesandbox:write and sandbox:executeLifts a pause, or runs an ended run again from the steps that didn't finish. With { "checkpoint": "cp-2" }, replays it from that checkpoint with the same inputs.

Pause, cancel and resume answer with the run. A run's status is queued, running, paused, failed, succeeded or cancelled. A succeeded run can only be replayed from a checkpoint, and a run that ended more than seven days ago can't be resumed (409). See Pause, resume and cancel.

Credentials and outbound access

A credential holds a key or token that caas.sh adds to sandboxes' outbound requests, so the key never enters a sandbox. Egress rules say which requests get which credential, and each sandbox's egress policy says which hosts it may reach. Credentials and outbound access explains how they work together.

Reading them needs sandbox:read. Changing credentials, apps and egress rules needs an owner's key with sandbox:write; changing a sandbox's egress policy needs sandbox:write.

Credential routes

RouteWhat it does
GET …/credentials{ "credentials": [...] }: each one's name, method, provider, status, scopes and how many rules name it, never its value
GET …/credentials/<name>One credential: { "credential": { … } }
POST …/credentialsConnects a credential: { "credential": { "name": "…", "method": "…" }, "secret": "…" }
POST …/credentials/<name>/reconnectReplaces an API key's value, { "secret": "…" }, or starts a new sign-in for any other method. An oauth credential may ask for new { "scopes": [...] }.
DELETE …/credentials/<name>Removes the credential and the rules that name it
GET …/credential-providersThe OAuth and GitHub apps credentials connect through: { "providers": [...] }
POST …/credential-providersAdds or replaces an app: { "provider": { "id": "…", "type": "oauth2", "config": { … } }, "secret": "…" }
DELETE …/credential-providers/<app-id>Removes an app that no credential uses

A credential's name is capitals, digits and _, up to 64 characters, not starting with CAAS_. Its method is one of:

  • api_key: a key you send as secret. It's ready at once.

  • oauth: a sign-in through an OAuth app you added, named in provider, optionally asking for scopes.

  • github_app: an installation of a GitHub App you added, named in provider.

  • github_user: a GitHub sign-in.

  • codex: a ChatGPT account, through a device code.

For every method but api_key, the answer's credential.action has the next step: a page to sign in at ("kind": "redirect"), or a device code to enter at a page ("kind": "device"). A sign-in finishes in the console, signed in as the key's creator.

For example, to connect an Anthropic key and add it to requests to api.anthropic.com:

shell
jq -n --rawfile key anthropic-key.txt \
  '{ credential: { name: "ANTHROPIC_API_KEY", method: "api_key" }, secret: ($key | rtrimstr("\n")) }' |
  curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/credentials \
    -H "Authorization: Bearer $CAAS_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @-

curl https://api.caas.sh/v1/tenants/$CAAS_TENANT/egress-rules \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rule": { "preset": "anthropic", "credential": "ANTHROPIC_API_KEY" } }'

Egress rules

RouteWhat it does
GET …/egress-rules{ "rules": [...] }
POST …/egress-rulesAdds a rule, or a preset's rules: 201 with { "rules": [...] }
DELETE …/egress-rules/<rule-id>Removes a rule: { "id": "…", "deleted": true }

A rule names a credential, the host it goes to, and how it's sent:

json
{ "rule": { "credential": "SEARCH_API_KEY", "host": "api.search.example.com", "header": "authorization", "scheme": "bearer" } }
  • host is an exact host, or *.example.com for example.com and every name under it.

  • header is the request header the credential goes in, such as authorization.

  • scheme is bearer (Bearer <value>), basic (with a username) or raw (the value alone).

  • sandboxId limits the rule to one sandbox. Without it, the rule applies to every sandbox in the workspace.

Or send { "rule": { "preset": "github", "credential": "GITHUB_TOKEN" } }, optionally with sandboxId, for a preset's rules:

PresetAdds the credential to
githubapi.github.com as a bearer token, and github.com for Git over HTTPS
anthropicapi.anthropic.com, in x-api-key
openaiapi.openai.com, as a bearer token
googlegenerativelanguage.googleapis.com, in x-goog-api-key

Up to 16 rules apply to one sandbox, the workspace's and its own together. A running sandbox takes changed rules when it next starts.

Egress policy

RouteScopeWhat it does
GET …/sandboxes/<sandbox-id>/egress-policysandbox:read{ "policy": [...] }
PUT …/sandboxes/<sandbox-id>/egress-policysandbox:writeReplaces the policy with { "rules": [...] }, and applies it to a running sandbox at once: { "policy": [...] }
shell
curl -X PUT https://api.caas.sh/v1/tenants/$CAAS_TENANT/sandboxes/$SANDBOX_ID/egress-policy \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rules": [
    { "host": "github.com", "action": "allow" },
    { "host": "*.example.com", "action": "deny" },
    { "host": "pypi.org", "action": "allow" }
  ] }'

The first rule that matches a host decides. Up to 64 rules come before the closing * deny, which the policy always ends with: you may leave it out, and * allow is refused. In a policy, *.example.com matches every name under example.com, but not example.com itself.

Billing

Billing routes need a key with Billing access, which only an owner can create.

RouteScopeWhat it does
GET …/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 …/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 …/billing/auto-rechargebilling:read{ enabled, minimumMicroUsd, targetMicroUsd, active, failure }
PUT …/billing/auto-rechargebilling:writeChanges any of enabled, minimumMicroUsd ($5 to $1,000) and targetMicroUsd (at least $5 more, up to $2,000)
POST …/billing/top-upsbilling:writeAdds funds, { "amountMicroUsd": 25000000 } ($5 to $2,000, in whole cents), with an idempotency key
shell
curl "https://api.caas.sh/v1/tenants/$CAAS_TENANT/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 …/settingssandbox:read{ "timeZone": "UTC" }
PATCH …/settingssandbox:writeSets the time zone new sandboxes take their nightly snapshot in, as an IANA name: { "timeZone": "Europe/Berlin" }
GET …/auditsandbox:readThe audit log, newest first: { records, cursor, retentionDays }

GET …/audit takes these filters, each at most once:

ParameterValue
actionAn action, such as sandbox.stop, or a group, such as sandbox or command
actorA user's or API key's ID, 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 cursor

Each record has its time (at), actor, action, target, outcome, HTTP status, requestId and a short detail. 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.