openapi: 3.1.0 info: title: Sandboxer Public API v1 version: 1.0.0 description: | Public JSON API for creating, operating, prompting, observing, and exposing sandboxes from backend services, CI jobs, hosted workers, and Sandboxer automation products. The v1 reference intentionally documents the developer integration surface only. Dashboard billing actions are not part of v1. servers: - url: https://sandboxer.com/api/v1 security: - SandboxerBearerAuth: [] webhooks: sandboxLifecycle: post: summary: Sandbox lifecycle event delivery operationId: deliverSandboxLifecycleEvent description: At-least-once delivery to each subscribed endpoint. Separate events may arrive out of order. Return any 2xx status within 5 seconds to acknowledge the event. security: [] parameters: - name: X-Sandboxer-Event in: header required: true schema: { $ref: '#/components/schemas/WebhookEventType' } - name: X-Sandboxer-Delivery in: header required: true schema: { type: string, pattern: '^evt_[a-f0-9]{32}$' } - name: X-Sandboxer-Timestamp in: header required: true schema: { type: string, pattern: '^[0-9]+$' } - name: X-Sandboxer-Signature in: header required: true description: HMAC-SHA256 over `delivery_id.timestamp.raw_body`, formatted as `v1=`. schema: { type: string, pattern: '^v1=[a-f0-9]{64}$' } - name: X-Sandboxer-Attempt in: header required: true schema: { type: integer, minimum: 1, maximum: 8 } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/WebhookEvent' } responses: '2XX': description: Delivery accepted. default: description: Delivery will be retried with exponential backoff. tags: - name: Sandboxer description: Unified Sandboxer account, setup, lifecycle, prompting, event history, desktop access, and SSH operations. components: securitySchemes: SandboxerBearerAuth: type: http scheme: bearer bearerFormat: sandbox_api_key description: Sandboxer bearer token in the form `sbx_live_...`. Service API keys authenticate sandbox operations. parameters: SandboxId: name: sandboxId in: path required: true schema: type: string pattern: '^sb_[a-z0-9]{10}$' description: Public Sandbox id returned by create/list/get sandbox calls. ProcessId: name: processId in: path required: true schema: type: integer minimum: 1 description: Process id returned by a detached command start. Limit: name: limit in: query schema: { type: integer, minimum: 1, maximum: 200, default: 100 } description: Maximum items to return. Cursor: name: cursor in: query schema: { type: [string, 'null'] } description: Opaque pagination cursor returned as `pageInfo.nextCursor`. Sort: name: sort in: query schema: { type: string, enum: [asc, desc], default: desc } description: Sort direction for cursor pagination. SnapshotId: name: snapshotId in: path required: true schema: { type: string, format: uuid } description: Snapshot id returned by the snapshot list/latest calls. OperationId: name: operationId in: path required: true schema: { type: string, pattern: '^bdop_[a-f0-9]{32}$' } description: Deletion operation id returned by an accepted delete request. ConfirmDelete: name: X-Sandboxer-Confirm-Delete in: header required: true schema: { type: string } description: Must exactly equal the target `sandboxId` or `snapshotId`. A missing or mismatched value returns `409` without accepting deletion. IdempotencyKey: name: Idempotency-Key in: header required: false schema: { type: string, maxLength: 255 } description: >- Optional exactly-once key for creating a sandbox. Send your own opaque, account-unique value (a UUID) to make `POST /sandboxes` safe to retry when the response is lost (network timeout, 5xx): the first request creates the sandbox and binds it to the key; every later request with the **same account, key, and request body** returns that same sandbox instead of creating a second, billable one. Behavior: keys are retained for **24 hours**; a concurrent or early retry while the first sandbox is still being minted returns `409` `idempotency_in_progress` (retry shortly, same key); reusing a key with a **different body** returns `409` `idempotency_key_reused`; timeouts and 5xx are safe to retry with the same key; a create that fails before the sandbox exists releases the key within ~2 minutes so a retry can create the sandbox. Omit the header to keep the default (non-idempotent) behavior. OrgId: name: org in: query required: false schema: { type: string } description: >- Billing wallet for this request: an organization you belong to, by id (`team_…`) or by name as `GET /orgs` lists it (case-insensitive), or `personal`. Omitted, the account's active wallet applies (`PATCH /orgs/active`; personal until set). Two organizations sharing the name answer `409 ambiguous_org` and need the id. Sandboxes, snapshots, and environments stay creator-private. OrgHeader: name: X-Sandboxer-Org in: header required: false schema: { type: string } description: Same as the `org` query parameter. Query wins when both are set. schemas: StopInterval: type: object required: [from, to] properties: from: { type: string, format: date-time } to: type: [string, 'null'] format: date-time description: Null while the sandbox is still stopped. MetricPoint: type: object description: One bucket of averaged samples; percentages are 0–100. required: [t, cpu, mem, disk, io] properties: t: { type: string, format: date-time, description: Bucket start. } cpu: { type: number } mem: { type: number } disk: { type: number } io: { type: number } SuccessBase: type: object required: [ok, type] properties: ok: type: boolean examples: [true] type: type: string description: Stable success envelope discriminator added by v1. ErrorEnvelope: type: object required: [ok, type, status, code, message, error, requestId] properties: ok: type: boolean examples: [false] type: type: string examples: [sandbox.error] status: type: integer examples: [409] code: type: string examples: [provider_not_configured] message: type: string examples: [Prompting is locked until Codex is configured on the Agents page.] requestId: type: string examples: [req_01HX...] error: type: object required: [code, message, status] properties: code: type: string message: type: string status: type: integer details: type: object additionalProperties: true Sandbox: type: object required: [id, name, state, desktopAvailable, snapshotAvailable] properties: id: type: string pattern: '^sb_[a-z0-9]{10}$' examples: [sb_k3j9x2m1qa] name: type: string examples: [Sandboxer 2026-05-31 12:00] state: type: string enum: [init, provisioning, provisioned, cloning, ready, idle, running, archiving, archived, error, cancelled] description: >- `cancelled` is terminal: a create or fork that could not get a machine was removed. `GET /sandboxes/{sandboxId}` reports it once, with only `id`, `state` and `error`, then answers 404. error: type: [string, 'null'] description: Why the sandbox is stopped, failed or cancelled, or null. health: type: string enum: [ok, degraded] description: >- `degraded` when the machine is alive but Sandboxer cannot fully reach it (for example a firewall rule inside the sandbox blocks the tunnel). The sandbox keeps running. healthReason: type: [string, 'null'] description: Why the sandbox is degraded, or null. degradedSince: type: [string, 'null'] format: date-time description: When the sandbox became degraded, or null. hydrated: type: boolean description: >- True once every file of the sandbox's snapshot is on its disk. A resumed or forked sandbox is usable before that: its files keep copying in the background, and its docker volumes move onto the real disk near the end. Its docker containers start earlier; at that point their volume mounts move onto the real disk in place, without a restart. The `sandbox.hydrated` webhook fires at the same moment this turns true. A sandbox that restored nothing is hydrated from the start. hydratedAt: type: [string, 'null'] format: date-time description: When the last resume or fork finished copying files in, or null. type: type: string enum: [small, default, large, xlarge] description: >- Current machine size: what the sandbox was created with, or the size it was last resumed or forked onto. vcpu: type: integer description: vCPUs guaranteed by this sandbox's type. examples: [4] memoryGB: type: integer description: RAM in GB guaranteed by this sandbox's type. examples: [8] billingMultiplier: type: number description: Rate at which this sandbox consumes machine time. 0.5 for `small`, 1 for `default`, 2 for `large`, and 50/9 (about 5.56) for `xlarge`. examples: [1] machineProvider: type: [string, 'null'] enum: [hetzner, baremetal, null] description: Machine provider of the machine the sandbox is on, or null when it has no machine. url: type: [string, 'null'] format: uri description: Machine URL when assigned. ip: type: [string, 'null'] description: Machine IPv6 or IPv4 address when assigned. sshEndpoint: type: [string, 'null'] description: >- Public IPv4 `host:port` that forwards to the sandbox SSH server. Set only when the machine has no public IPv4 of its own; null otherwise. Connect with `ssh -p user@`. examples: ['203.0.113.10:22001'] createdAt: type: [string, 'null'] format: date-time updatedAt: type: [string, 'null'] format: date-time archiveAfter: type: [string, 'null'] format: date-time description: Automatic archival time, or null when auto-stop is disabled. desktopAvailable: type: boolean desktopUrl: type: [string, 'null'] format: uri description: Secret-bearing desktop stream URL when available. Redact from logs. snapshots: type: boolean description: False when the sandbox was created with snapshots off. monitoring: type: boolean description: False when the sandbox was created with monitoring off. snapshotAvailable: type: boolean snapshotCompletedAt: type: [string, 'null'] format: date-time description: Timestamp of the most recent successfully completed snapshot, or null. snapshotVerifiedAt: type: [string, 'null'] format: date-time description: >- Last time a snapshot confirmed the sandbox's saved state, including checks that found nothing new to save. Falls back to `snapshotCompletedAt`; null if never. team: type: [object, 'null'] description: The organization billed for this sandbox, or null when the owner is billed. required: [id, name] properties: id: { type: string } name: { type: string } createdBy: type: [string, 'null'] description: Display name (or email) of the person who created the sandbox. Organization members see every sandbox the organization pays for, so this says whose it is. createdById: type: [string, 'null'] description: Account id of the person who created the sandbox. access: type: string enum: [owner, use, view] description: >- What you may do with this sandbox. `owner`: you created it. `use`: a teammate's organization sandbox that is `noEnv` (created with it, or shared and restarted), so you can open, run commands in, prompt, resume and stop it. `view`: a teammate's organization sandbox that still holds their personal logins, so you can see it and stop it until they share it (`POST /sandboxes/{sandboxId}/share`). holdsCreatorLogins: type: boolean description: True while the sandbox still holds its creator's personal logins (GitHub token, model logins, secrets). Other members can only see such a sandbox. wipePendingUntilRestart: type: boolean description: True once the sandbox is shared but not restarted yet. Its creator's logins are wiped at its next start, and the organization can use it from then on. subdomain: type: [string, 'null'] description: The sandbox's stable three-word subdomain slug (e.g. "frazil-pneuma-rallye"), or null before one is assigned. lastSnapshotAttemptAt: type: [string, 'null'] format: date-time description: Timestamp of the most recent snapshot attempt of any status (queued, in_progress, completed, failed, cancelled), or null. Use with snapshotCompletedAt to detect snapshots that keep failing. lastSnapshotStatus: type: [string, 'null'] enum: [queued, in_progress, completed, failed, cancelled, null] description: Status of the most recent snapshot attempt, or null if none. A value other than completed while snapshotCompletedAt stays stale indicates failing snapshots. setupStatus: type: [string, 'null'] enum: [pending, running, done, failed, null] description: >- Outcome of the create-time `setupScript`: `pending` (stored, not yet started), `running` (executing on the sandbox in the background), `done` (exit code 0) or `failed` (non-zero exit, or the sandbox lost track of the process). Null when the sandbox was created without a setup script. setupError: type: [string, 'null'] description: Short failure detail (exit code plus a stderr tail) when `setupStatus` is `failed`; while `pending`, may carry the last start/upload error from a retry in progress. Otherwise null. environment: type: [string, 'null'] description: >- Name of the sandbox environment this sandbox is running, or null if it is attached to none (a `noEnv` sandbox, or one whose environment was deleted). A sandbox freezes onto one environment version when it starts and keeps it for life, so this is what the sandbox actually holds, not what the environment says today. examples: [base] environmentVersion: type: [integer, 'null'] description: >- Version number of `environment` that this sandbox is pinned to. Compare it against the environment's latest version to see whether an upgrade is pending: a sandbox below the latest is still running the older configuration until someone calls `POST /environments/{environmentId}/upgrade`. examples: [3] stop: description: The last stop you requested (`POST /sandboxes/{sandboxId}/stop`), or null if you never stopped this sandbox. oneOf: - $ref: '#/components/schemas/StopOperation' - type: 'null' idleStopMinutes: type: integer description: >- Minutes without activity before the sandbox pauses itself (stops with memory, waking on use). `0` never pauses. examples: [30] lastActivityAt: type: [string, 'null'] format: date-time description: >- The last activity that counts toward idle pause (written at most every 30 s; hosted-URL traffic arrives with the node's next heartbeat). memoryRestored: type: [boolean, 'null'] description: >- Whether the last resume restored memory: `true` (running processes kept) or `false` (booted from disk: files intact, running processes not kept). Null before the first resume. resumeReason: type: [string, 'null'] enum: [cpu_mismatch, resized, no_memory_snapshot, restore_failed, null] description: >- Why the last resume booted from disk: `cpu_mismatch` (no machine with the sandbox's CPU type was free), `resized` (a new size always boots from disk), `no_memory_snapshot` (the stop kept no memory), `restore_failed`. Null when memory was restored. stoppedAt: type: [string, 'null'] format: date-time description: When the sandbox last stopped. Null while it is not stopped. dataExpiredAt: type: [string, 'null'] format: date-time description: >- When retention deleted the data of this stopped sandbox, 14 days after it stopped (sandboxes created on or after the Terms date; older ones are kept until deleted). Resume and fork then answer `409 snapshot_data_expired`. Null while the data is kept. region: type: [string, 'null'] description: The sandbox's region (`us-east`, `eu-central`). regionSource: type: [string, 'null'] enum: [requested, nearest, default, source, null] description: >- How the region was chosen: `requested` (named on create or fork), `nearest` (the caller's nearest region), `default` (no nearest region with a machine), `source` (a fork, or a sandbox made from a snapshot, in its source's region). Null for sandboxes from before regions. StopOperation: type: object required: [id, status, requestedAt, lastAttemptAt, error, endedAt] properties: id: type: string description: Stable id of this stop. Calling stop again while it is pending returns the same id. examples: [stop_3f9a1c2e7b4d5a60] status: type: string enum: [pending, failing, completed, superseded] description: >- `pending`: accepted, the sandbox is archiving. `failing`: the last attempt failed (see `error`); it is retried automatically until the sandbox archives. Running commands or prompts does not cancel it. `completed`: the sandbox is archived. `superseded`: a resume ended it before it completed. requestedAt: type: [string, 'null'] format: date-time description: When the stop was accepted. On the sandbox, null once it has ended. Read by id (`GET /sandboxes/{sandboxId}/stops/{stopId}`), it is kept. lastAttemptAt: type: [string, 'null'] format: date-time description: When the last archival attempt ended. error: type: [string, 'null'] description: Why the last attempt failed, while `status` is `failing`. Otherwise null. endedAt: type: [string, 'null'] format: date-time description: When the stop completed or was superseded. ApiKey: type: object required: [id, name, credentialLane, keyPrefix, keyLastFour, sandboxId, createdAt, lastUsedAt, usage, resources] properties: id: type: string examples: [sak_123] name: type: string examples: [Production worker] credentialLane: type: string enum: [legacy, scoped-v1] description: Credential storage lane. Scoped secrets are never stored in the legacy hash column. keyPrefix: type: string examples: [sandbox_live] keyLastFour: type: string examples: [9abc] sandboxId: type: [string, 'null'] description: Sandbox ID for a platform-managed machine key, or null for a user-created key. createdAt: type: string format: date-time lastUsedAt: type: [string, 'null'] format: date-time usage: $ref: '#/components/schemas/ApiKeyRequestUsage' resources: $ref: '#/components/schemas/ApiKeyResourceTotals' expiresAt: type: [string, 'null'] format: date-time expired: type: boolean expiringSoon: type: boolean scope: type: object properties: expiresAt: type: [string, 'null'] format: date-time actions: type: array items: { type: string } sandboxes: oneOf: - { type: string, enum: ['*'] } - { type: array, items: { type: string } } environments: oneOf: - { type: string, enum: ['*'] } - { type: array, items: { type: string } } grandfathered: type: boolean ApiKeyRequestUsage: type: object required: [requests, windowDays] properties: requests: type: integer minimum: 0 description: Requests authenticated with this key during the current UTC day and the previous 29 UTC days. examples: [1842] windowDays: type: integer const: 30 ApiKeyResourceTotals: type: object required: [total, sandboxes, agents] properties: total: type: integer minimum: 0 sandboxes: type: integer minimum: 0 agents: type: integer minimum: 0 ApiKeyCreatedResource: type: object required: [kind, id, name, state, createdAt] properties: kind: type: string enum: [sandbox, agent] id: type: string name: type: string state: type: string createdAt: type: [string, 'null'] format: date-time WebhookEventType: type: string enum: [sandbox.ready, sandbox.error, sandbox.archived, sandbox.hydrated, sandbox.degraded, sandbox.recovered] Webhook: type: object required: [id, name, url, events, createdAt, updatedAt] properties: id: type: string pattern: '^wh_[a-f0-9]{24}$' examples: [wh_0123456789abcdef01234567] name: type: [string, 'null'] maxLength: 100 examples: [Production automation] url: type: string format: uri description: Public HTTPS endpoint on port 443. examples: [https://example.com/hooks/sandbox] events: type: array minItems: 1 uniqueItems: true items: $ref: '#/components/schemas/WebhookEventType' createdAt: type: string format: date-time updatedAt: type: string format: date-time WebhookCreateRequest: type: object required: [url, events] properties: name: type: [string, 'null'] minLength: 1 maxLength: 100 url: type: string format: uri events: type: array minItems: 1 uniqueItems: true items: $ref: '#/components/schemas/WebhookEventType' WebhookUpdateRequest: type: object minProperties: 1 properties: name: type: [string, 'null'] maxLength: 100 url: type: string format: uri events: type: array minItems: 1 uniqueItems: true items: $ref: '#/components/schemas/WebhookEventType' WebhookEvent: type: object required: [id, type, createdAt, data] description: JSON body sent to the registered endpoint. This is not a v1 success envelope. properties: id: type: string pattern: '^evt_[a-f0-9]{32}$' type: $ref: '#/components/schemas/WebhookEventType' createdAt: type: string format: date-time data: type: object required: [sandbox] properties: sandbox: type: object required: [id, name] properties: id: type: string pattern: '^sb_[a-z0-9]{10}$' name: type: string previousState: type: string description: On sandbox.ready, sandbox.error and sandbox.archived only. state: type: string description: Absent on sandbox.hydrated. reason: type: [string, 'null'] description: On sandbox.degraded and sandbox.recovered. Why the sandbox is degraded (null on recovered). Repository: type: object additionalProperties: true properties: id: type: integer description: GitHub repository id. databaseId: type: string description: Internal repository id used when selecting repositories. name: type: string fullName: type: string examples: [acme/web] description: type: [string, 'null'] url: type: string format: uri examples: [https://github.com/acme/web] private: type: boolean permissions: type: string pushedAt: type: [string, 'null'] format: date-time lastUserCommitAt: type: [string, 'null'] format: date-time description: Reserved; always null. SelectedRepository: allOf: - $ref: '#/components/schemas/Repository' - type: object properties: baseBranch: type: string examples: [main] setupRoutineId: type: [string, 'null'] setupScript: type: string setupBlocking: type: boolean RepositoryInstallation: type: object properties: type: type: string examples: [Organization] accountLogin: type: string examples: [acme] accountAvatarUrl: type: [string, 'null'] format: uri repositories: type: array items: $ref: '#/components/schemas/Repository' SecretFile: type: object required: [path, contents] properties: path: type: string examples: [.config/service-account.json] contents: type: string description: Secret file contents. Treat as sensitive. StartWindowUsage: type: [object, 'null'] description: One rolling start window. Null when the account is unlimited. properties: limit: { type: integer } used: { type: integer } remaining: { type: integer } LimitsFields: type: object required: [activeSandboxes, maxActiveSandboxes, canStart, billingStatus] additionalProperties: true properties: storage: { $ref: '#/components/schemas/Storage' } accessTier: type: string examples: [trial] blockedReason: type: [string, 'null'] currentLimits: type: object additionalProperties: true properties: activeSandboxes: { type: integer } creationRatePerMinute: { type: integer } creationRequestsPerHour: { type: [integer, 'null'] } creationRequestsPerDay: { type: [integer, 'null'] } standardLimits: type: object additionalProperties: true properties: activeSandboxes: { type: integer } creationRatePerMinute: { type: integer } creationRequestsPerHour: { type: [integer, 'null'] } creationRequestsPerDay: { type: [integer, 'null'] } trialLimits: type: object additionalProperties: true properties: activeSandboxes: { type: integer } creationRatePerMinute: { type: integer } creationRequestsPerHour: { type: [integer, 'null'] } creationRequestsPerDay: { type: [integer, 'null'] } upgradeEffects: type: object additionalProperties: true canStart: type: boolean description: Whether the authenticated account can create or operate sandboxes right now. checkoutRequired: type: boolean startBlockedReason: type: [string, 'null'] contactMessage: type: [string, 'null'] activeSandboxes: type: integer activeStates: type: array items: type: string maxActiveSandboxes: type: integer maxCreationRequestsPerMinute: type: integer maxCreationRequestsPerDay: type: [integer, 'null'] startLimits: type: [object, 'null'] description: Plan caps for machine starts. Null on unlimited accounts. Create, fork and resume each count as one start. properties: perMinute: { type: integer } perHour: { type: integer } perDay: { type: integer } starts: type: object description: Remaining machine starts in the rolling minute, hour and day windows. Null windows mean the account is unlimited. properties: unlimited: { type: boolean } minute: $ref: '#/components/schemas/StartWindowUsage' hour: $ref: '#/components/schemas/StartWindowUsage' day: $ref: '#/components/schemas/StartWindowUsage' creditBalanceHours: type: [number, 'null'] description: Remaining machine time in hours (`creditBalanceSeconds / 3600`). Null on unlimited accounts. packBalanceHours: type: number description: Remaining purchased credit packs in hours. packBalanceDollars: type: number description: Remaining purchased credit packs in dollars. hasPaymentHistory: type: boolean package: type: object additionalProperties: true subscriptionQuotaSeconds: type: integer subscriptionRemainingSeconds: type: integer packBalanceSeconds: type: integer creditPurchasedSeconds: type: integer creditUsedSeconds: type: integer liveUsageSeconds: type: integer creditSecondsPerDollar: type: integer sandboxPlanKey: type: string description: Current plan key, or `trial` before any payment. examples: [box_20] sandboxPlanDollars: type: number description: Monthly price of the current plan in dollars. sandboxPlanTiers: type: array description: Every plan tier and its limits. items: type: object properties: key: type: string examples: [box_20] dollars: { type: number } displayPrice: type: string examples: [$20/mo] maxActiveSandboxes: { type: integer } startsPerMinute: { type: integer } startsPerHour: { type: integer } startsPerDay: { type: integer } includedSeconds: type: integer description: Machine time included each month. purchasable: type: boolean description: Whether this tier can be bought right now. accountPlan: type: string examples: [user, service] plan: type: [string, 'null'] description: '`service` for service accounts, otherwise null.' planName: type: [string, 'null'] description: '`Service` for service accounts, otherwise null.' serviceAccount: type: boolean unlimited: type: boolean description: True when the account has no machine-time or start limits. hasSeatPlan: type: boolean description: True when an organization has an active per-seat plan. last24hUsageSeconds: type: integer description: Machine time used in the last 24 hours, in seconds. Useful to estimate how long the balance lasts. trialComputeCapSeconds: type: [integer, 'null'] description: Lifetime machine-time allowance for a trial that never paid. Null once the account has paid or has a plan. trialLine: type: [string, 'null'] description: The trial limits in one readable line. Null on paid accounts. giftLimit: type: [object, 'null'] description: Fair-use caps on a gifted account, or null when there are none. properties: maxActiveSandboxes: { type: [integer, 'null'] } maxTtlSeconds: { type: [integer, 'null'] } line: type: [string, 'null'] description: The caps in one readable line. message: type: string description: The same text shown when a cap is hit. billingStatus: type: string description: Account access state returned by the current backend. Billing endpoints are not part of v1. subscriptionStatus: type: [string, 'null'] subscriptionCancelAtPeriodEnd: type: boolean hasSubscription: type: boolean subscriptionTrialEndsAt: type: [string, 'null'] format: date-time subscriptionCurrentPeriodEnd: type: [string, 'null'] format: date-time creditBalanceSeconds: type: integer teamId: type: string description: Present when limits were read for a team wallet (`?teamId=`, `?org=`, or `X-Sandboxer-Org`). teamRole: type: string description: Caller's role on that team when `teamId` is present. CreateSandboxRequest: type: object description: Options for provisioning a new cloud computer. properties: type: type: string enum: [small, default, large, xlarge] default: default description: >- Machine size. `small` consumes machine time at half rate and `large` at twice the default rate (see the Billing guide). `xlarge` needs the $100/month plan or higher; lower plans get a 403 `machine_class_plan_required` on create, resume and fork. A fork inherits the source sandbox's type unless the fork request passes its own, and resume and fork can move a sandbox between sizes. region: type: string examples: [us-east] description: >- Region to run in (`GET /regions` lists them: `us-east` Ashburn, Virginia; `eu-central` Helsinki, Finland). Omitted, the sandbox goes to the region nearest the caller (a country lookup of the request's IP), else to `us-east`. An unknown region is `400 invalid_region`; one with no machine available is `503 region_unavailable`. A sandbox made from a snapshot starts in the snapshot's region (another is `400 region_mismatch`). Resume and fork stay in the sandbox's region. ttlSeconds: oneOf: - type: integer minimum: 1 maximum: 2592000 - type: 'null' default: 3600 description: Number of seconds before automatic archival. `null` disables auto-stop. The backend also accepts the string `infinite` for legacy compatibility; new clients should send null. env: type: object additionalProperties: type: string description: >- Per-sandbox environment variables injected into the sandbox's tool environment, on top of the account environment's variables (per-sandbox values win on conflicts). Keys must match `[A-Za-z_][A-Za-z0-9_]{0,127}`; at most 100 variables and 64KB total. Reserved names (`SANDBOXER_TOKEN`, `SANDBOXER_API_URL`, `AGENT_ID`, `PRODUCT_MODE`, `ENVIRONMENT_ID`, `SANDBOXER_ID`, `SERVICE_PREVIEW_TOKEN`, `SANDBOXER_CLI_TOKEN`) are rejected with `invalid_env`. Forked sandboxes inherit the source sandbox's env unless the fork request supplies its own `env`. environment: type: string default: base description: >- Name of the sandbox environment to attach to this sandbox. Environments are managed in the Sandboxer dashboard and bundle the repositories, secrets, and credential toggles a sandbox gets. Omit to use your default environment (`base` unless you changed it). Unknown names are rejected with `unknown_environment`. An environment marked "safe for third parties" passes nothing to the sandbox, exactly like `noEnv`. examples: [base, customer-demos] noEnv: type: boolean default: false description: >- Create a sandbox with none of the secrets attached to your account (no environment variables, secret files, or credentials), confined to itself so it cannot act on your account or other sandboxes. For sandboxes you give to your own users. SSH, SCP, desktop, snapshots, and public URLs still work; pass `env` to give the sandbox a secret of its own. A fork of a no-env sandbox is always no-env. Equivalent to attaching an environment marked "safe for third parties". snapshots: type: boolean default: true description: >- Pass `false` to never snapshot this sandbox. Nothing is captured in the background, so it pays no CPU or memory for it, and stopping it erases its disk the way zero data retention does. It cannot be resumed, forked, or saved as a named snapshot, and if its machine fails there is no backup to rebuild it from. monitoring: type: boolean default: true description: >- Pass `false` for monitoring off. Sandboxer keeps no command or process output, process list or CPU/RAM samples for this sandbox, and its history, processes and metrics calls answer 409 `monitoring_off`. Live output of a running command still works. While the account default is on, `true` is refused with 409 `monitoring_off_for_account`. failFast: type: boolean default: false description: >- Pass `true` to get a sandbox only if a machine of this type is ready right now. The call answers within about 1.5 s: either the usual `202` with the sandbox, or `503` `no_ready_machine` with nothing created, billed, or counted against your start limits, so you can retry or fall back at once. Without it, a create waits for a machine to be built when none is ready. Works with `from`; fork and resume take it too. setupScript: type: string maxLength: 65536 description: >- Shell script that runs on the sandbox after it is ready. Ready means "ready to accept the user", not "setup done": the script starts in the background once provisioning completes and never blocks the sandbox becoming usable. It runs as the sandbox user (`user`, not root; `sudo` needs no password) via `bash`, with the sandbox's environment applied, and its output goes to a log file on the sandbox. Observe the outcome as `setupStatus` (pending/running/done/failed) and `setupError` on the sandbox. Rejected with a 400 `invalid_setup_script` error when it is not a string or exceeds 64KB. org: type: string description: >- Bill this sandbox to an organization you belong to (its shared wallet), by id or by name as `GET /orgs` lists it; `personal` or your own account id means personal billing. Omitted, the `X-Sandboxer-Org` / `?org=` request scope applies, then the account's active wallet (`PATCH /orgs/active`), then personal. Listing, snapshots, and environments stay yours: the org is a wallet, not a shared workspace. Takes precedence over `teamId`. teamId: type: string description: Legacy alias for `org`. Ignored when `org` is also set. from: type: string description: >- Create the sandbox from a named snapshot (saved with `POST /named-snapshots`, or `sbx snapshot ` in the CLI). The sandbox starts from that exact frozen state. Omitting `type` inherits the type the snapshot was saved from; env and no-env inherit from the snapshot's source sandbox unless the request passes its own, with the same rules as forking. Also accepts a share code (`bsh_...`) that another account made with `POST /named-snapshots/{name}/shares`. The new sandbox lives in and is billed to your account. It does not get the owner's per-sandbox env; your own environment applies normally. A wrong or revoked code returns `404 snapshot_not_found`. idleStopMinutes: type: integer minimum: 0 maximum: 10080 description: >- Pause (stop with memory) after this many minutes without activity; the sandbox wakes by itself on the next API call or hosted-URL request. `0` never pauses. Defaults to your plan's (15 on the free trial, 30 on $20, 60 on $100 and $500, 120 on $2,000). Must be `0` when snapshots are off or the account has zero data retention (400 `invalid_idle_stop`). examples: - ttlSeconds: 3600 - ttlSeconds: null - type: large ttlSeconds: 3600 - ttlSeconds: 3600 env: DATABASE_URL: postgres://user:pass@host:5432/app FEATURE_FLAG: "1" - ttlSeconds: null noEnv: true StopRequest: type: object description: Options for stopping a sandbox. properties: force: type: boolean default: false description: >- Stop the sandbox even if its disk cannot be snapshotted. It keeps its last successful snapshot and everything written since then is permanently LOST. Without this, a sandbox whose snapshot pipeline is failing is left running rather than discarding your work, and you are not billed for that time. Only use this after a stop has already been refused. ResumeRequest: type: object description: Options for resuming a stopped sandbox. properties: failFast: type: boolean default: false description: >- Pass `true` to resume only if a machine is ready right now. Within about 1.5 s you get the usual `202`, or `503` `no_ready_machine` with the sandbox left stopped and nothing counted against your start limits. requireMemory: type: boolean default: false description: >- Never boot from disk. Without it, a resume waits up to 2 minutes for a busy machine with the sandbox's CPU type, then boots from disk elsewhere (`memoryRestored: false`). With it, the resume waits up to 30 minutes for such a machine, then fails with `memory_unavailable` and the sandbox stays stopped. It can't be combined with a size change (400 `invalid_require_memory`), because a new size always boots from disk. type: type: string enum: [small, default, large, xlarge] description: >- Resume onto a different machine size. Omit to keep the sandbox's current type. `xlarge` needs the $100/month plan or higher (403 `machine_class_plan_required`). A resume already restores onto a fresh machine, so changing size costs nothing extra. Shrinking is rejected with `type_too_small` when the sandbox's data would not fit the smaller disk. env: type: object additionalProperties: type: string description: >- Replaces the sandbox's per-sandbox environment variables. Omit to keep the sandbox's current env. Same validation rules as `CreateSandboxRequest.env`. environment: type: string description: >- Optionally re-pin the sandbox to a different named sandbox environment on resume. Omit to keep the sandbox's current environment. Unknown names are rejected with `unknown_environment`. examples: [base, customer-demos] noEnv: type: boolean description: >- Resume into a no-env sandbox: withhold account secrets and scrub inherited owner secrets from the restored snapshot before the sandbox is exposed. One-way; once a sandbox is resumed as no-env it stays no-env on later resumes. ttlSeconds: oneOf: - type: integer minimum: 1 maximum: 2592000 - type: 'null' description: >- Auto-stop for the resumed sandbox, in seconds, stored on the sandbox. Omit to keep the TTL the sandbox already had. `null` disables auto-stop, which means nothing will ever stop this sandbox for you. examples: - ttlSeconds: 28800 - type: large ttlSeconds: 3600 UpdateSandboxRequest: type: object properties: name: type: string minLength: 1 maxLength: 120 description: New display name. Empty strings are rejected; longer names are truncated to 120 chars by the backend. ttlSeconds: oneOf: - type: integer minimum: 1 maximum: 2592000 - type: 'null' description: >- New auto-stop TTL. The deadline becomes now + `ttlSeconds`, so calling this again extends a running sandbox's life. `null` disables auto-stop. subdomain: type: string minLength: 3 maxLength: 40 pattern: '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' description: >- Rename the sandbox's stable subdomain (the `.box.sandboxer.com` label). Lowercase letters, digits and hyphens; no leading/trailing/double hyphens; cannot end in `-desktop` or `-` (reserved for the desktop and hosted-port URLs). Must be globally unique. The base URL, desktop URL and every live `host ` URL are re-pointed to the new name with no downtime and their access tokens preserved; the old URLs stop resolving. On a transient routing error the rename is saved but returns 502 `gateway_error` - retry with the same value to finish activating routes. idleStopMinutes: type: integer minimum: 0 maximum: 10080 description: >- Pause (stop with memory) after this many minutes without activity; the sandbox wakes by itself on the next API call or hosted-URL request. `0` never pauses. Defaults to your plan's (15 on the free trial, 30 on $20, 60 on $100 and $500, 120 on $2,000). Must be `0` when snapshots are off or the account has zero data retention (400 `invalid_idle_stop`). examples: - name: Customer support run ttlSeconds: 7200 - subdomain: acme-staging PromptRequest: type: object description: | Work item to queue inside an existing sandbox. Provider credentials must already be configured in the Sandboxer dashboard. **Providers (harnesses):** `codex`, `claude-code` (alias `claude`), `pi`, `opencode`, `prime-agent` (alias `prime`), `kimi` (Kimi Code CLI, alias `kimi-code`), `mistral` (Mistral Vibe). Omit `provider` to use the harness the user selected on the Agents dashboard. **Models & reasoning effort** are harness- and model-specific and change over time. Fetch the live catalog (every provider's models and which reasoning-effort levels each accepts) from `GET /provider-models`. Some models accept no reasoning control. `pi`, `opencode` and `prime-agent` also expose models routed through OpenRouter and llmgateway (ids like `openrouter:anthropic/claude-sonnet-4.5`). **Conversations:** a sandbox runs many conversations in parallel, each with its own history. Set `new: true` to start a fresh conversation, or `conversationId` to continue a specific one; omit both to continue the sandbox's most-recently-active conversation. The response returns the `conversationId` this prompt ran in. Conversations run concurrently up to a per-sandbox limit that scales with the sandbox's memory; beyond it, prompts queue. required: [provider, prompt] properties: provider: type: string enum: [codex, claude-code, claude, pi, opencode, prime-agent, prime, kimi, kimi-code, mistral] model: type: [string, 'null'] description: Optional provider model id from `GET /provider-models`. Omit to use the model selected for that harness on the Agents dashboard. Unknown explicit ids are currently forwarded rather than rejected by request validation. examples: [gpt-5.6-terra, claude-sonnet-5, 'openrouter:anthropic/claude-sonnet-4.5'] reasoningEffort: type: [string, 'null'] description: Optional reasoning/thinking level (e.g. `none`, `low`, `medium`, `high`, `xhigh`, `max`). Which levels a given model accepts is listed per model in `GET /provider-models`; some models accept none. examples: [high] fast: type: [boolean, 'null'] description: Optional. `true` runs this prompt in fast mode. The model stays the same and gives output faster. The provider bills fast mode at its higher fast rate. Omit to use the choice saved on the Agents dashboard for that model. `false` forces standard speed. Only models with `fastMode` set to `true` in `GET /provider-models` accept `true`. Other models return 400 `fast_mode_not_supported`. examples: [true] new: type: boolean description: Start a NEW conversation on the sandbox (runs in parallel with any existing ones) instead of continuing the most-recently-active one. Mutually exclusive with `conversationId`. examples: [true] conversationId: type: [string, 'null'] description: Continue a specific conversation by id (as returned by a previous prompt). Omit (and omit `new`) to continue the sandbox's most-recently-active conversation. format: uuid prompt: type: string minLength: 1 description: Natural-language task for the sandbox, including repo, preview, or browser-use instructions. examples: - provider: codex model: gpt-5.4 reasoningEffort: medium prompt: Work on the selected repo, run tests, fix failures, commit the result, and report any hosted preview URL. - provider: claude new: true prompt: Start a second, independent task on the auth module while the first keeps running. SteerRequest: type: object description: | A message for a turn that is ALREADY RUNNING. The agent takes it into account and keeps what it was doing, instead of queueing behind the turn (`POST /prompt`) or stopping it (`POST /interrupt`). **Conversations:** omit `conversation` to steer the sandbox's most-recently-active conversation, exactly like `POST /prompt` chooses one. Pass a `conversation` id to steer a specific one while the others keep running. The response's `native` says how the harness took it. See [Integrated agents](/integrated-agents). required: [message] properties: message: type: string minLength: 1 description: The extra instruction for the running turn. conversation: type: [string, 'null'] description: Conversation id to steer. Omit to steer the sandbox's most-recently-active conversation. format: uuid examples: - message: Also write /home/user/steered.txt containing the word STEERED, then finish. - conversation: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c message: Skip the integration tests, unit tests are enough. SteerResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, conversationId, promptId, native, status] properties: type: type: string const: prompt.steered id: type: string description: Sandbox id. conversationId: type: string description: The conversation whose running turn was steered. promptId: type: string description: Id of the steer record. It appears in `GET /events` as a `steer` event; it is not a prompt run and has no lifecycle to poll. native: type: boolean description: >- `true` when the harness accepted the message into the turn that was already running, so nothing was interrupted. `false` when Sandboxer interrupted that turn and immediately continued the SAME conversation with the instruction, which keeps the harness session and all of its memory but may repeat a little of the work in flight. mode: type: string enum: [native, fallback, late] description: >- How the message was delivered. `native` and `fallback` mirror the `native` field; `late` means the turn finished between the request and its delivery, so the message ran as an ordinary new turn on the conversation. A steer reported `native` here can settle as `native-continued` on the `steer` event: the harness accepted it but its turn ended without acting on it, so Sandboxer continued it immediately as its own turn on the same session. Read the event for the settled mode. status: type: string enum: [steered] RepoSelectionRequest: type: object required: [repositoryId] description: Idempotently selects a repository for future sandboxes. If the repository is already selected, the API updates its base branch instead of returning a conflict. properties: repositoryId: type: string description: Internal repository `databaseId` returned by `GET /repos`. baseBranch: type: string default: main examples: - repositoryId: repo_org_123 baseBranch: dev SecretsUpdateRequest: type: object description: Full replacement for the sandbox secret setup. Omitted `envContents` or `secretFiles` are treated as empty values, and successful updates are pushed to active sandboxes. properties: envContents: type: string description: Full .env-style content to sync into sandboxes. Send the complete desired file contents, not a patch. secretFiles: type: array description: Full list of secret files to keep configured. Send existing files again if they should remain. items: $ref: '#/components/schemas/SecretFile' examples: - envContents: "OPENAI_API_KEY=sk-...\nDATABASE_URL=postgres://..." secretFiles: - path: .config/service-account.json contents: '{"type":"service_account"}' SandboxEnvironmentVersionSummary: type: object required: [id, versionNumber, sandboxCount, createdAt] description: An immutable snapshot of an environment's config. Editing an environment mints a new version; sandboxes stay pinned to the version they were created on until upgraded. properties: id: type: string format: uuid versionNumber: type: integer description: Monotonically increasing per environment; version 1 is the first. sandboxCount: type: integer description: Number of the caller's active sandboxes currently pinned to this version. createdAt: type: string format: date-time SandboxEnvironment: type: object required: [id, name, isDefault, latestVersionId, safeForThirdParties, passGithub, passSecrets, passSandboxCredentials, passAgentsCredentials, envContents, secretFiles, versions] description: A named sandbox environment. All flags/contents reflect the latest version. properties: id: type: string format: uuid name: type: string examples: [base, customer-demos] isDefault: type: boolean description: Exactly one environment is the default; sandboxes created without an `environment` name use it. latestVersionId: type: [string, 'null'] format: uuid latestVersionNumber: type: integer description: Number of the latest version, 0 when there is none. examples: [3] safetyAnsweredAt: type: [string, 'null'] format: date-time description: When the owner last answered whether this environment is safe for third parties, or null if never asked. safeForThirdParties: type: boolean description: When true the environment passes nothing to a sandbox (repos, secrets, and all credentials withheld), overriding the fine-grained flags below. Use for sandboxes handed to third parties. passGithub: type: boolean description: Attach the environment's GitHub repositories and the GitHub token (so `gh` and pushes work). Ignored when `safeForThirdParties` is true. passSecrets: type: boolean description: Attach the environment's env variables and secret files. Ignored when `safeForThirdParties` is true. passSandboxCredentials: type: boolean description: Attach the sandbox's own service/preview credentials. Ignored when `safeForThirdParties` is true. passAgentsCredentials: type: boolean description: Attach agent-provider credentials configured on the Agents page. Ignored when `safeForThirdParties` is true. envContents: type: string description: The latest version's .env-style content. Treat as sensitive. secretFiles: type: array items: $ref: '#/components/schemas/SecretFile' selectedRepositories: type: array description: Repositories attached to the latest version, with base branch and setup script. items: $ref: '#/components/schemas/SelectedRepository' versions: type: array items: $ref: '#/components/schemas/SandboxEnvironmentVersionSummary' SandboxEnvironmentListResponse: type: object required: [environments] properties: ok: type: boolean examples: [true] type: type: string const: environments.list environments: type: array items: $ref: '#/components/schemas/SandboxEnvironment' SandboxEnvironmentResponse: type: object required: [success] properties: success: type: boolean environment: oneOf: - $ref: '#/components/schemas/SandboxEnvironment' - type: 'null' CreateSandboxEnvironmentRequest: type: object required: [name] properties: name: type: string maxLength: 64 pattern: '^[a-zA-Z0-9._-]+$' description: Unique environment name. Letters, numbers, dot, dash, underscore; max 64 chars. examples: [customer-demos] UpdateSandboxEnvironmentRequest: type: object description: Rename, set-default, and/or edit flags and contents. Any flag or content change mints a new immutable version; existing sandboxes are not touched until you call upgrade. properties: name: type: string description: New environment name. isDefault: type: boolean description: Set to true to make this the default environment (clears the flag on the previous default). safeForThirdParties: { type: boolean } passGithub: { type: boolean } passSecrets: { type: boolean } passSandboxCredentials: { type: boolean } passAgentsCredentials: { type: boolean } envContents: type: string description: Full .env-style content for the new version. secretFiles: type: array items: $ref: '#/components/schemas/SecretFile' repositories: type: array description: Full replacement of the version's repository selection. Each item selects one repository by its internal `databaseId` (from `GET /repos`). items: type: object required: [repositoryId] properties: repositoryId: type: string description: Internal repository databaseId. baseBranch: type: string default: main setupScript: type: string setupBlocking: type: boolean UpgradeSandboxEnvironmentRequest: type: object description: Repoint active sandboxes to the environment's latest version, scrubbing any owner secrets the new version drops and hot-pushing the new config. properties: agentIds: type: array items: type: string description: Restrict the upgrade to these sandbox (agent) ids. Omit to upgrade all of the caller's active sandboxes that are on an older version of this environment. UpgradeSandboxEnvironmentResponse: type: object required: [success, upgraded, failed] properties: success: type: boolean upgraded: type: integer upgradedLive: type: integer description: Running sandboxes that received the new configuration now. upgradedStopped: type: integer description: Stopped sandboxes moved to the latest version; they pick it up when they next resume. failed: type: integer latestVersionId: type: string description: Id of the version the sandboxes were moved to. EnvironmentItemChangeResponse: type: object description: >- Result of a granular environment change. Every change mints a new immutable version holding just that delta; existing sandboxes stay pinned until upgraded. required: [success] properties: success: type: boolean versionId: type: string description: >- Id of the newly minted environment version. This is a version id, not the environment's own id: the environment id you passed in the path is unchanged. versionNumber: type: integer SshKeyRequest: type: object required: [key] properties: key: type: string description: Public SSH key in OpenSSH format. Private keys are rejected. examples: [ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@host] DesktopRequest: type: object additionalProperties: true description: Optional desktop/VNC setup options used by the Sandboxer dashboard and CLI. Most callers send an empty body. properties: publicAccess: type: boolean default: false description: For `?vnc=1`, return a noVNC URL that does not require an access token. HostPortRequest: type: object required: [port] properties: port: type: integer minimum: 1 maximum: 65535 description: Port the service listens on inside the sandbox. public: type: boolean default: false description: Return an ungated URL instead of one that requires the `_token` query parameter. localhost: type: boolean default: false description: Set when the service listens on localhost (`127.0.0.1` or `::1`) only. The sandbox relays the URL to it. Leave unset for a service bound to `0.0.0.0`. Each call sets the value again. title: type: string description: Display title for the hosted port. MeResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [user] properties: type: type: string const: user.info user: type: object properties: id: type: string description: Your account id. login: type: string email: type: [string, 'null'] displayName: type: string connectionMethod: type: string enum: [github, google, email, service] description: The login method the account was created with. connectionMethods: type: array description: Every login method linked to the account. items: type: string examples: [github] accountPlan: type: string examples: [user, service] serviceAccount: type: boolean accountStatus: type: string enum: [active, suspended, closed] suspended: type: boolean suspendedAt: type: [string, 'null'] format: date-time suspendedReason: type: [string, 'null'] closed: type: boolean closedAt: type: [string, 'null'] format: date-time purgeAt: type: [string, 'null'] format: date-time description: When a closed account's data is scheduled for deletion. purgedAt: type: [string, 'null'] format: date-time description: When a closed account's data was deleted. closureContactEmail: type: string description: Address to contact about a suspended or closed account. zeroDataRetention: type: boolean description: Whether archived sandbox data is configured for deletion instead of retention. zeroDataRetentionEnabledAt: type: [string, 'null'] format: date-time Org: type: object required: [id, name, type, role, memberCount, active] properties: id: type: string description: Your account id for the personal row, `team_…` for an organization. name: type: string description: Accepted wherever an org is passed, like the id. type: type: string enum: [personal, org] role: type: string enum: [owner, member] memberCount: type: integer subscriptionStatus: type: string description: Stripe status of the organization's plan; absent on the personal row. active: type: boolean description: The wallet new sandboxes bill when a request names none. OrgListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [orgs, activeWalletChosen] properties: type: type: string const: org.list activeWalletChosen: type: boolean description: Whether this account ever set its active wallet. `false` means personal by default, never a choice. orgs: type: array items: { $ref: '#/components/schemas/Org' } ActiveOrgUpdateRequest: type: object required: [org] properties: org: type: [string, 'null'] description: Organization id or name you belong to; `personal`, your own id or `null` for personal. ActiveOrgResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [active] properties: type: type: string const: org.active_updated active: type: object required: [id, name, type] properties: id: { type: string } name: { type: string } type: type: string enum: [personal, org] LimitsResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - $ref: '#/components/schemas/LimitsFields' ReposResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [installations, environmentId, selectedRepositories] properties: installations: type: array items: $ref: '#/components/schemas/RepositoryInstallation' environmentId: type: string subenvironmentId: type: [string, 'null'] description: Id of the default environment. `environmentId` is the id of its latest version. selectedRepositories: type: array items: $ref: '#/components/schemas/SelectedRepository' pageInfo: $ref: '#/components/schemas/PageInfo' RepoSelectionResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [success, environmentId, selectedRepositories] properties: success: type: boolean environmentId: type: string subenvironmentId: type: [string, 'null'] description: Id of the default environment. `environmentId` is the id of its new version. selectedRepositories: type: array items: $ref: '#/components/schemas/SelectedRepository' SecretsResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [environmentId, envContents, secretFiles] properties: success: type: boolean environmentId: type: string subenvironmentId: type: string description: Id of the default environment. `environmentId` is the id of its latest version. envContents: type: string secretFiles: type: array items: $ref: '#/components/schemas/SecretFile' pushed: type: object additionalProperties: true description: Present on update; counts how many active sandboxes received the new environment. SandboxListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [sandboxes] properties: type: type: string const: sandbox.list sandboxes: type: array items: $ref: '#/components/schemas/Sandbox' pageInfo: $ref: '#/components/schemas/PageInfo' SandboxInfoResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [sandbox] properties: type: type: string examples: [sandbox.info] sandbox: $ref: '#/components/schemas/Sandbox' giftLimitNotice: type: string description: Present when a fair-use cap on a gifted account shortened the requested auto-stop time. CreateSandboxResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [status, ttlSeconds, sandbox] properties: type: type: string const: sandbox.created status: type: string enum: [provisioning, ready] description: '`ready` only when an idempotent retry finds a sandbox that is already past provisioning.' ttlSeconds: type: [integer, 'null'] sandbox: $ref: '#/components/schemas/Sandbox' giftLimitNotice: type: string description: Present when a fair-use cap on a gifted account shortened the requested auto-stop time. SandboxActionResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, status] properties: type: type: string examples: [sandbox.stopping] id: type: string status: type: string examples: [archiving] sandbox: oneOf: - $ref: '#/components/schemas/Sandbox' - type: 'null' giftLimitNotice: type: string description: Present on resume or fork when a fair-use cap on a gifted account shortened the requested auto-stop time. conversationId: type: string description: On interrupt, the conversation whose turn was interrupted. PageInfo: type: object required: [nextCursor, hasMore, limit] properties: nextCursor: type: [string, 'null'] hasMore: type: boolean limit: type: integer PromptRun: type: object required: [id, promptId, sandboxId, status, done] properties: id: { type: string } promptId: { type: string } sandboxId: { type: string } status: type: string enum: [sending, queued, running, finished, failed, interrupted] done: type: boolean createdAt: type: [string, 'null'] format: date-time model: type: [string, 'null'] reasoningEffort: type: [string, 'null'] fast: type: [boolean, 'null'] description: The prompt runs in fast mode. `null` means no speed is set yet. The prompt waits without a speed choice, or it ended without running (`done` is true). The speed is set when the prompt starts, for the model that runs. conversationId: type: [string, 'null'] description: The conversation this prompt ran in. A Sandbox runs many conversations in parallel; see [Integrated agents](/integrated-agents). StopOperationResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, stop] properties: type: type: string const: stop.status id: type: string stop: $ref: '#/components/schemas/StopOperation' PromptRunResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, promptRun] properties: type: type: string const: prompt.run id: type: string promptRun: $ref: '#/components/schemas/PromptRun' SandboxEvent: type: object required: [type] additionalProperties: true properties: id: type: string type: type: string timestamp: type: integer taskId: type: [string, 'null'] conversationId: type: [string, 'null'] description: The conversation this event belongs to. Filter the stream with `?conversation=`. See [Integrated agents](/integrated-agents). data: type: object additionalProperties: true UnknownEvent: type: object required: [type] additionalProperties: true properties: type: { type: string } PromptEvent: type: object required: [id, type, timestamp, data] properties: id: { type: string } type: { type: string, const: prompt } timestamp: { type: integer } taskId: { type: [string, 'null'] } conversationId: { type: [string, 'null'], description: 'The conversation this prompt belongs to.' } data: type: object required: [prompt, status] properties: prompt: { type: string } status: { type: string, enum: [sending, queued, running, finished, failed, interrupted] } is_reverted: { type: boolean } error: type: string description: Why the prompt failed. Present only when `status` is `failed`. attachments: type: array items: type: object properties: filename: { type: string } mimeType: { type: string } base64Data: { type: string } citations: type: array description: Earlier chat events quoted in this prompt. items: type: object properties: eventId: { type: string } type: { type: string } author: { type: string } text: { type: string } SteerEvent: type: object required: [id, type, timestamp, data] description: A message sent into a turn that was already running, via `POST /sandboxes/{sandboxId}/steer`. It is not a prompt run; the output it causes belongs to the turn that was already running. properties: id: { type: string } type: { type: string, const: steer } timestamp: { type: integer } taskId: { type: [string, 'null'], description: 'Id of the steer record, as returned by POST /steer.' } conversationId: { type: [string, 'null'], description: 'The conversation whose running turn was steered.' } data: type: object required: [message, native, mode] properties: message: { type: string } native: type: boolean description: True when the turn that was already running took the message (both native modes); false when Sandboxer interrupted that turn and immediately continued the same conversation with it. mode: type: string enum: [native, native-continued, fallback, late] description: >- `native`: the harness folded the message into the running turn. `native-continued`: the harness accepted it but its turn ended without acting on it, so Sandboxer ran it immediately as its own turn on the same session (nothing interrupted, the instruction lands one turn boundary later). `fallback`: the harness has no mid-turn primitive, so Sandboxer interrupted that turn and continued the same conversation with the instruction. `late`: the turn had already finished, so it ran as an ordinary new turn. ResponseEvent: type: object required: [id, type, timestamp, data] description: Agent response event. Text deltas/finals and tool-call batches are both represented as response events; tool-call batches have `data.tools`. properties: id: { type: string } type: { type: string, const: response } timestamp: { type: integer } taskId: { type: [string, 'null'] } conversationId: { type: [string, 'null'], description: 'The conversation this response belongs to.' } data: type: object required: [content] properties: content: { type: string } model: { type: [string, 'null'] } tools: type: array description: Tool call/result records emitted by the agent. Present for tool-call events and omitted for text-only response events. items: { type: object, additionalProperties: true } is_streaming: type: boolean description: True when this response is a streaming partial rather than the final assistant message. is_reverted: { type: boolean } ErrorEvent: type: object required: [id, type, timestamp, data] properties: id: { type: string } type: { type: string, const: usage_limit } timestamp: { type: integer } taskId: { type: [string, 'null'] } data: { type: object, additionalProperties: true } CompletionEvent: type: object required: [id, type, timestamp, data] properties: id: { type: string } type: { type: string, const: compaction_complete } timestamp: { type: integer } taskId: { type: [string, 'null'] } data: { type: object, additionalProperties: true } FileReadResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [success, path, encoding, size, content] properties: type: { type: string, const: file.read } success: { type: boolean } path: { type: string } encoding: { type: string, enum: [utf8, base64] } size: { type: integer } content: { type: string } FileWriteRequest: type: object required: [path, content] properties: path: { type: string, description: "Absolute path, or path relative to the sandbox work directory (/home/user). The canonicalized path must resolve under /home/user or /tmp; anything else is rejected with a 400 invalid_path error." } content: { type: string } encoding: { type: string, enum: [utf8, base64], default: utf8 } FileWriteResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [success, path, encoding, size] properties: type: { type: string, const: file.written } success: { type: boolean } path: { type: string } encoding: { type: string, enum: [utf8, base64] } size: { type: integer } CommandRequest: type: object required: [command] properties: command: { type: string } cwd: { type: string, description: Relative working directory inside the sandbox work directory. } timeoutSeconds: { type: integer, minimum: 1, maximum: 600, default: 30, description: Command timeout in seconds. Values outside 1-600 are rejected with a 400 invalid_timeout error. } detached: { type: boolean, default: false, description: Start the command in the background and return a process id immediately instead of waiting for it to finish. Output goes to a log file on the sandbox; poll the status endpoint for it. } stream: { type: boolean, default: false, description: 'Stream the output as the command writes it: the response is newline-delimited JSON (application/x-ndjson), one CommandStreamFrame per line. Ignored with detached.' } env: type: object additionalProperties: { type: string } description: Sandboxer extension. Extra environment variables for this command only. CommandStreamFrame: type: object description: One line of a streamed command. `started` first, then `stdout`/`stderr` chunks, then exactly one `exit` (the synchronous result, without stdout/stderr) or `error`. required: [type] properties: type: { type: string, enum: [started, stdout, stderr, exit, error] } commandId: { type: string, description: 'started: the command''s record in the command history.' } data: { type: string, description: 'stdout/stderr: the chunk of output, UTF-8.' } exitCode: { type: [integer, 'null'] } success: { type: boolean } timedOut: { type: boolean } error: { type: string } message: { type: string } retryable: { type: boolean, description: 'error: true when the command provably never ran (the sandbox is still restoring its disk), so sending it again is safe.' } CommandResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [success, exitCode, stdout, stderr, timedOut] properties: type: { type: string, const: command.finished } success: { type: boolean } commandId: { type: string, description: The command's record in the command history. } exitCode: { type: [integer, 'null'] } signal: { type: [string, 'null'] } oomKilled: { type: boolean, description: 'True when the memory ceiling killed the command or one of its processes and the command failed.' } stdout: { type: string } stderr: { type: string } stdoutTruncated: { type: boolean } stderrTruncated: { type: boolean } timedOut: { type: boolean } cwd: { type: string } startedAt: { type: string, format: date-time } finishedAt: { type: string, format: date-time } CommandStartedResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [success, processId, pid, command, startedAt] properties: type: { type: string, const: command.started } success: { type: boolean } commandId: { type: string, description: The command's record in the command history. Its output is copied there every 30 s. } processId: { type: integer, description: Process id to poll with the command status endpoint. } pid: { type: integer } command: { type: string } cwd: { type: string } startedAt: { type: string, format: date-time } logPath: { type: string, description: Stdout log file on the sandbox (~/.sandboxer/processes/.log). } errLogPath: { type: string, description: Stderr log file on the sandbox. } CommandStatusResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [success, processId, status, running, exitCode, stdout, stderr] properties: type: { type: string, const: command.status } success: { type: boolean } processId: { type: integer } pid: { type: integer } status: type: string enum: [running, exited, lost] description: "lost: the sandbox agent restarted and forgot the process; running/exitCode are then a best-effort probe and the logs come from the on-disk files." known: { type: boolean, description: Whether the process is still tracked by the sandbox agent. } running: { type: boolean } exitCode: { type: [integer, 'null'] } signal: { type: [string, 'null'] } oomKilled: { type: boolean, description: 'True when the memory ceiling killed the command or one of its processes and the command failed.' } command: { type: [string, 'null'] } cwd: { type: [string, 'null'] } startedAt: { type: [string, 'null'], format: date-time } finishedAt: { type: [string, 'null'], format: date-time } stdout: { type: string, description: 'Tail of the stdout log file. With `stdoutOffset`: the stdout written since that offset.' } stderr: { type: string, description: 'Tail of the stderr log file. With `stderrOffset`: the stderr written since that offset.' } stdoutTruncated: { type: boolean } stderrTruncated: { type: boolean } logPath: { type: string } errLogPath: { type: string } # With stdoutOffset / stderrOffset: stdout and stderr hold only the output # written since those offsets (at most 1 MiB each, cut on a whole UTF-8 character). stdoutOffset: { type: integer, description: 'Byte offset where `stdout` starts: the offset you asked for, or 0 when the log was replaced.' } stderrOffset: { type: integer, description: 'Byte offset where `stderr` starts: the offset you asked for, or 0 when the log was replaced.' } stdoutNextOffset: { type: integer, description: The `stdoutOffset` for the next read. } stderrNextOffset: { type: integer, description: The `stderrOffset` for the next read. } more: { type: boolean, description: True when more output is already written. Read again at once. } CommandAttachFrame: type: object description: | One line of `GET /sandboxes/{sandboxId}/commands/{processId}/stream` (application/x-ndjson). - `attached`: first line. `processId`, `stdoutOffset`, `stderrOffset` (where the output starts). - `stdout` / `stderr`: a chunk. `data`, and `nextOffset` for that stream. - `heartbeat`: sent after 15 s with no other line. - `exit`: last line when the command ended and all its output was sent. `status`, `exitCode`, `signal`, `stdoutOffset`, `stderrOffset`. - `reconnect`: last line after 10 minutes. Open the stream again with its `stdoutOffset` and `stderrOffset`. Nothing is lost or repeated. - `error`: last line on a failure. `error`, `message`, `stdoutOffset`, `stderrOffset`. required: [type] properties: type: { type: string, enum: [attached, stdout, stderr, heartbeat, exit, reconnect, error] } processId: { type: integer } data: { type: string } nextOffset: { type: integer } stdoutOffset: { type: integer } stderrOffset: { type: integer } status: { type: string, enum: [running, exited, lost] } exitCode: { type: [integer, 'null'] } signal: { type: [string, 'null'] } error: { type: string } message: { type: string } CommandRecord: type: object description: One command run through the commands endpoint (kept 90 days), or one process of the sandbox with mode `process` (kept 72 hours). Readable while the sandbox is stopped. required: [commandId, sandboxId, command, mode, status, stdoutBytes, stderrBytes, outputTruncated, startedAt] properties: commandId: { type: string, examples: [cmd_k3v9x2mq7tbw], description: Id of the record in the command history. } sandboxId: { type: string } command: { type: string, description: The command text as sent (first 64 KiB). } cwd: { type: [string, 'null'] } mode: type: string enum: [sync, stream, detached, process] description: 'sync: the request waited for the result. stream: the output was streamed. detached: started in the background. process: any other process of the sandbox, recorded from what it wrote to its stdout/stderr.' processId: { type: [integer, 'null'], description: 'detached: the process id on the sandbox.' } status: type: string enum: [running, exited, timed_out, failed, killed, lost] description: 'failed: the command could not start or its result never arrived. killed: ended by a signal or by DELETE /commands/{processId}. lost: the sandbox stopped or its agent restarted while the command ran.' exitCode: { type: [integer, 'null'] } signal: { type: [string, 'null'] } stdoutBytes: { type: integer, description: Bytes of stdout written so far. } stderrBytes: { type: integer, description: Bytes of stderr written so far. } outputTruncated: type: boolean description: 'True when part of the output is not kept: the sandbox cut the output of a waiting command at 8 MiB, or the account wrote more than 10 MiB (compressed) of command output, or of process output, in one hour.' startedAt: { type: string, format: date-time } finishedAt: { type: [string, 'null'], format: date-time } CommandListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [commands, nextCursor] properties: type: { type: string, const: command.list } commands: type: array description: Newest first. items: { $ref: '#/components/schemas/CommandRecord' } nextCursor: { type: [string, 'null'], description: Pass as `cursor` to get the next page. Null on the last page. } CommandInfoResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [command] properties: type: { type: string, const: command.info } command: { $ref: '#/components/schemas/CommandRecord' } CommandLogsResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [commandId, stream, offset, nextOffset, size, encoding, data, done] properties: type: { type: string, const: command.logs } commandId: { type: string } stream: { type: string, enum: [stdout, stderr] } offset: { type: integer, description: Byte position of the first byte in `data`. Greater than the requested offset only when output was not kept (see outputTruncated). } nextOffset: { type: integer, description: Pass as `offset` to read on. } size: { type: integer, description: Bytes the stream has so far. } encoding: { type: string, enum: [utf8, base64] } data: { type: string } done: { type: boolean, description: True when the command ended and this read reached the end of the stream. } CommandStopResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [processId, running, signaled, command] properties: type: { type: string, const: command.stopped } processId: { type: integer } running: { type: boolean, description: Always false after a stop. } signaled: { type: boolean, description: False when the process had already ended. } command: oneOf: - $ref: '#/components/schemas/CommandRecord' - type: 'null' description: The history record of the command, with its final state. SandboxProcess: type: object required: [pid, command] properties: pid: { type: integer } parentPid: { type: integer, description: Live list only. } user: { type: string, description: Live list only. } cpuPercent: { type: number } memoryMB: { type: number, description: Resident memory. } startedAt: { type: string, format: date-time, description: Live list only. } command: { type: string, description: The full command line (first 1000 characters; 120 in a stored sample). } commandId: { type: [string, 'null'], description: 'Live list: the history record holding this process''s output, when it has one.' } ProcessListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [live, sampledAt, processes, total] properties: type: { type: string, const: process.list } live: { type: boolean, description: 'True: read from the running sandbox now. False: the sandbox is stopped (or its agent is not upgraded yet), and this is the last sample kept (top 12 processes, every 30 s, 7 days).' } sampledAt: { type: [string, 'null'], format: date-time } total: { type: integer, description: Processes on the sandbox. The list holds at most 500, busiest first. } processes: type: array items: { $ref: '#/components/schemas/SandboxProcess' } PromptResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, promptId, promptRun, status, provider] properties: type: type: string const: prompt.queued id: type: string description: Sandbox id. promptId: type: string conversationId: type: [string, 'null'] description: The conversation this prompt was queued in. A new one when `new` was set, the one named by `conversationId`, or the sandbox's most-recently-active conversation. See [Integrated agents](/integrated-agents). promptRun: $ref: '#/components/schemas/PromptRun' status: type: string enum: [queued] provider: type: string model: type: [string, 'null'] reasoningEffort: type: [string, 'null'] fast: type: boolean description: The prompt runs in fast mode. EventsResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, events] properties: type: type: string const: events.list id: type: string events: type: array description: Sandbox work and lifecycle event objects. Event shapes are intentionally extensible; branch on each event `type` when present. items: $ref: '#/components/schemas/SandboxEvent' pageInfo: $ref: '#/components/schemas/PageInfo' Conversation: type: object required: [id, createdAt, lastPromptAt, prompts, running, lastHarness, lastModel, lastPromptPreview, current] properties: id: type: string description: Conversation id. Pass it as `conversationId` on `POST /prompt` to continue this thread. createdAt: type: [string, 'null'] format: date-time description: When the first prompt of this conversation was queued. lastPromptAt: type: [string, 'null'] format: date-time description: When its most recent prompt was queued. The list is sorted by this, newest first. prompts: type: integer description: Number of prompts sent to this conversation. running: type: boolean description: '`true` while a turn is queued or running in this conversation.' lastHarness: type: [string, 'null'] description: Harness the most recent prompt ran on (`claude`, `codex`, `pi`, `opencode`, `prime-agent`, `kimi`, `mistral`). lastModel: type: [string, 'null'] description: Model id of the most recent prompt that chose one, or `null` when every prompt used the harness default. lastPromptPreview: type: [string, 'null'] description: The first 80 characters of the most recent prompt. current: type: boolean description: '`true` on the conversation a `POST /prompt` without `new` or `conversationId` would continue: the sandbox''s most recently prompted one. Exactly one conversation is current unless the list is empty.' ConversationsResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, conversations] properties: type: type: string const: conversation.list id: type: string description: Sandbox id. conversations: type: array items: $ref: '#/components/schemas/Conversation' DesktopResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object additionalProperties: true properties: type: type: string examples: [desktop.url] success: type: boolean desktopUrl: type: [string, 'null'] format: uri description: Secret-bearing desktop or noVNC URL. Redact from logs. ip: type: [string, 'null'] mode: type: string examples: [vnc] provisioning: type: boolean description: For `?vnc=1`, true means VNC is still being prepared; poll again. message: type: string HostPortResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object additionalProperties: true properties: type: type: string examples: [port.hosted] success: type: boolean port: type: integer url: type: string format: uri description: Public HTTPS URL. Carries the `_token` query parameter unless the port is public; redact from logs. isProtected: type: boolean access: type: string enum: [private, public] localhost: type: boolean description: The sandbox relays the URL to a service that listens on localhost only. SshKeyResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object properties: success: type: boolean machineIp: type: [string, 'null'] sshUser: type: string examples: [user] sshEndpoint: type: [string, 'null'] description: >- Public IPv4 `host:port` that forwards to the sandbox SSH server, set only when the machine has no public IPv4 of its own. Connect with `ssh -p user@`. examples: ['203.0.113.10:22001'] hostKey: type: string description: The sandbox SSH server's public host key. Pin it in `known_hosts`. examples: [ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...] ApiKeyScopeResource: type: object required: [id, name, organizationId, organizationName] properties: id: { type: string } name: { type: string } organizationId: { type: string } organizationName: { type: string } ApiKeyCatalog: type: object required: [actions, presets, defaultTtl, maxTtl, scopedCreationEnabled] properties: actions: type: array items: { type: string } presets: type: object additionalProperties: type: array items: { type: string } defaultTtl: { type: string, examples: [90d] } maxTtl: { type: string, examples: [365d] } scopedCreationEnabled: type: boolean description: Whether POST /api-keys/scoped currently accepts creation requests. scopeResources: type: object description: Session-only selector resources. Omitted when the caller authenticates with an API key. required: [sandboxes, environments] properties: sandboxes: type: array items: { $ref: '#/components/schemas/ApiKeyScopeResource' } environments: type: array items: { $ref: '#/components/schemas/ApiKeyScopeResource' } ApiKeysResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [apiKeys, catalog] properties: apiKeys: type: array items: $ref: '#/components/schemas/ApiKey' catalog: $ref: '#/components/schemas/ApiKeyCatalog' ApiKeySecretResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [apiKey, secret, apiKeys] properties: apiKey: { $ref: '#/components/schemas/ApiKey' } secret: type: string description: Raw secret returned once. Store it before closing the response. apiKeys: type: array items: { $ref: '#/components/schemas/ApiKey' } ApiKeyRevokeResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [apiKeys] properties: apiKeys: type: array items: { $ref: '#/components/schemas/ApiKey' } SandboxUsageResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [sandboxId, sandboxType, billingMultiplier, since, until, seconds, dollars, secondsPerDollar, running] properties: type: type: string examples: [sandbox.usage] sandboxId: type: string pattern: '^sb_[a-z0-9]{10}$' examples: [sb_k3j9x2m1qa] sandboxType: type: string enum: [small, default, large, xlarge] description: The sandbox's current machine size, which sets `billingMultiplier`. billingMultiplier: type: number description: Rate at which this sandbox consumes machine time. 0.5 for `small`, 1 for `default`, 2 for `large`, and 50/9 (about 5.56) for `xlarge`. examples: [1] since: type: string format: date-time description: Start of the window the figures cover. The sandbox's creation time when the request did not pass `since`. until: type: string format: date-time description: End of the window. Now when the request did not pass `until`, or passed one in the future. seconds: type: integer description: >- Billable machine-seconds inside the window, the type multiplier already applied: a `large` sandbox that ran ten minutes reads 1200. Includes a running sandbox's time up to `until`. Time while stopped is never counted and time past a refused stop is excluded. examples: [4980] dollars: type: number description: '`seconds` at list price (`seconds / secondsPerDollar`), whatever plan, trial or gift actually paid for it. Up to six decimals.' examples: [0.0498] secondsPerDollar: type: integer description: How many billable seconds one dollar buys, so you can price `seconds` yourself. examples: [100000] running: type: boolean description: >- The meter is still moving: the sandbox is up and no refused stop is holding it paused. Read again after it stops for the final figure. `false` on a sandbox that is up but paused past a refused stop, whose figure will not change until it is used again. ApiKeyUsageResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - $ref: '#/components/schemas/ApiKey' - type: object required: [createdResources] properties: createdResources: type: array items: $ref: '#/components/schemas/ApiKeyCreatedResource' WebhookListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [webhooks] properties: webhooks: type: array items: $ref: '#/components/schemas/Webhook' WebhookResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [webhook] properties: webhook: $ref: '#/components/schemas/Webhook' WebhookSecretResponse: allOf: - $ref: '#/components/schemas/WebhookResponse' - type: object required: [secret] properties: secret: type: string pattern: '^whsec_[a-f0-9]{64}$' description: Signing secret returned only when created or rotated. WebhookDeleteResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id] properties: id: type: string TelemetryConfig: type: object required: [endpoint, headers, lastExportAt, lastError, createdAt] properties: endpoint: type: string format: uri description: The OTLP/HTTP base URL. Sandboxer posts to `/v1/metrics` and `/v1/logs`. headers: type: array items: { type: string } description: The names of the saved headers. The values are never returned. lastExportAt: type: [string, 'null'] format: date-time description: The time of the last metrics export that the collector accepted. lastError: description: The most recent error from the collector, for metrics or events. oneOf: - type: 'null' - type: object required: [message, status, at] properties: message: { type: string } status: { type: [integer, 'null'], description: 'HTTP status, or null for a network error.' } at: { type: string, format: date-time } createdAt: type: string format: date-time TelemetrySetRequest: type: object required: [endpoint] properties: endpoint: type: string description: HTTPS URL on port 443 or 4318. Sandboxer removes a trailing `/v1/metrics` or `/v1/logs`. example: https://otlp.example.com headers: type: object maxProperties: 20 additionalProperties: { type: string, maxLength: 8192 } description: Headers sent with every export, for example the vendor API key. Sandboxer encrypts them at rest. example: { dd-api-key: '' } TelemetryTestResult: type: object required: [ok, status, message] properties: ok: { type: boolean, description: True when the collector accepted the test export. } status: { type: [integer, 'null'] } message: { type: [string, 'null'], description: The collector error or partial-success message. } TelemetryResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [telemetry] properties: telemetry: oneOf: - type: 'null' - $ref: '#/components/schemas/TelemetryConfig' TelemetrySetResponse: allOf: - $ref: '#/components/schemas/TelemetryResponse' - type: object required: [test] properties: test: { $ref: '#/components/schemas/TelemetryTestResult' } SnapshotSummary: type: object required: [id, sandboxId, status, generation, createdAt, sizeBytes, fileCount] properties: id: type: string format: uuid sandboxId: type: string description: Public Sandbox id this snapshot belongs to. status: type: string enum: [completed] kind: type: [string, 'null'] enum: [base, incremental, null] description: '`base` (full) or `incremental` (delta on a base). `null` for legacy snapshots.' generation: type: [integer, 'null'] description: Position in the incremental chain (0 = base). chainId: type: [string, 'null'] format: uuid createdAt: type: string format: date-time completedAt: type: [string, 'null'] format: date-time sizeBytes: type: [integer, 'null'] description: Bytes this snapshot added (its delta), not the full restored size. fileCount: type: [integer, 'null'] description: Inventory entries alive in the chain at this generation (includes base-image system entries). contentSizeBytes: type: [integer, 'null'] description: Total bytes of your data restored by this snapshot (what resume/download returns; base image excluded). `null` on legacy snapshots. contentFileCount: type: [integer, 'null'] description: Number of your files restored by this snapshot (base image excluded). `null` on legacy snapshots. SnapshotListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [snapshots] properties: type: type: string const: snapshot.list snapshots: type: array items: $ref: '#/components/schemas/SnapshotSummary' pageInfo: $ref: '#/components/schemas/PageInfo' SnapshotLatestResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [snapshot] properties: type: type: string const: snapshot.latest snapshot: oneOf: - $ref: '#/components/schemas/SnapshotSummary' - type: 'null' NamedSnapshot: type: object description: >- A named snapshot: a frozen copy of a sandbox's disk at one moment, saved under a name you pick. Independent of the source sandbox's later life: the sandbox can change, stop, or be deleted and the named snapshot still deploys. Named snapshots are kept until you delete them. They count toward your plan's included storage (see the storage rules). Re-saving a name replaces its artifact. required: [name, status, sourceSandboxId, createdAt] properties: name: type: string pattern: '^[a-z0-9][a-z0-9-]{0,62}$' description: The user-chosen handle, unique within your account. status: type: string enum: [saving, ready, failed] description: >- `saving` while the capture and pin are in flight (a live source sandbox takes a fresh snapshot first, which can run minutes), `ready` when deployable, `failed` if the save did not complete (see `error`; save again to retry). error: type: string description: Failure reason. Only present when `status` is `failed`. sourceSandboxId: type: string description: The sandbox this snapshot was saved from (display only). snapshotId: type: string description: >- The frozen artifact behind the name. Present once `status` is `ready`. Accepted by `GET /api/v1/snapshots/{snapshotId}/tree` to browse its files. type: type: string description: Sandbox type the snapshot was saved from. Deploys default to it. sizeBytes: type: integer description: Restored content size of the frozen state, in bytes. createdAt: type: string format: date-time NamedSnapshotListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [snapshots] properties: type: type: string const: snapshot.named.list snapshots: type: array items: $ref: '#/components/schemas/NamedSnapshot' allowance: { $ref: '#/components/schemas/NamedSnapshotAllowance' } NamedSnapshotAllowance: type: object description: >- The request's wallet (your active organization, `?org=`, or personal): how many named snapshots it keeps, and its storage. Named snapshots have no separate charge; they count toward the plan's included storage. An organization's count includes every member's names billed to it. properties: org: { type: [string, 'null'], description: Organization the numbers are for. null = personal. } orgName: { type: [string, 'null'], description: That organization's name. } used: { type: integer, description: Named snapshots this wallet keeps. } storage: { $ref: '#/components/schemas/Storage' } Storage: type: object description: >- A wallet's storage: its stopped sandboxes (with their snapshots) and named snapshots, as measured on the nodes (data beyond the shared OS image; updated about every 10 minutes). Over the included amount, new sandboxes, forks and named snapshots answer `402 storage_limit_reached`, unless auto-delete (`autoCleanup`, off by default) is on: then the least recently used stopped sandboxes are deleted (after an email, at least 7 days later) until the account is back under. properties: usedBytes: { type: integer, format: int64 } includedBytes: { type: [integer, 'null'], format: int64, description: null = no limit. } autoCleanup: { type: boolean } blocked: { type: boolean, description: New sandboxes, forks and named snapshots are refused. } retentionDays: { type: [integer, 'null'], description: 'The data of a stopped sandbox (created on or after the Terms date) is deleted this many days after its stop, with an email 3 days before. Null while retention is not in force.' } measuredAt: { type: [string, 'null'], format: date-time } NamedSnapshotInfoResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [snapshot] properties: type: type: string const: snapshot.named.info snapshot: $ref: '#/components/schemas/NamedSnapshot' NamedSnapshotSaveRequest: type: object required: [sandboxId, name] properties: sandboxId: type: string description: The sandbox whose current state to freeze. name: type: string pattern: '^[a-z0-9][a-z0-9-]{0,62}$' description: >- Name to save under. 1-63 lowercase letters, digits, or dashes, starting with a letter or digit. Reusing one of your existing names replaces that snapshot's artifact. `latest`, `tree`, `pull`, `rm`, `save`, `current`, `self`, and `new` are reserved. NamedSnapshotSavingResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [status, snapshot] properties: type: type: string const: snapshot.named.saving status: type: string const: saving snapshot: $ref: '#/components/schemas/NamedSnapshot' NamedSnapshotDeletedResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [name, status] properties: type: type: string const: snapshot.named.deleted name: type: string status: type: string const: deleted NamedSnapshotShare: type: object description: >- A share code for a named snapshot. Any Sandboxer account that holds the code can create a sandbox from the snapshot. Sandboxer stores only a hash of the code. The full code is in the create response only. required: [id, codePrefix, status, useCount, lastUsedAt, revokedAt, createdAt] properties: id: type: string description: Share id, for example `shr_...`. Pass it to revoke the code. codePrefix: type: string description: The first characters of the code, for example `bsh_xxxxxxxx`. Use it to tell codes apart. status: type: string enum: [active, revoked] useCount: type: integer description: Number of sandboxes created with this code. lastUsedAt: type: [string, 'null'] format: date-time description: When a sandbox was last created with this code. null if never. revokedAt: type: [string, 'null'] format: date-time description: When the code was revoked. null while active. createdAt: type: string format: date-time NamedSnapshotShareCreatedResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [name, shareCode, share] properties: type: type: string const: snapshot.share.created name: type: string description: The named snapshot the code deploys. shareCode: type: string description: >- The share code, for example `bsh_...`. Sandboxer shows it only in this response. Store it now; you cannot get it again. share: $ref: '#/components/schemas/NamedSnapshotShare' NamedSnapshotShareListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [name, shares] properties: type: type: string const: snapshot.share.list name: type: string shares: type: array items: $ref: '#/components/schemas/NamedSnapshotShare' NamedSnapshotShareRevokedResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [name, share] properties: type: type: string const: snapshot.share.revoked name: type: string share: $ref: '#/components/schemas/NamedSnapshotShare' SnapshotTreeEntry: type: object required: [path, kind] properties: path: type: string description: Path relative to the snapshot root. Docker named volumes appear under `__dockervol__/`. kind: type: string enum: [file, dir, symlink] size: type: integer description: File size in bytes. Omitted for directories. SnapshotTreeResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [snapshotId, sandboxId, generation, treeAvailable, truncated, fileCount, totalSizeBytes, entries] properties: type: type: string const: snapshot.tree snapshotId: type: string format: uuid sandboxId: type: string generation: type: integer treeAvailable: type: boolean description: '`false` for legacy snapshots or inventories too large to expand; see `reason`.' truncated: type: boolean description: '`true` when the file list was capped; not every entry is returned.' fileCount: type: integer description: Number of your files in this snapshot (base-image system files excluded). totalSizeBytes: type: integer description: Total bytes of your data in this snapshot (base-image system files excluded). entries: type: array description: Exactly the files/dirs a resume or download returns, your data only; base-image system entries are not listed. items: $ref: '#/components/schemas/SnapshotTreeEntry' reason: type: string description: Why the tree is unavailable, when `treeAvailable` is `false`. examples: [legacy_snapshot, inventory_too_large] SnapshotChunk: type: object required: [snapshotId, generation, chunkIndex, r2Key, sizeBytes, sha256, signedUrl] properties: snapshotId: type: string format: uuid generation: type: integer chunkIndex: type: integer r2Key: type: string sizeBytes: type: [integer, 'null'] sha256: type: [string, 'null'] signedUrl: type: string format: uri description: Time-limited download URL (see `expiresInSeconds`). SnapshotDownloadResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [snapshotId, sandboxId, kind, generation, expiresInSeconds, reconstruct, chunks] properties: type: type: string const: snapshot.download snapshotId: type: string format: uuid sandboxId: type: string kind: type: string enum: [base, incremental, legacy] generation: type: integer expiresInSeconds: type: integer description: Lifetime of every `signedUrl` in this response. reconstruct: type: string description: Human-readable note on how to reassemble the chunks into a filesystem. inventory: oneOf: - type: object required: [r2Key, signedUrl] properties: r2Key: { type: string } signedUrl: { type: string, format: uri } - type: 'null' chunks: type: array description: Every chunk across the chain (all generations up to and including this snapshot), ordered by `(generation, chunkIndex)`. items: $ref: '#/components/schemas/SnapshotChunk' DeletionOperation: type: object required: [id, kind, targetId, reason, status, attemptCount, requestedAt, completedAt] properties: id: type: string pattern: '^bdop_[a-f0-9]{32}$' kind: type: string enum: [sandbox, snapshot] targetId: type: string reason: type: string enum: [explicit, zdr, account] status: type: string enum: [pending, processing, blocked, completed] stage: type: string enum: [removing, waiting_for_uploads, kept_for_newer_snapshots, waiting_for_restore, retrying, completed] description: >- What the background purge is doing. The target is already gone from every list, restore and fork once the operation exists. `waiting_for_uploads`: stored data is erased once the last upload URL issued for it expires (`expectedBy`). `kept_for_newer_snapshots`: a newer snapshot you kept is built on this one; its data goes when they go. `waiting_for_restore`: a sandbox is still restoring from it. `retrying`: a transient fault, retried automatically. expectedBy: type: [string, 'null'] format: date-time description: When `waiting_for_uploads` ends. attemptCount: type: integer minimum: 0 requestedAt: type: string format: date-time completedAt: type: [string, 'null'] format: date-time DeletionOperationListResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [operations] properties: type: type: string enum: [snapshot.deleting] operations: type: array items: $ref: '#/components/schemas/DeletionOperation' DeletionOperationResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [operation] properties: type: type: string enum: [deletion.operation, sandbox.deleting, snapshot.deleting] operation: $ref: '#/components/schemas/DeletionOperation' DataRetentionPolicyResponse: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [enabled, enabledAt] properties: type: type: string enum: [data_retention.info, data_retention.updated] enabled: type: boolean enabledAt: type: [string, 'null'] format: date-time queuedSandboxes: type: integer minimum: 0 description: Archived Sandboxes newly queued for deletion by this policy update. acceptedDeletionOperationsIrreversible: type: boolean description: Always true on updates. Disabling the policy does not cancel accepted deletion operations. snapshotsOff: type: boolean description: When true, every new sandbox of this account is created with snapshots off. snapshotsOffAt: type: [string, 'null'] format: date-time monitoringOff: type: boolean description: When true, every new sandbox of this account is created with monitoring off. monitoringOffAt: type: [string, 'null'] format: date-time DataRetentionUpdateRequest: type: object description: Pass `enabled` to change zero data retention, or `snapshotsOff` or `monitoringOff` alone to change that account setting. additionalProperties: false properties: enabled: type: boolean snapshotsOff: type: boolean description: >- Turn snapshots off for every sandbox created from now on. Existing sandboxes keep their setting. While on, a create or fork that passes `snapshots: true` is refused with 409 `snapshots_off_for_account`. monitoringOff: type: boolean description: >- Turn monitoring off for every sandbox created from now on. Existing sandboxes keep their setting. While on, a create or fork that passes `monitoring: true` is refused with 409 `monitoring_off_for_account`. confirmation: type: string description: Required when enabling and must exactly equal `delete archived sandbox data`. const: delete archived sandbox data responses: BadRequest: description: Invalid request body or parameters. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' examples: invalid: value: ok: false type: sandbox.error status: 400 code: invalid_json message: Request body must be valid JSON. error: { code: invalid_json, message: Request body must be valid JSON., status: 400 } requestId: req_01HX... Unauthorized: description: Missing or invalid bearer token. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' examples: unauthorized: value: ok: false type: sandbox.error status: 401 code: unauthorized message: Unauthorized error: { code: unauthorized, message: Unauthorized, status: 401 } requestId: req_01HX... Forbidden: description: Authenticated token is not allowed to perform this action. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' examples: forbidden: value: ok: false type: sandbox.error status: 403 code: forbidden message: Forbidden error: { code: forbidden, message: Forbidden, status: 403 } requestId: req_01HX... PaymentRequired: description: Account cannot currently create or operate sandboxes. The error body may include a dashboard billing URL, but billing actions are not part of the v1 API. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' NotFound: description: Resource not found. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' Conflict: description: Request conflicts with current account or sandbox state. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' RateLimited: description: >- Machine start or concurrent-sandbox limit reached. Create, fork and resume each count as one machine start against your plan's start limits (see the Billing guide; `rate_limited`, naming the window you hit). A sandbox that would exceed your plan's concurrent-sandbox cap is refused with `limit_reached`, or `member_limit_reached` when an organization owner has capped you below the plan. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' NoReadyMachine: description: >- No capacity. `no_ready_machine`: a `failFast` create, fork or resume found no ready machine of that type; nothing was started, billed, or counted, and a resumed sandbox stays stopped. `out_of_capacity`: creates and forks are paused until the provisioning queue drains. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' paths: /me: get: tags: [Sandboxer] summary: Get current Sandboxer user description: Returns GitHub identity for the authenticated Sandboxer account. operationId: me responses: '200': description: Current user information. content: application/json: schema: { $ref: '#/components/schemas/MeResponse' } examples: user: value: ok: true type: user.info user: login: octocat email: octocat@example.com zeroDataRetention: false zeroDataRetentionEnabledAt: null '401': { $ref: '#/components/responses/Unauthorized' } /orgs: get: tags: [Sandboxer] summary: List organizations description: >- The wallets this account can bill: your personal account first, then every organization you belong to, each with its id and name. Either spelling works wherever an org is passed (`org`, `X-Sandboxer-Org`, `teamId`). `active` marks the wallet new sandboxes bill when a request names none; change it with `PATCH /orgs/active`. operationId: listOrganizations responses: '200': description: Personal account and organizations. content: application/json: schema: { $ref: '#/components/schemas/OrgListResponse' } examples: orgs: value: ok: true type: org.list activeWalletChosen: true orgs: - id: 4f1c2e0a-7b3d-4a6e-9c8b-2d5e1f0a3b7c name: Personal type: personal role: owner memberCount: 1 active: false - id: team_583fd89e-6d29-4007-b48d-52477656cb0f name: acme type: org role: owner memberCount: 3 subscriptionStatus: active active: true '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Sandboxer] summary: Create an organization description: Sandboxer extension. The caller becomes its owner. Needs a session or an `account.admin` key. operationId: createOrganization requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, minLength: 1, maxLength: 64 } responses: '201': description: Organization created. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object properties: org: { $ref: '#/components/schemas/Org' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /orgs/{orgId}/members: get: tags: [Sandboxer] summary: List organization members and pending invites description: Sandboxer extension (dashboard). `members:[{id,email,displayName,role,joinedAt,capActiveSandboxes}]`, `invites:[{id,email,createdAt,expiresAt}]`. operationId: orgMembers parameters: - name: orgId in: path required: true schema: { type: string } responses: '200': description: Members and invites. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object additionalProperties: true '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /orgs/active: patch: tags: [Sandboxer] summary: Set the active organization description: >- Set the wallet new sandboxes bill when a request names none, for this account on every client: the API, the CLI (`sbx org switch` sets the same value) and the dashboard. Pass an organization id or name you belong to, or `personal` (or `null`) for your own account. `GET /limits` follows it. Sandboxes already running keep the wallet they were created with; leaving or deleting the organization resets the account to personal. operationId: setActiveOrganization requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ActiveOrgUpdateRequest' } examples: byName: value: { org: acme } personal: value: { org: personal } responses: '200': description: The wallet now active. content: application/json: schema: { $ref: '#/components/schemas/ActiveOrgResponse' } examples: org: value: ok: true type: org.active_updated active: id: team_583fd89e-6d29-4007-b48d-52477656cb0f name: acme type: org '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': description: '`not_org_member`: no organization with that id or name that you belong to.' content: application/json: schema: { $ref: '#/components/schemas/ErrorEnvelope' } '409': description: '`ambiguous_org`: several of your organizations share that name; pass the id.' content: application/json: schema: { $ref: '#/components/schemas/ErrorEnvelope' } /account/data-retention: get: tags: [Sandboxer] summary: Get account data-retention policy description: Returns the current zero-data-retention policy. Per-Sandboxer API keys cannot access this account-wide setting. Responses are never cached. operationId: getDataRetention responses: '200': description: Current retention policy. content: application/json: schema: { $ref: '#/components/schemas/DataRetentionPolicyResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } patch: tags: [Sandboxer] summary: Update account data-retention policy description: >- Requires an interactive sandbox session; API keys and legacy permanent tokens are refused. Enabling requires the exact confirmation phrase `delete archived sandbox data`. Disabling affects future archives only and cannot cancel deletion operations already accepted. operationId: updateDataRetention requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/DataRetentionUpdateRequest' } responses: '200': description: Updated retention policy. Responses are never cached. content: application/json: schema: { $ref: '#/components/schemas/DataRetentionPolicyResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } /account/storage: patch: tags: [Sandboxer] summary: Turn storage auto-delete on or off description: >- For the request's wallet (your active organization, `X-Sandboxer-Org`, or personal); an organization's is changed by its owner only (`403 owner_only`). Off (the default): nothing is deleted for being over; new sandboxes, forks and named snapshots answer `402 storage_limit_reached` at the limit. On: over the plan's storage, the least recently used stopped sandboxes are deleted until the account is back under (the owner is emailed the list and the date, at least 7 days before, and again after). operationId: updateStorage requestBody: required: true content: application/json: schema: type: object required: [autoCleanup] properties: autoCleanup: { type: boolean } responses: '200': description: The wallet's storage. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object properties: storage: { $ref: '#/components/schemas/Storage' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /deletion-operations/{operationId}: get: tags: [Sandboxer] summary: Get deletion operation description: Poll an accepted Sandboxer or snapshot deletion. Only operations owned by the authenticated account are returned. Responses are never cached. operationId: getDeletionOperation parameters: - $ref: '#/components/parameters/OperationId' responses: '200': description: Current deletion operation state. content: application/json: schema: { $ref: '#/components/schemas/DeletionOperationResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /regions: get: tags: [Sandboxer] summary: List regions description: >- The regions a sandbox can run in, whether each has a machine available, the default, and the region nearest the caller (by the request's IP; IP geolocation by DB-IP). Needs no API key. operationId: listRegions security: [] responses: '200': description: Regions. content: application/json: schema: type: object required: [ok, type, regions, default, nearest] properties: ok: { type: boolean, const: true } type: { type: string, const: region.list } regions: type: array items: type: object required: [id, name, location, available] properties: id: { type: string, examples: [us-east] } name: { type: string, examples: [US East] } location: { type: string, examples: ['Ashburn, Virginia'] } available: { type: boolean, description: A machine in this region can take new sandboxes. } default: { type: string, description: The region used when none is given and the nearest has no machine. } nearest: { type: [string, 'null'], description: The region nearest the caller, or null when unknown. } /limits: get: tags: [Sandboxer] summary: Get Sandboxer limits description: >- Check remaining machine starts, compute time, credits, access readiness, and concurrent-sandbox capacity for the wallet a create would bill: the `org` / `X-Sandboxer-Org` you pass (id or name), else the account's active wallet, else personal. The response carries `teamId` when an organization wallet was read. operationId: limits parameters: - $ref: '#/components/parameters/OrgId' - $ref: '#/components/parameters/OrgHeader' - name: teamId in: query required: false schema: { type: string } description: Legacy alias for `org`. Takes precedence over `org` / `X-Sandboxer-Org` when set. responses: '200': description: Current creation and concurrent-sandboxer limits. content: application/json: schema: { $ref: '#/components/schemas/LimitsResponse' } examples: ready: value: ok: true type: limits.info canStart: true activeSandboxes: 1 activeStates: [provisioned, cloning, ready, idle, running] sandboxPlanKey: box_20 sandboxPlanDollars: 20 maxActiveSandboxes: 100 maxCreationRequestsPerMinute: 10 maxCreationRequestsPerDay: null startLimits: { perMinute: 10, perHour: 50, perDay: 150 } starts: unlimited: false minute: { limit: 10, used: 3, remaining: 7 } hour: { limit: 50, used: 12, remaining: 38 } day: { limit: 150, used: 47, remaining: 103 } billingStatus: active creditBalanceSeconds: 7200 creditBalanceHours: 2 packBalanceSeconds: 500000 packBalanceHours: 138.89 packBalanceDollars: 5 '401': { $ref: '#/components/responses/Unauthorized' } /repos: get: tags: [Sandboxer] summary: List GitHub repositories available to Sandboxer description: Returns GitHub repositories grouped by installation plus the current selected repositories for new Sandboxes. operationId: repos parameters: - name: sync in: query schema: { type: boolean } description: When true, sync from GitHub before returning repository groups. - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Sort' - name: q in: query schema: { type: string } description: Case-insensitive repository name/fullName filter. - name: selected in: query schema: { type: boolean } description: Filter to selected or unselected repositories. responses: '200': description: Repository groups and current sandbox repository selection. content: application/json: schema: { $ref: '#/components/schemas/ReposResponse' } examples: repos: value: ok: true type: repos.list environmentId: env_123 installations: - type: Organization accountLogin: acme accountAvatarUrl: https://github.com/acme.png repositories: - id: 123456 databaseId: repo_org_123456 name: web fullName: acme/web description: Marketing site url: https://github.com/acme/web private: true permissions: admin pushedAt: '2026-05-31T12:00:00Z' selectedRepositories: - id: 123456 databaseId: repo_org_123456 name: web fullName: acme/web private: true permissions: admin pushedAt: '2026-05-31T12:00:00Z' baseBranch: dev setupRoutineId: null setupScript: '' setupBlocking: false '401': { $ref: '#/components/responses/Unauthorized' } '409': { $ref: '#/components/responses/Conflict' } post: tags: [Sandboxer] summary: Select repository for sandboxes description: Idempotently selects one repository for the sandbox environment. Use `databaseId` from `GET /repos` as `repositoryId`; selecting an already-selected repository updates its `baseBranch`. operationId: selectRepo requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RepoSelectionRequest' } responses: '200': description: Updated repository selection. content: application/json: schema: { $ref: '#/components/schemas/RepoSelectionResponse' } examples: selected: value: ok: true type: repos.updated success: true environmentId: env_123 selectedRepositories: - databaseId: repo_org_123456 name: web fullName: acme/web baseBranch: dev setupRoutineId: null setupScript: '' setupBlocking: false '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '409': { $ref: '#/components/responses/Conflict' } /api-keys: get: tags: [Sandboxer] summary: List API keys description: "Lists API key metadata only. Raw key secrets are not returned after creation/rotation. Results include expiry and scope. A restricted API-key caller receives `apiKeys: []`; only an unrestricted account key or browser/CLI session receives the account inventory. Bearer header is unchanged; scope lives on the key." operationId: apiKeys responses: '200': description: API key metadata. content: application/json: schema: { $ref: '#/components/schemas/ApiKeysResponse' } examples: keys: value: ok: true type: api_key.list apiKeys: - id: sak_123 name: Production worker credentialLane: scoped-v1 keyPrefix: sandbox_live keyLastFour: 9abc sandboxId: null createdAt: '2026-05-31T12:00:00Z' lastUsedAt: null usage: requests: 1842 windowDays: 30 resources: total: 3 sandboxes: 2 agents: 1 expiresAt: '2026-11-21T12:00:00Z' expired: false expiringSoon: false scope: actions: ['*'] sandboxes: '*' environments: '*' grandfathered: false catalog: actions: [sandbox.read, exec, '*'] presets: read-only: [sandbox.read, file.read, snapshot.read, environment.read, account.read] defaultTtl: 90d maxTtl: 365d scopedCreationEnabled: true '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /api-keys/scoped: post: tags: [Sandboxer] summary: Create a scoped API key description: Creates a scoped, expiring API key. Requires a dashboard/CLI session or an admin-scoped token. The secret is returned once. This dedicated path returns a typed 503 until scoped credential creation is activated. operationId: createScopedApiKey requestBody: required: true content: application/json: schema: type: object properties: name: { type: string } ttl: { type: string, description: 'Duration such as 90d, 24h, or seconds. Max 365d.', examples: [90d] } preset: { type: string, enum: [read-only, full-sandbox, ci, admin] } actions: { type: array, items: { type: string } } sandboxIds: { type: array, items: { type: string } } environmentIds: { type: array, items: { type: string } } responses: '201': description: Created key metadata plus the one-time secret. content: application/json: schema: { $ref: '#/components/schemas/ApiKeySecretResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '503': description: Scoped credential creation has not been activated yet. content: application/json: schema: type: object required: [error, message, scopedCreationEnabled] properties: error: { type: string, enum: [scoped_api_key_creation_disabled] } message: { type: string } scopedCreationEnabled: { type: boolean, examples: [false] } /api-keys/{apiKeyId}: parameters: - name: apiKeyId in: path required: true schema: { type: string } delete: tags: [Sandboxer] summary: Revoke an API key description: Revokes the key immediately. Requires a dashboard or CLI session. operationId: revokeApiKey responses: '200': description: Key revoked. content: application/json: schema: { $ref: '#/components/schemas/ApiKeyRevokeResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /api-keys/{apiKeyId}/rotate: parameters: - name: apiKeyId in: path required: true schema: { type: string } post: tags: [Sandboxer] summary: Rotate an API key description: Replaces the raw secret without changing expiry or scope. Requires a dashboard or CLI session. operationId: rotateApiKey responses: '200': description: Rotated key metadata plus the one-time replacement secret. content: application/json: schema: { $ref: '#/components/schemas/ApiKeySecretResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': description: The scoped API key has already expired and cannot be rotated. content: application/json: schema: type: object required: [error, message] properties: error: { type: string, enum: [api_key_expired] } message: { type: string } /api-keys/{apiKeyId}/usage: parameters: - name: apiKeyId in: path required: true description: API key ID returned by `GET /api-keys`. schema: { type: string } get: tags: [Sandboxer] summary: Get API key usage description: Returns the key's request total for the 30-day UTC window and the sandboxes and Agents it created that still exist, including sandboxes that never got a sandbox row. Works for revoked keys owned by the authenticated account. operationId: apiKeyUsage responses: '200': description: Usage and created resources for the key. content: application/json: schema: { $ref: '#/components/schemas/ApiKeyUsageResponse' } examples: usage: value: ok: true type: api_key.usage id: sak_123 name: Production worker keyPrefix: sandbox_live keyLastFour: 9abc sandboxId: null createdAt: '2026-05-31T12:00:00Z' lastUsedAt: '2026-08-25T09:30:00Z' usage: requests: 1842 windowDays: 30 resources: total: 2 sandboxes: 1 agents: 1 createdResources: - kind: sandbox id: sb_123 name: CI build state: ready createdAt: '2026-08-24T14:00:00Z' - kind: agent id: agent_456 name: Review state: idle createdAt: '2026-08-23T12:00:00Z' '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /webhooks: get: tags: [Sandboxer] summary: List webhooks description: Lists account-wide sandbox lifecycle webhook endpoints. Signing secrets are never included. operationId: listWebhooks responses: '200': description: Registered webhook endpoints. content: application/json: schema: { $ref: '#/components/schemas/WebhookListResponse' } '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Sandboxer] summary: Create webhook description: | Registers an account-wide endpoint for sandbox lifecycle events. The endpoint must use HTTPS on port 443 and resolve only to public addresses. Redirects are not followed during delivery. The signing secret is returned only in this response. An account can register at most 10 unique endpoint URLs. operationId: createWebhook requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/WebhookCreateRequest' } examples: readyAndError: value: name: Production automation url: https://example.com/hooks/sandbox events: [sandbox.ready, sandbox.error] responses: '201': description: Webhook created. Store the one-time signing secret now. content: application/json: schema: { $ref: '#/components/schemas/WebhookSecretResponse' } examples: created: value: ok: true type: webhook.created webhook: id: wh_0123456789abcdef01234567 name: Production automation url: https://example.com/hooks/sandbox events: [sandbox.ready, sandbox.error] createdAt: '2026-08-11T12:00:00Z' updatedAt: '2026-08-11T12:00:00Z' secret: whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '409': { $ref: '#/components/responses/Conflict' } /webhooks/{webhookId}: parameters: - name: webhookId in: path required: true schema: type: string pattern: '^wh_[a-f0-9]{24}$' get: tags: [Sandboxer] summary: Get webhook description: Returns webhook metadata without the signing secret. operationId: getWebhook responses: '200': description: Webhook metadata. content: application/json: schema: { $ref: '#/components/schemas/WebhookResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } patch: tags: [Sandboxer] summary: Update webhook description: Updates the name, endpoint URL, or subscribed events without changing the signing secret. operationId: updateWebhook requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/WebhookUpdateRequest' } responses: '200': description: Updated webhook metadata. content: application/json: schema: { $ref: '#/components/schemas/WebhookResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } delete: tags: [Sandboxer] summary: Delete webhook description: Deletes the endpoint and its queued deliveries. An attempt already in flight can still arrive. operationId: deleteWebhook responses: '200': description: Webhook deleted. content: application/json: schema: { $ref: '#/components/schemas/WebhookDeleteResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /webhooks/{webhookId}/rotate: parameters: - name: webhookId in: path required: true schema: type: string pattern: '^wh_[a-f0-9]{24}$' post: tags: [Sandboxer] summary: Rotate webhook signing secret description: Replaces the endpoint's signing secret. The new secret is returned only in this response; briefly accept the old secret for attempts already in flight. operationId: rotateWebhookSigningSecret responses: '200': description: Signing secret rotated. Store the new secret now. content: application/json: schema: { $ref: '#/components/schemas/WebhookSecretResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /telemetry: get: tags: [Sandboxer] summary: Get OpenTelemetry export description: Returns the account's OpenTelemetry export, or `telemetry` null when none is set. Header values are never included. Needs an API key with `account.admin`. operationId: getTelemetry responses: '200': description: The export and its last result. content: application/json: schema: { $ref: '#/components/schemas/TelemetryResponse' } examples: set: value: ok: true type: telemetry.info telemetry: endpoint: https://otlp.example.com headers: [dd-api-key] lastExportAt: '2026-10-09T12:00:00Z' lastError: null createdAt: '2026-10-09T11:00:00Z' '401': { $ref: '#/components/responses/Unauthorized' } put: tags: [Sandboxer] summary: Set OpenTelemetry export description: | Sets the account's one OTLP/HTTP destination, or replaces it. Sandboxer then sends one metrics export at once and returns the collector's answer in `test`. Every 60 seconds Sandboxer sends the sandbox and account gauges to `/v1/metrics`. Each sandbox lifecycle event goes to `/v1/logs`. The endpoint must use HTTPS on port 443 or 4318 and resolve only to public addresses. Needs an API key with `account.admin`. operationId: setTelemetry requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TelemetrySetRequest' } examples: datadog: value: endpoint: https://otlp.datadoghq.com headers: { dd-api-key: '' } responses: '200': description: Export saved. `test` holds the result of the first export. content: application/json: schema: { $ref: '#/components/schemas/TelemetrySetResponse' } examples: saved: value: ok: true type: telemetry.updated telemetry: endpoint: https://otlp.datadoghq.com headers: [dd-api-key] lastExportAt: null lastError: null createdAt: '2026-10-09T11:00:00Z' test: { ok: true, status: 202, message: null } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } delete: tags: [Sandboxer] summary: Remove OpenTelemetry export description: Removes the export and its queued deliveries. Needs an API key with `account.admin`. operationId: deleteTelemetry responses: '200': description: Export removed. content: application/json: schema: { $ref: '#/components/schemas/TelemetryResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /secrets: get: tags: [Sandboxer] summary: Get Sandboxer secrets setup description: Returns the environment variables and secret files configured for sandboxes. operationId: secrets responses: '200': description: Current secret setup metadata. content: application/json: schema: { $ref: '#/components/schemas/SecretsResponse' } examples: secrets: value: ok: true type: secrets.info environmentId: env_123 envContents: "OPENAI_API_KEY=sk-...\n" secretFiles: - path: .config/service-account.json contents: '{"type":"service_account"}' '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Sandboxer] summary: Update Sandboxer secrets setup operationId: updateSecrets requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SecretsUpdateRequest' } responses: '200': description: Updated secret setup metadata. content: application/json: schema: { $ref: '#/components/schemas/SecretsResponse' } examples: updated: value: ok: true type: secrets.updated success: true environmentId: env_123 envContents: "OPENAI_API_KEY=sk-...\n" secretFiles: [] pushed: { updated: 2, failed: 0 } '401': { $ref: '#/components/responses/Unauthorized' } /environments: get: tags: [Sandboxer] summary: List Sandbox environments description: Returns every non-deleted sandbox environment with its latest-version flags, contents, and per-version sandbox usage. A default environment named `base` is created on first access if none exists. operationId: environments responses: '200': description: The account's sandbox environments. content: application/json: schema: { $ref: '#/components/schemas/SandboxEnvironmentListResponse' } examples: environments: value: environments: - id: 8f1c2d3e-0000-4000-8000-000000000001 name: default isDefault: true latestVersionId: 8f1c2d3e-0000-4000-8000-0000000000a1 safeForThirdParties: false passGithub: true passSecrets: true passSandboxCredentials: true passAgentsCredentials: true envContents: "OPENAI_API_KEY=sk-...\n" secretFiles: [] versions: - id: 8f1c2d3e-0000-4000-8000-0000000000a1 versionNumber: 1 sandboxCount: 3 createdAt: '2026-06-01T12:00:00Z' '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Sandboxer] summary: Create a sandbox environment description: Create a new named environment. It starts on version 1 with all fine-grained flags on and `safeForThirdParties` off. operationId: createEnvironment requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CreateSandboxEnvironmentRequest' } responses: '200': description: Environment created; full list returned. content: application/json: schema: { $ref: '#/components/schemas/SandboxEnvironmentListResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '409': { $ref: '#/components/responses/Conflict' } /environments/{environmentId}: parameters: - name: environmentId in: path required: true schema: { type: string, format: uuid } description: Environment id returned by `GET /environments`. put: tags: [Sandboxer] summary: Update a sandbox environment description: Rename, set as default, and/or edit flags and contents. Any flag or content change mints a new immutable version; existing sandboxes stay on their pinned version until you call upgrade. operationId: updateEnvironment requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UpdateSandboxEnvironmentRequest' } responses: '200': description: Updated environment. content: application/json: schema: { $ref: '#/components/schemas/SandboxEnvironmentResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Delete a sandbox environment description: Soft-deletes an environment. The default environment cannot be deleted, and at least one environment is always kept. operationId: deleteEnvironment responses: '200': description: Environment soft-deleted. content: application/json: schema: { $ref: '#/components/schemas/SandboxEnvironmentResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /environments/{environmentId}/upgrade: post: tags: [Sandboxer] summary: Upgrade sandboxes to an environment's latest version description: Repoints the caller's active sandboxes from an older version of this environment to its latest version, scrubbing any owner secrets the new version drops and hot-pushing the new config. operationId: upgradeEnvironment parameters: - name: environmentId in: path required: true schema: { type: string, format: uuid } requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/UpgradeSandboxEnvironmentRequest' } responses: '200': description: Upgrade result counts. content: application/json: schema: { $ref: '#/components/schemas/UpgradeSandboxEnvironmentResponse' } examples: upgraded: value: success: true upgraded: 2 failed: 0 '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/vars/{key}: parameters: - name: environmentId in: path required: true schema: { type: string, format: uuid } - name: key in: path required: true schema: { type: string, pattern: '^[A-Za-z_][A-Za-z0-9_]*$' } description: Environment variable name. put: tags: [Sandboxer] summary: Set one environment variable description: Sets or replaces a single variable without reading or rewriting the rest. Mints a new immutable version. operationId: setEnvironmentVar requestBody: required: true content: application/json: schema: type: object required: [value] properties: value: { type: string } responses: '200': description: New version minted. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Remove one environment variable description: Removes a single variable. Mints a new immutable version. operationId: deleteEnvironmentVar responses: '200': description: New version minted. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/secret-files: parameters: - name: environmentId in: path required: true schema: { type: string, format: uuid } put: tags: [Sandboxer] summary: Write one secret file description: Adds or replaces a single secret file by path. Mints a new immutable version. operationId: setEnvironmentSecretFile requestBody: required: true content: application/json: schema: type: object required: [path, contents] properties: path: type: string description: In-sandbox path relative to the workspace, e.g. `.env` or `repo/.env`. contents: { type: string } responses: '200': description: New version minted. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Remove one secret file description: Removes the secret file at `path`. Mints a new immutable version. operationId: deleteEnvironmentSecretFile parameters: - name: path in: query required: true schema: { type: string } responses: '200': description: New version minted. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/repos: post: tags: [Sandboxer] summary: Add a repository to an environment description: Adds a repository (or updates its base branch) by database id from the repository list. Mints a new immutable version. operationId: addEnvironmentRepo parameters: - name: environmentId in: path required: true schema: { type: string, format: uuid } requestBody: required: true content: application/json: schema: type: object required: [repositoryId] properties: repositoryId: type: string description: The repository `databaseId` from `GET /repos`. baseBranch: type: string default: main responses: '200': description: New version minted. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /environments/{environmentId}/repos/{repositoryId}: delete: tags: [Sandboxer] summary: Remove a repository from an environment description: Removes one repository from the environment's selection. Mints a new immutable version. operationId: deleteEnvironmentRepo parameters: - name: environmentId in: path required: true schema: { type: string, format: uuid } - name: repositoryId in: path required: true schema: { type: string } description: The repository `databaseId`. responses: '200': description: New version minted. content: application/json: schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes: get: tags: [Sandboxer] summary: List sandboxes description: >- Your sandboxes. In an organization's scope (the active wallet, `org`, or `X-Sandboxer-Org`) the list also holds every sandbox that organization pays for, whoever created it; `createdBy` and `access` say whose it is and what you may do with it. operationId: sandboxes parameters: - $ref: '#/components/parameters/OrgId' - $ref: '#/components/parameters/OrgHeader' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Sort' - name: state in: query schema: { type: string } description: Comma-separated sandbox state filter, for example `ready,idle,running`. - name: dataExpired in: query schema: { type: boolean } description: '`true` keeps sandboxes whose data retention deleted (`dataExpiredAt` is set); `false` the others.' - name: stoppedBefore in: query schema: { type: string, format: date-time } description: Keep stopped sandboxes that stopped before this ISO 8601 time. responses: '200': description: Sandboxes owned by the authenticated Sandboxer user. content: application/json: schema: { $ref: '#/components/schemas/SandboxListResponse' } examples: sandboxes: value: ok: true type: sandbox.list sandboxes: - id: sb_k3j9x2m1qa name: Sandbox 2026-05-31 12:00 state: idle url: https://machine.box.sandboxer.com ip: 203.0.113.10 createdAt: '2026-05-31T12:00:00Z' updatedAt: '2026-05-31T12:05:00Z' archiveAfter: '2026-05-31T13:00:00Z' desktopAvailable: true desktopUrl: https://desktop.example/stream.html?token=redacted snapshotAvailable: false snapshotCompletedAt: null '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Sandboxer] summary: Create sandbox description: >- Provision a new cloud computer. Store the returned `sandbox.id` with your product job/session record. Send an `Idempotency-Key` header to make this call safe to retry after a lost response without creating a second billable sandbox. operationId: create parameters: - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/OrgId' - $ref: '#/components/parameters/OrgHeader' requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/CreateSandboxRequest' } responses: '202': description: Sandboxer accepted for provisioning. content: application/json: schema: { $ref: '#/components/schemas/CreateSandboxResponse' } examples: provisioning: value: ok: true type: sandbox.created status: provisioning ttlSeconds: 3600 sandbox: id: sb_k3j9x2m1qa name: Sandbox 2026-05-31 12:00 state: provisioning url: null ip: null createdAt: '2026-05-31T12:00:00Z' updatedAt: '2026-05-31T12:00:00Z' archiveAfter: '2026-05-31T13:00:00Z' desktopAvailable: false desktopUrl: null snapshotAvailable: false snapshotCompletedAt: null '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/RateLimited' } '503': { $ref: '#/components/responses/NoReadyMachine' } /sandboxes/{sandboxId}: parameters: - $ref: '#/components/parameters/SandboxId' get: tags: [Sandboxer] summary: Get sandbox description: Poll this endpoint after create, stop, resume, or fork operations. operationId: get responses: '200': description: Sandboxer details. content: application/json: schema: { $ref: '#/components/schemas/SandboxInfoResponse' } examples: sandbox: value: ok: true type: sandbox.info sandbox: id: sb_k3j9x2m1qa name: Sandbox 2026-05-31 12:00 state: idle url: https://machine.box.sandboxer.com ip: 203.0.113.10 createdAt: '2026-05-31T12:00:00Z' updatedAt: '2026-05-31T12:05:00Z' archiveAfter: '2026-05-31T13:00:00Z' desktopAvailable: true desktopUrl: https://desktop.example/stream.html?token=redacted snapshotAvailable: false snapshotCompletedAt: null '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } patch: tags: [Sandboxer] summary: Update sandbox operationId: update requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/UpdateSandboxRequest' } responses: '200': description: Updated sandbox details. content: application/json: schema: { $ref: '#/components/schemas/SandboxInfoResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Permanently delete sandbox data description: >- Accept irreversible background deletion of the sandbox machine, snapshots, and content-bearing records. This is not archive: the sandbox cannot be resumed. Named snapshots and other shared artifacts remain independent. Poll the returned operation until `completed`. operationId: deleteSandbox parameters: - $ref: '#/components/parameters/ConfirmDelete' responses: '202': description: Deletion confirmed and accepted for background processing. content: application/json: schema: { $ref: '#/components/schemas/DeletionOperationResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/stop: post: tags: [Sandboxer] summary: Stop and archive sandbox description: | Stop active work and archive/snapshot the sandbox for later resume or fork. A stop always saves the sandbox's disk first. If that save is failing, the stop is refused and the sandbox keeps running so your work is not discarded, we retry automatically and email you. You are not billed for time spent in that state. The stop is an operation: the response and the sandbox carry it as `stop`, with an `id` and a `status`. Poll `GET /sandboxes/{sandboxId}` until `stop.status` is `completed`. A failed attempt shows `failing` with its `error` and is retried until the sandbox archives; commands and prompts you send meanwhile do not cancel it. Only a resume ends it early (`superseded`). Calling stop again while one is pending returns the same `id`. Set `force: true` to stop anyway, keeping the last successful snapshot and losing everything written since then. This is irreversible; only reach for it after a stop has already failed. A force on a stop that is already pending escalates that stop. operationId: stop parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/StopRequest' } responses: '202': description: Sandboxer archival started or already in progress. content: application/json: schema: { $ref: '#/components/schemas/SandboxActionResponse' } examples: archiving: value: ok: true type: sandbox.stopping id: sb_k3j9x2m1qa status: archiving sandbox: { id: sb_k3j9x2m1qa, name: Sandbox 2026-05-31 12:00, state: archiving, desktopAvailable: true, snapshotAvailable: false } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/share: post: tags: [Sandboxer] summary: Share sandbox with the organization description: | Let every member of the organization that pays for this sandbox use it. Your personal logins (GitHub token, Claude and Codex logins, environment secrets, the sandbox's own CLI key) are never pushed to it again and are wiped off its disk at its next start, so a teammate never acts as you. Files you wrote stay. One-way: the sandbox stays like a `noEnv` sandbox from then on. A running sandbox stays `view` for the organization until you stop and resume it (`restartRequired`). Only the person who created the sandbox can share it, and only a sandbox billed to an organization. Sharing an already shared sandbox is a no-op. operationId: share parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: The sandbox is shared. content: application/json: schema: type: object properties: sandbox: { $ref: '#/components/schemas/Sandbox' } restartRequired: type: boolean description: >- True when the sandbox was running: what runs on it still holds your logins in memory, so it stays `view` for the organization until you stop and resume it. message: { type: string } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/recover: post: tags: [Sandboxer] summary: Recover a sandbox that stopped responding description: | Power-cycle the machine of a sandbox that stopped answering. The disk is not touched. The sandbox usually answers again within 1-2 minutes and then saves your work by itself; `sandbox.recovered` fires when it does. Refused with `409`: - `sandbox_is_reachable`: the sandbox answers normally. - `too_soon`: a recover started less than 10 minutes ago. - `no_machine`: the sandbox has no machine. Resume it instead. - `recover_unsupported`: its machine cannot be power-cycled. Wait for it to answer, or resume it from its last snapshot. - `reset_failed`: the power-cycle failed. Reply to the incident email. operationId: recover parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: The power-cycle started. content: application/json: schema: type: object properties: ok: { type: boolean } type: { type: string, examples: [sandbox.recovering] } status: { type: string, enum: [recovering] } message: { type: string } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: The sandbox cannot be recovered now. `code` says why (see above). content: application/json: schema: { $ref: '#/components/schemas/ErrorEnvelope' } example: ok: false type: sandbox.error status: 409 code: recover_unsupported message: This sandbox runs on a machine that cannot be power-cycled. Wait for it to answer again, or resume it from its last snapshot. requestId: req_01HX... error: code: recover_unsupported message: This sandbox runs on a machine that cannot be power-cycled. Wait for it to answer again, or resume it from its last snapshot. status: 409 /sandboxes/{sandboxId}/resume: post: tags: [Sandboxer] summary: Resume sandbox operationId: resume parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/ResumeRequest' } responses: '202': description: Resume started. content: application/json: schema: { $ref: '#/components/schemas/SandboxActionResponse' } examples: resuming: value: ok: true type: sandbox.resuming id: sb_k3j9x2m1qa status: resuming sandbox: { id: sb_k3j9x2m1qa, name: Sandbox 2026-05-31 12:00, state: provisioning, desktopAvailable: false, snapshotAvailable: true } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/RateLimited' } '503': { $ref: '#/components/responses/NoReadyMachine' } /sandboxes/{sandboxId}/fork: post: tags: [Sandboxer] summary: Fork sandbox description: >- Provision a new sandbox from an existing one. Send an `Idempotency-Key` header to make this call safe to retry after a lost response without creating a second billable fork. operationId: fork parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: false content: application/json: schema: type: object properties: failFast: type: boolean default: false description: >- Pass `true` to fork only if a machine is ready right now. Within about 1.5 s you get the usual `202`, or `503` `no_ready_machine` with no fork created and nothing counted against your start limits. region: type: string description: A fork runs in the source sandbox's region; naming another is `400 region_mismatch`. monitoring: type: boolean description: >- `false` creates the fork with monitoring off: Sandboxer keeps no command or process output, process list or CPU/RAM samples for it. Omitted, the fork keeps the source sandbox's setting. `true` turns it on for the fork, unless the account has monitoring off by default (`409` `monitoring_off_for_account`). env: type: object additionalProperties: type: string description: >- Replaces the env the fork would otherwise inherit from the source sandbox. Same validation rules as `CreateSandboxRequest.env`. environment: type: string description: >- Optionally pin the fork to a different named sandbox environment. Omit to inherit the source sandbox's environment. Unknown names are rejected with `unknown_environment`. examples: [base, customer-demos] noEnv: type: boolean description: >- Make the fork no-env (see `CreateSandboxRequest.noEnv`). A fork of a no-env sandbox is always no-env regardless of this field. type: type: string enum: [small, default, large, xlarge] description: >- Machine size for the fork. Omit to inherit the source sandbox's type. `xlarge` needs the $100/month plan or higher (403 `machine_class_plan_required`). The source sandbox is never modified. Shrinking is rejected with `type_too_small` when the source's data would not fit the smaller disk. ttlSeconds: oneOf: - type: integer minimum: 1 maximum: 2592000 - type: 'null' default: 3600 description: >- Auto-stop for the fork, in seconds. Omit for the 1 hour default; the fork does NOT inherit the source sandbox's TTL, so forking a sandbox that has auto-stop disabled still gives you a fork that stops itself. `null` disables auto-stop, which means nothing will ever stop this sandbox for you. responses: '202': description: Fork started. The response `id` is the new forked sandbox id. content: application/json: schema: { $ref: '#/components/schemas/SandboxActionResponse' } examples: forking: value: ok: true type: sandbox.forking id: sb_7f2kq9hd4m status: forking sandbox: { id: sb_7f2kq9hd4m, name: Sandbox fork, state: provisioning, desktopAvailable: false, snapshotAvailable: false } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '429': { $ref: '#/components/responses/RateLimited' } '503': { $ref: '#/components/responses/NoReadyMachine' } /sandboxes/{sandboxId}/prompt: post: tags: [Sandboxer] summary: Prompt Sandbox description: >- Queue a natural-language work item for an agent harness (Codex, Claude Code, pi, OpenCode, or Prime Agent) inside the sandbox. Observe progress with `GET /sandboxes/{sandboxId}/events`. A Sandbox runs many conversations in parallel. Set `new: true` to start a fresh one, `conversationId` to continue a specific one, or omit both to continue the most-recently-active conversation; the response returns the `conversationId`. See [Integrated agents](/integrated-agents). operationId: prompt parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/PromptRequest' } examples: repoWork: value: provider: codex model: gpt-5.4 reasoningEffort: medium prompt: Work on the selected repo, run tests, fix failures, commit the result, and report any hosted preview URL. computerUse: value: provider: claude-code model: sonnet reasoningEffort: high prompt: Use the browser to research flight options to France and return the best itinerary summary with source links. responses: '202': description: Prompt queued. content: application/json: schema: { $ref: '#/components/schemas/PromptResponse' } examples: queued: value: ok: true type: prompt.queued id: sb_k3j9x2m1qa promptId: prompt_123 conversationId: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c promptRun: id: prompt_123 promptId: prompt_123 sandboxId: sb_k3j9x2m1qa status: queued done: false conversationId: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c status: queued provider: codex model: gpt-5.4 reasoningEffort: medium fast: false '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/events: get: tags: [Sandboxer] summary: List sandboxer events description: Return Sandbox work, lifecycle, and progress events so product UIs can show what the sandbox is doing. A Sandbox runs many conversations in parallel; this streams **all** of them by default and every event carries a `conversationId`. Filter to one or more with `conversation`. Event payloads are extensible. Clients can long-poll this endpoint with `sort=asc` and a cursor to stream responses. Response events expose agent text, streaming partials via `data.is_streaming`, and tool calls/results via `data.tools`. operationId: events parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Sort' - name: type in: query schema: { type: string } description: Comma-separated event type filter, for example `prompt,response,steer`. - name: conversation in: query schema: { type: string } description: Only return events for this conversation id. Repeat the parameter (or comma-separate) for several. Omit to stream every conversation on the sandbox. See [Integrated agents](/integrated-agents). responses: '200': description: >- The agent's work on the sandbox (prompts, responses), not sandbox lifecycle. A sandbox that was never prompted returns an empty list even though it started, stopped and resumed. content: application/json: schema: { $ref: '#/components/schemas/EventsResponse' } examples: progress: value: ok: true type: events.list id: sb_k3j9x2m1qa events: - id: prompt_123 type: prompt timestamp: 1780347055370 taskId: prompt_123 data: prompt: Run tests and summarize failures. status: running is_reverted: false - id: response_123-tools type: response timestamp: 1780347063427 taskId: prompt_123 data: content: "" model: gpt-5.4 tools: - use: id: call_123 type: tool_use name: Bash input: command: npm test result: type: tool_result tool_use_id: call_123 is_error: false content: '{"exitCode":0}' is_reverted: false - id: response_123 type: response timestamp: 1780347065650 taskId: prompt_123 data: content: Tests passed. model: gpt-5.4 is_reverted: false pageInfo: nextCursor: null hasMore: false limit: 100 '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/conversations: get: tags: [Sandboxer] summary: List sandboxer conversations description: >- Every conversation on the sandbox, most recently prompted first, with what you need to pick one: how many prompts it holds, whether a turn is running in it right now, the last harness and model it ran on, a preview of its last prompt, and `current`, the one a `POST /prompt` without `new` or `conversationId` would continue. Pass an `id` back as `conversationId` on `POST /prompt` to resume that thread, or as `conversation` on `POST /steer`, `POST /interrupt` and `GET /events` to scope those. See [Integrated agents](/integrated-agents). operationId: conversations parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: Conversations on the sandbox. A Sandbox that was never prompted returns an empty list. content: application/json: schema: { $ref: '#/components/schemas/ConversationsResponse' } examples: twoConversations: value: ok: true type: conversation.list id: sb_k3j9x2m1qa conversations: - id: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c createdAt: '2026-09-08T10:05:00.000Z' lastPromptAt: '2026-09-08T10:05:00.000Z' prompts: 1 running: true lastHarness: pi lastModel: claude-sonnet-5 lastPromptPreview: Investigate the flaky CI job current: true - id: 2c0d9e4b-7a1f-4c3e-8b5d-6e7f8a9b0c1d createdAt: '2026-09-08T09:40:00.000Z' lastPromptAt: '2026-09-08T10:03:00.000Z' prompts: 2 running: false lastHarness: claude lastModel: claude-opus-4-8 lastPromptPreview: Now add tests current: false '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/prompts/{promptId}: get: tags: [Sandboxer] summary: Get prompt run status description: Returns first-class status for a queued/running/finished prompt so clients do not infer completion from sandbox state and events. operationId: promptRunStatus parameters: - $ref: '#/components/parameters/SandboxId' - name: promptId in: path required: true schema: { type: string } responses: '200': description: Prompt run status. content: application/json: schema: { $ref: '#/components/schemas/PromptRunResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/stops/{stopId}: get: tags: [Sandboxer] summary: Get a stop by id description: Returns one stop of this sandbox by the id that `POST /sandboxes/{sandboxId}/stop` returned. Old stops stay readable after a resume or a later stop. Returns 404 when the id is unknown or belongs to another sandbox. operationId: getStop parameters: - $ref: '#/components/parameters/SandboxId' - name: stopId in: path required: true schema: { type: string } responses: '200': description: The stop. content: application/json: schema: { $ref: '#/components/schemas/StopOperationResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/files: get: tags: [Sandboxer] summary: Read a file from a sandbox description: Files up to 5 MiB. Use `GET /sandboxes/{sandboxId}/artifacts` for files up to 50 MiB, and SCP over SSH for larger ones. operationId: readFile parameters: - $ref: '#/components/parameters/SandboxId' - name: path in: query required: true schema: { type: string, description: "Absolute path, or path relative to the sandbox work directory (/home/user). The canonicalized path must resolve under /home/user or /tmp; anything else is rejected with a 400 invalid_path error." } - name: encoding in: query schema: { type: string, enum: [utf8, base64], default: utf8 } responses: '200': description: File contents. content: application/json: schema: { $ref: '#/components/schemas/FileReadResponse' } '400': { $ref: '#/components/responses/BadRequest' } put: tags: [Sandboxer] summary: Write a file in a sandbox description: Files up to 5 MiB after decoding. Use SCP over SSH for larger files. operationId: writeFile parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/FileWriteRequest' } responses: '200': description: File written. content: application/json: schema: { $ref: '#/components/schemas/FileWriteResponse' } '400': { $ref: '#/components/responses/BadRequest' } /sandboxes/{sandboxId}/commands: post: tags: [Sandboxer] summary: Execute a command in a sandbox description: "Runs the command synchronously by default (timeout configurable via `timeoutSeconds`, 600s cap). With `detached: true` the command starts in the background and a process id is returned immediately; poll `/sandboxes/{sandboxId}/commands/{processId}` for status and logs. With `stream: true` the output arrives as the command writes it, as newline-delimited JSON (`application/x-ndjson`): `{\"type\":\"started\"}`, then `{\"type\":\"stdout\"|\"stderr\",\"data\":\"...\"}` per chunk, and last `{\"type\":\"exit\",...}` (the synchronous result without stdout/stderr) or `{\"type\":\"error\",\"error\":\"...\",\"message\":\"...\"}`. Returns 400 invalid_timeout when `timeoutSeconds` is not an integer in 1-600. A command sent while the sandbox is still starting (right after a resume) waits up to 60s for it to be ready instead of failing; 409 sandbox_starting (retryable) is returned only if it is still not ready by then. A sandbox that is not starting (`error`, `stopped`) returns 409 sandbox_not_ready with `retryable: false` and its `state` at once: resume or recover it first. Command execution is never retried automatically: a 502 sandbox_direct_failed means the command may already be running on the sandbox. Every command and its output are kept 90 days in the command history (`/sandboxes/{sandboxId}/command-history`), readable while the sandbox is stopped." operationId: command parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/CommandRequest' } responses: '200': description: 'Command result (synchronous), process start confirmation (detached), or the output stream (`stream: true`).' content: application/json: schema: oneOf: - $ref: '#/components/schemas/CommandResponse' - $ref: '#/components/schemas/CommandStartedResponse' application/x-ndjson: schema: { $ref: '#/components/schemas/CommandStreamFrame' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/commands/{processId}: get: tags: [Sandboxer] summary: Get detached command status and logs operationId: commandStatus description: 'Without offsets, the answer holds the end of each log. With `stdoutOffset` or `stderrOffset`, it holds only the output written since those byte offsets, so each poll returns only new output. A sandbox agent too old for offsets answers 409 `agent_outdated`.' parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/ProcessId' - name: tailBytes in: query schema: { type: integer, minimum: 1, maximum: 8388608 } description: Cap each returned log to its last N bytes. Defaults to 8388608 (8 MiB). Ignored with offsets. - name: stdoutOffset in: query schema: { type: integer, minimum: 0 } description: Return only stdout written from this byte offset. Use `stdoutNextOffset` of the previous answer. - name: stderrOffset in: query schema: { type: integer, minimum: 0 } description: Return only stderr written from this byte offset. Use `stderrNextOffset` of the previous answer. responses: '200': description: Process status and log tails, or the output since the offsets. content: application/json: schema: { $ref: '#/components/schemas/CommandStatusResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } delete: tags: [Sandboxer] summary: Stop a detached command description: Sends SIGTERM to the command and every process it started, then SIGKILL to what is left after 5 s. Only a process started with `detached` can be stopped. The response carries the command's history record with its final state and output. operationId: stopCommand parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/ProcessId' responses: '200': description: The command is stopped. content: application/json: schema: { $ref: '#/components/schemas/CommandStopResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/commands/{processId}/stream: get: tags: [Sandboxer] summary: Attach to a detached command description: 'The output of a detached command as it is written, one JSON object per line, until the command ends. After 10 minutes the stream ends with a `reconnect` line: open it again with its offsets to continue with no gap. One open stream replaces polling. Needs the `exec` key scope.' operationId: attachCommand parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/ProcessId' - name: stdoutOffset in: query schema: { type: integer, minimum: 0, default: 0 } description: Start stdout at this byte offset. 0 sends the whole log. - name: stderrOffset in: query schema: { type: integer, minimum: 0, default: 0 } description: Start stderr at this byte offset. 0 sends the whole log. responses: '200': description: The output stream. content: application/x-ndjson: schema: { $ref: '#/components/schemas/CommandAttachFrame' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/command-history: get: tags: [Sandboxer] summary: List the command and process history description: 'Every command run through `POST /sandboxes/{sandboxId}/commands` in the last 90 days, and every other process that wrote to its stdout/stderr in the last 72 hours (mode `process`), newest first. Works while the sandbox is stopped.' operationId: commandHistory parameters: - $ref: '#/components/parameters/SandboxId' - name: mode in: query schema: { type: string, examples: [process, commands, 'sync,detached'] } description: 'Only these modes, comma-separated: sync, stream, detached, process, or `commands` for the first three. Default: all.' - name: q in: query schema: { type: string, maxLength: 200, examples: ['npm run'] } description: Only commands whose command line contains this text, in any case. - name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 20 } - name: cursor in: query schema: { type: string } description: The `nextCursor` of the previous page. responses: '200': description: One page of commands. content: application/json: schema: { $ref: '#/components/schemas/CommandListResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/command-history/{commandId}: get: tags: [Sandboxer] summary: Get one command from the history operationId: getCommand parameters: - $ref: '#/components/parameters/SandboxId' - name: commandId in: path required: true schema: { type: string } responses: '200': description: The command. content: application/json: schema: { $ref: '#/components/schemas/CommandInfoResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/command-history/{commandId}/logs: get: tags: [Sandboxer] summary: Read a command's stored output description: Reads stdout or stderr from a byte offset, at most 1 MiB per call. Read on with `nextOffset` until `done`. A running detached command gains output every 30 s; a streamed one every 2 s. Works while the sandbox is stopped. operationId: commandLogs parameters: - $ref: '#/components/parameters/SandboxId' - name: commandId in: path required: true schema: { type: string } - name: stream in: query schema: { type: string, enum: [stdout, stderr], default: stdout } - name: offset in: query schema: { type: integer, minimum: 0, default: 0 } - name: limit in: query schema: { type: integer, minimum: 1, maximum: 1048576, default: 1048576 } - name: encoding in: query schema: { type: string, enum: [utf8, base64], default: utf8 } description: base64 returns the exact bytes. utf8 can split a character at the edge of a read. responses: '200': description: The bytes from `offset`. content: application/json: schema: { $ref: '#/components/schemas/CommandLogsResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/processes: get: tags: [Sandboxer] summary: List the processes in a sandbox description: 'A running sandbox: every user-space process, read now, busiest first. A stopped sandbox: the last sample kept (`live: false`).' operationId: processes parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: The processes. content: application/json: schema: { $ref: '#/components/schemas/ProcessListResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/artifacts: get: tags: [Sandboxer] summary: Download a sandbox artifact description: Downloads one file of up to 50 MiB, under /home/user or /tmp, as raw bytes. operationId: artifact parameters: - $ref: '#/components/parameters/SandboxId' - name: path in: query required: true schema: { type: string } responses: '200': description: Artifact bytes. content: application/octet-stream: schema: type: string format: binary /sandboxes/{sandboxId}/steer: post: tags: [Sandboxer] summary: Steer a running turn description: >- Send a new message to a turn that is already running, without losing what the agent is doing. This is what typing into a coding agent while it works does: the instruction is taken into account and the work continues. Where the harness has a native mid-turn primitive (Claude Code, Codex, pi, Prime Agent) nothing is interrupted and the response has `native: true`. Where it does not (OpenCode), Sandboxer interrupts that turn and immediately continues the SAME conversation with the instruction, keeping the session and its memory, and the response has `native: false`. Refused with **409 `no_running_turn`** when the conversation has nothing in flight; queue work with `POST /prompt` instead. `error.details.reason` says why: `turn_ended` (with `error.details.lastTurn.status` and `.error` of the newest turn), `turn_not_started` (the turn is starting; retry in a moment), `no_turn`, `not_running` (the sandbox is not running) or `delivery_failed` (the sandbox did not accept the steer; check the turn before sending more work). The steer shows up in `GET /events` as a `steer` event. See [Integrated agents](/integrated-agents). operationId: steer parameters: - $ref: '#/components/parameters/SandboxId' - name: conversation in: query schema: { type: string } description: Conversation id to steer. Also accepted in the body. Omit to steer the sandbox's most-recently-active conversation. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SteerRequest' } examples: addATask: value: message: Also write /home/user/steered.txt containing the word STEERED, then finish. oneConversation: value: conversation: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c message: Skip the integration tests, unit tests are enough. responses: '200': description: Message delivered to the running turn. content: application/json: schema: { $ref: '#/components/schemas/SteerResponse' } examples: steered: value: ok: true type: prompt.steered id: sb_k3j9x2m1qa conversationId: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c promptId: prompt_456 native: true mode: native status: steered '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/interrupt: post: tags: [Sandboxer] summary: Interrupt running work description: Interrupt running agent work. By default this stops **every** conversation on the sandbox. Pass `conversation` to stop just one, leaving the others running. See [Integrated agents](/integrated-agents). operationId: interrupt parameters: - $ref: '#/components/parameters/SandboxId' - name: conversation in: query schema: { type: string } description: Interrupt only this conversation id. Omit to interrupt the whole sandbox. responses: '200': description: Interrupt requested. content: application/json: schema: { $ref: '#/components/schemas/SandboxActionResponse' } examples: interrupted: value: ok: true type: sandbox.interrupted id: sb_k3j9x2m1qa status: interrupted '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/desktop: post: tags: [Sandboxer] summary: Get desktop streaming URL description: "Create or fetch a secret-bearing desktop/noVNC URL for live computer-use visibility. Use `?vnc=1` for noVNC; a `provisioning: true` response means poll again." operationId: desktop parameters: - $ref: '#/components/parameters/SandboxId' - name: vnc in: query schema: type: integer enum: [1] description: Use VNC/noVNC streaming mode. - name: theme in: query schema: type: string enum: [light, dark] description: Desktop streaming theme for non-VNC mode. requestBody: required: false content: application/json: schema: { $ref: '#/components/schemas/DesktopRequest' } responses: '200': description: Desktop streaming URL, or provisioning state for VNC setup. content: application/json: schema: { $ref: '#/components/schemas/DesktopResponse' } examples: ready: value: ok: true type: desktop.url success: true desktopUrl: https://sandbox-preview.example/vnc.html?_token=redacted ip: 203.0.113.10 mode: vnc provisioning: value: ok: true type: desktop.provisioning provisioning: true message: Preparing VNC desktop… '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/host: post: tags: [Sandboxer] summary: Expose a sandbox port on a public HTTPS URL description: "Registers a stable `https://-.box.sandboxer.com` route for a port inside the sandbox and opens the sandbox firewall for it. Idempotent: calling it again for the same sandbox and port returns the same URL and token." operationId: hostPort parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/HostPortRequest' } responses: '200': description: The public HTTPS URL for the port. content: application/json: schema: { $ref: '#/components/schemas/HostPortResponse' } examples: hosted: value: ok: true type: port.hosted success: true port: 3000 url: https://swift-otter-9021-3000.box.sandboxer.com?_token=redacted isProtected: true access: private '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } get: tags: [Sandboxer] summary: List hosted ports description: Sandboxer extension. Every hosted port of the sandbox with its URL. operationId: listHostedPorts parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: Hosted ports. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object properties: ports: { type: array, items: { $ref: '#/components/schemas/HostPortResponse' } } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Hide a hosted port description: Sandboxer extension. Removes the route for `port`. operationId: unhostPort parameters: - $ref: '#/components/parameters/SandboxId' - name: port in: query required: true schema: { type: integer, minimum: 1, maximum: 65535 } responses: '200': description: Port hidden. content: application/json: schema: { $ref: '#/components/schemas/SuccessBase' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/sshkey: post: tags: [Sandboxer] summary: Configure sandbox SSH key description: | Adds an OpenSSH public key to the running sandbox so the caller can SSH as user `user`. The response carries `machineIp`, which is the host to connect to on port 22. This is also how you reach your own machine from inside a sandbox without the CLI: authorize the key, then open a reverse tunnel with stock OpenSSH, and a port on your machine answers at `127.0.0.1:` inside the sandbox for as long as the ssh process runs. ```bash ssh -o ExitOnForwardFailure=yes -i ~/.ssh/id_ed25519 -N -R 7777:127.0.0.1:7777 user@ ``` `sbx forward --reverse --local 7777` is the same tunnel with the key handling and redial done for you. operationId: sshKey parameters: - $ref: '#/components/parameters/SandboxId' requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/SshKeyRequest' } responses: '200': description: SSH key setup result. content: application/json: schema: { $ref: '#/components/schemas/SshKeyResponse' } examples: configured: value: ok: true type: ssh_key.configured success: true machineIp: 203.0.113.10 sshUser: user '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '402': { $ref: '#/components/responses/PaymentRequired' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/exec/ws: get: tags: [Sandboxer] summary: Interactive exec over WebSocket description: | Sandboxer extension. Upgrades to a WebSocket speaking the exec protocol: the first client text frame is `{"command": string|string[]|null, "pty": bool, "cols", "rows", "cwd"?, "env"?, "user"?}` (`command` null with `pty: true` opens a login shell); client binary frames are stdin; client text frames are control messages (`{"type":"resize","cols","rows"}`, `{"type":"eof"}`, `{"type":"signal","signal":"SIGINT"}`). Server binary frames start with a stream byte (1 stdout, 2 stderr); the last text frame is `{"type":"exit","exitCode":N}`. Browsers may authenticate with `?access_token=`. operationId: execWebSocket parameters: - $ref: '#/components/parameters/SandboxId' - name: access_token in: query schema: { type: string } description: API key or CLI token for clients that cannot set the Authorization header. responses: '101': { description: Switching protocols. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/ssh: get: tags: [Sandboxer] summary: Raw TCP tunnel to the sandbox SSH server over WebSocket description: Sandboxer extension used by `sbx ssh`. Binary frames carry the raw TCP stream to `127.0.0.1:22` inside the sandbox (or `port`). operationId: sshWebSocket parameters: - $ref: '#/components/parameters/SandboxId' - name: port in: query schema: { type: integer, minimum: 1, maximum: 65535, default: 22 } - name: access_token in: query schema: { type: string } responses: '101': { description: Switching protocols. } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/stats: get: tags: [Sandboxer] summary: Live resource usage description: Sandboxer extension. CPU, memory and disk of a running sandbox, read from the machine now. operationId: stats parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: Stats, or `live:false` when the sandbox is not running. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object properties: id: { type: string } live: { type: boolean } state: { type: string } sampledAt: { type: [string, 'null'], format: date-time } stats: type: [object, 'null'] properties: cpuPercent: { type: number } memoryUsedMb: { type: integer } memoryTotalMb: { type: integer } diskUsedGb: { type: number } diskTotalGb: { type: number } uptimeSeconds: { type: integer } loadAvg: { type: array, items: { type: number } } ioPercent: { type: number, description: 'Share of the last 10 s some task waited on IO (Linux PSI).' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/metrics: get: tags: [Sandboxer] summary: Resource usage history description: >- Sandboxer extension. CPU, memory, disk and IO of a sandbox with monitoring on, sampled every 30 s and kept 8 days, averaged into buckets (1h → 30 s, 24h → 10 min, 1w → 1 h). `current` is a live reading while the sandbox runs, else the newest stored sample. operationId: metrics parameters: - $ref: '#/components/parameters/SandboxId' - name: window in: query schema: { type: string, enum: ['1h', '24h', '1w'], default: '1h' } responses: '200': description: The history and current reading. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [id, window, stepSeconds, live, points] properties: id: { type: string } window: { type: string, enum: ['1h', '24h', '1w'] } stepSeconds: { type: integer } live: { type: boolean } current: type: [object, 'null'] properties: cpuPercent: { type: number } memoryUsedMb: { type: integer } memoryTotalMb: { type: integer } memoryPercent: { type: number } diskUsedGb: { type: number } diskTotalGb: { type: number } diskPercent: { type: number } ioPercent: { type: number } sampledAt: { type: string, format: date-time } points: type: array items: { $ref: '#/components/schemas/MetricPoint' } stops: type: array description: >- When the sandbox wasn't running inside the window (stops, auto-stops, idle pauses, billing stops), oldest first, so a chart can break there and bridge missed samples elsewhere. items: { $ref: '#/components/schemas/StopInterval' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/metrics: get: tags: [Sandboxer] summary: Usage sparklines for many sandboxes description: >- Sandboxer extension. Up to 30 bucketed points per sandbox (monitoring on, newest 100 sandboxes the caller can see), for list sparklines. Sandboxes with no samples in the window are omitted. operationId: metricsSummary parameters: - name: window in: query schema: { type: string, enum: ['1h', '24h', '1w'], default: '1h' } - name: ids in: query description: Comma-separated sandbox ids to limit the answer to. schema: { type: string } responses: '200': description: Points keyed by sandbox id. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [window, stepSeconds, sandboxes] properties: window: { type: string, enum: ['1h', '24h', '1w'] } stepSeconds: { type: integer } sandboxes: type: object additionalProperties: type: array items: { $ref: '#/components/schemas/MetricPoint' } stops: type: object description: The stopped intervals per sandbox id (see the single-sandbox metrics). additionalProperties: type: array items: { $ref: '#/components/schemas/StopInterval' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /provider-models: get: tags: [Sandboxer] summary: List agent providers and models description: The live catalog of harnesses, their models and the reasoning-effort levels each model accepts. operationId: providerModels responses: '200': description: Provider catalog. content: application/json: schema: allOf: - $ref: '#/components/schemas/SuccessBase' - type: object required: [providers] properties: providers: type: array items: type: object properties: id: { type: string } provider: { type: string } name: { type: string } aliases: { type: array, items: { type: string } } default: { type: boolean } models: type: array items: type: object properties: id: { type: string } name: { type: string } default: { type: boolean } reasoningEfforts: { type: array, items: { type: string } } fastMode: { type: boolean } reasoningEfforts: { type: array, items: { type: string } } '401': { $ref: '#/components/responses/Unauthorized' } /named-snapshots: get: tags: [Sandboxer] summary: List named snapshots description: List your named snapshots, newest first. operationId: listNamedSnapshots responses: '200': description: Named snapshots owned by the authenticated Sandboxer user. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotListResponse' } '401': { $ref: '#/components/responses/Unauthorized' } post: tags: [Sandboxer] summary: Save a named snapshot description: >- Freeze a sandbox's current state under a name (`sbx snapshot ` in the CLI). Answers `202` immediately with the snapshot in `saving`; a running sandbox takes a fresh capture first, which can run minutes. Poll `GET /named-snapshots/{name}` until the status settles at `ready` or `failed`. Reusing one of your existing names replaces that snapshot's artifact once the new save is ready. operationId: saveNamedSnapshot requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotSaveRequest' } responses: '202': description: Save accepted and running in the background. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotSavingResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '402': description: >- `storage_limit_reached`: the wallet's storage is full (see `Storage`). Delete stopped sandboxes or named snapshots, upgrade, or turn auto-delete on (`details.settingsUrl`). `details`: `usedBytes`, `includedBytes`, `plan`, `planName`, `billingUrl`, `settingsUrl`, `autoCleanup`. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' '409': description: >- `save_in_progress` when a save under this name is already running; wait for it to settle rather than retrying immediately. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' /named-snapshots/{name}: get: tags: [Sandboxer] summary: Get a named snapshot operationId: getNamedSnapshot parameters: - name: name in: path required: true schema: { type: string } responses: '200': description: The named snapshot. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotInfoResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Remove a named snapshot description: >- Remove a named snapshot and release its storage (`sbx snapshot rm ` in the CLI). Sandboxes already deployed from it are unaffected. operationId: deleteNamedSnapshot parameters: - name: name in: path required: true schema: { type: string } responses: '200': description: Removed. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotDeletedResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: >- `save_in_progress`: a save under this name is still running. Wait for it to settle before removing it. content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' /named-snapshots/{name}/shares: parameters: - name: name in: path required: true schema: { type: string } get: tags: [Sandboxer] summary: List share codes of a named snapshot description: >- List the share codes of a named snapshot, active and revoked (`sbx snapshot shares ` in the CLI). The full codes are not returned. Needs `snapshot.read`. operationId: listSnapshotShares responses: '200': description: The snapshot's share codes. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotShareListResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } post: tags: [Sandboxer] summary: Share a named snapshot description: >- Make a secret code that lets any Sandboxer account create a sandbox from this named snapshot (`sbx snapshot share ` in the CLI). The response shows the code once; Sandboxer stores only a hash. Re-saving the name changes what the code deploys. Removing the name removes its codes. Everything on the disk is shared, so do not keep secrets in files on it. Needs `snapshot.write`. operationId: createSnapshotShare responses: '201': description: Code created. Store `shareCode` now; Sandboxer does not show it again. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotShareCreatedResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /named-snapshots/{name}/shares/{shareId}: parameters: - name: name in: path required: true schema: { type: string } - name: shareId in: path required: true schema: { type: string } description: Share id, for example `shr_...`. delete: tags: [Sandboxer] summary: Revoke a share code description: >- Revoke a share code (`sbx snapshot unshare ` in the CLI). The code stops creating new sandboxes. Sandboxes already created from it keep their own copy. Needs `snapshot.write`. operationId: revokeSnapshotShare responses: '200': description: Revoked. content: application/json: schema: { $ref: '#/components/schemas/NamedSnapshotShareRevokedResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /snapshots: get: tags: [Sandboxer] summary: List snapshots description: >- List visible completed snapshots across all of your sandboxes using immutable ownership attribution. Pinned, deletion-scheduled, and privacy-fenced sandbox snapshots are omitted. Follow `nextCursor` through the complete history; there is no hidden 500-snapshot cap. operationId: listSnapshots parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Sort' responses: '200': description: Completed snapshots owned by the authenticated Sandboxer user. content: application/json: schema: { $ref: '#/components/schemas/SnapshotListResponse' } examples: snapshots: value: ok: true type: snapshot.list snapshots: - id: 7417be09-d419-4ae0-b3fc-7f04a5a71ef1 sandboxId: sb_k3j9x2m1qa status: completed kind: incremental generation: 3 chainId: 4ced5b04-d2cb-4ec3-b127-3b3ed836cab5 createdAt: '2026-06-24T06:24:00Z' completedAt: '2026-06-24T06:24:50Z' sizeBytes: 18874368 fileCount: 6781 pageInfo: nextCursor: null hasMore: false limit: 50 '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } /sandboxes/{sandboxId}/snapshots: get: tags: [Sandboxer] summary: List sandboxer snapshots description: List visible completed snapshots for one sandbox with uncapped cursor pagination. operationId: listSandboxSnapshots parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - $ref: '#/components/parameters/Sort' responses: '200': description: Completed snapshots for this sandbox. content: application/json: schema: { $ref: '#/components/schemas/SnapshotListResponse' } '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Sandboxer] summary: Permanently delete all snapshots of a sandbox description: >- Accept irreversible deletion of every unpinned snapshot of this sandbox. The sandbox itself and its named snapshots stay. Snapshots disappear from lists, restores and forks immediately; their stored data is purged in the background. Set `X-Sandboxer-Confirm-Delete` to the sandbox id. operationId: deleteSandboxSnapshots parameters: - $ref: '#/components/parameters/SandboxId' - $ref: '#/components/parameters/ConfirmDelete' responses: '202': description: One accepted deletion operation per snapshot. content: application/json: schema: { $ref: '#/components/schemas/DeletionOperationListResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /sandboxes/{sandboxId}/snapshots/latest: get: tags: [Sandboxer] summary: Get latest sandboxer snapshot description: Return the most recent completed snapshot for this sandbox, or `null` if it has none. operationId: getLatestSandboxSnapshot parameters: - $ref: '#/components/parameters/SandboxId' responses: '200': description: Most recent completed snapshot, or `null`. content: application/json: schema: { $ref: '#/components/schemas/SnapshotLatestResponse' } examples: latest: value: ok: true type: snapshot.latest snapshot: id: 7417be09-d419-4ae0-b3fc-7f04a5a71ef1 sandboxId: sb_k3j9x2m1qa status: completed kind: incremental generation: 3 chainId: 4ced5b04-d2cb-4ec3-b127-3b3ed836cab5 createdAt: '2026-06-24T06:24:00Z' completedAt: '2026-06-24T06:24:50Z' sizeBytes: 18874368 fileCount: 6781 none: value: ok: true type: snapshot.latest snapshot: null '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /sandboxes/{sandboxId}/usage: get: tags: [Sandboxer] summary: Get sandboxer usage description: >- Machine time one sandbox consumed, and what it costs, so you can bill your own users per sandbox. Same meter as `GET /limits`: billable seconds with the sandbox type's multiplier applied, paused past a refused stop, never counting time stopped. Narrow it to a billing period with `since` and `until`; a period boundary that falls inside a running stretch splits that stretch pro rata. Works while the sandbox runs and after it stops. operationId: usage parameters: - $ref: '#/components/parameters/SandboxId' - name: since in: query required: false schema: { type: string } description: Count from this time, ISO 8601 or Unix epoch seconds. Default is the sandbox's creation. examples: iso: { value: '2026-09-01T00:00:00Z' } epoch: { value: '1756684800' } - name: until in: query required: false schema: { type: string } description: Count up to this time, ISO 8601 or Unix epoch seconds. Default is now. examples: iso: { value: '2026-10-01T00:00:00Z' } responses: '200': description: Billable machine time for the sandbox inside the window. content: application/json: schema: { $ref: '#/components/schemas/SandboxUsageResponse' } examples: usage: value: ok: true type: sandbox.usage sandboxId: sb_k3j9x2m1qa sandboxType: default billingMultiplier: 1 since: '2026-09-01T00:00:00.000Z' until: '2026-09-14T09:30:00.000Z' seconds: 4980 dollars: 0.0498 secondsPerDollar: 100000 running: false '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /snapshots/{snapshotId}: delete: tags: [Sandboxer] summary: Permanently delete snapshot data description: >- Accept irreversible deletion of an unpinned snapshot owned by the authenticated account. Any such snapshot is accepted: it disappears from lists, restores and forks immediately, and its stored data is purged in the background once nothing still reads it (see the operation's `stage`). `409` only means `X-Sandboxer-Confirm-Delete` is missing or wrong. operationId: deleteSnapshot parameters: - $ref: '#/components/parameters/SnapshotId' - $ref: '#/components/parameters/ConfirmDelete' responses: '202': description: Deletion confirmed and accepted for background processing. content: application/json: schema: { $ref: '#/components/schemas/DeletionOperationResponse' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } /snapshots/{snapshotId}/tree: get: tags: [Sandboxer] summary: Get snapshot file tree description: List the files and folders captured in a snapshot, with sizes. Returns a flat list of entries you can render as a tree. operationId: getSnapshotTree parameters: - $ref: '#/components/parameters/SnapshotId' responses: '200': description: Flat file/folder listing for the snapshot. content: application/json: schema: { $ref: '#/components/schemas/SnapshotTreeResponse' } examples: tree: value: ok: true type: snapshot.tree snapshotId: 7417be09-d419-4ae0-b3fc-7f04a5a71ef1 sandboxId: sb_k3j9x2m1qa generation: 3 treeAvailable: true truncated: false fileCount: 6781 totalSizeBytes: 458291 entries: - { path: src, kind: dir } - { path: src/main.ts, kind: file, size: 1024 } legacy: value: ok: true type: snapshot.tree snapshotId: 7417be09-d419-4ae0-b3fc-7f04a5a71ef1 sandboxId: sb_k3j9x2m1qa generation: 0 treeAvailable: false truncated: false fileCount: 0 totalSizeBytes: 0 entries: [] reason: legacy_snapshot '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } /snapshots/{snapshotId}/files: get: tags: [Sandboxer] summary: Download a file or folder from a snapshot description: >- Stream a single file's bytes, or a folder subtree as a tar archive, directly out of a snapshot, the sandbox can be stopped or archived; the machine is never contacted. Use `GET /snapshots/{snapshotId}/tree` to list paths, then pass one here. Paths are relative to the snapshot root (same space as `tree` entries; docker volumes under `__dockervol__/`). An empty or `/` path downloads the whole snapshot as a tar. Folder responses set `X-Snapshot-File-Count`, `X-Snapshot-Total-Size-Bytes` and `X-Snapshot-Skipped-Base-Image-Files` headers; both response shapes set `X-Snapshot-Entry-Kind` (`file` or `dir`). File responses accept a single `Range: bytes=` header and answer 206 with `Content-Range`, so large files can be downloaded in parts and resumed. operationId: getSnapshotFile parameters: - $ref: '#/components/parameters/SnapshotId' - name: path in: query required: false schema: { type: string } description: File or folder path inside the snapshot. Empty for the whole snapshot. responses: '200': description: File bytes (`application/octet-stream`) or folder tar (`application/x-tar`). content: application/octet-stream: schema: type: string format: binary application/x-tar: schema: type: string format: binary '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '409': description: Path has no downloadable bytes, one of `legacy_snapshot` (pre-inventory snapshot), `snapshot_not_indexed` (content captured before the indexed snapshot format; take a new snapshot or use the download bundle), `base_image_file` (stock image file, not stored in snapshots), or `is_symlink` (request the symlink's target instead). content: application/json: schema: { $ref: '#/components/schemas/ErrorEnvelope' } /snapshots/{snapshotId}/download: get: tags: [Sandboxer] summary: Get snapshot download description: | Return time-limited signed URLs for every chunk needed to reconstruct the sandbox filesystem at this snapshot (the full chain, base through this generation), plus the inventory. Download the chunks and reassemble them as described by `reconstruct`. The `sbx snapshot pull` CLI command does this for you. operationId: getSnapshotDownload parameters: - $ref: '#/components/parameters/SnapshotId' responses: '200': description: Signed chunk URLs for the snapshot chain. content: application/json: schema: { $ref: '#/components/schemas/SnapshotDownloadResponse' } examples: download: value: ok: true type: snapshot.download snapshotId: 7417be09-d419-4ae0-b3fc-7f04a5a71ef1 sandboxId: sb_k3j9x2m1qa kind: incremental generation: 3 expiresInSeconds: 3600 reconstruct: Download every chunk. For each snapshot in ascending generation, concat its chunks by chunkIndex and pipe through `zstd -d | tar -x`. inventory: r2Key: chains/4ced5b04/inventory-3.json.zst signedUrl: https://r2.example/chains/4ced5b04/inventory-3.json.zst?sig=redacted chunks: - snapshotId: 2db50582-716c-424c-817e-9495484f88dd generation: 0 chunkIndex: 0 r2Key: chains/4ced5b04/snapshots/2db50582/chunks/00.tar.zst sizeBytes: 209715200 sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 signedUrl: https://r2.example/chains/4ced5b04/snapshots/2db50582/chunks/00.tar.zst?sig=redacted '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' }