# API reference

The full, machine-readable reference is the **OpenAPI 3.1 spec**:

- Hosted: `https://api.task.cloud/v1/openapi.yaml`
- On this site: [`openapi.yaml`](openapi.yaml) (a copy of `api/openapi.yaml` from the repository, published with every docs build)

Generate a client in any language from it (for example with `openapi-generator`), or import it into Postman or Insomnia. The official [TypeScript](sdk/typescript.md) and [Python](sdk/python.md) SDKs are hand-written from the same file and tested against it.

## Basics

| Topic | Rule |
| --- | --- |
| Base URL | `https://api.task.cloud/v1` |
| Auth | `Authorization: Bearer taskcloud_sk_...` (scheme name case-insensitive); each endpoint needs the scope listed below ([Security](security.md)) |
| Format | JSON with `snake_case` fields; file content is raw bytes; streams are Server-Sent Events; PTY is a WebSocket |
| IDs | `sbx_` sandbox, `cmd_` command, `pty_` PTY, `dsk_` desktop stream, `snap_` snapshot, `tpl_` template, `whk_` webhook, `evt_` event, `key_` API key |
| Timestamps | RFC 3339, UTC |
| Pagination | `?limit=` (1–100, default 20) and `?cursor=`; responses `{"data": [...], "has_more": true, "next_cursor": "..."}` |
| Idempotency | `Idempotency-Key` header on `POST /sandboxes`, `/fork`, `/snapshots`, `/templates` (kept 24 h) |
| Rate limits | `RateLimit-Policy` and `RateLimit` headers on every response, including `401` and unknown routes; `429` with `Retry-After` ([Limits](limits.md)) |
| Request ID | `X-Request-Id` on every response |
| Wake on request | Calls that need a running VM wake a paused sandbox; add `?auto_resume=false` to get `409 sandbox_paused` instead |

## Errors

Every error has the same shape:

```json
{
  "error": {
    "code": "concurrency_limit_reached",
    "message": "Your plan allows 10 concurrent sandboxes.",
    "retryable": false,
    "status": 409,
    "request_id": "req_01HF3X",
    "details": { "limit": 10, "used": 10 }
  }
}
```

Branch on `code`, never on `message`. Retry only when `retryable` is `true`, after `Retry-After` if present.

| Status | Codes |
| --- | --- |
| 400 | `invalid_request`, `invalid_path`, `invalid_template`, `invalid_image` |
| 401 | `unauthorized` |
| 402 | `payment_required`, `spend_limit_reached`, `free_hours_exhausted` |
| 403 | `forbidden`, `insufficient_scope` |
| 404 | `not_found` |
| 408 | `exec_timeout` |
| 409 | `invalid_state`, `sandbox_paused`, `sandbox_busy` (retryable, `Retry-After`), `concurrency_limit_reached`, `idempotency_conflict`, `already_exists`, `desktop_not_available`, `snapshot_in_use` |
| 412 | `precondition_failed` |
| 413 | `payload_too_large` |
| 416 | `invalid_request`: a `Range` on `GET .../files/content` starts past the end of the file (`details.size`) |
| 429 | `rate_limited` (retryable); `details.reason = "too_many_operations"`: too many concurrent operations on one sandbox |
| 500 | `internal_error` (retryable) |
| 502 | `sandbox_unreachable` (see `retryable`) |
| 503 | `capacity_unavailable` (retryable; `details.reason = "incompatible_host"` when a paused sandbox's snapshot needs a compatible host, see `pause_reason`), `service_unavailable` (retryable) |
| 507 | `insufficient_storage`: the sandbox disk is full (not retryable) |

## Endpoints

### Sandboxes

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/sandboxes` | List sandboxes | `sandboxes:read` |
| `POST` | `/sandboxes` | Create a sandbox | `sandboxes:write` |
| `GET` | `/sandboxes/{sandbox_id}` | Get a sandbox | `sandboxes:read` |
| `PATCH` | `/sandboxes/{sandbox_id}` | Update a sandbox | `sandboxes:write` |
| `DELETE` | `/sandboxes/{sandbox_id}` | Delete a sandbox | `sandboxes:write` |
| `POST` | `/sandboxes/{sandbox_id}/pause` | Pause a sandbox | `sandboxes:write` |
| `POST` | `/sandboxes/{sandbox_id}/resume` | Resume a sandbox | `sandboxes:write` |
| `POST` | `/sandboxes/{sandbox_id}/fork` | Fork a sandbox | `sandboxes:write` |

### Exec

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `POST` | `/sandboxes/{sandbox_id}/exec` | Run a command | `exec` |
| `GET` | `/sandboxes/{sandbox_id}/commands` | List commands | `exec` |
| `GET` | `/sandboxes/{sandbox_id}/commands/{command_id}` | Get a command | `exec` |
| `GET` | `/sandboxes/{sandbox_id}/commands/{command_id}/stream` | Attach to a command's output (SSE) | `exec` |
| `POST` | `/sandboxes/{sandbox_id}/commands/{command_id}/stdin` | Write to a command's stdin | `exec` |
| `POST` | `/sandboxes/{sandbox_id}/commands/{command_id}/kill` | Send a signal to a command | `exec` |
| `POST` | `/sandboxes/{sandbox_id}/pty` | Create an interactive PTY session | `exec` |
| `GET` | `/sandboxes/{sandbox_id}/pty/{pty_id}` | Get a PTY session (and a fresh connect URL) | `exec` |
| `DELETE` | `/sandboxes/{sandbox_id}/pty/{pty_id}` | Close a PTY session | `exec` |
| `GET` | `/sandboxes/{sandbox_id}/pty/{pty_id}/connect` | PTY WebSocket endpoint | signed token |

### Files

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/sandboxes/{sandbox_id}/files` | List a directory | `files:read` |
| `DELETE` | `/sandboxes/{sandbox_id}/files` | Delete a file or directory | `files:write` |
| `GET` | `/sandboxes/{sandbox_id}/files/content` | Read a file | `files:read` |
| `PUT` | `/sandboxes/{sandbox_id}/files/content` | Write a file | `files:write` |
| `GET` | `/sandboxes/{sandbox_id}/files/stat` | Get file metadata | `files:read` |
| `POST` | `/sandboxes/{sandbox_id}/files/mkdir` | Create a directory | `files:write` |
| `POST` | `/sandboxes/{sandbox_id}/files/move` | Move or rename a file or directory | `files:write` |
| `POST` | `/sandboxes/{sandbox_id}/files/upload` | Upload files (multipart) | `files:write` |
| `GET` | `/sandboxes/{sandbox_id}/files/download` | Download a directory as an archive | `files:read` |
| `POST` | `/sandboxes/{sandbox_id}/files/upload-url` | Create a signed upload URL | `files:write` |
| `POST` | `/sandboxes/{sandbox_id}/files/download-url` | Create a signed download URL | `files:read` |
| `GET` | `/sandboxes/{sandbox_id}/files/watch` | Watch a path for changes (SSE) | `files:read` |

### Networking

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/sandboxes/{sandbox_id}/ports` | List exposed ports | `sandboxes:read` |
| `POST` | `/sandboxes/{sandbox_id}/ports` | Expose a port on a public HTTPS URL | `sandboxes:write` |
| `DELETE` | `/sandboxes/{sandbox_id}/ports/{port}` | Remove a public URL | `sandboxes:write` |
| `POST` | `/sandboxes/{sandbox_id}/access-tokens` | Create an access token for private ports | `sandboxes:write` |
| `POST` | `/sandboxes/{sandbox_id}/ssh` | Get SSH access | `ssh` |
| `GET` | `/ssh-keys` | List account SSH keys | `ssh` |
| `POST` | `/ssh-keys` | Add an account SSH key | `ssh` |
| `DELETE` | `/ssh-keys/{ssh_key_id}` | Remove an account SSH key | `ssh` |

### Desktop

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `POST` | `/sandboxes/{sandbox_id}/desktop` | Create a desktop stream URL | `desktop` |
| `GET` | `/sandboxes/{sandbox_id}/desktop` | List active desktop stream URLs | `desktop` |
| `DELETE` | `/sandboxes/{sandbox_id}/desktop/{stream_id}` | Revoke a desktop stream URL | `desktop` |
| `GET` | `/sandboxes/{sandbox_id}/screenshot` | Take a screenshot | `desktop` |
| `POST` | `/sandboxes/{sandbox_id}/input` | Send mouse and keyboard input | `desktop` |
| `GET` | `/sandboxes/{sandbox_id}/cursor` | Get the cursor position | `desktop` |

### Snapshots

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `POST` | `/sandboxes/{sandbox_id}/snapshots` | Create a named snapshot | `snapshots:write` |
| `GET` | `/snapshots` | List snapshots | `snapshots:read` |
| `GET` | `/snapshots/{snapshot_id}` | Get a snapshot | `snapshots:read` |
| `DELETE` | `/snapshots/{snapshot_id}` | Delete a snapshot | `snapshots:write` |

### Templates

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/templates` | List templates | `templates:read` |
| `POST` | `/templates` | Create a template | `templates:write` |
| `GET` | `/templates/{template_id}` | Get a template | `templates:read` |
| `DELETE` | `/templates/{template_id}` | Delete a template | `templates:write` |
| `GET` | `/templates/{template_id}/logs` | Get template build logs | `templates:read` |

### Webhooks

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/webhooks` | List webhook endpoints | `webhooks:manage` |
| `POST` | `/webhooks` | Create a webhook endpoint | `webhooks:manage` |
| `GET` | `/webhooks/{webhook_id}` | Get a webhook endpoint | `webhooks:manage` |
| `PATCH` | `/webhooks/{webhook_id}` | Update a webhook endpoint | `webhooks:manage` |
| `DELETE` | `/webhooks/{webhook_id}` | Delete a webhook endpoint | `webhooks:manage` |
| `POST` | `/webhooks/{webhook_id}/rotate-secret` | Rotate the signing secret | `webhooks:manage` |
| `POST` | `/webhooks/{webhook_id}/test` | Send a test event | `webhooks:manage` |
| `GET` | `/webhooks/{webhook_id}/deliveries` | List recent deliveries | `webhooks:manage` |

### Usage

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/usage` | Get usage and cost | `usage:read` |
| `GET` | `/sandboxes/{sandbox_id}/usage` | Get usage for one sandbox | `usage:read` |
| `GET` | `/limits` | Get account limits and current consumption | `usage:read` |

### API keys

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/api-keys` | List API keys | `api_keys:manage` |
| `POST` | `/api-keys` | Create an API key | `api_keys:manage` |
| `GET` | `/api-keys/{api_key_id}` | Get an API key | `api_keys:manage` |
| `DELETE` | `/api-keys/{api_key_id}` | Revoke an API key | `api_keys:manage` |
| `POST` | `/api-keys/{api_key_id}/rotate` | Rotate an API key | `api_keys:manage` |

### Account

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/account` | Get the account | `account:read` |
| `PUT` | `/account/spend-limit` | Set the monthly spend limit | `account:manage` |

### Members and billing

Used by the web console with a session, or with an API key ([Teams and roles](teams.md)).

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/members` | List organization members | `account:read` |
| `PATCH` | `/members/{user_id}` | Change a member's role | `account:manage` |
| `DELETE` | `/members/{user_id}` | Remove a member (or leave) | `account:manage` |
| `GET` | `/invitations` | List invitations | `account:read` |
| `POST` | `/invitations` | Invite someone to the organization | `account:manage` |
| `DELETE` | `/invitations/{invitation_id}` | Revoke a pending invitation | `account:manage` |
| `GET` | `/billing` | Get the billing overview | `account:read` |
| `POST` | `/billing/checkout` | Upgrade to a paid plan | `account:manage` |
| `POST` | `/billing/portal` | Open the billing portal | `account:manage` |

### Console sign-in and sessions

Cookie-based endpoints of the web console (`app.task.cloud`); API keys get `403` on the session-only ones.

| Method | Path | Summary | Auth |
| --- | --- | --- | --- |
| `POST` | `/auth/magic-link` | E-mail a sign-in link | none |
| `POST` | `/auth/magic-link/verify` | Sign in with a magic-link token | none |
| `GET` | `/auth/github/start` | Start GitHub sign-in | none |
| `GET` | `/auth/github/callback` | GitHub sign-in callback | none |
| `POST` | `/auth/logout` | Sign out | session |
| `GET` | `/me` | Get the signed-in user | session |
| `PATCH` | `/me` | Update the profile | session |
| `POST` | `/me/switch-org` | Switch the session's organization | session |
| `GET` | `/me/sessions` | List my active sessions | session |
| `DELETE` | `/me/sessions/{session_id}` | Revoke one of my sessions | session |
| `POST` | `/organizations` | Create a team organization | session |
| `POST` | `/invitations/accept` | Accept an invitation | session |

## Outgoing webhooks

Events `sandbox.ready`, `sandbox.paused`, `sandbox.resumed`, `sandbox.error`, `sandbox.deleted`, `usage.threshold` (and `webhook.test`) are `POST`ed to your endpoints, signed with HMAC-SHA256 in `Webhook-Signature`. Defined under `webhooks` in the spec; see [Webhooks](guides/webhooks.md).
