> ## 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.

# Publish a port

> Publishes a port from inside the Sailbox so it can be reached from outside. This works on a running Sailbox without restarting it, and on a paused or sleeping one.

An `http` listener gets an HTTPS URL. A `tcp` listener gets a host and port to dial, which is what SSH and database clients need.

Without an `allowlist` the port is reachable by anyone who has the address, so set one unless you mean to publish to the internet.

Publishing a port that is already published under the same protocol sets its `allowlist` to what you send, so send the whole list every time. Sending none clears the restriction and reopens the port.



## OpenAPI

````yaml /sailbox-openapi.json post /sailboxes/{sailbox_id}/listeners
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/{sailbox_id}/listeners:
    post:
      tags:
        - Networking
      summary: Publish a port
      description: >-
        Publishes a port from inside the Sailbox so it can be reached from
        outside. This works on a running Sailbox without restarting it, and on a
        paused or sleeping one.


        An `http` listener gets an HTTPS URL. A `tcp` listener gets a host and
        port to dial, which is what SSH and database clients need.


        Without an `allowlist` the port is reachable by anyone who has the
        address, so set one unless you mean to publish to the internet.


        Publishing a port that is already published under the same protocol sets
        its `allowlist` to what you send, so send the whole list every time.
        Sending none clears the restriction and reopens the port.
      operationId: addSailboxListener
      parameters:
        - $ref: '#/components/parameters/SailboxId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngressPort'
            example:
              guest_port: 8000
              protocol: http
              allowlist:
                - 203.0.113.0/24
      responses:
        '200':
          description: The port is published.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddListenerResponse'
              example:
                guest_port: 8000
                protocol: http
                public_url: https://sb-9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d-8000.sail.box
          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
          headers:
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '404':
          description: No Sailbox with that id exists in your organization.
          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: >-
            That port is already published under a different protocol, the
            Sailbox is not in a state that allows this, or the `Idempotency-Key`
            was reused for a different request.
          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 64 KiB 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. Retry with the same key.
          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:
    SailboxId:
      name: sailbox_id
      in: path
      required: true
      schema:
        type: string
        pattern: >-
          ^sb_[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
      description: >-
        Id of the Sailbox, as returned by create. It is `sb_` followed by a
        UUID.
    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:
    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.
    AddListenerResponse:
      type: object
      properties:
        guest_port:
          type: integer
          description: Port inside the Sailbox that traffic is sent to.
        protocol:
          type: string
          description: >-
            Protocol the listener serves. Values in use today are `http` and
            `tcp`. New values can appear over time, so treat this as an open
            set.
        public_url:
          type: string
          description: HTTPS URL for an `http` listener.
        public_host:
          type: string
          description: Host to dial for a `tcp` listener.
        public_port:
          type: integer
          description: Port to dial for a `tcp` listener.
        allowlist:
          type: array
          items:
            type: string
          description: >-
            Sources allowed to reach the listener: addresses, ranges, and app
            names.
      required:
        - guest_port
        - protocol
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorObject'
    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
  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

````