> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sailresearch.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a Sailbox

> Creates a Sailbox and waits for startup to finish, which can take a few minutes. `image` has to describe an image that is ready to boot: a base image on its own, or one already built from that same spec through an SDK or the CLI.

Send an `Idempotency-Key` so a network retry replays this answer instead of starting a second Sailbox. A server error can still arrive once the Sailbox exists, so read [Retrying safely](/sailboxes-http-api#retrying-safely) before you retry one.



## OpenAPI

````yaml /sailbox-openapi.json post /sailboxes
openapi: 3.1.0
info:
  title: Sail Sailbox API
  version: '2026-07-26'
  description: >-
    Create and operate Sailboxes over HTTP. Sailboxes are fast-booting virtual
    machines that you can pause, resume, checkpoint, and fork.


    Three things need an SDK or the CLI: running commands and moving files,
    turning on SSH, and building an image. Once SSH is on you can use `ssh` and
    `scp` for the first of those. See the [HTTP API guide](/sailboxes-http-api).
servers:
  - url: https://sailbox-api.sailresearch.com/v1
security:
  - BearerAuth: []
tags:
  - name: Apps
    description: Groups that own Sailboxes. One app per workload.
  - name: Lifecycle
    description: >-
      A Sailbox's whole life: create it, watch it, stop and start it, terminate
      it.
  - name: Checkpoints
    description: Save a Sailbox to start from later, and copy a running one.
  - name: Networking
    description: >-
      Publish ports, serve them on hostnames you own, and identify a Sailbox to
      other Sailboxes.
  - name: Volumes
    description: >-
      Shared storage that outlives any one Sailbox. Alpha: these endpoints can
      still change.
  - name: SSH access
    description: Certificates for connecting to a Sailbox with `ssh`.
  - name: Usage
    description: Resource usage and spend.
  - name: Identity
    description: Who your API key belongs to.
paths:
  /sailboxes:
    post:
      tags:
        - Lifecycle
      summary: Create a Sailbox
      description: >-
        Creates a Sailbox and waits for startup to finish, which can take a few
        minutes. `image` has to describe an image that is ready to boot: a base
        image on its own, or one already built from that same spec through an
        SDK or the CLI.


        Send an `Idempotency-Key` so a network retry replays this answer instead
        of starting a second Sailbox. A server error can still arrive once the
        Sailbox exists, so read [Retrying
        safely](/sailboxes-http-api#retrying-safely) before you retry one.
      operationId: createSailbox
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSailboxRequest'
            example:
              name: worker-1
              app_id: app_0f6a2c31-8b4d-4e7a-9c15-2d8e6f4a1b03
              size: m
              image:
                base: BASE_IMAGE_DEBIAN
              ingress_ports:
                - guest_port: 8000
                  protocol: http
                  allowlist:
                    - 203.0.113.0/24
      responses:
        '200':
          description: >-
            Check `status`: `running` means the Sailbox is ready, and `failed`
            means it did not start, with `error_message` saying why.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedSailbox'
              example:
                sailbox_id: sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d
                status: running
          headers:
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '400':
          description: >-
            The request was rejected. See the error message for the offending
            field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: memory_gib for size m must be between 8 and 128
                  type: invalid_request_error
                  param: null
                  code: null
          headers:
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '401':
          description: The API key is missing or invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: Invalid API key
                  type: authentication_error
                  param: null
                  code: invalid_api_key
        '402':
          description: >-
            The organization is out of credits. The error carries a
            `billing_url` pointing at the page where you add them. Retry once
            you have.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: >-
                    Your API key has been disabled due to insufficient credits.
                    Visit https://app.sailresearch.com/billing to add credits.
                  type: billing_error
                  param: null
                  code: credits_exhausted
                  billing_url: https://app.sailresearch.com/billing
        '403':
          description: The API key is not allowed to perform this operation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: sailboxes require an organization-scoped API key
                  type: permission_error
                  param: null
                  code: null
        '404':
          description: >-
            No app with that `app_id` exists in your organization, or a volume
            named in `volume_mounts` does not.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: sailbox "sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d" not found
                  type: not_found_error
                  param: null
                  code: null
          headers:
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '409':
          description: >-
            The request cannot be served as written: `image` names an image that
            has not been built, `visibility` is `private` on a key that carries
            no user, or a requested `tcp` port is not available. An
            `Idempotency-Key` reused for a different request lands here too.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: idempotency key reused with a different request body
                  type: conflict_error
                  param: null
                  code: null
          headers:
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '413':
          description: The request body is larger than the 256 MiB this endpoint accepts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: request body too large
                  type: invalid_request_error
                  param: null
                  code: null
        '429':
          description: >-
            Too many requests in flight, or the organization is at a limit. A
            `Retry-After` header means the request never started, so retry it
            unchanged after that delay. Without one, the request ran and hit the
            limit `message` names. Some of those clear on their own, such as a
            shortage of public ports; others need you to free something up
            first. Send the request again once the cause is gone. If you sent an
            `Idempotency-Key`, use a new one, because the old key now replays
            this answer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: >-
                    Too many concurrent requests. Please retry after some of
                    your organization's in-flight requests complete.
                  type: rate_limit_error
                  param: null
                  code: rate_limited
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '500':
          description: Server error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: failed to fetch sailbox
                  type: server_error
                  param: null
                  code: null
        '503':
          description: >-
            Sail is momentarily unavailable. Retry after the delay in the
            `Retry-After` header when one is present, and with backoff
            otherwise.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: Authentication service unavailable
                  type: server_error
                  param: null
                  code: null
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
        '504':
          description: >-
            The operation outran its deadline, or a retry sent with the same
            `Idempotency-Key` waited about 30 seconds and the original request
            was still going. The new Sailbox can exist even though the call
            failed, so list your Sailboxes before you retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: idempotent request still in flight
                  type: server_error
                  param: null
                  code: null
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: >-
        Makes the request retry-safe. Sail remembers the answer it sent under a
        key, including a 400 or a 409, and replays it with `Idempotent-Replayed:
        true` for a retry that repeats the key, the method, the path, and the
        body. Bodies are compared byte for byte. Reusing a key for a different
        request returns 409, so send a corrected request under a new key. A key
        that is blank or only spaces is ignored, and the request runs without
        idempotency.


        Any string of up to 255 bytes works, and characters outside ASCII count
        for more than one. A UUID is a good default. Keys are remembered for at
        least 24 hours and scoped to the API key that sent them.


        A server error, or a failure to record the answer, can leave a key
        unsettled, and creating a Sailbox is where that matters. See [Retrying
        safely](/sailboxes-http-api#retrying-safely).
  schemas:
    CreateSailboxRequest:
      type: object
      required:
        - name
        - app_id
        - image
      properties:
        name:
          type: string
          minLength: 1
          pattern: \S
          maxLength: 128
          description: >-
            Human-readable name for the new Sailbox. Names are not unique, so
            keep hold of the returned `sailbox_id`. It is stored as you send it,
            surrounding spaces included, so trim it yourself if you compare
            names later. Control characters are not allowed.
        app_id:
          type: string
          minLength: 1
          pattern: \S
          description: >-
            Id of the app that will own the Sailbox. Use `POST /apps/find` to
            look one up by name.
        image:
          $ref: '#/components/schemas/ImageSpec'
        size:
          type: string
          enum:
            - s
            - m
            - l
            - ''
          default: m
          description: Machine size. `s` is 1 vCPU, `m` is 4 vCPUs, `l` is 8 vCPUs.
        memory_limit_gib:
          type: integer
          description: >-
            Memory limit in GiB. Allowed ranges are 2 to 64 for size `s`, 8 to
            128 for `m`, and 16 to 256 for `l`. Omit it to take the default for
            the size: 16 for `s`, 32 for `m`, and 64 for `l`.
        state_disk_limit_gib:
          type: integer
          description: >-
            Disk limit in GiB. Allowed ranges are 8 to 128 for size `s`, 32 to
            512 for `m`, and 64 to 1024 for `l`. Omit it to take the default for
            the size: 32 for `s`, 128 for `m`, and 256 for `l`.
        ingress_ports:
          type: array
          items:
            $ref: '#/components/schemas/IngressPort'
          description: >-
            Ports to publish. You can also publish ports later without
            restarting.
        volume_mounts:
          type: array
          items:
            $ref: '#/components/schemas/VolumeMount'
          description: Volumes to mount into the Sailbox.
        visibility:
          type: string
          enum:
            - org
            - private
            - ''
          default: org
          description: >-
            `org` lets anyone in your organization operate the Sailbox.
            `private` restricts that to the user whose key created it, so it
            needs a user-scoped key. An organization admin can still pause,
            resume, sleep, schedule a wake, upgrade, or terminate it by sending
            `X-Sail-Owner-Override-Reason`.
      allOf:
        - if:
            required:
              - size
            properties:
              size:
                const: s
          then:
            properties:
              memory_limit_gib:
                anyOf:
                  - const: 0
                  - minimum: 2
                    maximum: 64
              state_disk_limit_gib:
                anyOf:
                  - const: 0
                  - minimum: 8
                    maximum: 128
        - if:
            anyOf:
              - required:
                  - size
                properties:
                  size:
                    enum:
                      - m
                      - ''
              - not:
                  required:
                    - size
          then:
            properties:
              memory_limit_gib:
                anyOf:
                  - const: 0
                  - minimum: 8
                    maximum: 128
              state_disk_limit_gib:
                anyOf:
                  - const: 0
                  - minimum: 32
                    maximum: 512
        - if:
            required:
              - size
            properties:
              size:
                const: l
          then:
            properties:
              memory_limit_gib:
                anyOf:
                  - const: 0
                  - minimum: 16
                    maximum: 256
              state_disk_limit_gib:
                anyOf:
                  - const: 0
                  - minimum: 64
                    maximum: 1024
    CreatedSailbox:
      type: object
      properties:
        sailbox_id:
          type: string
          description: Id of the new Sailbox.
        status:
          type: string
          description: >-
            Current state of the Sailbox. Values in use today are `creating`,
            `running`, `paused`, `sleeping`, `terminating`, `terminated`,
            `failed`, `create_failed`, `interrupted_restorable`, and
            `interrupted_unsafe_to_retry`. New values can appear over time, so
            treat this as an open set.
        error_message:
          type: string
          description: Why the Sailbox failed to start, when it did.
      required:
        - sailbox_id
        - status
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorObject'
    ImageSpec:
      type: object
      additionalProperties: false
      description: >-
        Describes the environment the Sailbox boots into. A base image on its
        own is ready to boot. Adding build steps, environment variables, or a
        Python version describes an image that has to be built before a Sailbox
        can start from it, and building one takes an SDK or the CLI.
      required:
        - base
      properties:
        base:
          type: string
          enum:
            - BASE_IMAGE_DEBIAN
            - BASE_IMAGE_DEVBOX
          description: >-
            Starting point for the image. `BASE_IMAGE_DEBIAN` is a plain Debian
            base you can build on. `BASE_IMAGE_DEVBOX` is a prebuilt development
            environment that accepts no build steps, environment variables, or
            Python version.
        build_steps:
          type: array
          items:
            $ref: '#/components/schemas/ImageBuildStep'
          description: Steps applied to the base image, in order.
        env:
          type: object
          propertyNames:
            pattern: ^[A-Za-z_][A-Za-z0-9_]*$
          additionalProperties:
            type: string
          description: >-
            Environment variables baked into the image and present in every
            process. A variable name must start with a letter or `_` and contain
            only letters, numbers, and `_`.
        architecture:
          type: string
          enum:
            - IMAGE_ARCHITECTURE_AMD64
            - IMAGE_ARCHITECTURE_ARM64
          description: >-
            CPU architecture to build for. Defaults to
            `IMAGE_ARCHITECTURE_AMD64`.
        python_version:
          type: string
          pattern: ^([0-9]+\.[0-9]+\.[0-9]+)?$
          description: >-
            Python version to install as `python3`. Give the full patch version,
            such as `3.12.13`; `3.12` is rejected.
          examples:
            - 3.12.13
      allOf:
        - if:
            required:
              - base
            properties:
              base:
                const: BASE_IMAGE_DEVBOX
          then:
            properties:
              build_steps:
                maxItems: 0
              env:
                maxProperties: 0
              python_version:
                maxLength: 0
    IngressPort:
      type: object
      required:
        - guest_port
      properties:
        guest_port:
          type: integer
          minimum: 1
          maximum: 65535
          description: >-
            Port your process listens on inside the Sailbox. Ports 10000, 10001,
            15001, and 15002 are reserved and cannot be published on either
            protocol. Port 22 is reserved on `http` and available on `tcp`,
            which is what SSH needs.


            A guest port is published under one protocol at a time. Publishing
            it again under that same protocol replaces its allowlist; publishing
            it under the other protocol is refused.
        protocol:
          type: string
          enum:
            - http
            - tcp
            - ''
          default: http
          description: >-
            `http` publishes an HTTPS URL. `tcp` publishes a host and port for
            raw TCP, which is what SSH and database clients need.
        allowlist:
          type: array
          items:
            type: string
          description: >-
            Restricts who can reach the listener. An address or a range, such as
            `203.0.113.0/24`, lets matching source IPs in, and works on both
            protocols. An app name, which lets Sailboxes in that app call this
            one, works on `http` only and is rejected on `tcp`.


            An app name must not look like an address or a range, and must not
            contain a `/`. Every entry that reads as an address or a range is
            taken as one, and any other entry holding a `/` is refused as a
            malformed range. An address must not carry an IPv6 zone, such as
            `fe80::1%eth0`, which names an interface on one machine rather than
            a source.


            Entries come back canonicalized: a range is reported by its network
            address, a bare address as a single-address range, and blank or
            repeated entries are dropped.


            Leaving it empty makes the listener reachable by anyone who has the
            address. That is refused on a `tcp` listener for a well-known
            unauthenticated service port such as 5432 or 6379: publish those
            with an explicit allowlist, using `0.0.0.0/0` and `::/0` if you do
            want the whole internet.
    VolumeMount:
      type: object
      required:
        - volume_id
        - mount_path
      properties:
        volume_id:
          type: string
          minLength: 1
          pattern: \S
          description: Id of a volume you created earlier.
        mount_path:
          type: string
          pattern: ^\s*/.
          description: >-
            Absolute path inside the Sailbox where the volume appears. Repeated
            slashes and `.`/`..` segments are resolved first, and the result
            cannot be `/`. Two mounts in one request cannot nest inside each
            other.
          examples:
            - /mnt/shared
    ErrorObject:
      type: object
      required:
        - message
        - type
        - param
        - code
      properties:
        message:
          type: string
          description: Human-readable description of what went wrong.
        type:
          type: string
          description: >-
            Error category: `invalid_request_error`, `authentication_error`,
            `permission_error`, `billing_error`, `not_found_error`,
            `conflict_error`, `rate_limit_error`, or `server_error`.
        param:
          oneOf:
            - type: string
            - type: 'null'
          description: >-
            The request field the error refers to, when the error is about one
            field.
        code:
          oneOf:
            - type: string
            - type: integer
            - type: 'null'
          description: Stable short code for the error, when one applies.
        billing_url:
          type: string
          format: uri
          description: Page where you add credits. Sent with a 402.
      additionalProperties: true
    ImageBuildStep:
      type: object
      additionalProperties: false
      description: One build step. Set exactly one field per step. Steps run in order.
      minProperties: 1
      maxProperties: 1
      properties:
        apt_install:
          $ref: '#/components/schemas/PackageInstall'
        pip_install:
          $ref: '#/components/schemas/PackageInstall'
        run_command:
          type: object
          additionalProperties: false
          required:
            - command
          properties:
            command:
              type: string
              minLength: 1
              pattern: \S
              description: Shell command to run while building the image.
        add_local_file:
          $ref: '#/components/schemas/AddLocalFile'
        add_local_dir:
          $ref: '#/components/schemas/AddLocalDir'
    PackageInstall:
      type: object
      additionalProperties: false
      required:
        - packages
      properties:
        packages:
          type: array
          minItems: 1
          items:
            type: string
            minLength: 1
            pattern: \S
          description: Package names to install.
    AddLocalFile:
      type: object
      additionalProperties: false
      description: >-
        Copies one file into the image. Its contents are uploaded while the
        image is built.
      required:
        - content_sha256
        - remote_path
      properties:
        content_sha256:
          type: string
          pattern: ^[0-9a-f]{64}$
          description: SHA-256 of the file contents, lowercase hex.
        remote_path:
          type: string
          pattern: ^/
          description: >-
            Absolute path the file lands on inside the image. No trailing `/`
            and no `..` segment. A space, `$`, `"`, or `\` anywhere in the path
            is rejected.
          examples:
            - /opt/app/entrypoint.sh
        mode:
          type: integer
          minimum: 0
          maximum: 511
          description: >-
            Permission bits for the file, as a number. `493` is `rwxr-xr-x`.
            Omit it, or send `0`, for `420` (`rw-r--r--`).
    AddLocalDir:
      type: object
      additionalProperties: false
      description: >-
        Copies a directory of files into the image. Their contents are uploaded
        while the image is built.
      required:
        - remote_path
        - files
      properties:
        remote_path:
          type: string
          pattern: ^/
          description: >-
            Absolute directory the files land in inside the image. It cannot be
            `/`. No trailing `/` and no `..` segment. A space, `$`, `"`, or `\`
            anywhere in the path is rejected.
          examples:
            - /opt/app
        files:
          type: array
          minItems: 1
          maxItems: 50000
          items:
            $ref: '#/components/schemas/AddLocalDirFile'
          description: The files to copy. Every `relative_path` has to be distinct.
    AddLocalDirFile:
      type: object
      additionalProperties: false
      description: One file within an `add_local_dir` step.
      required:
        - relative_path
        - content_sha256
      properties:
        relative_path:
          type: string
          maxLength: 1024
          pattern: ^[^/]
          description: >-
            Path within the directory, relative to `remote_path`. No leading or
            trailing `/`, and no empty, `.`, or `..` segment. A `$`, `"`, or `\`
            anywhere in the path is rejected. The limit is 1024 bytes, and
            characters outside ASCII count for more than one.
          examples:
            - src/main.py
        content_sha256:
          type: string
          pattern: ^[0-9a-f]{64}$
          description: SHA-256 of the file contents, lowercase hex.
        mode:
          type: integer
          minimum: 0
          maximum: 511
          description: >-
            Permission bits for the file, as a number. `493` is `rwxr-xr-x`.
            Omit it, or send `0`, for `420` (`rw-r--r--`).
  headers:
    IdempotentReplayed:
      description: >-
        `true` when this response is the stored answer to an earlier request
        that sent the same `Idempotency-Key`. Absent when the request ran for
        the first time.
      schema:
        type: string
        enum:
          - 'true'
    RetryAfter:
      description: >-
        How many seconds to wait before retrying. On a 429 it also means the
        request never started, so the retry can reuse the same
        `Idempotency-Key`.
      schema:
        type: integer
        minimum: 0
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key

````