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

# HTTPS API

> Create and operate Sailboxes over plain HTTPS

Sailboxes have a public HTTPS API. The SDKs and the CLI are built on it, and
you can call it directly from any language, or from `curl`. Every endpoint is
listed under [Reference → Sailbox → HTTP API](/api-reference/lifecycle/create-a-sailbox);
this page covers what applies to all of them.

```text theme={null}
https://sailbox-api.sailresearch.com/v1
```

The apps endpoints live at `https://api.sailresearch.com/v1`. Your key works
on both.

## Authentication

Send your API key as a bearer token on every request. Create keys in the
[dashboard](https://app.sailresearch.com); a key belongs to one organization
and only ever sees that organization's Sailboxes.

```bash theme={null}
curl https://sailbox-api.sailresearch.com/v1/whoami \
  -H "Authorization: Bearer $SAIL_API_KEY"
```

```json theme={null}
{ "org_id": "org_1a2b3c", "user_id": "user_9z8y" }
```

`user_id` is the member the key belongs to, or `null` for a key that belongs to
the organization rather than a person. Compare it with a Sailbox's
`created_by_user_id` to tell your Sailboxes from a teammate's. A private
Sailbox can only be operated by the user whose key created it; an org admin
can override some operations by sending an `X-Sail-Owner-Override-Reason`
header, which is recorded in the audit log. See
[Access Control](/sailboxes-access-control).

## Example

Every Sailbox belongs to an app. Get an app id, then create a Sailbox in it:

```bash theme={null}
export SAIL_API_KEY="sk_..."

curl -X POST https://api.sailresearch.com/v1/apps/find \
  -H "Authorization: Bearer $SAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "batch-jobs", "mint_if_missing": true}'
```

```json theme={null}
{
  "id": "app_0f6a2c31-8b4d-4e7a-9c15-2d8e6f4a1b03",
  "name": "batch-jobs",
  "created_at": 1753027200
}
```

```bash theme={null}
curl -X POST https://sailbox-api.sailresearch.com/v1/sailboxes \
  -H "Authorization: Bearer $SAIL_API_KEY" \
  -H "Idempotency-Key: create-worker-1-attempt-1" \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": "app_0f6a2c31-8b4d-4e7a-9c15-2d8e6f4a1b03",
    "name": "worker-1",
    "size": "m",
    "image": { "base": "BASE_IMAGE_DEBIAN" }
  }'
```

```json theme={null}
{ "sailbox_id": "sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d", "status": "running" }
```

Three things to know about that create:

* **It blocks until the Sailbox is up**, which can take a few minutes while it
  waits for a machine. Set a generous client timeout.
* **Read `status`.** A create that is accepted and then cannot bring the
  machine up still returns 200, with `status` set to `failed` and
  `error_message` saying why.
* **The `Idempotency-Key` makes it safe to retry.** Send the same key and body
  again and you get the first answer back instead of a second Sailbox. Use a
  fresh key for every Sailbox you mean to create. See
  [Retrying safely](#retrying-safely).

Terminate it when you are done:

```bash theme={null}
curl -X POST https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/terminate \
  -H "Authorization: Bearer $SAIL_API_KEY"
```

## Run commands on a Sailbox

Send `command` as a shell string or an argument array. A string supports
`cwd` and `background`; an array runs the program directly.

```bash theme={null}
curl --no-buffer -X POST \
  "https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/exec" \
  -H "Authorization: Bearer $SAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":["sh","-c","printf output; printf error >&2"],"timeout":30}'
```

The response streams newline-delimited JSON. Output bytes are base64 so every
byte value is safe in JSON, and the last event carries the exit code:

```jsonl theme={null}
{"type":"started","exec_request_id":"exec_123"}
{"type":"stdout","data":"b3V0cHV0","seq":1}
{"type":"stderr","data":"ZXJyb3I=","seq":1}
{"type":"exit","status":"succeeded","return_code":0}
```

A `heartbeat` event arrives every 30 seconds while the command runs. If the connection drops
before `exit`, call `POST .../exec/$EXEC_ID/wait` with the id from the
`started` event to get the result and a bounded tail of the output. A failure
after `started` ends the stream with an `error` event whose `error_code` is a
lowercase category such as `unavailable` or `permission_denied`.

<Accordion title="Standard input, reconnects, and limits">
  * To write standard input, set `open_stdin` or `pty` when you start the exec,
    then `PUT .../exec/$EXEC_ID/stdin?offset=N&eof=true` with the raw bytes.
    Writes carry their byte offset, so an overlapping retry does not duplicate
    input; the response reports `accepted_through`.
  * To reconnect to a live exec, send the same `idempotency_key` with the
    highest `seq` you received for stdout and stderr. Reconnect is best-effort
    and does not guarantee exact replay. An exec id that does not fit a URL
    segment goes in the `exec_request_id` query parameter with `-` in the path.
  * The idempotency key can be up to 256 KiB of UTF-8, trimmed. Environment
    variable names match `[A-Za-z_][A-Za-z0-9_]*`, and names and values cannot
    contain NUL. The encoded request body can be up to 25 MiB; after decoding,
    4 MiB.
  * The reference also lists cancel, PTY resize, and PTY resync.
</Accordion>

## Move files

Files stream in both directions without being buffered whole.

```bash theme={null}
# Upload
curl -X PUT --data-binary @local.bin \
  "https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/files?path=/workspace/input.bin&mode=420" \
  -H "Authorization: Bearer $SAIL_API_KEY" \
  -H "Content-Type: application/octet-stream"

# Download
curl --fail-with-body \
  "https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/files?path=/workspace/input.bin" \
  -H "Authorization: Bearer $SAIL_API_KEY" \
  --output local.bin
```

`mode` is decimal, 0 through 511, and `create_parents` defaults to true. A
complete retry of an upload replaces the file safely; an interrupted one
leaves the target unconfirmed. A download's `X-Sail-File-Mode` header carries
the mode, and `Content-Length` may be absent, so read until the response
ends. For directories, run `mkdir`, `find`, `tar`, and `rm` through the
command endpoint, which is what the SDKs do.

## What needs an SDK

Two things happen outside this API:

* **Building an image** with your own packages or files. Creating a Sailbox
  over HTTPS needs an image that is already built: a base image, or one an SDK
  built earlier from the same `image` block. Asking for an unbuilt image
  returns 409.
* **Turning on SSH** inside a Sailbox for the first time. After that the rest
  is HTTPS.

Everything else, from the whole lifecycle to ports, custom domains, secrets,
policies, metrics, and spend, is available over HTTPS. Volumes are in alpha,
so those endpoints can still change.

## Troubleshooting

Failures come back as an HTTP status and one JSON shape:

```json theme={null}
{
  "error": {
    "message": "app_id is required",
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}
```

Match on the status and `type`; `message` is for people and can change.

| Status        | `type`                  | What to do                                                                                                   |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ |
| 400           | `invalid_request_error` | Fix the field it names. Custom-domain registration is the exception: it answers 400 until your DNS resolves. |
| 401           | `authentication_error`  | The key is missing or invalid.                                                                               |
| 402           | `billing_error`         | Add credits, then retry.                                                                                     |
| 403           | `permission_error`      | The key cannot do this, or the Sailbox is private and someone else's.                                        |
| 404           | `not_found_error`       | No such Sailbox, volume, or checkpoint in your organization.                                                 |
| 409           | `conflict_error`        | Conflicts with current state. Some clear on their own, such as a volume a terminating Sailbox still mounts.  |
| 413           | `invalid_request_error` | The body is too large. Most endpoints cap at 64 KiB; `POST /sailboxes` and `POST /apps/find` allow 256 MiB.  |
| 429           | `rate_limit_error`      | With `Retry-After`, the request never ran: wait and retry. Without it, the request ran and hit a limit.      |
| 500, 503, 504 | `server_error`          | Retry with backoff. A proxy can also return a 502 or 504 with no body; treat those the same.                 |

## Retrying safely

Every `POST` that creates or changes a Sailbox, a listener, or a volume takes an
`Idempotency-Key` header. Generate one key per logical operation, any unique
string up to 255 bytes, and send it on the first attempt and every retry. A
retry with the same key, method, path, and byte-identical body gets the first
response back, marked `Idempotent-Replayed: true`, instead of running again.

<Accordion title="The details">
  * A key is remembered for at least 24 hours and is scoped to the API key that
    sent it.
  * Sail remembers 400 and 409 answers too, so after fixing a request send it
    under a fresh key. Reusing a key for a different request returns 409.
  * If the original is still running when the retry arrives, the retry waits
    for it. After 30 seconds it gets a 504; retry again with the same key.
  * A 500, 503, or 504 usually means nothing happened and the same key runs the
    request again. Creating a Sailbox is the case to watch: the error can arrive
    after the Sailbox exists, and a retry can leave you with two. List your
    Sailboxes, then continue under a fresh key.
  * Terminating a terminated Sailbox, creating a volume that already exists, and
    registering a domain the same way twice are safe without a key. Domain
    registration ignores the header.
</Accordion>

<Accordion title="Listing, resume results, and unknown fields">
  * `GET /sailboxes` pages with `limit` (up to 100) and `offset`; stop when
    `has_more` is false. `app`, `status`, and `search` filter, and
    `manageable_by_caller=true` hides private Sailboxes you cannot operate.
  * Resume returns 200 either way and reports `resume_state`: `running`,
    `already_running`, or `terminal_unavailable`, in which case `error_message`
    says why and you should create a new Sailbox.
  * `status` and `resume_state` are open sets and responses grow new fields.
    Match the values you care about and ignore the rest.
  * For its first ten minutes a new organization is capped on requests in
    flight; over it you get a 429 with `Retry-After`.
</Accordion>
