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

# Add a custom domain

> Serves a published `http` port under a hostname you own.

Point the hostname at the target first, with a CNAME record. A name at the root of a domain cannot hold a CNAME, so route it with your provider's ALIAS or ANAME record and add a TXT record at `_sail-domains.` plus the hostname carrying the same target. The TXT record proves the hostname is yours; it does not route on its own. Read the target from `GET /custom-domains`.

Sail checks DNS while handling this request, so create the record and give it time to propagate before calling. Until it resolves the request answers 400, which is worth sending again once the record is live. Turn off any proxying or acceleration your DNS provider applies to the record: that resolves the hostname to the provider rather than to the target, and no amount of waiting clears it.

The port has to be published as an `http` listener already. The TLS certificate is obtained on the first HTTPS request to the hostname, so that one request can take up to a minute; the rest are served straight away, and renewal is automatic.

Registering a hostname that already serves the same port returns what is there, so the call is safe to repeat. Registering one of your own hostnames against a different Sailbox moves it, with no need to remove it first.

This works on a running, paused, or sleeping Sailbox. A request to the hostname wakes a sleeping one.



## OpenAPI

````yaml /sailbox-openapi.json post /sailboxes/{sailbox_id}/domains
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}/domains:
    post:
      tags:
        - Networking
      summary: Add a custom domain
      description: >-
        Serves a published `http` port under a hostname you own.


        Point the hostname at the target first, with a CNAME record. A name at
        the root of a domain cannot hold a CNAME, so route it with your
        provider's ALIAS or ANAME record and add a TXT record at
        `_sail-domains.` plus the hostname carrying the same target. The TXT
        record proves the hostname is yours; it does not route on its own. Read
        the target from `GET /custom-domains`.


        Sail checks DNS while handling this request, so create the record and
        give it time to propagate before calling. Until it resolves the request
        answers 400, which is worth sending again once the record is live. Turn
        off any proxying or acceleration your DNS provider applies to the
        record: that resolves the hostname to the provider rather than to the
        target, and no amount of waiting clears it.


        The port has to be published as an `http` listener already. The TLS
        certificate is obtained on the first HTTPS request to the hostname, so
        that one request can take up to a minute; the rest are served straight
        away, and renewal is automatic.


        Registering a hostname that already serves the same port returns what is
        there, so the call is safe to repeat. Registering one of your own
        hostnames against a different Sailbox moves it, with no need to remove
        it first.


        This works on a running, paused, or sleeping Sailbox. A request to the
        hostname wakes a sleeping one.
      operationId: addSailboxCustomDomain
      parameters:
        - $ref: '#/components/parameters/SailboxId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddCustomDomainRequest'
            example:
              domain: app.example.com
              guest_port: 8000
      responses:
        '200':
          description: The hostname serves the port.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomDomain'
              example:
                domain: app.example.com
                sailbox_id: sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d
                guest_port: 8000
                url: https://app.example.com
                cname_target: 1a2b3c4d5e6f7890.sailboxes.sailresearch.com
                created_at: '2026-07-30T12:00:00Z'
        '400':
          description: >-
            The request was rejected. See the error message for what to fix.


            One cause is worth sending again unchanged: a hostname whose DNS has
            not reached the target yet clears once the record propagates. It
            never clears while your DNS provider proxies or accelerates the
            record, because that resolves the hostname to the provider rather
            than to the target. Turn that off for this record.
          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
        '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, your
            organization has reached its limit of 200 registered hostnames, or
            the Sailbox is private and you did not create it.
          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 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
        '409':
          description: >-
            Someone else's registration holds the hostname and your DNS does not
            prove otherwise, or the Sailbox has stopped for good and can no
            longer serve one. A third case is worth retrying unchanged: when
            another request changed the same hostname or listener while this one
            ran, the message says so.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  message: this domain is already registered
                  type: conflict_error
                  param: null
                  code: null
        '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: >-
            For its first ten minutes a new organization is capped on how many
            requests it can have in flight at once, and yours is over that cap.
            The request never started, so retry it unchanged after the delay in
            the `Retry-After` header. The limit clears as those requests finish.
          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'
        '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'
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.
  schemas:
    AddCustomDomainRequest:
      type: object
      required:
        - domain
        - guest_port
      properties:
        domain:
          type: string
          maxLength: 253
          description: >-
            Hostname you own, such as `app.example.com`. Give the bare name,
            with no scheme, port, or path. It needs at least two labels, and
            each label runs to at most 63 characters. Wildcards and IP addresses
            are refused.
          examples:
            - app.example.com
        guest_port:
          type: integer
          minimum: 1
          maximum: 65535
          description: >-
            Port inside the Sailbox to send this hostname's traffic to. The
            Sailbox has to publish it as an `http` listener already.
          examples:
            - 8000
    CustomDomain:
      type: object
      description: A hostname you own, serving one published `http` port.
      properties:
        domain:
          type: string
          description: >-
            The hostname, lowercased. A name with non-ASCII characters comes
            back in its ASCII form.
          examples:
            - app.example.com
        sailbox_id:
          type: string
          description: Sailbox the hostname serves.
        guest_port:
          type: integer
          minimum: 1
          maximum: 65535
          description: Port inside the Sailbox that traffic to this hostname reaches.
        url:
          type: string
          format: uri
          description: The hostname's HTTPS address.
          examples:
            - https://app.example.com
        cname_target:
          type: string
          description: >-
            Hostname to point your DNS at with a CNAME record. It is the same
            for every domain in your organization, so you can read it once and
            reuse it.
        created_at:
          type: string
          format: date-time
          description: When the hostname was registered.
      required:
        - domain
        - sailbox_id
        - guest_port
        - url
        - cname_target
        - created_at
    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:
    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

````