openapi: 3.1.0
info:
  title: Boat 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 Boat automation products.

    The v1 reference intentionally documents the developer integration surface only. Dashboard billing actions are not part of v1.
servers:
  - url: https://boat.dev/api/v1
security:
  - BoatBearerAuth: []
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-Ascii-Event
          in: header
          required: true
          schema: { $ref: '#/components/schemas/WebhookEventType' }
        - name: X-Ascii-Delivery
          in: header
          required: true
          schema: { type: string, pattern: '^evt_[a-f0-9]{32}$' }
        - name: X-Ascii-Timestamp
          in: header
          required: true
          schema: { type: string, pattern: '^[0-9]+$' }
        - name: X-Ascii-Signature
          in: header
          required: true
          description: HMAC-SHA256 over `delivery_id.timestamp.raw_body`, formatted as `v1=<hex>`.
          schema: { type: string, pattern: '^v1=[a-f0-9]{64}$' }
        - name: X-Ascii-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: Boat
    description: Unified Boat account, setup, lifecycle, prompting, event history, desktop access, and SSH operations.
components:
  securitySchemes:
    BoatBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sandbox_api_key
      description: Boat bearer token in the form `boat_...`. Service API keys authenticate sandbox operations.
  parameters:
    SandboxId:
      name: sandboxId
      in: path
      required: true
      schema:
        type: string
        pattern: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
      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-Ascii-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 and snapshots
        stay creator-private. On `/environments` routes, only this query
        parameter selects the organization's shared environments; the
        `X-Boat-Org` header and the active wallet do not apply there. Members
        read them, owners and admins change them.
    OrgHeader:
      name: X-Boat-Org
      in: header
      required: false
      schema: { type: string }
      description: Same as the `org` query parameter. Query wins when both are set.
  schemas:
    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: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
          examples: [bx_23456789]
        name:
          type: string
          examples: [Boat 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.
        dataExpiredAt:
          type: [string, 'null']
          format: date-time
          description: >-
            When Boat deleted the snapshot data of this stopped sandbox, 14 days after it stopped.
            Resume and fork then fail with `409 snapshot_data_expired`. Null while the data is kept.
        health:
          type: string
          enum: [ok, degraded]
          description: >-
            `degraded` when the machine is alive but Boat 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.
        hydration:
          description: >-
            Restore progress while `hydrated` is false, on GET /sandboxes/{id} only. Null in
            lists, after hydration, or when the sandbox does not answer within 1.5 s.
          oneOf:
            - type: 'null'
            - type: object
              required: [phase, entriesDone, entriesTotal, percent]
              properties:
                phase:
                  type: string
                  description: >-
                    Restore step: `dvol` (docker volumes), `sysdelta` (system files), `images`
                    (saved docker images), `hydrating` (the home directory), `reconciling` (the
                    last check). `degraded` and `stuck` mean a step failed and is retried.
                entriesDone:
                  type: integer
                  description: Files, directories and links copied in so far.
                entriesTotal:
                  type: [integer, 'null']
                  description: >-
                    Files, directories and links this restore copies in. Null for the first
                    seconds, until the system files are counted.
                percent:
                  type: [integer, 'null']
                  description: >-
                    entriesDone of entriesTotal, 0-99. Files you change during the restore are
                    yours, not copied, so it can stop short of 99; `hydrated` is the end signal.
        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 <port> user@<host>`.
          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: When the sandbox stops by itself (the stop starts within about 5 seconds), or null when auto-stop is disabled. A sandbox whose agent is working gets a fresh full TTL, and this time moves forward.
        ttlSeconds:
          type: [integer, 'null']
          description: The auto-stop length as last set on create, resume, fork or PATCH, 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 `shared` (see `visibility`), so you can open,
            run commands in, prompt, resume and stop it. `view`: a teammate's organization
            sandbox that is `private`, 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.
        visibility:
          type: [string, 'null']
          enum: [shared, private, null]
          description: >-
            Organization sandboxes only; null for a personal sandbox. `shared`: every member can use it.
            It holds none of its creator's personal logins (`noEnv`, shared and restarted, or created on
            an environment that passes nothing) and its creator did not make it private. `private`: only
            its creator can use it. It holds their logins, or its creator made it private
            (`POST /sandboxes/{sandboxId}/unshare`, or the organization's `newSandboxesPrivate` default).
        cost:
          type: object
          description: Only with `include=cost` on `GET /sandboxes`. Machine time this sandbox used this calendar month (UTC), at list price. Same meter as `GET /sandboxes/{sandboxId}/usage`.
          required: [since, seconds, dollars]
          properties:
            since: { type: string, format: date-time, description: 'The 1st of this month, 00:00 UTC.' }
            seconds: { type: integer, description: 'Billable machine-seconds, size multiplier applied.' }
            dollars: { type: number, description: 'seconds / secondsPerDollar, at list price.' }
        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.
        lastReboot:
          type: [object, 'null']
          description: The last time the sandbox's kernel restarted on its machine, or null if it never did. Every process running at that time ended. `crash` means the system log ended with no shutdown, for example after a kernel panic.
          properties:
            at:
              type: string
              format: date-time
            reason:
              type: string
              enum: [crash, clean, unknown]
        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'
    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: [boat_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: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
                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:
        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 }
        wallet:
          type: [object, 'null']
          description: >-
            The one wallet of this account or organization, in US dollars. It pays overage only:
            VM time past the plan, saved snapshots past the included count, paid starts and the AI
            proxy. It never pays the plan. Null on unlimited accounts.
          properties:
            balanceDollars:
              type: number
              description: Money in the wallet. Below zero while you owe during the grace period.
        planIncluded:
          type: [object, 'null']
          description: What the plan includes this billing period. Null on unlimited accounts.
          properties:
            vmHours:
              type: number
              description: VM time included this period, in hours of a `default` machine.
            vmHoursRemaining:
              type: number
              description: Included VM time left this period, in hours of a `default` machine.
            periodEnd:
              type: [string, 'null']
              format: date-time
              description: When the included amounts reset.
        overagePrices:
          type: object
          description: What the wallet pays past the plan.
          properties:
            vmHourDollars:
              type: number
              description: Dollars per hour of a `default` machine. `small` is 0.5x, `large` 2x, `xlarge` 5.556x.
            savedSnapshotDayDollars:
              type: number
              description: Dollars per day for each saved snapshot above the included count.
            startBrackets:
              type: array
              description: >-
                Price of a start above a start limit, by how far past the limit it falls, in each window
                (minute, hour, day). A start above limits in several windows pays the highest bracket among them.
              items:
                type: object
                properties:
                  aboveMultiple: { type: number, description: The bracket starts above this multiple of the limit. }
                  upToMultiple: { type: number, description: The bracket ends at this multiple of the limit (included). }
                  dollars: { type: number, description: Dollars per start in this bracket. }
            startLimitMultiplier:
              type: number
              description: Paid starts stop at this multiple of each start limit (the last bracket's upToMultiple).
        nextStartDollars:
          type: [number, 'null']
          description: What the next start costs now. 0 inside every limit; null when it would be refused.
        startLimitsWithWallet:
          type: [object, 'null']
          description: >-
            Start ceilings with paid starts: each start limit times `overagePrices.startLimitMultiplier`.
            Starts above `startLimits` and up to these ceilings are paid from the wallet at the
            `overagePrices.startBrackets` prices. Null when paid starts are not available now (before
            the first payment, an empty wallet, or an unlimited account).
          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']
          deprecated: true
          description: Deprecated. Read `planIncluded.vmHoursRemaining` and `wallet.balanceDollars`.
        packBalanceHours:
          type: number
          deprecated: true
          description: Deprecated. Read `wallet.balanceDollars`.
        packBalanceDollars:
          type: number
          deprecated: true
          description: Deprecated. Read `wallet.balanceDollars`.
        hasPaymentHistory:
          type: boolean
        subscriptionQuotaSeconds:
          type: integer
        subscriptionRemainingSeconds:
          type: integer
        packBalanceSeconds:
          type: integer
          deprecated: true
          description: Deprecated. Read `wallet.balanceDollars`.
        creditPurchasedSeconds:
          type: integer
          deprecated: true
          description: Deprecated.
        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 included time and the wallet last.
        trialComputeCapSeconds:
          type: [integer, 'null']
          description: Lifetime VM-time allowance for new sandboxes while the account has not paid, a plan trial included. Null once the account has paid.
        trialComputeUsedSeconds:
          type: [integer, 'null']
          description: VM time already used against trialComputeCapSeconds. Null once the account has paid.
        trialLine:
          type: [string, 'null']
          description: The trial limits in one readable line. Null on paid accounts.
        machineTypes:
          type: array
          description: Each sandbox type, and whether this account can create it now. A blocked type carries the reason.
          items:
            type: object
            required: [type, available, reason]
            properties:
              type:
                type: string
                enum: [small, default, large, xlarge]
              available: { type: boolean }
              reason:
                type: [string, 'null']
                description: Why the type is blocked, for example `not during the free trial; the first payment unlocks it`. Null when available.
        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
          deprecated: true
          description: Deprecated. Read `planIncluded.vmHoursRemaining` and `wallet.balanceDollars`.
        teamId:
          type: string
          description: Present when limits were read for a team wallet (`?teamId=`, `?org=`, or `X-Boat-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,
            or an account access grant from an administrator. Otherwise, create, resume and fork return
            403 `machine_class_plan_required`. 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.
        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
            (`ASCII_TOKEN`, `ASCII_API_URL`, `AGENT_ID`, `PRODUCT_MODE`, `ENVIRONMENT_ID`, `BOAT_ID`,
            `SERVICE_PREVIEW_TOKEN`, `BOAT_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
            Boat 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 saved 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. Boat 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.
        count:
          type: integer
          minimum: 1
          maximum: 1000
          default: 1
          description: >-
            With `from`: create this many sandboxes from the saved snapshot in one request. Boat
            creates them together, which is faster than separate creates when you need many
            sandboxes at once. Start limits and the active-sandbox limit count every sandbox of
            the batch; if the batch does not fit, nothing is created. The answer lists all of
            them in `sandboxes` (and the first one in `sandbox`). Refused with 400
            `count_requires_from` without `from`, and with 400 `count_with_fail_fast` together
            with `failFast`.
        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. When the request also has
            `files`, the script starts after every file is written.
        name:
          type: string
          maxLength: 120
          description: >-
            Display name of the sandbox, the same value `PATCH /sandboxes/{id}` sets. A
            sandbox created `from` a saved snapshot is named after the snapshot when this
            is omitted.
        visibility:
          type: string
          enum: [private, shared]
          description: >-
            Who can use a sandbox billed to an organization, with the same rules as the
            sandbox's `visibility` field. `private`: only you can use it. `shared`: every
            member can, as long as it holds none of your personal logins (`noEnv`, or an
            environment that passes nothing). Omitted, the organization's default for new
            sandboxes applies (`PATCH /orgs/{org}` `newSandboxesPrivate`). It is set before
            the sandbox gets a machine, so a `private` sandbox is never shared, not even for
            a moment, and needs no `unshare` call. A personal sandbox is always private:
            `shared` is refused with 400 `not_org_sandbox`. Change it later with
            `POST /sandboxes/{id}/share` and `POST /sandboxes/{id}/unshare`.
        hostedPorts:
          type: array
          maxItems: 50
          description: >-
            Ports to host on public HTTPS URLs, each exactly as `POST /sandboxes/{id}/host`
            does. The create waits for the sandbox's machine (up to 60 s, usually under
            a second), hosts every port at once and answers the URLs in `hostedPorts`. The
            ports are routed before the sandbox is ready, so a service that starts later is
            reachable as soon as it listens. Refused with 400 `invalid_hosted_ports` for a
            bad entry or a duplicate port, with 400 `count_with_hosted_ports` together with
            `count`, and with 400 `fail_fast_with_hosted_ports` together with `failFast`.
          items:
            $ref: '#/components/schemas/HostPortRequest'
        files:
          type: array
          maxItems: 32
          description: >-
            Files to write on the sandbox, each like `PUT /sandboxes/{id}/files` (a path under
            `/home/user` or `/tmp`, relative paths resolve against `/home/user`; missing
            folders are created). Boat writes them once the sandbox's disk is in place and
            before it reports `ready`, so a client that sees `ready`, and the `setupScript`,
            always find them. They are written once, at the first start: a resume does not
            write them again. At most 256KB in total; upload bigger files with
            `POST /sandboxes/{id}/files/upload-url`. `setupStatus` covers them: it stays
            `pending` until they are written, and a write that fails sets it `failed` with
            the path and the reason in `setupError` (the setup script then does not run).
            Refused with 400 `invalid_files` for a bad path, a duplicate path or a body over
            the limits.
          items:
            type: object
            required: [path, content]
            properties:
              path:
                type: string
                examples: [workspace/.env, /home/user/.config/app.json, /tmp/boot.txt]
              content:
                type: string
                description: File contents, as text or base64 (see `encoding`).
              encoding:
                type: string
                enum: [utf8, base64]
                default: utf8
        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-Boat-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 saved snapshot (saved with `POST /saved-snapshots`, or
            `boat snapshot <id> <name>` 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 /saved-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`.
      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
        - from: my-template
          name: session-42
          visibility: private
          hostedPorts:
            - port: 8080
              public: true
              title: code-server
            - port: 3000
          files:
            - path: workspace/.session.env
              content: "SESSION_ID=42\n"
    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.
        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, or an account access grant from an administrator
            (403 `machine_class_plan_required` otherwise).
            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 `<subdomain>.on.boat.dev` label).
            Lowercase letters, digits and hyphens; no leading/trailing/double hyphens;
            cannot end in `-desktop` or `-<number>` (reserved for the desktop and
            hosted-port URLs). Must be globally unique. The base URL, desktop URL and
            every live `host <port>` 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.
      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 Boat 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 Boat 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 Boat 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. The one exception is `proxyAiCredentials`.
        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.
        credentialProfileId:
          type: [string, 'null']
          description: The AI credential profile the environment's sandboxes get. Null means the sandbox owner's default profile.
        proxyAiCredentials:
          type: boolean
          description: Only with `safeForThirdParties` true. The sandboxes still get AI credentials, as AI credentials proxy placeholders. Real keys never enter them.
        access:
          $ref: '#/components/schemas/ResourceAccess'
        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 }
        credentialProfileId:
          type: [string, 'null']
          description: Id or name of a credential profile you can use, or null for the sandbox owner's default profile. Unknown profiles return `404 credential_profile_not_found`.
        proxyAiCredentials:
          type: boolean
          description: With `safeForThirdParties` true, pass AI credentials as proxy placeholders. Setting true needs the AI credentials proxy on for the profile owner, else `400 ai_proxy_off`.
        access:
          $ref: '#/components/schemas/ResourceAccessUpdate'
        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 Boat 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, agentid, 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'
            deletedCost:
              type: object
              description: >-
                Only with `include=cost`, and not for an API key limited to some sandboxes. Machine time this
                calendar month (UTC) of deleted sandboxes this list would otherwise show. They are not in
                the list, but their time was billed. Add it to the sandboxes' `cost` to get the full total.
              required: [since, seconds, dollars]
              properties:
                since: { type: string, format: date-time, description: 'The 1st of this month, 00:00 UTC.' }
                seconds: { type: integer, description: 'Billable machine-seconds, size multiplier applied.' }
                dollars: { type: number, description: 'seconds / secondsPerDollar, at list price.' }
    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'
            sandboxes:
              type: array
              items:
                $ref: '#/components/schemas/Sandbox'
              description: Present when the request set `count` above 1. Every sandbox of the batch, in creation order.
            hostedPorts:
              type: array
              description: >-
                Present when the request set `hostedPorts`, in request order. Each entry is what
                `POST /sandboxes/{id}/host` answers for that port: `url`, `access`, `isProtected`
                and `localhost`, or `error` and `message` when that one port could not be hosted
                (the sandbox is created anyway; host the port again with `POST /sandboxes/{id}/host`).
              items:
                type: object
                additionalProperties: true
                required: [port]
                properties:
                  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
                  error:
                    type: string
                  message:
                    type: string
            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=<id>`. 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 Boat 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 Boat 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 Boat 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 }
    FileUrlRequest:
      type: object
      required: [path]
      properties:
        path: { type: string, description: "Absolute path, or path relative to the sandbox work directory (/home/user). It must resolve under /home/user or /tmp." }
        ttlSeconds: { type: integer, minimum: 1, maximum: 3600, default: 900, description: How long the URL stays valid. }
    FileUrlResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [url, method, path, expiresAt]
          properties:
            type: { type: string, enum: [file.download_url, file.upload_url] }
            url: { type: string, format: uri, description: The signed URL. Send the request straight to it; it needs no Authorization header. }
            method: { type: string, enum: [GET, PUT], description: GET downloads the file. PUT uploads the request body as the file. }
            path: { type: string, description: The canonical path the URL is bound to. }
            expiresAt: { type: string, format: date-time }
    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.' }
    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 (~/.ascii/processes/<pid>.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 <port> user@<host>`.
              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: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
              examples: [bx_23456789]
            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. Boat posts to `<endpoint>/v1/metrics` and `<endpoint>/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. Boat 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. Boat encrypts them at rest.
          example: { dd-api-key: '<DATADOG_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' }
    CredentialId:
      type: string
      description: >-
        One AI credential in a credential profile. The `*-api-key` ids are API keys you can
        set with `PUT /credential-profiles/{profile}/keys/{credential}`. The sign-ins
        (`*-subscription`) and Bedrock (`*-bedrock`) are connected on the dashboard Agents & Proxying page,
        with the profile picked at the top; you can remove them here.
      enum:
        - anthropic-api-key
        - openai-api-key
        - openrouter-api-key
        - llmgateway-api-key
        - deepseek-api-key
        - moonshot-api-key
        - mistral-api-key
        - xai-api-key
        - opencode-api-key
        - claude-subscription
        - chatgpt-subscription
        - kimi-subscription
        - mistral-vibe-subscription
        - grok-subscription
        - claude-bedrock
        - codex-bedrock
    CredentialProfile:
      type: object
      required: [id, name, isDefault, owner, canManage, connected, customApis, createdAt, updatedAt]
      description: A named set of AI credentials. Secret values are never returned.
      properties:
        id: { type: string }
        name:
          type: string
          examples: [default, work]
        isDefault:
          type: boolean
          description: True for the one profile of its owner that sandboxes get when nothing names a profile. The dashboard Agents page edits this profile.
        owner:
          type: object
          required: [id, kind, name, aiProxyEnabled]
          properties:
            id: { type: string, description: 'The account that owns the profile, a person or an organization.' }
            kind: { type: string, enum: [personal, org] }
            name: { type: string }
            aiProxyEnabled: { type: boolean, description: True when the owner has the AI credentials proxy on. }
        canManage:
          type: boolean
          description: True when you can change the profile. Org members use an org profile; org owners manage it.
        connected:
          type: array
          items: { $ref: '#/components/schemas/CredentialId' }
          description: The credentials the profile holds.
        accounts:
          type: object
          description: Credentials with several accounts, keyed by credential id. Each lists every account (labels only), the one in use marked `active`. Empty when you cannot manage the profile.
          additionalProperties:
            type: array
            items: { $ref: '#/components/schemas/CredentialAccount' }
        customApis:
          type: array
          items: { type: string }
          description: The names of the profile's custom APIs. Values are never returned.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        access:
          $ref: '#/components/schemas/ResourceAccess'
    CredentialProfileListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [profiles]
          properties:
            profiles:
              type: array
              items: { $ref: '#/components/schemas/CredentialProfile' }
    CredentialProfileResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success, profile]
          properties:
            success: { type: boolean }
            profile: { $ref: '#/components/schemas/CredentialProfile' }
    CredentialProfileDeletedResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success]
          properties:
            success: { type: boolean }
    CreateCredentialProfileRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: Unique among the owner's profiles.
          examples: [work]
        org:
          type: string
          description: Organization id or name. The profile then belongs to that organization. Omit for a personal profile.
        copyFrom:
          type: string
          description: Id or name of a profile you manage. The new profile starts with a copy of its credentials.
    UpdateCredentialProfileRequest:
      type: object
      properties:
        name: { type: string, description: New profile name. }
        isDefault:
          type: boolean
          description: Send true to make this profile its owner's default. The previous default loses the flag. False is ignored; make another profile the default instead.
        access:
          allOf:
            - $ref: '#/components/schemas/ResourceAccessUpdate'
          description: Organization profiles only. Organization owner only.
    ResourceAccess:
      type: object
      required: [restricted, members]
      description: >-
        Which organization members can use this organization environment or profile.
        `restricted` false means every member. `restricted` true means owners, admins and `members` only.
        Others do not see it in lists. Personal environments and profiles always return `restricted` false.
      properties:
        restricted: { type: boolean }
        members:
          type: array
          items: { type: string }
          description: Member ids. Empty when `restricted` is false.
    ResourceAccessUpdate:
      type: object
      required: [restricted]
      description: >-
        Set who can use an organization environment (owner or admin) or profile (owner).
        A change does not touch running sandboxes. A bad list returns `400 invalid_access`.
      properties:
        restricted:
          type: boolean
          description: False opens it to every member. True limits it to owners, admins and `members`.
        members:
          type: array
          items: { type: string }
          description: Member ids or emails of active members. Replaces the full list.
          examples: [[a@example.com, b@example.com]]
    SetCredentialKeyRequest:
      type: object
      required: [value]
      properties:
        value: { type: string, description: The API key. Boat encrypts it at rest. }
        keepCurrent:
          type: boolean
          description: Keep the key this one replaces as another account of the credential, ready to switch back to.
    CredentialAccount:
      type: object
      required: [id, label, active]
      description: One account of a credential. Never a secret.
      properties:
        id: { type: string, description: The account's id. It stays the same across switches. }
        label: { type: string, description: "The account's email for a sign-in, or the key's last characters (…1234).", examples: ['you@example.com', '…1234'] }
        active: { type: boolean, description: True for the account every sandbox on the profile uses. }
        addedAt: { type: string, format: date-time, description: When the account was set aside (accounts not in use). }
    CredentialAccountsResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success, profileId, credential, accounts]
          properties:
            success: { type: boolean }
            profileId: { type: string }
            credential: { $ref: '#/components/schemas/CredentialId' }
            accounts:
              type: array
              items: { $ref: '#/components/schemas/CredentialAccount' }
              description: In the order the accounts were added, so their numbers stay the same across switches. The one in use is marked `active`.
    SetCustomApiRequest:
      type: object
      required: [value, baseUrl]
      properties:
        value:
          type: string
          maxLength: 8192
          description: The secret. The sandbox gets it as the variable named in the path.
        baseUrl:
          type: string
          format: uri
          description: Public HTTPS base URL of the API, on the default port, with a DNS name. The sandbox gets it as `<NAME>_BASE_URL`.
          examples: [https://api.stripe.com/v1]
    AiProxySettings:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success, accountId, org, enabled, enabledAt, pricing, proxyUrl]
          properties:
            success: { type: boolean }
            accountId: { type: string, description: 'The account the switch belongs to, a person or an organization.' }
            org: { type: boolean, description: True when `accountId` is an organization. }
            enabled: { type: boolean }
            enabledAt: { type: [string, 'null'], format: date-time }
            pricing:
              type: object
              required: [dailyDollars, dollarsPer1000Requests]
              properties:
                dailyDollars: { type: number, examples: [1], description: Charged to the account for each UTC day the proxy is on. }
                dollarsPer1000Requests: { type: number, examples: [1], description: 'Charged per 1,000 proxied requests.' }
            proxyUrl: { type: string, format: uri, description: The base URL sandboxes call instead of the providers. }
    UpdateAiProxyRequest:
      type: object
      required: [enabled]
      properties:
        enabled: { type: boolean }
    AiUsageTotals:
      type: object
      required: [requests, errors, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens]
      description: Token counts are the numbers the provider reports in its replies.
      properties:
        requests: { type: integer }
        errors: { type: integer, description: 'Requests the provider answered with an HTTP error, or did not answer.' }
        inputTokens: { type: integer }
        outputTokens: { type: integer }
        cacheReadTokens: { type: integer }
        cacheWriteTokens: { type: integer }
    AiUsageRow:
      allOf:
        - type: object
          required: [day, credential, model]
          properties:
            day: { type: string, format: date, description: UTC day. }
            credential: { type: string, description: 'The credential used: a `CredentialId`, or `custom:<NAME>` for a custom API.' }
            model: { type: string, description: 'The model the provider reported, or empty.' }
        - $ref: '#/components/schemas/AiUsageTotals'
    SandboxAiUsage:
      type: object
      required: [sandboxId, name, blocked, usage, totals, dollars]
      properties:
        sandboxId: { type: [string, 'null'] }
        name: { type: [string, 'null'] }
        blocked: { type: boolean }
        usage:
          type: array
          items: { $ref: '#/components/schemas/AiUsageRow' }
        totals: { $ref: '#/components/schemas/AiUsageTotals' }
        dollars: { type: number, description: 'Request charge for the period, at the price per 1,000 requests.' }
    AccountAiUsageResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [accountId, profileId, since, sandboxes, usage, totals, dollars]
          properties:
            accountId: { type: [string, 'null'], description: The wallet the usage is charged to. Null with `profile`. }
            profileId: { type: [string, 'null'], description: The credential profile whose requests are counted. Null without `profile`. }
            since: { type: string, format: date, description: First UTC day included. }
            sandboxes:
              type: array
              items: { $ref: '#/components/schemas/SandboxAiUsage' }
            usage:
              type: array
              items: { $ref: '#/components/schemas/AiUsageRow' }
            totals: { $ref: '#/components/schemas/AiUsageTotals' }
            dollars: { type: number }
    SandboxAiUsageResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [sandboxId, blocked, proxied, credentialProfileId, since, usage, totals, dollars]
          properties:
            sandboxId: { type: string }
            blocked: { type: boolean, description: True when the proxy refuses this sandbox's placeholders. }
            proxied: { type: boolean, description: 'True when the sandbox holds placeholders, not real keys.' }
            credentialProfileId: { type: [string, 'null'], description: The profile the sandbox uses. Null means its owner's default. }
            since: { type: string, format: date }
            usage:
              type: array
              items: { $ref: '#/components/schemas/AiUsageRow' }
            totals: { $ref: '#/components/schemas/AiUsageTotals' }
            dollars: { type: number }
    SetSandboxAiAccessRequest:
      type: object
      required: [blocked]
      properties:
        blocked: { type: boolean }
    SandboxAiAccessResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success, sandboxId, blocked]
          properties:
            success: { type: boolean }
            sandboxId: { type: string }
            blocked: { type: boolean }
    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.
        origin:
          type: string
          enum: [captured, template]
          description: '`template` = this Sandbox has snapshots off, so Boat never captures it: the row is a copy of the template, saved snapshot or Sandbox it was created from. `captured` = this Sandbox has snapshots on: its own captures, plus the copies Boat makes when the Sandbox is created from a source, forked or resumed.'
    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'
    SavedSnapshot:
      type: object
      description: >-
        A saved 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 saved snapshot still deploys. Saved snapshots never
        expire; 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/boat/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
    SavedSnapshotListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [snapshots]
          properties:
            type:
              type: string
              const: snapshot.saved.list
            snapshots:
              type: array
              items:
                $ref: '#/components/schemas/SavedSnapshot'
            allowance: { $ref: '#/components/schemas/SavedSnapshotAllowance' }
    SavedSnapshotAllowance:
      type: object
      description: >-
        What the request's wallet (your active organization, `?org=`, or personal) pays for
        saved snapshots. The plan includes some; each one above costs $0.056 a day from the
        wallet. 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. }
        included: { type: integer, description: 'Saved snapshots the plan includes: 10 per $20 of plan per seat, never fewer than 10.' }
        free: { type: integer, deprecated: true, description: Same value as included. Kept for older clients. }
        used: { type: integer, description: Saved snapshots this wallet keeps. }
        extra: { type: integer, description: Saved snapshots above the included ones. }
        extraMonthlyDollars: { type: number, description: What the extra ones cost per month. }
        extraDailyDollars: { type: number, description: 'What the wallet pays per day for the extra ones, in dollars.' }
        pricePerExtraMonthlyDollars: { type: number, description: Price of one extra saved snapshot for 30 days (1.68). }
        pricePerExtraDailyDollars: { type: number, description: Daily price of one extra saved snapshot (0.056). }
    SavedSnapshotInfoResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [snapshot]
          properties:
            type:
              type: string
              const: snapshot.saved.info
            snapshot:
              $ref: '#/components/schemas/SavedSnapshot'
    SavedSnapshotSaveRequest:
      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.
    SavedSnapshotSavingResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [status, snapshot]
          properties:
            type:
              type: string
              const: snapshot.saved.saving
            status:
              type: string
              const: saving
            snapshot:
              $ref: '#/components/schemas/SavedSnapshot'
    SavedSnapshotDeletedResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [name, status]
          properties:
            type:
              type: string
              const: snapshot.saved.deleted
            name:
              type: string
            status:
              type: string
              const: deleted
    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 never
        expire; 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/boat/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: >-
        What the request's wallet (your active organization, `?org=`, or personal) pays for
        named snapshots. The plan includes some; each one above costs $0.056 a day from the
        wallet. 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. }
        free: { type: integer, description: Named snapshots kept for free. }
        used: { type: integer, description: Named snapshots this wallet keeps. }
        extra: { type: integer, description: Named snapshots above the free ones. }
        extraMonthlyDollars: { type: number, description: What the extra ones cost per month. }
        extraDailyDollars: { type: number, description: 'What the wallet pays per day for the extra ones, in dollars.' }
        pricePerExtraMonthlyDollars: { type: number, description: Price of one extra named snapshot for 30 days (1.68). }
        pricePerExtraDailyDollars: { type: number, description: Daily price of one extra named snapshot (0.056). }
    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 Boat account that holds the code can create a
        sandbox from the snapshot. Boat 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_...`. Boat 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, 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`). `retrying`: a transient fault,
            retried automatically. `completed`: a newer snapshot, a fork or a saved snapshot made
            from it keeps the data it reads, until that is deleted too.
        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...
    EnvironmentForbidden:
      description: >-
        `environment_forbidden`: the organization limits this environment to other members.
        Ask an owner or admin to add you. Other `403` codes can also apply.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            environmentForbidden:
              value:
                ok: false
                type: sandbox.error
                status: 403
                code: environment_forbidden
                message: You cannot use this environment.
                error: { code: environment_forbidden, message: You cannot use this environment., 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 and the `limit` that applied). With money in the wallet after the first
        payment, starts above a limit are paid, up to `startLimitsWithWallet`; the body carries
        `startLimitsWithWallet`, null when paid starts are not available. 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: [Boat]
      summary: Get current Boat user
      description: Returns GitHub identity for the authenticated Boat 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: [Boat]
      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-Boat-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' }
  /orgs/{org}:
    patch:
      tags: [Boat]
      summary: Update organization settings
      description: >-
        Owners and admins set whether new sandboxes of the organization start private
        (only their creator can use them) or shared (every member can use those that hold
        none of their creator's logins). The default is shared. Existing sandboxes keep
        their setting; their creators change it with `POST /sandboxes/{sandboxId}/share`
        and `POST /sandboxes/{sandboxId}/unshare`. A fork of a private sandbox is private.
      operationId: updateOrganization
      parameters:
        - name: org
          in: path
          required: true
          schema: { type: string }
          description: Organization id or name.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [newSandboxesPrivate]
              properties:
                newSandboxesPrivate:
                  type: boolean
                  description: True = new sandboxes start private. False = they start shared.
            examples:
              private:
                value: { newSandboxesPrivate: true }
      responses:
        '200':
          description: The setting is saved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  type: { type: string, examples: [org.updated] }
                  success: { type: boolean }
                  newSandboxesPrivate: { type: boolean }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /orgs/active:
    patch:
      tags: [Boat]
      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 (`boat 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: [Boat]
      summary: Get account data-retention policy
      description: Returns the current zero-data-retention policy. Per-Boat 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: [Boat]
      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' }
  /deletion-operations/{operationId}:
    get:
      tags: [Boat]
      summary: Get deletion operation
      description: Poll an accepted Boat 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' }
  /limits:
    get:
      tags: [Boat]
      summary: Get Boat limits
      description: >-
        Check remaining machine starts, included VM time, the wallet, overage prices, access readiness, and
        concurrent-sandbox capacity for the wallet a create would bill: the `org` /
        `X-Boat-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-Boat-Org` when set.
      responses:
        '200':
          description: Current creation and concurrent-boat 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
                    wallet: { balanceDollars: 52.4 }
                    planIncluded: { vmHours: 555.56, vmHoursRemaining: 312.7, periodEnd: '2026-11-01T00:00:00.000Z' }
                    overagePrices: { vmHourDollars: 0.036, savedSnapshotDayDollars: 0.056, startBrackets: [{ aboveMultiple: 1, upToMultiple: 1.5, dollars: 0.03 }, { aboveMultiple: 1.5, upToMultiple: 2, dollars: 0.05 }, { aboveMultiple: 2, upToMultiple: 3, dollars: 0.07 }], startLimitMultiplier: 3 }
                    startLimitsWithWallet: { perMinute: 30, perHour: 150, perDay: 450 }
                    machineTypes:
                      - { type: small, available: true, reason: null }
                      - { type: default, available: true, reason: null }
                      - { type: large, available: true, reason: null }
                      - { type: xlarge, available: false, reason: needs the $100/month plan or higher }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /repos:
    get:
      tags: [Boat]
      summary: List GitHub repositories available to Boat
      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: [Boat]
      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: [Boat]
      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: boat_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: [Boat]
      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. An unknown field is a 400, so a misspelled scope never creates a full-access key. environmentIds takes your own environments and the org environments you can use. This dedicated path returns a typed 503 until scoped credential creation is activated.
      operationId: createScopedApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                name: { type: string }
                ttl: { type: string, description: 'Duration such as 90d, 24h, or seconds, up to 365d; or `never` for a key that works until revoked. Default 90d.', examples: [90d, never] }
                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: [Boat]
      summary: Revoke an API key
      description: Revokes the key immediately. Requires a dashboard or CLI session, or an admin-scoped key. A key revokes itself or a key it could create (same or fewer actions, sandboxes and environments, no later expiry).
      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: [Boat]
      summary: Rotate an API key
      description: Replaces the raw secret without changing expiry or scope. Requires a dashboard or CLI session, or an admin-scoped key. A key rotates itself or a key it could create.
      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: [Boat]
      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: boat_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: bx_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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      summary: Set OpenTelemetry export
      description: |
        Sets the account's one OTLP/HTTP destination, or replaces it. Boat then sends one metrics export at once and returns the collector's answer in `test`.

        Every 60 seconds Boat sends the sandbox and account gauges to `<endpoint>/v1/metrics`. Each sandbox lifecycle event goes to `<endpoint>/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: '<DATADOG_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: [Boat]
      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' }
  /credential-profiles:
    get:
      tags: [Boat]
      summary: List credential profiles
      description: Returns every AI credential profile you can use, yours and your organizations'. Secret values are never returned. Needs an API key with `account.admin`.
      operationId: listCredentialProfiles
      responses:
        '200':
          description: The profiles.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileListResponse' }
              examples:
                profiles:
                  value:
                    ok: true
                    type: credential_profile.list
                    profiles:
                      - id: 5b0f3c1e-8d2a-4c47-9a5e-2f1d7c3b9a10
                        name: default
                        isDefault: true
                        owner: { id: usr_123, kind: personal, name: alex, aiProxyEnabled: true }
                        canManage: true
                        connected: [claude-subscription, openai-api-key]
                        customApis: [STRIPE_KEY]
                        createdAt: '2026-10-01T12:00:00Z'
                        updatedAt: '2026-10-09T08:00:00Z'
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Boat]
      summary: Create a credential profile
      description: Creates an empty profile, or a copy of `copyFrom`. With `org`, the profile belongs to that organization and only its owners can create it. Needs an API key with `account.admin`.
      operationId: createCredentialProfile
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateCredentialProfileRequest' }
            examples:
              copy:
                value: { name: work, copyFrom: default }
      responses:
        '201':
          description: Profile created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
  /credential-profiles/{profile}:
    parameters:
      - name: profile
        in: path
        required: true
        schema: { type: string }
        description: Profile id or name.
    put:
      tags: [Boat]
      summary: Update a credential profile
      description: Renames the profile, or makes it its owner's default. Sandboxes that use the owner's default switch to the new default at their next start. Needs an API key with `account.admin`.
      operationId: updateCredentialProfile
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateCredentialProfileRequest' }
            examples:
              makeDefault:
                value: { isDefault: true }
      responses:
        '200':
          description: The updated profile.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Boat]
      summary: Delete a credential profile
      description: Deletes the profile. A default profile returns `409 is_default`. A profile that an environment names returns `409 in_use`. Needs an API key with `account.admin`.
      operationId: deleteCredentialProfile
      responses:
        '200':
          description: Profile deleted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileDeletedResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /credential-profiles/{profile}/keys/{credential}:
    parameters:
      - name: profile
        in: path
        required: true
        schema: { type: string }
        description: Profile id or name.
      - name: credential
        in: path
        required: true
        schema: { $ref: '#/components/schemas/CredentialId' }
    put:
      tags: [Boat]
      summary: Set a credential profile API key
      description: Sets one API key. A sign-in id returns `400 not_an_api_key`; connect sign-ins on the dashboard Agents page. Running sandboxes that use the profile get the new key. Needs an API key with `account.admin`.
      operationId: setCredentialProfileKey
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SetCredentialKeyRequest' }
      responses:
        '200':
          description: The updated profile.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Boat]
      summary: Remove a credential profile credential
      description: Removes one credential, an API key or a sign-in. Needs an API key with `account.admin`.
      operationId: deleteCredentialProfileKey
      responses:
        '200':
          description: The updated profile.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /credential-profiles/{profile}/accounts/{credential}:
    parameters:
      - name: profile
        in: path
        required: true
        schema: { type: string }
        description: Profile id or name.
      - name: credential
        in: path
        required: true
        schema: { $ref: '#/components/schemas/CredentialId' }
    get:
      tags: [Boat]
      summary: List the accounts of a credential
      description: "Every account of one credential (several ChatGPT sign-ins, several OpenAI keys), in the order they were added, the one in use marked. Labels only, never a secret. Needs an API key with `account.admin`, on a profile you manage: an org member who only uses an org profile gets `403`."
      operationId: listCredentialAccounts
      responses:
        '200':
          description: The accounts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialAccountsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Boat]
      summary: Add another API key to a credential
      description: Adds an API key as another account of the credential, ready to switch to. The first key of a credential becomes the one in use. A sign-in returns `400 not_an_api_key`; add another sign-in with "Add another account" on the dashboard. Needs an API key with `account.admin`.
      operationId: addCredentialAccount
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SetCredentialKeyRequest' }
      responses:
        '201':
          description: The accounts, the new one included.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialAccountsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /credential-profiles/{profile}/accounts/{credential}/{account}/active:
    parameters:
      - name: profile
        in: path
        required: true
        schema: { type: string }
        description: Profile id or name.
      - name: credential
        in: path
        required: true
        schema: { $ref: '#/components/schemas/CredentialId' }
      - name: account
        in: path
        required: true
        schema: { type: string }
        description: The account id from the list.
    put:
      tags: [Boat]
      summary: Switch a credential to another account
      description: Makes this account the one in use; the previous one stays as another account. Switching to the account already in use changes nothing. Every sandbox that uses the profile gets it at once, running ones included, with no restart. Use it when one account runs out of credits. Needs an API key with `account.admin`.
      operationId: switchCredentialAccount
      responses:
        '200':
          description: The accounts after the switch.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialAccountsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /credential-profiles/{profile}/accounts/{credential}/{account}:
    parameters:
      - name: profile
        in: path
        required: true
        schema: { type: string }
        description: Profile id or name.
      - name: credential
        in: path
        required: true
        schema: { $ref: '#/components/schemas/CredentialId' }
      - name: account
        in: path
        required: true
        schema: { type: string }
        description: The account id from the list.
    delete:
      tags: [Boat]
      summary: Forget an account of a credential
      description: Deletes an account. For the account in use, the credential switches to another account first, or is removed when it was the last one. Needs an API key with `account.admin`.
      operationId: deleteCredentialAccount
      responses:
        '200':
          description: The accounts left.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialAccountsResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /credential-profiles/{profile}/custom-apis/{name}:
    parameters:
      - name: profile
        in: path
        required: true
        schema: { type: string }
        description: Profile id or name.
      - name: name
        in: path
        required: true
        schema: { type: string, pattern: '^[A-Z_][A-Z0-9_]{0,63}$' }
        description: Environment variable name of the custom API, for example `STRIPE_KEY`. Provider names (`ANTHROPIC_*`, `OPENAI_*`, and so on) and names that end in `_BASE_URL` are reserved.
    put:
      tags: [Boat]
      summary: Set a custom API
      description: Adds or replaces one custom API. The sandbox gets `<NAME>` and `<NAME>_BASE_URL`. Needs an API key with `account.admin`.
      operationId: setCredentialProfileCustomApi
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SetCustomApiRequest' }
            examples:
              stripe:
                value: { value: sk_live_123, baseUrl: 'https://api.stripe.com/v1' }
      responses:
        '200':
          description: The updated profile.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Boat]
      summary: Remove a custom API
      description: Removes one custom API from the profile. Needs an API key with `account.admin`.
      operationId: deleteCredentialProfileCustomApi
      responses:
        '200':
          description: The updated profile.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CredentialProfileResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /ai-proxy:
    get:
      tags: [Boat]
      summary: Get AI credentials proxy
      description: Returns the AI credentials proxy switch of the account in scope, a person or an organization, with its price. Needs an API key with `account.admin`.
      operationId: getAiProxy
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/OrgHeader'
      responses:
        '200':
          description: The switch and its price.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AiProxySettings' }
              examples:
                on:
                  value:
                    ok: true
                    type: ai_proxy.info
                    success: true
                    accountId: usr_123
                    org: false
                    enabled: true
                    enabledAt: '2026-10-09T08:00:00Z'
                    pricing: { dailyDollars: 1, dollarsPer1000Requests: 1 }
                    proxyUrl: https://boat.dev/api/aiproxy
        '401': { $ref: '#/components/responses/Unauthorized' }
    put:
      tags: [Boat]
      summary: Turn the AI credentials proxy on or off
      description: |
        With the proxy on, sandboxes get placeholder keys and base URLs that point at Boat. Boat swaps in the real key on each request. Real keys never enter the sandbox.

        Running sandboxes switch at their next start. While on, the wallet of the account pays $1 for each UTC day, plus $1 per 1,000 proxied requests, charged to the wallet each sandbox bills. For an organization, only its owner can change the switch. Needs an API key with `account.admin`.
      operationId: updateAiProxy
      parameters:
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateAiProxyRequest' }
      responses:
        '200':
          description: The new state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AiProxySettings' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /ai-usage:
    get:
      tags: [Boat]
      summary: Get AI usage
      description: Proxied AI usage, per sandbox and in total, per UTC day, credential and model. Without `profile`, the usage charged to the wallet in scope. With `profile`, the requests made with that profile's credentials, whoever pays; only its managers can read it. Needs an API key with `account.read`.
      operationId: getAiUsage
      parameters:
        - name: days
          in: query
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
          description: Days of history, today included.
        - name: profile
          in: query
          schema: { type: string }
          description: A credential profile id. Counts that profile's requests instead of a wallet's.
        - $ref: '#/components/parameters/OrgId'
        - $ref: '#/components/parameters/OrgHeader'
      responses:
        '200':
          description: The usage.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AccountAiUsageResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /sandboxes/{sandboxId}/ai-usage:
    get:
      tags: [Boat]
      summary: Get sandbox AI usage
      description: One sandbox's proxied AI usage per UTC day, credential and model, and whether it is blocked. Needs an API key with `sandbox.read` on the sandbox.
      operationId: getSandboxAiUsage
      parameters:
        - $ref: '#/components/parameters/SandboxId'
        - name: days
          in: query
          schema: { type: integer, minimum: 1, maximum: 365, default: 30 }
          description: Days of history, today included.
      responses:
        '200':
          description: The usage.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SandboxAiUsageResponse' }
              examples:
                usage:
                  value:
                    ok: true
                    type: sandbox.ai_usage
                    sandboxId: bx_23456789
                    blocked: false
                    proxied: true
                    credentialProfileId: null
                    since: '2026-09-11'
                    usage:
                      - { day: '2026-10-10', credential: claude-subscription, model: claude-sonnet-4-5, requests: 412, errors: 3, inputTokens: 51234, outputTokens: 80321, cacheReadTokens: 1203344, cacheWriteTokens: 90233 }
                    totals: { requests: 412, errors: 3, inputTokens: 51234, outputTokens: 80321, cacheReadTokens: 1203344, cacheWriteTokens: 90233 }
                    dollars: 0.412
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /sandboxes/{sandboxId}/ai-access:
    put:
      tags: [Boat]
      summary: Block or unblock sandbox AI access
      description: Blocks or unblocks the sandbox's AI placeholders, running or stopped. The proxy refuses a blocked sandbox's requests with `403`. It has no effect on a sandbox that holds real keys. The sandbox creator, or a manager of its credential profile, can call it. Needs an API key with `account.admin`.
      operationId: setSandboxAiAccess
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SetSandboxAiAccessRequest' }
      responses:
        '200':
          description: The new state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SandboxAiAccessResponse' }
              examples:
                blocked:
                  value: { ok: true, type: sandbox.ai_access_updated, success: true, sandboxId: bx_23456789, blocked: true }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /secrets:
    get:
      tags: [Boat]
      summary: Get Boat 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: [Boat]
      summary: Update Boat 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: [Boat]
      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
      parameters:
        - $ref: '#/components/parameters/OrgId'
      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
                        credentialProfileId: null
                        proxyAiCredentials: false
                        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: [Boat]
      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
      parameters:
        - $ref: '#/components/parameters/OrgId'
      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`.
      - $ref: '#/components/parameters/OrgId'
    put:
      tags: [Boat]
      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: [Boat]
      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: [Boat]
      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 }
        - $ref: '#/components/parameters/OrgId'
      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.
      - $ref: '#/components/parameters/OrgId'
    put:
      tags: [Boat]
      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: [Boat]
      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 }
      - $ref: '#/components/parameters/OrgId'
    put:
      tags: [Boat]
      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: [Boat]
      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: [Boat]
      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 }
        - $ref: '#/components/parameters/OrgId'
      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: [Boat]
      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`.
        - $ref: '#/components/parameters/OrgId'
      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: [Boat]
      summary: List sandboxes
      description: >-
        Your sandboxes. In an organization's scope (the active wallet, `org`, or `X-Boat-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: include
          in: query
          schema: { type: string, enum: [cost] }
          description: '`cost` adds each sandbox''s `cost` this calendar month (UTC), at list price.'
      responses:
        '200':
          description: Sandboxes owned by the authenticated Boat user.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SandboxListResponse' }
              examples:
                sandboxes:
                  value:
                    ok: true
                    type: sandbox.list
                    sandboxes:
                      - id: bx_23456789
                        name: Sandbox 2026-05-31 12:00
                        state: idle
                        url: https://machine.on.boat.dev
                        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: [Boat]
      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: Boat 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: bx_23456789
                      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' }
        '403': { $ref: '#/components/responses/EnvironmentForbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/NoReadyMachine' }
  /sandboxes/{sandboxId}:
    parameters:
      - $ref: '#/components/parameters/SandboxId'
    get:
      tags: [Boat]
      summary: Get sandbox
      description: Poll this endpoint after create, stop, resume, or fork operations.
      operationId: get
      responses:
        '200':
          description: Boat details.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SandboxInfoResponse' }
              examples:
                sandbox:
                  value:
                    ok: true
                    type: sandbox.info
                    sandbox:
                      id: bx_23456789
                      name: Sandbox 2026-05-31 12:00
                      state: idle
                      url: https://machine.on.boat.dev
                      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: [Boat]
      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: [Boat]
      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. Saved 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: [Boat]
      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: Boat archival started or already in progress.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SandboxActionResponse' }
              examples:
                archiving:
                  value:
                    ok: true
                    type: sandbox.stopping
                    id: bx_23456789
                    status: archiving
                    sandbox: { id: bx_23456789, 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: [Boat]
      summary: Share sandbox with the organization
      description: |
        Let every member of the organization that pays for this sandbox use it.

        A sandbox that holds none of your personal logins is shared at once.

        A sandbox that holds them: 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. The wipe is 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`): its running programs hold your logins in memory.

        Teammates never read history recorded before the share.

        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. Undo with
        `POST /sandboxes/{sandboxId}/unshare`.
      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}/unshare:
    post:
      tags: [Boat]
      summary: Make an organization sandbox private
      description: |
        Make a shared organization sandbox private again, at once. Only you can then use it.
        Teammates still see it in their list and can stop it. Their SSH keys are removed from
        the running machine.

        Nothing on the sandbox changes: files, installs and its environment stay. Undo with
        `POST /sandboxes/{sandboxId}/share`.

        Only the person who created the sandbox can make it private, and only a sandbox billed
        to an organization. Making a private sandbox private is a no-op.
      operationId: unshare
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      responses:
        '200':
          description: The sandbox is private.
          content:
            application/json:
              schema:
                type: object
                properties:
                  sandbox: { $ref: '#/components/schemas/Sandbox' }
                  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: [Boat]
      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 stop it with `{"force": true}` and resume it from its last
          good snapshot. With no snapshot, a forced stop loses everything.
        - `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 run `boat stop --force` to stop it with its last good snapshot (anything written after it is lost; with no snapshot, everything is lost), then resume it.
                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 run `boat stop --force` to stop it with its last good snapshot (anything written after it is lost; with no snapshot, everything is lost), then resume it.
                  status: 409
  /sandboxes/{sandboxId}/resume:
    post:
      tags: [Boat]
      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: bx_23456789
                    status: resuming
                    sandbox: { id: bx_23456789, name: Sandbox 2026-05-31 12:00, state: provisioning, desktopAvailable: false, snapshotAvailable: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/EnvironmentForbidden' }
        '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: [Boat]
      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.
                monitoring:
                  type: boolean
                  description: >-
                    `false` creates the fork with monitoring off: Boat 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: bx_abcdef23
                    status: forking
                    sandbox: { id: bx_abcdef23, name: Sandbox fork, state: provisioning, desktopAvailable: false, snapshotAvailable: false }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '403': { $ref: '#/components/responses/EnvironmentForbidden' }
        '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: [Boat]
      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: bx_23456789
                    promptId: prompt_123
                    conversationId: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c
                    promptRun:
                      id: prompt_123
                      promptId: prompt_123
                      sandboxId: bx_23456789
                      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: [Boat]
      summary: List boat 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: bx_23456789
                    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: [Boat]
      summary: List boat 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: bx_23456789
                    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: [Boat]
      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: [Boat]
      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: [Boat]
      summary: Read a file from a sandbox
      description: >-
        Files up to 5 MiB. For a larger file, use `POST /sandboxes/{sandboxId}/files/download-url`.
        On a sandbox that is still starting, the call waits until it is ready (up to 60 s,
        like a command), so it reads the sandbox's own disk and never the machine's stock one.
      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: [Boat]
      summary: Write a file in a sandbox
      description: >-
        Files up to 5 MiB after decoding. For a larger file, use `POST /sandboxes/{sandboxId}/files/upload-url`.
        On a sandbox that is still starting, the call waits until it is ready (up to 60 s,
        like a command): a sandbox's files are in place only once it is ready, so a write
        any earlier would not stay. A `200` always means the file is on the sandbox's disk.
      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}/files/download-url:
    post:
      tags: [Boat]
      summary: Get a download URL for a sandbox file
      description: >-
        Returns a short-lived URL that the sandbox serves itself. GET it to download the file
        as raw bytes, with no size limit and no Authorization header. The URL is bound to this
        path and expires after `ttlSeconds` (default 15 minutes, at most 1 hour). Needs
        `file.read`. Treat the URL as a secret until it expires.
      operationId: createDownloadUrl
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FileUrlRequest' }
      responses:
        '200':
          description: Signed download URL.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FileUrlResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /sandboxes/{sandboxId}/files/upload-url:
    post:
      tags: [Boat]
      summary: Get an upload URL for a sandbox file
      description: >-
        Returns a short-lived URL that the sandbox serves itself. PUT the file as the raw
        request body to write it, with no size limit and no Authorization header. Missing
        directories are created; an existing file is replaced only once the upload completes.
        The URL is bound to this path and expires after `ttlSeconds` (default 15 minutes, at
        most 1 hour). Needs `file.write`. Treat the URL as a secret until it expires.
      operationId: createUploadUrl
      parameters:
        - $ref: '#/components/parameters/SandboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FileUrlRequest' }
      responses:
        '200':
          description: Signed upload URL.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FileUrlResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /sandboxes/{sandboxId}/commands:
    post:
      tags: [Boat]
      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 boat_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 boat_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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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: [Boat]
      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), Boat 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: bx_23456789
                    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: [Boat]
      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: bx_23456789
                    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: [Boat]
      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: [Boat]
      summary: Expose a sandbox port on a public HTTPS URL
      description: "Registers a stable `https://<sandbox-subdomain>-<port>.on.boat.dev` 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.on.boat.dev?_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' }
  /sandboxes/{sandboxId}/sshkey:
    post:
      tags: [Boat]
      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:<port>` 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@<machineIp>
        ```

        `boat forward <id> --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' }
  /named-snapshots:
    get:
      tags: [Boat]
      summary: List named snapshots
      description: List your named snapshots, newest first.
      operationId: listNamedSnapshots
      deprecated: true
      x-hidden: true
      responses:
        '200':
          description: Named snapshots owned by the authenticated Boat user.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NamedSnapshotListResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Boat]
      summary: Save a named snapshot
      description: >-
        Freeze a sandbox's current state under a name (`boat snapshot <id> <name>` 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
      deprecated: true
      x-hidden: true
      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: >-
            `credit_required`: the wallet already keeps its included named snapshots and is
            empty. Each named snapshot above the included count costs $0.056 a day from the wallet.
            Add money to the wallet or remove one.
          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: [Boat]
      summary: Get a named snapshot
      operationId: getNamedSnapshot
      deprecated: true
      x-hidden: true
      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: [Boat]
      summary: Remove a named snapshot
      description: >-
        Remove a named snapshot and release its storage (`boat snapshot rm <name>` in the
        CLI). Sandboxes already deployed from it are unaffected.
      operationId: deleteNamedSnapshot
      deprecated: true
      x-hidden: true
      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'
  /saved-snapshots:
    get:
      tags: [Boat]
      summary: List saved snapshots
      description: List your saved snapshots, newest first.
      operationId: listSavedSnapshots
      responses:
        '200':
          description: Saved snapshots owned by the authenticated Boat user.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavedSnapshotListResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Boat]
      summary: Save a saved snapshot
      description: >-
        Freeze a sandbox's current state under a name (`boat snapshot <id> <name>` 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 /saved-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: saveSavedSnapshot
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SavedSnapshotSaveRequest' }
      responses:
        '202':
          description: Save accepted and running in the background.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavedSnapshotSavingResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '402':
          description: >-
            `credit_required`: the wallet already keeps its included saved snapshots and is
            empty. Each saved snapshot above the plan's included count costs $0.056 a day from the wallet.
            Add money to the wallet or remove one.
          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'
  /saved-snapshots/{name}:
    get:
      tags: [Boat]
      summary: Get a saved snapshot
      operationId: getSavedSnapshot
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The saved snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavedSnapshotInfoResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Boat]
      summary: Remove a saved snapshot
      description: >-
        Remove a saved snapshot and release its storage (`boat snapshot rm <name>` in the
        CLI). Sandboxes already deployed from it are unaffected.
      operationId: deleteSavedSnapshot
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Removed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SavedSnapshotDeletedResponse' }
        '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'
  /saved-snapshots/{name}/shares:
    parameters:
      - name: name
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Boat]
      summary: List share codes of a saved snapshot
      description: >-
        List the share codes of a saved snapshot, active and revoked (`boat snapshot shares
        <name>` 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: [Boat]
      summary: Share a saved snapshot
      description: >-
        Make a secret code that lets any Boat account create a sandbox from this saved snapshot
        (`boat snapshot share <name>` in the CLI). The response shows the code once; Boat 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; Boat does not show it again.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NamedSnapshotShareCreatedResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /saved-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: [Boat]
      summary: Revoke a share code
      description: >-
        Revoke a share code (`boat snapshot unshare <name> <share-id>` 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: [Boat]
      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 Boat 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: bx_23456789
                        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: [Boat]
      summary: List boat 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: [Boat]
      summary: Permanently delete all snapshots of a sandbox
      description: >-
        Accept irreversible deletion of every unpinned snapshot of this sandbox. The sandbox itself
        and its saved snapshots stay. Snapshots disappear from lists, restores and forks immediately;
        their stored data is purged in the background. Set `X-Ascii-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: [Boat]
      summary: Get latest boat 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: bx_23456789
                      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: [Boat]
      summary: Get boat 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: bx_23456789
                    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: [Boat]
      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-Ascii-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: [Boat]
      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: bx_23456789
                    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: bx_23456789
                    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: [Boat]
      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: [Boat]
      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 `boat 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: bx_23456789
                    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' }
