openapi: 3.1.0 info: title: "TaskCloud API" version: "1.0.0-beta.1" summary: Persistent cloud computers (Firecracker microVMs) for AI agents. Pay only while they work. description: | REST API for `TaskCloud` sandboxes. Every sandbox is a full Firecracker microVM with its own kernel, Ubuntu 24.04, root, systemd and Docker, a persistent disk, pause/resume with a RAM snapshot, fork, exec, files, public HTTPS ports, SSH and an optional desktop with a computer-use API. **Status:** private beta. Latency numbers in this document are targets, not guarantees. ## Conventions - **Base URL:** `https://api.task.cloud/v1`. JSON request and response bodies use `snake_case`. - **Auth:** `Authorization: Bearer ` (the scheme name is case-insensitive). Keys look like `taskcloud_sk_...` and carry scopes (see `ApiKeyScope`). A missing scope returns `403 insufficient_scope`. - **Console sessions:** the web console signs users in (`/auth/*`) and calls every operation with the `taskcloud_session` cookie instead of a key. The session acts in its current organization with the scopes of the user's `Role` (a missing one returns `403 forbidden`, `details.reason = insufficient_role`). Non-GET requests with a session need `X-CSRF-Token` (the `taskcloud_csrf` cookie value) and an allowed `Origin` (`403 forbidden`, `details.reason = csrf_failed | origin_not_allowed`). Operations marked `x-session-only` accept only a session (`403`, `details.reason = session_required` for API keys). Sessions slide for 30 days of inactivity and end at most 90 days after sign-in. In an organization that requires two-factor authentication, a session of a user without 2FA gets `403 forbidden` (`details.reason = two_factor_required`) on organization operations; `/me` operations keep working. An API key created by a person never exceeds that person's current role in the organization (its scopes are intersected with the role's at every request) and stops working (`401`) when the person leaves. Per-IP limits and the IP addresses in sessions and audit records use the real client address: the API honours `X-Forwarded-For` only from its configured trusted proxies. - **IDs** are prefixed strings: `sbx_` sandbox, `cmd_` command, `snap_` snapshot, `tpl_` template, `whk_` webhook, `evt_` event, `key_` API key, `dsk_` desktop stream, `pty_` PTY session. - **Timestamps** are RFC 3339 UTC strings. - **Errors** always use the `Error` envelope: `{"error": {"code", "message", "retryable", "status", "request_id", "details"}}`. Branch on `code`, not on `message`. Retry only when `retryable` is `true`, honouring `Retry-After`. - **Pagination** is cursor based: `?limit=` (1–100, default 20) and `?cursor=`. List responses are `{"data": [...], "has_more": bool, "next_cursor": string|null}`. Webhooks, API keys, SSH keys, members and invitations (unpaginated before 2026-11) default to 100 items per page instead of 20. - **Idempotency:** `POST` requests that create resources accept an `Idempotency-Key` header (up to 255 chars, kept for 24 h). Replaying the same key with a different body returns `409 idempotency_conflict`. Responses that contain a secret shown once (`createApiKey`, `createWebhook`) are never stored: replaying such a request returns `409 idempotency_conflict` with `details.reason = "secret_not_replayable"` and `details.resource_id` of the resource the first request created (rotate it to get a new secret). A replayed response has `Idempotent-Replayed: true`, its own `X-Request-Id`, and, for an error, the same ID in `error.request_id`. - **Rate limits:** every response carries `RateLimit-Policy` and `RateLimit` headers (IETF draft `draft-ietf-httpapi-ratelimit-headers`), including `401` and unknown routes; requests without a valid key count against a per-client-IP `default` quota. Lifecycle calls (create, resume, fork) carry an additional `starts` policy. Exceeding a limit returns `429 rate_limited` with `Retry-After`. - **Request IDs:** every response has `X-Request-Id`; include it when contacting support. - **Unknown fields** in a JSON body are rejected with `400 invalid_request` and `details.field`. - **Common errors:** every operation documents `401`, `403` (scoped operations), `429`, `400` (operations with a body) and `404` (operations addressing a resource by ID) through shared `components/responses`. - **Wake on request:** calls that need a running VM (exec, files, desktop, ports traffic, SSH) on a `paused` sandbox resume it transparently. The call waits for the resume (target p95 < 800 ms). Pass `auto_resume=false` (query) to get `409 sandbox_paused` instead. contact: name: "TaskCloud support" url: "https://task.cloud/support" email: "support@task.cloud" license: name: Proprietary identifier: LicenseRef-Proprietary servers: - url: https://api.task.cloud/v1 description: Production (EU) security: - bearerAuth: [] - cookieAuth: [] tags: - name: Sandboxes description: Create, inspect and manage the lifecycle of sandboxes. - name: Exec description: Run commands (sync, background, SSE stream) and interactive PTY sessions. - name: Files description: Read, write, list, upload, download and watch files inside a sandbox. - name: Networking description: Public HTTPS URLs for ports and SSH access. - name: Desktop description: Desktop streaming (KasmVNC), screenshots and computer-use input. - name: Snapshots description: Point-in-time RAM + disk snapshots of a sandbox. - name: Templates description: Reusable starting points built from an OCI image or a snapshot. - name: Webhooks description: Signed event notifications. - name: Usage description: Metered usage, cost and account limits. - name: API keys description: Scoped API keys. - name: Account description: Account, plan and spend limits. - name: Auth description: Console sign-in (magic link, GitHub) and sign-out; cookie sessions. - name: Me description: The signed-in console user, their organizations and sessions. - name: Members description: Organization members, roles and invitations. - name: Billing description: Self-service billing for the console (plan, invoices, checkout, portal). paths: # ---------------------------------------------------------------- Sandboxes /sandboxes: get: tags: [Sandboxes] operationId: listSandboxes summary: List sandboxes x-required-scopes: [sandboxes:read] parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" - name: state in: query description: Filter by state. Repeat the parameter for several states. schema: type: array items: { $ref: "#/components/schemas/SandboxState" } style: form explode: true - name: label in: query description: Filter by label, `key=value`. Repeat for AND. schema: type: array items: { type: string, example: "customer=acme" } style: form explode: true - name: name_prefix in: query schema: { type: string } - name: template_id in: query schema: { type: string } responses: "200": description: A page of sandboxes. headers: RateLimit-Policy: { $ref: "#/components/headers/RateLimit-Policy" } RateLimit: { $ref: "#/components/headers/RateLimit" } X-Request-Id: { $ref: "#/components/headers/X-Request-Id" } content: application/json: schema: allOf: - $ref: "#/components/schemas/ListMeta" - type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/Sandbox" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } post: tags: [Sandboxes] operationId: createSandbox summary: Create a sandbox description: | Creates a sandbox from a template (default `base`), a snapshot, or an OCI image. Exactly one of `template`, `snapshot_id` or `image` may be set; when none is set, `template: "base"` is used. - With `wait: true` (default) the call returns `201` once the sandbox is `running` (target p95 < 500 ms from a template). - With `wait: false` it returns `202` immediately with state `creating`. - With `priority: "batch"` the sandbox may be held in state `queued` until capacity is available and is billed at the batch rate (−50%). Batch requests always return `202`. - When no host can take the sandbox the call returns `503 capacity_unavailable`; when the chosen host is unreachable it returns `503 service_unavailable`. Both are retryable, carry `Retry-After`, and no sandbox is created. x-required-scopes: [sandboxes:write] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/CreateSandboxRequest" } examples: fromTemplate: summary: From the base template value: { template: base, size: default, name: "user-42", labels: { customer: acme } } fromOfficialTemplate: summary: From an official template (Python 3.12 + data stack) value: { template: python } desktop: summary: Desktop template, never pauses value: { template: desktop, size: default, always_on: true } fromImage: summary: From an OCI image value: { image: { ref: "ghcr.io/acme/agent-env:1.4" }, size: large } batch: summary: Batch priority, may queue value: { template: base, priority: batch, wait: false } responses: "201": description: Sandbox is running. headers: Location: { schema: { type: string }, description: URL of the new sandbox. } RateLimit-Policy: { $ref: "#/components/headers/RateLimit-Policy" } RateLimit: { $ref: "#/components/headers/RateLimit" } X-Request-Id: { $ref: "#/components/headers/X-Request-Id" } content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "202": description: Sandbox accepted; state is `creating` or `queued`. headers: Location: { schema: { type: string } } content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /sandboxes/{sandbox_id}: parameters: - $ref: "#/components/parameters/SandboxId" get: tags: [Sandboxes] operationId: getSandbox summary: Get a sandbox description: Returns the sandbox. Does not wake a paused sandbox. x-required-scopes: [sandboxes:read] responses: "200": description: The sandbox. content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } patch: tags: [Sandboxes] operationId: updateSandbox summary: Update a sandbox description: | Updates mutable settings: `name`, `labels`, `auto_pause`, `always_on`, `ttl_seconds`. Setting `always_on: true` disables auto-pause and switches the sandbox to the always-on monthly price (`default` size only). x-required-scopes: [sandboxes:write] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/UpdateSandboxRequest" } examples: idleTimeout: summary: Pause after 15 minutes idle value: { auto_pause: { enabled: true, idle_timeout_seconds: 900 } } alwaysOn: value: { always_on: true } responses: "200": description: Updated sandbox. content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } delete: tags: [Sandboxes] operationId: deleteSandbox summary: Delete a sandbox description: | Permanently deletes the sandbox, its disk and its automatic pause snapshot. Named snapshots created with `POST /sandboxes/{sandbox_id}/snapshots` are kept. Returns `202`; the sandbox moves to `deleting` and then `deleted`, and a `sandbox.deleted` webhook is sent. x-required-scopes: [sandboxes:write] responses: "202": description: Deletion accepted. content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/pause: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Sandboxes] operationId: pauseSandbox summary: Pause a sandbox description: | Snapshots RAM and disk at the same instant and stops the VM. Processes, open windows and sessions survive. Compute billing stops when the state becomes `paused`. Pausing an already paused sandbox is a no-op (`200`). Concurrent calls: a call that finds the sandbox `pausing` waits for that pause. If the pause does not end in `paused` (the host refused it, or a resume won the race), the call returns `409 sandbox_busy` (retryable) at once; it never pauses the sandbox later. A call that finds it `creating` or `resuming` returns `409 sandbox_busy`. When the sandbox host is unreachable the call returns `503 service_unavailable` (retryable) and the sandbox stays `running`. x-required-scopes: [sandboxes:write] requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/WaitRequest" } responses: "200": description: Sandbox is `paused` (when `wait` is true). content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "202": description: Pause started; state is `pausing`. content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /sandboxes/{sandbox_id}/resume: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Sandboxes] operationId: resumeSandbox summary: Resume a sandbox description: | Restores RAM and disk from the pause snapshot (target p95 < 800 ms). You rarely need this call: any exec, file, desktop, port or SSH request wakes a paused sandbox automatically. Resuming a running sandbox is a no-op (`200`). A call that finds the sandbox `pausing` waits for that pause to end and then resumes it (or returns `200` if the pause failed and the sandbox is still `running`). When the host is unreachable or busy, the call returns `503 service_unavailable` or `409 sandbox_busy` (both retryable) and the sandbox stays `paused`. A snapshot written by another Firecracker version is resumed on a compatible host; without one the call returns `503 capacity_unavailable` with `details.reason = "incompatible_host"` and the sandbox stays `paused` with `pause_reason: incompatible_host`. x-required-scopes: [sandboxes:write] requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/WaitRequest" } responses: "200": description: Sandbox is `running` (when `wait` is true). headers: RateLimit-Policy: { $ref: "#/components/headers/RateLimit-Policy" } RateLimit: { $ref: "#/components/headers/RateLimit" } content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "202": description: Resume started; state is `resuming`. content: application/json: schema: { $ref: "#/components/schemas/Sandbox" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /sandboxes/{sandbox_id}/fork: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Sandboxes] operationId: forkSandbox summary: Fork a sandbox description: | Creates a new sandbox from the current state of this one, **including memory** (target p95 < 2 s). A running source is paused for a few hundred milliseconds while its RAM and disk are captured, then continues. A paused source is forked from its pause snapshot. The fork gets its own IP, network namespace and copy-on-write disk; changes in the fork never affect the source. The fork has the same size as the source. **Bulk fork.** `count` creates that many forks from a single capture of the source, for RL rollouts and evals. The maximum depends on the account (`GET /limits` → `bulk_fork.max_count`): Free 2, Starter 10, Team 50, Enterprise 100; higher limits on request (up to 500). New Free accounts that are not verified yet (first 7 days, no card or GitHub verification) cannot bulk fork (`max_count` 1). Admission is all or nothing and atomic (parallel requests cannot together exceed the quota): - `count` above the account's limit: `403 forbidden` with `details.reason = "fork_count_limit"`, `details.limit` and `details.requested` (not retryable; ask for a higher limit); - the concurrency quota cannot take every fork: `409 concurrency_limit_reached`; - the capacity next to the source cannot take every fork: `503 capacity_unavailable` (retryable); - `count` outside 1–500: `400 invalid_request`. In every failure case nothing is created. With `count` omitted or `1` the response is the single `Sandbox` (unchanged); with `count > 1` it is `{"data": [Sandbox, ...]}` with exactly `count` items. `201` when every fork is running, `202` otherwise (`wait: false`, or `priority: batch`, where the forks wait in `queued` and are admitted as capacity frees up). `name`, `labels`, `env`, `metadata`, `auto_pause`, `priority` and `ttl_seconds` apply to every fork. x-required-scopes: [sandboxes:write] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/ForkSandboxRequest" } responses: "201": description: "The new sandbox (running), or with `count > 1` the list of new sandboxes (all running)." headers: RateLimit-Policy: { $ref: "#/components/headers/RateLimit-Policy" } RateLimit: { $ref: "#/components/headers/RateLimit" } content: application/json: schema: oneOf: - $ref: "#/components/schemas/Sandbox" - $ref: "#/components/schemas/ForkSandboxesResponse" "202": description: "Fork accepted (when `wait` is false or `priority` is `batch`); with `count > 1` the list of new sandboxes." content: application/json: schema: oneOf: - $ref: "#/components/schemas/Sandbox" - $ref: "#/components/schemas/ForkSandboxesResponse" "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "402": { $ref: "#/components/responses/PaymentRequired" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/ServiceUnavailable" } # ---------------------------------------------------------------- Exec /sandboxes/{sandbox_id}/exec: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" post: tags: [Exec] operationId: execCommand summary: Run a command description: | Runs a command in the sandbox. Three modes: - **Sync** (default): waits for the command to exit and returns `200` with `ExecResult`. `timeout_seconds` defaults to 60, maximum 600. Captured output is limited to 8 MiB per stream (`truncated: true` when cut). - **Background** (`background: true`): returns `202` with a `Command` immediately. No time limit. Follow it with `GET .../commands/{command_id}` or `GET .../commands/{command_id}/stream`. - **Stream** (`stream: true` or `Accept: text/event-stream`): returns `200` with a Server-Sent Events stream of `ExecEvent`s (`start`, `stdout`, `stderr`, `exit`, `error`), without output limits. At least 10 commands can run concurrently per sandbox. Commands run through `/bin/bash -lc` when `command` is a string; pass `args` to execute a program directly without a shell. x-required-scopes: [exec] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ExecRequest" } examples: sync: value: { command: "npm test", cwd: "/home/user/app", timeout_seconds: 300 } background: value: { command: "npm run dev -- --port 3000", cwd: "/home/user/app", background: true } stream: value: { command: "pip install -r requirements.txt", stream: true } responses: "200": description: | Sync result (`application/json`) or SSE stream (`text/event-stream`). For a stream, each event has `event: ` and `data: `. Base64 is used for non-UTF-8 output (`encoding: base64`). content: application/json: schema: { $ref: "#/components/schemas/ExecResult" } text/event-stream: schema: type: string description: SSE stream; each `data:` line is an `ExecEvent`. example: | event: start data: {"type":"start","command_id":"cmd_9fz2cq4kd7mw","pid":4211} event: stdout data: {"type":"stdout","command_id":"cmd_9fz2cq4kd7mw","data":"added 312 packages\n"} event: exit data: {"type":"exit","command_id":"cmd_9fz2cq4kd7mw","exit_code":0,"duration_ms":8123} "202": description: Background command started. content: application/json: schema: { $ref: "#/components/schemas/Command" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "408": { $ref: "#/components/responses/Timeout" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "502": { $ref: "#/components/responses/BadGateway" } /sandboxes/{sandbox_id}/commands: parameters: - $ref: "#/components/parameters/SandboxId" get: tags: [Exec] operationId: listCommands summary: List commands description: Lists recent commands (running and finished in the last 7 days), newest first. x-required-scopes: [exec] parameters: - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" - name: state in: query schema: { $ref: "#/components/schemas/CommandState" } responses: "200": description: A page of commands. content: application/json: schema: allOf: - $ref: "#/components/schemas/ListMeta" - type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/Command" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/commands/{command_id}: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/CommandId" get: tags: [Exec] operationId: getCommand summary: Get a command x-required-scopes: [exec] responses: "200": description: The command. content: application/json: schema: { $ref: "#/components/schemas/Command" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/commands/{command_id}/stream: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/CommandId" get: tags: [Exec] operationId: streamCommand summary: Attach to a command's output (SSE) description: | Streams `ExecEvent`s for a running or finished command. Output produced before you attached is replayed first (up to 8 MiB per stream). Reconnect with the standard `Last-Event-ID` header to resume. x-required-scopes: [exec] parameters: - name: Last-Event-ID in: header required: false schema: { type: string } responses: "200": description: SSE stream of `ExecEvent`. content: text/event-stream: schema: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/commands/{command_id}/stdin: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/CommandId" post: tags: [Exec] operationId: writeCommandStdin summary: Write to a command's stdin x-required-scopes: [exec] requestBody: required: true content: application/json: schema: type: object required: [data] properties: data: { type: string, description: Bytes to write. } encoding: { type: string, enum: [utf8, base64], default: utf8 } eof: { type: boolean, default: false, description: Close stdin after writing. } responses: "204": { description: Written. } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/commands/{command_id}/kill: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/CommandId" post: tags: [Exec] operationId: killCommand summary: Send a signal to a command x-required-scopes: [exec] requestBody: required: false content: application/json: schema: type: object properties: signal: type: string enum: [SIGTERM, SIGKILL, SIGINT, SIGHUP, SIGQUIT, SIGUSR1, SIGUSR2] default: SIGTERM responses: "200": description: Signal delivered. content: application/json: schema: { $ref: "#/components/schemas/Command" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/pty: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Exec] operationId: createPtySession summary: Create an interactive PTY session description: | Starts a pseudo-terminal and returns a short-lived WebSocket URL (`websocket_url`, valid for 60 s until first connect). Connect with any WebSocket client; no `Authorization` header is needed because the URL carries a signed token, so it can be opened from a browser. **The token is single-use.** A second connect with the same URL gets `401`. To reconnect to the same session, either call `getPtySession` for a fresh `websocket_url`, or present the reconnect ticket returned with the first `101` response: header `X-TaskCloud-Pty-Ticket` (send it back in the same header) and an `HttpOnly; SameSite=Strict` cookie `__Host-taskcloud_pty_` that same-site browsers send automatically. A ticket opens only its own session, lives for the session's `idle_timeout_seconds`, and every reconnect returns a fresh one. **WebSocket protocol** (`wss://api.task.cloud/v1/sandboxes/{sandbox_id}/pty/{pty_id}/connect?token=...`): - **Binary frames, client → server:** raw bytes written to the terminal (keystrokes). - **Binary frames, server → client:** raw terminal output (UTF-8 with ANSI escapes). - **Text frames** carry JSON control messages: - client → server: `{"type":"resize","cols":120,"rows":40}`, `{"type":"signal","signal":"SIGINT"}`, `{"type":"ping"}` - server → client: `{"type":"ready","pty_id":"pty_..."}`, `{"type":"exit","exit_code":0}`, `{"type":"pong"}`, `{"type":"error","error":{...Error}}` - The session ends when the process exits or after `idle_timeout_seconds` without traffic. An open PTY counts as activity, so it prevents auto-pause. - Reconnect to a live session within 60 s of a drop with `GET .../pty/{pty_id}` to get a fresh URL; up to 64 KiB of missed output is replayed. x-required-scopes: [exec] requestBody: required: false content: application/json: schema: { $ref: "#/components/schemas/CreatePtyRequest" } responses: "201": description: PTY session created. content: application/json: schema: { $ref: "#/components/schemas/PtySession" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /sandboxes/{sandbox_id}/pty/{pty_id}: parameters: - $ref: "#/components/parameters/SandboxId" - name: pty_id in: path required: true schema: { type: string, example: pty_7hc2zq5mxk3d } get: tags: [Exec] operationId: getPtySession summary: Get a PTY session (and a fresh connect URL) x-required-scopes: [exec] responses: "200": description: PTY session. content: application/json: schema: { $ref: "#/components/schemas/PtySession" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } delete: tags: [Exec] operationId: closePtySession summary: Close a PTY session x-required-scopes: [exec] responses: "204": { description: Closed. } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/pty/{pty_id}/connect: parameters: - $ref: "#/components/parameters/SandboxId" - name: pty_id in: path required: true schema: { type: string } - name: token in: query required: false description: Single-use signed token from `PtySession.websocket_url` (required unless a reconnect ticket is sent). schema: { type: string } - name: X-TaskCloud-Pty-Ticket in: header required: false description: Reconnect ticket from the `101` response of an earlier connect to this session (see `createPtySession`). schema: { type: string } get: tags: [Exec] operationId: connectPty summary: PTY WebSocket endpoint description: WebSocket upgrade endpoint. See `createPtySession` for the frame protocol. security: [] responses: "101": { description: Switching Protocols (WebSocket). } "401": { $ref: "#/components/responses/Unauthorized" } "404": { $ref: "#/components/responses/NotFound" } # ---------------------------------------------------------------- Files /sandboxes/{sandbox_id}/files: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" get: tags: [Files] operationId: listFiles summary: List a directory x-required-scopes: [files:read] parameters: - $ref: "#/components/parameters/Path" - name: recursive in: query schema: { type: boolean, default: false } - name: max_depth in: query description: Maximum depth when `recursive` is true. schema: { type: integer, minimum: 1, maximum: 32, default: 8 } - $ref: "#/components/parameters/Limit" - $ref: "#/components/parameters/Cursor" responses: "200": description: Directory entries. content: application/json: schema: allOf: - $ref: "#/components/schemas/ListMeta" - type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/FileEntry" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } delete: tags: [Files] operationId: deleteFile summary: Delete a file or directory x-required-scopes: [files:write] parameters: - $ref: "#/components/parameters/Path" - name: recursive in: query description: Required to delete a non-empty directory. schema: { type: boolean, default: false } responses: "204": { description: Deleted. } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/files/content: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" - $ref: "#/components/parameters/Path" get: tags: [Files] operationId: readFile summary: Read a file description: | Returns the raw file bytes as a stream (`application/octet-stream`). Supports `Range` requests. Up to 1 GiB per request; use `POST .../files/download-url` for larger files. x-required-scopes: [files:read] parameters: - name: Range in: header required: false schema: { type: string, example: "bytes=0-1023" } responses: "200": description: File content. headers: Content-Length: { schema: { type: integer } } ETag: { schema: { type: string } } Last-Modified: { schema: { type: string } } content: application/octet-stream: schema: { type: string, format: binary } "206": description: Partial content. content: application/octet-stream: schema: { type: string, format: binary } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "416": { $ref: "#/components/responses/RangeNotSatisfiable" } "429": { $ref: "#/components/responses/RateLimited" } put: tags: [Files] operationId: writeFile summary: Write a file description: | Creates or replaces a file with the raw request body (streamed, up to 1 GiB). Parent directories are created by default. Use `If-Match` with an `ETag` for optimistic concurrency. x-required-scopes: [files:write] parameters: - name: mode in: query description: Octal permission bits for a new file. schema: { type: string, default: "0644", pattern: "^0?[0-7]{3}$" } - name: create_parents in: query schema: { type: boolean, default: true } - name: owner in: query description: Owner user of a new file. schema: { type: string, default: user } - name: If-Match in: header required: false schema: { type: string } requestBody: required: true content: application/octet-stream: schema: { type: string, format: binary } responses: "200": description: Written. content: application/json: schema: { $ref: "#/components/schemas/FileEntry" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "412": { $ref: "#/components/responses/PreconditionFailed" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "429": { $ref: "#/components/responses/RateLimited" } "507": { $ref: "#/components/responses/InsufficientStorage" } /sandboxes/{sandbox_id}/files/stat: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" - $ref: "#/components/parameters/Path" get: tags: [Files] operationId: statFile summary: Get file metadata x-required-scopes: [files:read] responses: "200": description: Metadata. content: application/json: schema: { $ref: "#/components/schemas/FileEntry" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/files/mkdir: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Files] operationId: makeDirectory summary: Create a directory x-required-scopes: [files:write] requestBody: required: true content: application/json: schema: type: object required: [path] properties: path: { type: string } parents: { type: boolean, default: true } mode: { type: string, default: "0755" } responses: "201": description: Created. content: application/json: schema: { $ref: "#/components/schemas/FileEntry" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "507": { $ref: "#/components/responses/InsufficientStorage" } /sandboxes/{sandbox_id}/files/move: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Files] operationId: moveFile summary: Move or rename a file or directory x-required-scopes: [files:write] requestBody: required: true content: application/json: schema: type: object required: [from, to] properties: from: { type: string } to: { type: string } overwrite: { type: boolean, default: false } responses: "200": description: Moved. content: application/json: schema: { $ref: "#/components/schemas/FileEntry" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } "507": { $ref: "#/components/responses/InsufficientStorage" } /sandboxes/{sandbox_id}/files/upload: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" post: tags: [Files] operationId: uploadFiles summary: Upload files (multipart) description: | Uploads one or more files with `multipart/form-data` into the directory `path`. Each file part's filename may contain sub-directories (`src/index.ts`). Send a `.tar` or `.tar.gz` part with `extract=true` to unpack an archive. Up to 1 GiB per request. x-required-scopes: [files:write] requestBody: required: true content: multipart/form-data: schema: type: object required: [path, files] properties: path: { type: string, description: Destination directory. } extract: { type: boolean, default: false } files: type: array items: { type: string, format: binary } responses: "200": description: Uploaded entries. content: application/json: schema: type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/FileEntry" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "413": { $ref: "#/components/responses/PayloadTooLarge" } "429": { $ref: "#/components/responses/RateLimited" } "507": { $ref: "#/components/responses/InsufficientStorage" } /sandboxes/{sandbox_id}/files/download: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" - $ref: "#/components/parameters/Path" get: tags: [Files] operationId: downloadArchive summary: Download a directory as an archive x-required-scopes: [files:read] parameters: - name: format in: query schema: { type: string, enum: [tar.gz, tar, zip], default: tar.gz } responses: "200": description: Archive stream. content: application/gzip: schema: { type: string, format: binary } application/x-tar: schema: { type: string, format: binary } application/zip: schema: { type: string, format: binary } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/files/upload-url: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Files] operationId: createUploadUrl summary: Create a signed upload URL description: Returns a pre-signed URL that accepts a single `PUT` of up to 10 GiB without an API key (the URL is consumed by the first `PUT`; a second one gets `401`). x-required-scopes: [files:write] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SignedUrlRequest" } responses: "201": description: Signed URL. content: application/json: schema: { $ref: "#/components/schemas/SignedUrl" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/files/download-url: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Files] operationId: createDownloadUrl summary: Create a signed download URL description: Returns a pre-signed URL for a single file (any size up to 10 GiB) usable without an API key. x-required-scopes: [files:read] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SignedUrlRequest" } responses: "201": description: Signed URL. content: application/json: schema: { $ref: "#/components/schemas/SignedUrl" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/files/watch: parameters: - $ref: "#/components/parameters/SandboxId" - $ref: "#/components/parameters/AutoResume" - $ref: "#/components/parameters/Path" get: tags: [Files] operationId: watchFiles summary: Watch a path for changes (SSE) description: | Streams `FileEvent`s (`created`, `modified`, `deleted`, `renamed`) as Server-Sent Events. An open watch does **not** count as activity, so it does not prevent auto-pause; the stream stays open across pause/resume and resumes delivering events after the VM wakes. x-required-scopes: [files:read] parameters: - name: recursive in: query schema: { type: boolean, default: true } - name: include_hidden in: query schema: { type: boolean, default: false } responses: "200": description: SSE stream; each `data:` line is a `FileEvent`. content: text/event-stream: schema: { type: string } example: | event: modified data: {"type":"modified","path":"/home/user/app/src/index.ts","is_dir":false,"timestamp":"2026-11-09T12:00:01Z"} "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } # ---------------------------------------------------------------- Networking /sandboxes/{sandbox_id}/ports: parameters: - $ref: "#/components/parameters/SandboxId" get: tags: [Networking] operationId: listPorts summary: List exposed ports x-required-scopes: [sandboxes:read] responses: "200": description: Exposed ports. content: application/json: schema: type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/Port" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } post: tags: [Networking] operationId: exposePort summary: Expose a port on a public HTTPS URL description: | Publishes a port at `https://-.taskhost.cloud` (HTTP/1.1, HTTP/2 and WebSocket), where `` is the sandbox ID without the `sbx_` prefix (`_` is not valid in hostnames). `visibility: private` requires a valid access token (header `X-Sandbox-Token` or query `sandbox_token`) or an API key with `sandboxes:read`; `public` is open to anyone. A request to the URL of a paused sandbox wakes it (wake on request). Exposing an already exposed port updates its settings. Sandbox URLs (ports, desktop, SSH gateway) use the separate domain `taskhost.cloud`, never the API domain `task.cloud`, so sandbox content is never same-site with the API. x-required-scopes: [sandboxes:write] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/ExposePortRequest" } responses: "201": description: Port exposed. content: application/json: schema: { $ref: "#/components/schemas/Port" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/ports/{port}: parameters: - $ref: "#/components/parameters/SandboxId" - name: port in: path required: true schema: { type: integer, minimum: 1, maximum: 65535 } delete: tags: [Networking] operationId: unexposePort summary: Remove a public URL x-required-scopes: [sandboxes:write] responses: "204": { description: Removed. } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/access-tokens: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Networking] operationId: createAccessToken summary: Create an access token for private ports description: | Issues a short-lived token that grants HTTP access to this sandbox's **private** ports only. Give it to your end user's browser instead of an API key. x-required-scopes: [sandboxes:write] requestBody: required: false content: application/json: schema: type: object properties: ttl_seconds: { type: integer, minimum: 60, maximum: 86400, default: 3600 } ports: type: array description: Restrict to these ports. Default is all private ports. items: { type: integer } responses: "201": description: Token. content: application/json: schema: type: object required: [token, expires_at] properties: token: { type: string, example: "sbt_4fQ...", description: Shown once. } expires_at: { type: string, format: date-time } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /sandboxes/{sandbox_id}/ssh: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Networking] operationId: createSshAccess summary: Get SSH access description: | Returns connection details for the SSH gateway `ssh.taskhost.cloud:22`. Authenticate either with a public key registered via `POST /ssh-keys` (or passed in `public_key` here, valid for `ttl_seconds`), or with the one-time `password` returned when `public_key` is omitted and no account key exists. Connecting to a paused sandbox wakes it. SSH supports port forwarding (`-L`, `-R`), `scp`, `sftp` and VS Code / Cursor Remote-SSH. x-required-scopes: [ssh] requestBody: required: false content: application/json: schema: type: object properties: public_key: { type: string, description: "OpenSSH public key, e.g. `ssh-ed25519 AAAA...`." } ttl_seconds: { type: integer, minimum: 60, maximum: 604800, default: 3600 } user: { type: string, default: user, description: Linux user to log in as. } responses: "201": description: SSH access. content: application/json: schema: { $ref: "#/components/schemas/SshAccess" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } /ssh-keys: get: tags: [Networking] operationId: listSshKeys summary: List account SSH keys x-required-scopes: [ssh] parameters: - $ref: "#/components/parameters/LimitDefault100" - $ref: "#/components/parameters/Cursor" responses: "200": description: SSH keys. content: application/json: schema: allOf: - $ref: "#/components/schemas/ListMeta" - type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/SshKey" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } post: tags: [Networking] operationId: addSshKey summary: Add an account SSH key description: The key can log in to every sandbox of the account through the SSH gateway. x-required-scopes: [ssh] requestBody: required: true content: application/json: schema: type: object required: [public_key] properties: name: { type: string, maxLength: 120 } public_key: { type: string } responses: "201": description: Added. content: application/json: schema: { $ref: "#/components/schemas/SshKey" } "400": { $ref: "#/components/responses/BadRequest" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "429": { $ref: "#/components/responses/RateLimited" } /ssh-keys/{ssh_key_id}: parameters: - name: ssh_key_id in: path required: true schema: { type: string } delete: tags: [Networking] operationId: deleteSshKey summary: Remove an account SSH key x-required-scopes: [ssh] responses: "204": { description: Removed. } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "429": { $ref: "#/components/responses/RateLimited" } # ---------------------------------------------------------------- Desktop /sandboxes/{sandbox_id}/desktop: parameters: - $ref: "#/components/parameters/SandboxId" post: tags: [Desktop] operationId: createDesktopStream summary: Create a desktop stream URL description: | Returns a signed URL that opens the sandbox desktop (KasmVNC, 1920x1080) in a browser, at `https://-desktop.taskhost.cloud/?token=...`. Requires a desktop-capable template (e.g. `desktop`) and size `default` or larger. Opening the URL wakes a paused sandbox. An active viewer counts as activity (no auto-pause); an idle desktop with no viewers pauses normally. The URL can be embedded in an `