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 Boat 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://ascii.dev/api/box/v1
security:
  - BoxBearerAuth: []
webhooks:
  boxLifecycle:
    post:
      summary: Boat lifecycle event delivery
      operationId: deliverBoxLifecycleEvent
      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: Box
    description: Unified Boat account, setup, lifecycle, prompting, event history, desktop access, and SSH operations.
components:
  securitySchemes:
    BoxBearerAuth:
      type: http
      scheme: bearer
      bearerFormat: box_api_key
      description: Boat bearer token in the form `box_...`. Service API keys authenticate Boat operations.
  parameters:
    BoxId:
      name: boxId
      in: path
      required: true
      schema:
        type: string
        pattern: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
      description: Public Box id returned by create/list/get box 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 `boxId` 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 box. Send your own opaque,
        account-unique value (a UUID) to make `POST /boxes` safe to retry when the
        response is lost (network timeout, 5xx): the first request creates the box
        and binds it to the key; every later request with the **same account, key,
        and request body** returns that same box instead of creating a second,
        billable one. Behavior: keys are retained for **24 hours**; a concurrent or
        early retry while the first box 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 box
        exists releases the key within ~2 minutes so a retry can create the box.
        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. A team id you belong to reads that team's
        limits / bills a create to that team. Your own account id is personal.
        Boxes, snapshots, and environments stay creator-private.
    OrgHeader:
      name: X-Box-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: [box.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
    Box:
      type: object
      required: [id, name, state, desktopAvailable, snapshotAvailable]
      properties:
        id:
          type: string
          pattern: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
          examples: [bx_23456789]
        name:
          type: string
          examples: [Box 2026-05-31 12:00]
        state:
          type: string
          enum: [init, provisioning, provisioned, cloning, ready, idle, running, archiving, archived, error]
        type:
          type: string
          enum: [small, default, large]
          description: >-
            Current machine size: what the box was created with, or the size it was last resumed
            or forked onto.
        vcpu:
          type: integer
          description: vCPUs guaranteed by this box's type.
          examples: [4]
        memoryGB:
          type: integer
          description: RAM in GB guaranteed by this box's type.
          examples: [8]
        billingMultiplier:
          type: number
          description: Rate at which this box consumes machine time. 0.5 for `small`, 1 for `default`, and 2 for `large`.
          examples: [1]
        url:
          type: [string, 'null']
          format: uri
          description: Machine URL when assigned.
        ip:
          type: [string, 'null']
          description: Machine IPv6 or IPv4 address when assigned.
        createdAt:
          type: [string, 'null']
          format: date-time
        updatedAt:
          type: [string, 'null']
          format: date-time
        archiveAfter:
          type: [string, 'null']
          format: date-time
          description: Automatic archival time, or null when auto-stop is disabled.
        desktopAvailable:
          type: boolean
        desktopUrl:
          type: [string, 'null']
          format: uri
          description: Secret-bearing desktop stream URL when available. Redact from logs.
        snapshotAvailable:
          type: boolean
        snapshotCompletedAt:
          type: [string, 'null']
          format: date-time
          description: Timestamp of the most recent successfully completed snapshot, or null.
        subdomain:
          type: [string, 'null']
          description: The box's stable three-word subdomain slug (e.g. "frazil-pneuma-rallye"), or null before one is assigned.
        lastSnapshotAttemptAt:
          type: [string, 'null']
          format: date-time
          description: Timestamp of the most recent snapshot attempt of any status (queued, in_progress, completed, failed, cancelled), or null. Use with snapshotCompletedAt to detect snapshots that keep failing.
        lastSnapshotStatus:
          type: [string, 'null']
          enum: [queued, in_progress, completed, failed, cancelled, null]
          description: Status of the most recent snapshot attempt, or null if none. A value other than completed while snapshotCompletedAt stays stale indicates failing snapshots.
        setupStatus:
          type: [string, 'null']
          enum: [pending, running, done, failed, null]
          description: >-
            Outcome of the create-time `setupScript`: `pending` (stored, not yet
            started), `running` (executing on the box in the background), `done`
            (exit code 0) or `failed` (non-zero exit, or the box lost track of
            the process). Null when the box 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 Boat environment this box is running, or null if it is attached to
            none (a `noEnv` box, or one whose environment was deleted). A box freezes onto
            one environment version when it starts and keeps it for life, so this is what
            the box actually holds, not what the environment says today.
          examples: [base]
        environmentVersion:
          type: [integer, 'null']
          description: >-
            Version number of `environment` that this box is pinned to. Compare it against
            the environment's latest version to see whether an upgrade is pending: a box
            below the latest is still running the older configuration until someone calls
            `POST /environments/{environmentId}/upgrade`.
          examples: [3]
    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: [box_live]
        keyLastFour:
          type: string
          examples: [9abc]
        sandboxId:
          type: [string, 'null']
          description: Box 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 }
            boxes:
              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, boxes, agents]
      properties:
        total:
          type: integer
          minimum: 0
        boxes:
          type: integer
          minimum: 0
        agents:
          type: integer
          minimum: 0
    ApiKeyCreatedResource:
      type: object
      required: [kind, id, name, state, createdAt]
      properties:
        kind:
          type: string
          enum: [box, agent]
        id:
          type: string
        name:
          type: string
        state:
          type: string
        createdAt:
          type: [string, 'null']
          format: date-time
    WebhookEventType:
      type: string
      enum: [box.ready, box.error, box.archived, box.hydrated, box.degraded, box.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/box]
        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: [box]
          properties:
            box:
              type: object
              required: [id, name]
              properties:
                id:
                  type: string
                  pattern: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
                name:
                  type: string
            previousState:
              type: string
              description: On box.ready, box.error and box.archived only.
            state:
              type: string
              description: Absent on box.hydrated.
            reason:
              type: [string, 'null']
              description: On box.degraded and box.recovered. Why the box 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]
        private:
          type: boolean
        permissions:
          type: string
        pushedAt:
          type: [string, 'null']
          format: date-time
    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: [activeBoxes, maxActiveBoxes, canStart, billingStatus]
      additionalProperties: true
      properties:
        accessTier:
          type: string
          examples: [trial]
        blockedReason:
          type: [string, 'null']
        currentLimits:
          type: object
          additionalProperties: true
          properties:
            activeBoxes: { type: integer }
            creationRatePerMinute: { type: integer }
            creationRequestsPerHour: { type: [integer, 'null'] }
            creationRequestsPerDay: { type: [integer, 'null'] }
        standardLimits:
          type: object
          additionalProperties: true
          properties:
            activeBoxes: { type: integer }
            creationRatePerMinute: { type: integer }
            creationRequestsPerHour: { type: [integer, 'null'] }
            creationRequestsPerDay: { type: [integer, 'null'] }
        trialLimits:
          type: object
          additionalProperties: true
          properties:
            activeBoxes: { 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 boxes right now.
        checkoutRequired:
          type: boolean
        startBlockedReason:
          type: [string, 'null']
        contactMessage:
          type: [string, 'null']
        activeBoxes:
          type: integer
        activeStates:
          type: array
          items:
            type: string
        maxActiveBoxes:
          type: integer
        maxCreationRequestsPerMinute:
          type: integer
        maxCreationRequestsPerDay:
          type: [integer, 'null']
        startLimits:
          type: [object, 'null']
          description: Plan caps for machine starts. Null on unlimited accounts. Create, fork and resume each count as one start.
          properties:
            perMinute: { type: integer }
            perHour: { type: integer }
            perDay: { type: integer }
        starts:
          type: object
          description: Remaining machine starts in the rolling minute, hour and day windows. Null windows mean the account is unlimited.
          properties:
            unlimited: { type: boolean }
            minute:
              $ref: '#/components/schemas/StartWindowUsage'
            hour:
              $ref: '#/components/schemas/StartWindowUsage'
            day:
              $ref: '#/components/schemas/StartWindowUsage'
        creditBalanceHours:
          type: [number, 'null']
          description: Remaining machine time in hours (`creditBalanceSeconds / 3600`). Null on unlimited accounts.
        packBalanceHours:
          type: number
          description: Remaining purchased credit packs in hours.
        packBalanceDollars:
          type: number
          description: Remaining purchased credit packs in dollars.
        hasPaymentHistory:
          type: boolean
        package:
          type: object
          additionalProperties: true
        subscriptionQuotaSeconds:
          type: integer
        subscriptionRemainingSeconds:
          type: integer
        packBalanceSeconds:
          type: integer
        creditPurchasedSeconds:
          type: integer
        creditUsedSeconds:
          type: integer
        liveUsageSeconds:
          type: integer
        creditSecondsPerDollar:
          type: integer
        billingStatus:
          type: string
          description: Account access state returned by the current backend. Billing endpoints are not part of v1.
        subscriptionStatus:
          type: [string, 'null']
        subscriptionCancelAtPeriodEnd:
          type: boolean
        hasSubscription:
          type: boolean
        subscriptionTrialEndsAt:
          type: [string, 'null']
          format: date-time
        subscriptionCurrentPeriodEnd:
          type: [string, 'null']
          format: date-time
        creditBalanceSeconds:
          type: integer
        teamId:
          type: string
          description: Present when limits were read for a team wallet (`?teamId=`, `?org=`, or `X-Box-Org`).
        teamRole:
          type: string
          description: Caller's role on that team when `teamId` is present.
    CreateBoxRequest:
      type: object
      description: Options for provisioning a new cloud computer.
      properties:
        type:
          type: string
          enum: [small, default, large]
          default: default
          description: >-
            Machine size. `small` consumes machine time at half rate and `large` at twice the
            default rate (see the Billing guide). A fork inherits the source box's type unless the fork request passes its
            own, and resume and fork can move a box 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-box environment variables injected into the box's tool environment, on top of the
            account environment's variables (per-box 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`, `BOX_ID`,
            `SERVICE_PREVIEW_TOKEN`, `BOX_CLI_TOKEN`) are rejected with `invalid_env`. Forked boxes
            inherit the source box's env unless the fork request supplies its own `env`.
        environment:
          type: string
          default: base
          description: >-
            Name of the Boat environment to attach to this box. Environments are managed in the
            Boat dashboard and bundle the repositories, secrets, and credential toggles a box
            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 box, exactly like `noEnv`.
          examples: [base, customer-demos]
        noEnv:
          type: boolean
          default: false
          description: >-
            Create a box 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 boxes. For boxes you give to your own users. SSH, SCP, desktop,
            snapshots, and public URLs still work; pass `env` to give the box a secret of its own.
            A fork of a no-env box is always no-env. Equivalent to attaching an environment marked
            "safe for third parties".
        setupScript:
          type: string
          maxLength: 65536
          description: >-
            Shell script that runs on the box 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 box
            becoming usable. It runs as the box user via `bash`, with the box's
            environment applied, and its output goes to a log file on the box.
            Observe the outcome as `setupStatus` (pending/running/done/failed)
            and `setupError` on the box. Rejected with a 400 `invalid_setup_script`
            error when it is not a string or exceeds 64KB.
        org:
          type: string
          description: >-
            Bill this box to a team you belong to (the team's shared wallet). Your own
            account id means personal billing. Listing, snapshots, and environments stay
            yours: the org is a wallet, not a shared workspace. Takes precedence over
            `teamId` and over the `X-Box-Org` / `?org=` request scope.
        teamId:
          type: string
          description: Legacy alias for `org`. Ignored when `org` is also set.
        from:
          type: string
          description: >-
            Create the box from a named snapshot (saved with `POST /named-snapshots`, or
            `box snapshot <id> <name>` in the CLI). The box 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 box unless the request passes its own,
            with the same rules as forking.
      examples:
        - ttlSeconds: 3600
        - ttlSeconds: null
        - type: large
          ttlSeconds: 3600
        - ttlSeconds: 3600
          env:
            DATABASE_URL: postgres://user:pass@host:5432/app
            FEATURE_FLAG: "1"
        - ttlSeconds: null
          noEnv: true
    StopRequest:
      type: object
      description: Options for stopping a Boat.
      properties:
        force:
          type: boolean
          default: false
          description: >-
            Stop the box even if its disk cannot be snapshotted. Everything written
            since the last successful snapshot is permanently LOST. Without this, a box
            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 Boat.
      properties:
        type:
          type: string
          enum: [small, default, large]
          description: >-
            Resume onto a different machine size. Omit to keep the box's current type.
            A resume already restores onto a fresh machine, so changing size costs nothing
            extra. Shrinking is rejected with `type_too_small` when the box's data would
            not fit the smaller disk.
        env:
          type: object
          additionalProperties:
            type: string
          description: >-
            Replaces the box's per-box environment variables. Omit to keep the box's
            current env. Same validation rules as `CreateBoxRequest.env`.
        environment:
          type: string
          description: >-
            Optionally re-pin the box to a different named Boat environment on resume. Omit to
            keep the box's current environment. Unknown names are rejected with
            `unknown_environment`.
          examples: [base, customer-demos]
        noEnv:
          type: boolean
          description: >-
            Resume into a no-env box: withhold account secrets and scrub inherited owner
            secrets from the restored snapshot before the box is exposed. One-way; once a
            box 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 Boat, in seconds, stored on the Boat. Omit to keep
            the TTL the Boat already had. `null` disables auto-stop, which means nothing
            will ever stop this Boat for you.
      examples:
        - ttlSeconds: 28800
        - type: large
          ttlSeconds: 3600
    UpdateBoxRequest:
      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 archival TTL. `null` disables auto-stop.
        subdomain:
          type: string
          minLength: 3
          maxLength: 40
          pattern: '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$'
          description: >-
            Rename the Boat's stable subdomain (the `<subdomain>.on.ascii.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 Boat. 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`). 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 Box 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 Box's most-recently-active conversation. The response returns the `conversationId` this prompt ran in. Conversations run concurrently up to a per-box limit that scales with the Box'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]
        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]
        new:
          type: boolean
          description: Start a NEW conversation on the Box (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 Box's most-recently-active conversation.
          format: uuid
        prompt:
          type: string
          minLength: 1
          description: Natural-language task for the Boat, 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 Box'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](/box/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 Box'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: Box 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 Box 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 Box 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 Boxes. 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 Boat secret setup. Omitted `envContents` or `secretFiles` are treated as empty values, and successful updates are pushed to active Boxes.
      properties:
        envContents:
          type: string
          description: Full .env-style content to sync into Boxes. 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"}'
    BoxEnvironmentVersionSummary:
      type: object
      required: [id, versionNumber, boxCount, createdAt]
      description: An immutable snapshot of an environment's config. Editing an environment mints a new version; boxes 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.
        boxCount:
          type: integer
          description: Number of the caller's active boxes currently pinned to this version.
        createdAt:
          type: string
          format: date-time
    BoxEnvironment:
      type: object
      required: [id, name, isDefault, latestVersionId, safeForThirdParties, passGithub, passSecrets, passBoxCredentials, passAgentsCredentials, envContents, secretFiles, versions]
      description: A named Boat 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; boxes created without an `environment` name use it.
        latestVersionId:
          type: [string, 'null']
          format: uuid
        safeForThirdParties:
          type: boolean
          description: When true the environment passes nothing to a box (repos, secrets, and all credentials withheld), overriding the fine-grained flags below. Use for boxes handed to third parties.
        passGithub:
          type: boolean
          description: Attach the environment's GitHub repositories and the GitHub token (so `gh` and pushes work). Ignored when `safeForThirdParties` is true.
        passSecrets:
          type: boolean
          description: Attach the environment's env variables and secret files. Ignored when `safeForThirdParties` is true.
        passBoxCredentials:
          type: boolean
          description: Attach the box's own service/preview credentials. Ignored when `safeForThirdParties` is true.
        passAgentsCredentials:
          type: boolean
          description: Attach agent-provider credentials configured on the Agents page. Ignored when `safeForThirdParties` is true.
        envContents:
          type: string
          description: The latest version's .env-style content. Treat as sensitive.
        secretFiles:
          type: array
          items:
            $ref: '#/components/schemas/SecretFile'
        selectedRepositories:
          type: array
          description: Repositories attached to the latest version, with base branch and setup script.
          items:
            $ref: '#/components/schemas/SelectedRepository'
        versions:
          type: array
          items:
            $ref: '#/components/schemas/BoxEnvironmentVersionSummary'
    BoxEnvironmentListResponse:
      type: object
      required: [environments]
      properties:
        environments:
          type: array
          items:
            $ref: '#/components/schemas/BoxEnvironment'
    BoxEnvironmentResponse:
      type: object
      required: [success]
      properties:
        success:
          type: boolean
        environment:
          oneOf:
            - $ref: '#/components/schemas/BoxEnvironment'
            - type: 'null'
    CreateBoxEnvironmentRequest:
      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]
    UpdateBoxEnvironmentRequest:
      type: object
      description: Rename, set-default, and/or edit flags and contents. Any flag or content change mints a new immutable version; existing boxes 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 }
        passBoxCredentials: { type: boolean }
        passAgentsCredentials: { type: boolean }
        envContents:
          type: string
          description: Full .env-style content for the new version.
        secretFiles:
          type: array
          items:
            $ref: '#/components/schemas/SecretFile'
        repositories:
          type: array
          description: Full replacement of the version's repository selection. Each item selects one repository by its internal `databaseId` (from `GET /repos`).
          items:
            type: object
            required: [repositoryId]
            properties:
              repositoryId:
                type: string
                description: Internal repository databaseId.
              baseBranch:
                type: string
                default: main
              setupScript:
                type: string
              setupBlocking:
                type: boolean
    UpgradeBoxEnvironmentRequest:
      type: object
      description: Repoint active boxes 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 box (agent) ids. Omit to upgrade all of the caller's active boxes that are on an older version of this environment.
    UpgradeBoxEnvironmentResponse:
      type: object
      required: [success, upgraded, failed]
      properties:
        success:
          type: boolean
        upgraded:
          type: integer
        failed:
          type: integer
    EnvironmentItemChangeResponse:
      type: object
      description: >-
        Result of a granular environment change. Every change mints a new immutable
        version holding just that delta; existing boxes 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 Boat.
        public:
          type: boolean
          default: false
          description: Return an ungated URL instead of one that requires the `_token` query parameter.
        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:
                login:
                  type: string
                email:
                  type: [string, 'null']
                zeroDataRetention:
                  type: boolean
                  description: Whether archived Boat data is configured for deletion instead of retention.
                zeroDataRetentionEnabledAt:
                  type: [string, 'null']
                  format: date-time
    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
            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
            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
            envContents:
              type: string
            secretFiles:
              type: array
              items:
                $ref: '#/components/schemas/SecretFile'
            pushed:
              type: object
              additionalProperties: true
              description: Present on update; counts how many active Boxes received the new environment.
    BoxListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [boxes]
          properties:
            type:
              type: string
              const: box.list
            boxes:
              type: array
              items:
                $ref: '#/components/schemas/Box'
            pageInfo:
              $ref: '#/components/schemas/PageInfo'
    BoxInfoResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [box]
          properties:
            type:
              type: string
              examples: [box.info]
            box:
              $ref: '#/components/schemas/Box'
    CreateBoxResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [status, ttlSeconds, box]
          properties:
            type:
              type: string
              const: box.created
            status:
              type: string
              enum: [provisioning]
            ttlSeconds:
              type: [integer, 'null']
            box:
              $ref: '#/components/schemas/Box'
    BoxActionResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [id, status]
          properties:
            type:
              type: string
              examples: [box.stopping]
            id:
              type: string
            status:
              type: string
              examples: [archiving]
            box:
              oneOf:
                - $ref: '#/components/schemas/Box'
                - type: 'null'
    PageInfo:
      type: object
      required: [nextCursor, hasMore, limit]
      properties:
        nextCursor:
          type: [string, 'null']
        hasMore:
          type: boolean
        limit:
          type: integer
    PromptRun:
      type: object
      required: [id, promptId, boxId, status, done]
      properties:
        id: { type: string }
        promptId: { type: string }
        boxId: { 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']
        conversationId:
          type: [string, 'null']
          description: The conversation this prompt ran in. A Box runs many conversations in parallel; see [Integrated agents](/box/integrated-agents).
    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'
    BoxEvent:
      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](/box/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 }
    SteerEvent:
      type: object
      required: [id, type, timestamp, data]
      description: A message sent into a turn that was already running, via `POST /boxes/{boxId}/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 Box 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 Box 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 Box 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.
    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 Boat work directory (/home/user). The canonicalized path must resolve under /home/user or /tmp; anything else is rejected with a 400 invalid_path error." }
        content: { type: string }
        encoding: { type: string, enum: [utf8, base64], default: utf8 }
    FileWriteResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success, path, encoding, size]
          properties:
            type: { type: string, const: file.written }
            success: { type: boolean }
            path: { type: string }
            encoding: { type: string, enum: [utf8, base64] }
            size: { type: integer }
    CommandRequest:
      type: object
      required: [command]
      properties:
        command: { type: string }
        cwd: { type: string, description: Relative working directory inside the Boat 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 Boat; 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] }
        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 }
    CommandResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [success, exitCode, stdout, stderr, timedOut]
          properties:
            type: { type: string, const: command.finished }
            success: { 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.' }
            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 }
            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 Boat (~/.ascii/processes/<pid>.log). }
            errLogPath: { type: string, description: Stderr log file on the Boat. }
    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 Boat 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 Boat 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. }
            stderr: { type: string, description: Tail of the stderr log file. }
            stdoutTruncated: { type: boolean }
            stderrTruncated: { type: boolean }
            logPath: { type: string }
            errLogPath: { type: string }
    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: Box 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 Box's most-recently-active conversation. See [Integrated agents](/box/integrated-agents).
            promptRun:
              $ref: '#/components/schemas/PromptRun'
            status:
              type: string
              enum: [queued]
            provider:
              type: string
            model:
              type: [string, 'null']
            reasoningEffort:
              type: [string, 'null']
    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: Boat work and lifecycle event objects. Event shapes are intentionally extensible; branch on each event `type` when present.
              items:
                $ref: '#/components/schemas/BoxEvent'
            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`).
        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 Box''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: Box 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]
    SshKeyResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          properties:
            success:
              type: boolean
            machineIp:
              type: [string, 'null']
            sshUser:
              type: string
              examples: [user]
    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: [boxes, environments]
          properties:
            boxes:
              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' }
    BoxUsageResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [boxId, boxType, billingMultiplier, since, until, seconds, dollars, secondsPerDollar, running]
          properties:
            type:
              type: string
              examples: [box.usage]
            boxId:
              type: string
              pattern: '^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$'
              examples: [bx_23456789]
            boxType:
              type: string
              enum: [small, default, large]
              description: The box's current machine size, which sets `billingMultiplier`.
            billingMultiplier:
              type: number
              description: Rate at which this box consumes machine time. 0.5 for `small`, 1 for `default`, and 2 for `large`.
              examples: [1]
            since:
              type: string
              format: date-time
              description: Start of the window the figures cover. The box'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`
                box that ran ten minutes reads 1200. Includes a running box'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 box is up and no refused stop is holding it paused. Read
                again after it stops for the final figure. `false` on a box 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
    SnapshotSummary:
      type: object
      required: [id, boxId, status, generation, createdAt, sizeBytes, fileCount]
      properties:
        id:
          type: string
          format: uuid
        boxId:
          type: string
          description: Public Box 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
          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
          description: Bytes this snapshot added (its delta), not the full restored size.
        fileCount:
          type: integer
          description: Inventory entries alive in the chain at this generation (includes base-image system entries).
        contentSizeBytes:
          type: [integer, 'null']
          description: Total bytes of your data restored by this snapshot (what resume/download returns; base image excluded). `null` on legacy snapshots.
        contentFileCount:
          type: [integer, 'null']
          description: Number of your files restored by this snapshot (base image excluded). `null` on legacy snapshots.
    SnapshotListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [snapshots]
          properties:
            type:
              type: string
              const: snapshot.list
            snapshots:
              type: array
              items:
                $ref: '#/components/schemas/SnapshotSummary'
            pageInfo:
              $ref: '#/components/schemas/PageInfo'
    SnapshotLatestResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [snapshot]
          properties:
            type:
              type: string
              const: snapshot.latest
            snapshot:
              oneOf:
                - $ref: '#/components/schemas/SnapshotSummary'
                - type: 'null'
    NamedSnapshot:
      type: object
      description: >-
        A named snapshot: a frozen copy of a box's disk at one moment, saved under a
        name you pick. Independent of the source box's later life: the box 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, sourceBoxId, 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 box 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`.
        sourceBoxId:
          type: string
          description: The box 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/box/snapshots/{snapshotId}/tree` to browse its files.
        type:
          type: string
          description: Box 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'
    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: [boxId, name]
      properties:
        boxId:
          type: string
          description: The box 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
    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, boxId, generation, treeAvailable, truncated, fileCount, totalSizeBytes, entries]
          properties:
            type:
              type: string
              const: snapshot.tree
            snapshotId:
              type: string
              format: uuid
            boxId:
              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
        sha256:
          type: string
        signedUrl:
          type: string
          format: uri
          description: Time-limited download URL (see `expiresInSeconds`).
    SnapshotDownloadResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [snapshotId, boxId, kind, generation, expiresInSeconds, reconstruct, chunks]
          properties:
            type:
              type: string
              const: snapshot.download
            snapshotId:
              type: string
              format: uuid
            boxId:
              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: [box, snapshot]
        targetId:
          type: string
        reason:
          type: string
          enum: [explicit, zdr, account]
        status:
          type: string
          enum: [pending, processing, blocked, completed]
        stage:
          type: string
          enum: [removing, waiting_for_uploads, kept_for_newer_snapshots, waiting_for_restore, retrying, completed]
          description: >-
            What the background purge is doing. The target is already gone from every list, restore
            and fork once the operation exists. `waiting_for_uploads`: stored data is erased once the
            last upload URL issued for it expires (`expectedBy`). `kept_for_newer_snapshots`: a newer
            snapshot you kept is built on this one; its data goes when they go. `waiting_for_restore`:
            a box is still restoring from it. `retrying`: a transient fault, retried automatically.
        expectedBy:
          type: [string, 'null']
          format: date-time
          description: When `waiting_for_uploads` ends.
        attemptCount:
          type: integer
          minimum: 0
        requestedAt:
          type: string
          format: date-time
        completedAt:
          type: [string, 'null']
          format: date-time
    DeletionOperationListResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [operations]
          properties:
            type:
              type: string
              enum: [snapshot.deleting]
            operations:
              type: array
              items:
                $ref: '#/components/schemas/DeletionOperation'
    DeletionOperationResponse:
      allOf:
        - $ref: '#/components/schemas/SuccessBase'
        - type: object
          required: [operation]
          properties:
            type:
              type: string
              enum: [deletion.operation, box.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
            queuedBoxes:
              type: integer
              minimum: 0
              description: Archived Boxes 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.
    DataRetentionUpdateRequest:
      type: object
      required: [enabled]
      additionalProperties: false
      properties:
        enabled:
          type: boolean
        confirmation:
          type: string
          description: Required when enabling and must exactly equal `delete archived box data`.
          const: delete archived box data
  responses:
    BadRequest:
      description: Invalid request body or parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            invalid:
              value:
                ok: false
                type: box.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: box.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: box.error
                status: 403
                code: forbidden
                message: Forbidden
                error: { code: forbidden, message: Forbidden, status: 403 }
                requestId: req_01HX...
    PaymentRequired:
      description: Account cannot currently create or operate Boxes. 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 box state.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: >-
        Machine start or concurrent-box limit reached. Create, fork and resume each count as one
        machine start against your plan's start limits (see the Billing guide; `rate_limited`,
        naming the window you hit). A box that would exceed your plan's concurrent-box 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'
paths:
  /me:
    get:
      tags: [Box]
      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' }
  /account/data-retention:
    get:
      tags: [Box]
      summary: Get account data-retention policy
      description: Returns the current zero-data-retention policy. Per-box 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: [Box]
      summary: Update account data-retention policy
      description: >-
        Requires an interactive Boat session; API keys and legacy permanent tokens are refused.
        Enabling requires the exact confirmation phrase `delete archived box 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: [Box]
      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: [Box]
      summary: Get Boat limits
      description: Check remaining machine starts, compute time, credits, access readiness, and concurrent-box capacity for the authenticated account. Pass `org` / `X-Box-Org` (or `teamId`) to read a team wallet you belong to.
      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-Box-Org` when set.
      responses:
        '200':
          description: Current creation and concurrent-box limits.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LimitsResponse' }
              examples:
                ready:
                  value:
                    ok: true
                    type: limits.info
                    canStart: true
                    activeBoxes: 1
                    activeStates: [provisioned, cloning, ready, idle, running]
                    boxPlanKey: box_20
                    boxPlanDollars: 20
                    maxActiveBoxes: 100
                    maxCreationRequestsPerMinute: 12
                    maxCreationRequestsPerDay: 200
                    startLimits: { perMinute: 12, perHour: 60, perDay: 200 }
                    starts:
                      unlimited: false
                      minute: { limit: 12, used: 3, remaining: 9 }
                      hour: { limit: 60, used: 12, remaining: 48 }
                      day: { limit: 200, used: 47, remaining: 153 }
                    billingStatus: active
                    creditBalanceSeconds: 7200
                    creditBalanceHours: 2
                    packBalanceSeconds: 500000
                    packBalanceHours: 138.89
                    packBalanceDollars: 5
        '401': { $ref: '#/components/responses/Unauthorized' }
  /repos:
    get:
      tags: [Box]
      summary: List GitHub repositories available to Boat
      description: Returns GitHub repositories grouped by installation plus the current selected repositories for new Boxes.
      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 Boat 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: [Box]
      summary: Select repository for boxes
      description: Idempotently selects one repository for the Boat 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: [Box]
      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: box_live
                        keyLastFour: 9abc
                        sandboxId: null
                        createdAt: '2026-05-31T12:00:00Z'
                        lastUsedAt: null
                        usage:
                          requests: 1842
                          windowDays: 30
                        resources:
                          total: 3
                          boxes: 2
                          agents: 1
                        expiresAt: '2026-11-21T12:00:00Z'
                        expired: false
                        expiringSoon: false
                        scope:
                          actions: ['*']
                          boxes: '*'
                          environments: '*'
                          grandfathered: false
                    catalog:
                      actions: [box.read, exec, '*']
                      presets:
                        read-only: [box.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: [Box]
      summary: Create a scoped API key
      description: Creates a scoped, expiring API key. Requires a dashboard/CLI session or an admin-scoped token. The secret is returned once. This dedicated path returns a typed 503 until scoped credential creation is activated.
      operationId: createScopedApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                ttl: { type: string, description: 'Duration such as 90d, 24h, or seconds. Max 365d.', examples: [90d] }
                preset: { type: string, enum: [read-only, full-box, ci, admin] }
                actions: { type: array, items: { type: string } }
                boxIds: { 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, enum: [false] }
  /api-keys/{apiKeyId}:
    parameters:
      - name: apiKeyId
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Box]
      summary: Revoke an API key
      description: Revokes the key immediately. Requires a dashboard or CLI session.
      operationId: revokeApiKey
      responses:
        '200':
          description: Key revoked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKeyRevokeResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api-keys/{apiKeyId}/rotate:
    parameters:
      - name: apiKeyId
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Box]
      summary: Rotate an API key
      description: Replaces the raw secret without changing expiry or scope. Requires a dashboard or CLI session.
      operationId: rotateApiKey
      responses:
        '200':
          description: Rotated key metadata plus the one-time replacement secret.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKeySecretResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: The scoped API key has already expired and cannot be rotated.
          content:
            application/json:
              schema:
                type: object
                required: [error, message]
                properties:
                  error: { type: string, enum: [api_key_expired] }
                  message: { type: string }
  /api-keys/{apiKeyId}/usage:
    parameters:
      - name: apiKeyId
        in: path
        required: true
        description: API key ID returned by `GET /api-keys`.
        schema: { type: string }
    get:
      tags: [Box]
      summary: Get API key usage
      description: Returns the key's request total for the 30-day UTC window and the boxes and Agents it created that still exist, including Boxes 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: box_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
                      boxes: 1
                      agents: 1
                    createdResources:
                      - kind: box
                        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: [Box]
      summary: List webhooks
      description: Lists account-wide Boat 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: [Box]
      summary: Create webhook
      description: |
        Registers an account-wide endpoint for Boat 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/box
                  events: [box.ready, box.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/box
                      events: [box.ready, box.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: [Box]
      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: [Box]
      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: [Box]
      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: [Box]
      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' }
  /secrets:
    get:
      tags: [Box]
      summary: Get Boat secrets setup
      description: Returns the environment variables and secret files configured for Boxes.
      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: [Box]
      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: [Box]
      summary: List Boat environments
      description: Returns every non-deleted Boat environment with its latest-version flags, contents, and per-version box usage. A default environment named `base` is created on first access if none exists.
      operationId: environments
      responses:
        '200':
          description: The account's Boat environments.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxEnvironmentListResponse' }
              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
                        passBoxCredentials: true
                        passAgentsCredentials: true
                        envContents: "OPENAI_API_KEY=sk-...\n"
                        secretFiles: []
                        versions:
                          - id: 8f1c2d3e-0000-4000-8000-0000000000a1
                            versionNumber: 1
                            boxCount: 3
                            createdAt: '2026-06-01T12:00:00Z'
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      tags: [Box]
      summary: Create a Boat environment
      description: Create a new named environment. It starts on version 1 with all fine-grained flags on and `safeForThirdParties` off.
      operationId: createEnvironment
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateBoxEnvironmentRequest' }
      responses:
        '200':
          description: Environment created; full list returned.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxEnvironmentListResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '409': { $ref: '#/components/responses/Conflict' }
  /environments/{environmentId}:
    parameters:
      - name: environmentId
        in: path
        required: true
        schema: { type: string, format: uuid }
        description: Environment id returned by `GET /environments`.
    put:
      tags: [Box]
      summary: Update a Boat environment
      description: Rename, set as default, and/or edit flags and contents. Any flag or content change mints a new immutable version; existing boxes stay on their pinned version until you call upgrade.
      operationId: updateEnvironment
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateBoxEnvironmentRequest' }
      responses:
        '200':
          description: Updated environment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxEnvironmentResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Box]
      summary: Delete a Boat 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/BoxEnvironmentResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
  /environments/{environmentId}/upgrade:
    post:
      tags: [Box]
      summary: Upgrade boxes to an environment's latest version
      description: Repoints the caller's active boxes from an older version of this environment to its latest version, scrubbing any owner secrets the new version drops and hot-pushing the new config.
      operationId: upgradeEnvironment
      parameters:
        - name: environmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpgradeBoxEnvironmentRequest' }
      responses:
        '200':
          description: Upgrade result counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UpgradeBoxEnvironmentResponse' }
              examples:
                upgraded:
                  value:
                    success: true
                    upgraded: 2
                    failed: 0
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /environments/{environmentId}/vars/{key}:
    parameters:
      - name: environmentId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: key
        in: path
        required: true
        schema: { type: string, pattern: '^[A-Za-z_][A-Za-z0-9_]*$' }
        description: Environment variable name.
    put:
      tags: [Box]
      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: [Box]
      summary: Remove one environment variable
      description: Removes a single variable. Mints a new immutable version.
      operationId: deleteEnvironmentVar
      responses:
        '200':
          description: New version minted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /environments/{environmentId}/secret-files:
    parameters:
      - name: environmentId
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      tags: [Box]
      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-box 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: [Box]
      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: [Box]
      summary: Add a repository to an environment
      description: Adds a repository (or updates its base branch) by database id from the repository list. Mints a new immutable version.
      operationId: addEnvironmentRepo
      parameters:
        - name: environmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [repositoryId]
              properties:
                repositoryId:
                  type: string
                  description: The repository `databaseId` from `GET /repos`.
                baseBranch:
                  type: string
                  default: main
      responses:
        '200':
          description: New version minted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /environments/{environmentId}/repos/{repositoryId}:
    delete:
      tags: [Box]
      summary: Remove a repository from an environment
      description: Removes one repository from the environment's selection. Mints a new immutable version.
      operationId: deleteEnvironmentRepo
      parameters:
        - name: environmentId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: repositoryId
          in: path
          required: true
          schema: { type: string }
          description: The repository `databaseId`.
      responses:
        '200':
          description: New version minted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EnvironmentItemChangeResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /boxes:
    get:
      tags: [Box]
      summary: List boxes
      operationId: boxes
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Sort'
        - name: state
          in: query
          schema: { type: string }
          description: Comma-separated Box state filter, for example `ready,idle,running`.
      responses:
        '200':
          description: Boxes owned by the authenticated Boat user.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxListResponse' }
              examples:
                boxes:
                  value:
                    ok: true
                    type: box.list
                    boxes:
                      - id: bx_23456789
                        name: Box 2026-05-31 12:00
                        state: idle
                        url: https://machine.on.ascii.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: [Box]
      summary: Create box
      description: >-
        Provision a new cloud computer. Store the returned `box.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 box.
      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/CreateBoxRequest' }
      responses:
        '202':
          description: Box accepted for provisioning.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreateBoxResponse' }
              examples:
                provisioning:
                  value:
                    ok: true
                    type: box.created
                    status: provisioning
                    ttlSeconds: 3600
                    box:
                      id: bx_23456789
                      name: Box 2026-05-31 12:00
                      state: provisioning
                      url: null
                      ip: null
                      createdAt: '2026-05-31T12:00:00Z'
                      updatedAt: '2026-05-31T12:00:00Z'
                      archiveAfter: '2026-05-31T13:00:00Z'
                      desktopAvailable: false
                      desktopUrl: null
                      snapshotAvailable: false
                      snapshotCompletedAt: null
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /boxes/{boxId}:
    parameters:
      - $ref: '#/components/parameters/BoxId'
    get:
      tags: [Box]
      summary: Get box
      description: Poll this endpoint after create, stop, resume, or fork operations.
      operationId: get
      responses:
        '200':
          description: Box details.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxInfoResponse' }
              examples:
                box:
                  value:
                    ok: true
                    type: box.info
                    box:
                      id: bx_23456789
                      name: Box 2026-05-31 12:00
                      state: idle
                      url: https://machine.on.ascii.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: [Box]
      summary: Update box
      operationId: update
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UpdateBoxRequest' }
      responses:
        '200':
          description: Updated box details.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxInfoResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Box]
      summary: Permanently delete Boat data
      description: >-
        Accept irreversible background deletion of the Boat machine, snapshots, and content-bearing
        records. This is not archive: the Boat cannot be resumed. Named snapshots and other shared
        artifacts remain independent. Poll the returned operation until `completed`.
      operationId: deleteBox
      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' }
  /boxes/{boxId}/stop:
    post:
      tags: [Box]
      summary: Stop and archive box
      description: |
        Stop active work and archive/snapshot the box for later resume or fork.

        A stop always saves the box's disk first. If that save is failing, the stop is
        refused and the box keeps running so your work is not discarded, we retry
        automatically and email you. You are not billed for time spent in that state.

        Set `force: true` to stop anyway, accepting the loss of everything written since
        the last successful snapshot. This is irreversible; only reach for it after a
        stop has already failed.
      operationId: stop
      parameters:
        - $ref: '#/components/parameters/BoxId'
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StopRequest' }
      responses:
        '202':
          description: Box archival started or already in progress.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxActionResponse' }
              examples:
                archiving:
                  value:
                    ok: true
                    type: box.stopping
                    id: bx_23456789
                    status: archiving
                    box: { id: bx_23456789, name: Box 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' }
  /boxes/{boxId}/resume:
    post:
      tags: [Box]
      summary: Resume box
      operationId: resume
      parameters:
        - $ref: '#/components/parameters/BoxId'
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ResumeRequest' }
      responses:
        '202':
          description: Resume started.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxActionResponse' }
              examples:
                resuming:
                  value:
                    ok: true
                    type: box.resuming
                    id: bx_23456789
                    status: resuming
                    box: { id: bx_23456789, name: Box 2026-05-31 12:00, state: provisioning, desktopAvailable: false, snapshotAvailable: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /boxes/{boxId}/fork:
    post:
      tags: [Box]
      summary: Fork box
      description: >-
        Provision a new box 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/BoxId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                env:
                  type: object
                  additionalProperties:
                    type: string
                  description: >-
                    Replaces the env the fork would otherwise inherit from the source box.
                    Same validation rules as `CreateBoxRequest.env`.
                environment:
                  type: string
                  description: >-
                    Optionally pin the fork to a different named Boat environment. Omit to inherit
                    the source box's environment. Unknown names are rejected with
                    `unknown_environment`.
                  examples: [base, customer-demos]
                noEnv:
                  type: boolean
                  description: >-
                    Make the fork no-env (see `CreateBoxRequest.noEnv`). A fork of a no-env box is
                    always no-env regardless of this field.
                type:
                  type: string
                  enum: [small, default, large]
                  description: >-
                    Machine size for the fork. Omit to inherit the source box's type. The source
                    box 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 box's TTL, so forking a Boat that has auto-stop
                    disabled still gives you a fork that stops itself. `null` disables
                    auto-stop, which means nothing will ever stop this Boat for you.
      responses:
        '202':
          description: Fork started. The response `id` is the new forked box id.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxActionResponse' }
              examples:
                forking:
                  value:
                    ok: true
                    type: box.forking
                    id: bx_abcdef23
                    status: forking
                    box: { id: bx_abcdef23, name: Box fork, state: provisioning, desktopAvailable: false, snapshotAvailable: false }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /boxes/{boxId}/prompt:
    post:
      tags: [Box]
      summary: Prompt Boat
      description: >-
        Queue a natural-language work item for an agent harness (Codex, Claude Code, pi, OpenCode, or Prime Agent) inside the box. Observe progress with `GET /boxes/{boxId}/events`.


        A Box 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](/box/integrated-agents).
      operationId: prompt
      parameters:
        - $ref: '#/components/parameters/BoxId'
      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
                      boxId: bx_23456789
                      status: queued
                      done: false
                      conversationId: 8f1c2b7a-3d4e-4f5a-9b0c-1d2e3f4a5b6c
                    status: queued
                    provider: codex
                    model: gpt-5.4
                    reasoningEffort: medium
        '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' }
  /boxes/{boxId}/events:
    get:
      tags: [Box]
      summary: List box events
      description: Return Boat work, lifecycle, and progress events so product UIs can show what the box is doing. A Box 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/BoxId'
        - $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 Box. See [Integrated agents](/box/integrated-agents).
      responses:
        '200':
          description: >-
            The agent's work on the box (prompts, responses), not box
            lifecycle. A box 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' }
  /boxes/{boxId}/conversations:
    get:
      tags: [Box]
      summary: List box conversations
      description: >-
        Every conversation on the Box, 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](/box/integrated-agents).
      operationId: conversations
      parameters:
        - $ref: '#/components/parameters/BoxId'
      responses:
        '200':
          description: Conversations on the Box. A Box 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' }
  /boxes/{boxId}/prompts/{promptId}:
    get:
      tags: [Box]
      summary: Get prompt run status
      description: Returns first-class status for a queued/running/finished prompt so clients do not infer completion from box state and events.
      operationId: promptRunStatus
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - 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' }
  /boxes/{boxId}/files:
    get:
      tags: [Box]
      summary: Read a file from a Boat
      operationId: readFile
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - name: path
          in: query
          required: true
          schema: { type: string, description: "Absolute path, or path relative to the Boat 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: [Box]
      summary: Write a file in a Boat
      operationId: writeFile
      parameters:
        - $ref: '#/components/parameters/BoxId'
      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' }
  /boxes/{boxId}/commands:
    post:
      tags: [Box]
      summary: Execute a command in a Boat
      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 `/boxes/{boxId}/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 Boat is still starting (right after a resume) waits up to 60s for it to be ready instead of failing; 409 box_starting (retryable) is returned only if it is still not ready by then. A Boat that is not starting (`error`, `stopped`) returns 409 box_not_ready with `retryable: false` and its `state` at once: resume or recover it first. Command execution is never retried automatically: a 502 box_direct_failed means the command may already be running on the Boat."
      operationId: command
      parameters:
        - $ref: '#/components/parameters/BoxId'
      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' }
  /boxes/{boxId}/commands/{processId}:
    get:
      tags: [Box]
      summary: Get detached command status and logs
      operationId: commandStatus
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - $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).
      responses:
        '200':
          description: Process status and log tails.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CommandStatusResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /boxes/{boxId}/artifacts:
    get:
      tags: [Box]
      summary: Download a Boat artifact
      operationId: artifact
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - name: path
          in: query
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Artifact bytes.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
  /boxes/{boxId}/steer:
    post:
      tags: [Box]
      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), Box 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. The steer shows up in `GET /events` as a `steer` event. See [Integrated agents](/box/integrated-agents).
      operationId: steer
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - name: conversation
          in: query
          schema: { type: string }
          description: Conversation id to steer. Also accepted in the body. Omit to steer the Box'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' }
  /boxes/{boxId}/interrupt:
    post:
      tags: [Box]
      summary: Interrupt running work
      description: Interrupt running agent work. By default this stops **every** conversation on the Box. Pass `conversation` to stop just one, leaving the others running. See [Integrated agents](/box/integrated-agents).
      operationId: interrupt
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - name: conversation
          in: query
          schema: { type: string }
          description: Interrupt only this conversation id. Omit to interrupt the whole Box.
      responses:
        '200':
          description: Interrupt requested.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxActionResponse' }
              examples:
                interrupted:
                  value:
                    ok: true
                    type: box.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' }
  /boxes/{boxId}/desktop:
    post:
      tags: [Box]
      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/BoxId'
        - 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://box-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' }
  /boxes/{boxId}/host:
    post:
      tags: [Box]
      summary: Expose a box port on a public HTTPS URL
      description: "Registers a stable `https://<box-subdomain>-<port>.on.ascii.dev` route for a port inside the Boat and opens the Boat firewall for it. Idempotent: calling it again for the same Boat and port returns the same URL and token."
      operationId: hostPort
      parameters:
        - $ref: '#/components/parameters/BoxId'
      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.ascii.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' }
  /boxes/{boxId}/sshkey:
    post:
      tags: [Box]
      summary: Configure box SSH key
      description: |
        Adds an OpenSSH public key to the running Boat 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 Box 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 Box 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>
        ```

        `box forward <id> --reverse --local 7777` is the same tunnel with the key handling and redial done for you.
      operationId: sshKey
      parameters:
        - $ref: '#/components/parameters/BoxId'
      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: [Box]
      summary: List named snapshots
      description: List your named snapshots, newest first.
      operationId: listNamedSnapshots
      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: [Box]
      summary: Save a named snapshot
      description: >-
        Freeze a box's current state under a name (`box snapshot <id> <name>` in the CLI).
        Answers `202` immediately with the snapshot in `saving`; a running box takes a fresh
        capture first, which can run minutes. Poll `GET /named-snapshots/{name}` until the
        status settles at `ready` or `failed`. Reusing one of your existing names replaces
        that snapshot's artifact once the new save is ready.
      operationId: saveNamedSnapshot
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NamedSnapshotSaveRequest' }
      responses:
        '202':
          description: Save accepted and running in the background.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NamedSnapshotSavingResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: >-
            `named_snapshot_limit` when you already keep the maximum of 10 named snapshots;
            remove one first. `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: [Box]
      summary: Get a named snapshot
      operationId: getNamedSnapshot
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The named snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NamedSnapshotInfoResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Box]
      summary: Remove a named snapshot
      description: >-
        Remove a named snapshot and release its storage (`box snapshot rm <name>` in the
        CLI). Boxes already deployed from it are unaffected.
      operationId: deleteNamedSnapshot
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Removed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NamedSnapshotDeletedResponse' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: >-
            `save_in_progress`: a save under this name is still running. Wait for it to settle
            before removing it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /snapshots:
    get:
      tags: [Box]
      summary: List snapshots
      description: >-
        List visible completed snapshots across all of your boxes using immutable ownership attribution.
        Pinned, deletion-scheduled, and privacy-fenced Box 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
                        boxId: 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' }
  /boxes/{boxId}/snapshots:
    get:
      tags: [Box]
      summary: List box snapshots
      description: List visible completed snapshots for one Box with uncapped cursor pagination.
      operationId: listBoxSnapshots
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Sort'
      responses:
        '200':
          description: Completed snapshots for this box.
          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: [Box]
      summary: Permanently delete all snapshots of a box
      description: >-
        Accept irreversible deletion of every unpinned snapshot of this box. The box itself and its
        named 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 box id.
      operationId: deleteBoxSnapshots
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - $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' }
  /boxes/{boxId}/snapshots/latest:
    get:
      tags: [Box]
      summary: Get latest box snapshot
      description: Return the most recent completed snapshot for this box, or `null` if it has none.
      operationId: getLatestBoxSnapshot
      parameters:
        - $ref: '#/components/parameters/BoxId'
      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
                      boxId: 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' }
  /boxes/{boxId}/usage:
    get:
      tags: [Box]
      summary: Get box usage
      description: >-
        Machine time one box consumed, and what it costs, so you can bill your own users per box. Same
        meter as `GET /limits`: billable seconds with the box 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 box runs and after it stops.
      operationId: usage
      parameters:
        - $ref: '#/components/parameters/BoxId'
        - name: since
          in: query
          required: false
          schema: { type: string }
          description: Count from this time, ISO 8601 or Unix epoch seconds. Default is the box'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 box inside the window.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BoxUsageResponse' }
              examples:
                usage:
                  value:
                    ok: true
                    type: box.usage
                    boxId: bx_23456789
                    boxType: 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: [Box]
      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: [Box]
      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
                    boxId: 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
                    boxId: 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: [Box]
      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 box 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`).
      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: [Box]
      summary: Get snapshot download
      description: |
        Return time-limited signed URLs for every chunk needed to reconstruct the box 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 `box 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
                    boxId: 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' }
