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

# HTTP API

> Create and operate Sailboxes over plain HTTP

Sailboxes have a public HTTP API. The SDKs and the CLI call the same one.
Reach for it directly when you work in a language Sail does not publish an SDK
for, or when installing an SDK is awkward.

Every published endpoint is listed under
[Reference → Sailbox → HTTP API](/api-reference/lifecycle/create-a-sailbox). This page
covers the parts that apply to all of them.

## Base URL

```
https://sailbox-api.sailresearch.com/v1
```

The apps endpoints use a different base URL:

```
https://api.sailresearch.com/v1
```

Your API key works on both.

## Authentication

Send your API key as a bearer token on every request.

```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" }
```

Create keys in the [dashboard](https://app.sailresearch.com). A key belongs to
one organization, and it only ever sees that organization's Sailboxes.

`user_id` tells you which member of the organization the key belongs to. Compare
it against a Sailbox's `created_by_user_id` to tell your own Sailboxes apart from
a teammate's. Service keys have no user, so `user_id` is `null`.

A Sailbox created with `"visibility": "private"` can only be operated by the user
whose key created it. An organization admin can still pause, resume, sleep,
schedule a wake, upgrade, or terminate one by sending an
`X-Sail-Owner-Override-Reason` header saying why. The reason goes into your
organization's audit log, and the operation is refused without it. Publishing and
unpublishing ports, forking, checkpointing, and creating from a checkpoint stay
with the creator.

## What this API covers

Over HTTP you can create Sailboxes, run their whole lifecycle, publish and
unpublish ports, serve them on hostnames you own, manage volumes, and read
status, metrics, and spend.

Three things need an SDK or the CLI:

* **Running commands and moving files.** This API has no endpoint for running a
  command or transferring a file. Use an SDK or the CLI, or turn on SSH and use
  `ssh` and `scp`.
* **Turning on SSH.** SSH has to be enabled inside the Sailbox once, and that
  step needs an SDK or the CLI. After that the rest is HTTP: issue certificates
  and publish port 22 for as long as the Sailbox lives. See
  [Networking](/sailboxes-networking).
* **Building an image.** Creating a Sailbox needs an image that is ready to
  boot: a base image on its own, or one built earlier from the same `image`
  block. Building an image with your own packages, environment variables, or
  Python version takes an SDK or the CLI, and asking for one that has never been
  built returns 409. Once the image is built, you can create Sailboxes from it
  over HTTP as often as you like.

Everything else is available over HTTP. Volumes are in Alpha, so those
endpoints can still change.

## Create your first Sailbox

Export your key so the commands below run as written.

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

Every Sailbox belongs to an app, which groups the Sailboxes that belong to one
workload. Start by getting an app id.

```bash theme={null}
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
}
```

Use the `id` you get back in the requests below. `image` says what the Sailbox
boots into. A plain Debian base needs nothing built first, which is what this
example uses.

```bash theme={null}
KEY="create-worker-1-attempt-1"

curl -X POST https://sailbox-api.sailresearch.com/v1/sailboxes \
  -H "Authorization: Bearer $SAIL_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -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" }
```

The `Idempotency-Key` is what makes a create safe to retry. Run the same
command again while `$KEY` still holds the same value and the retry returns the
first attempt's answer instead of creating a second Sailbox. Any unique string
works.

Take a fresh `$KEY` for every new Sailbox you mean to create. A create that
comes back 500, 503, or 504, or that never comes back at all, is the one case
that needs a closer look, because the Sailbox can exist even though the call
failed. See [Retrying safely](#retrying-safely).

This request blocks until startup finishes, so set a generous client timeout. A
create can take a few minutes, because it waits for a machine to run on.

Read `status` before you use the Sailbox. A create that is accepted and then
cannot bring the machine up still returns 200, with `status` set to `failed` and
`error_message` giving the reason. A client that checks only the HTTP status will
think it has a Sailbox it does not have.

When you are finished, terminate the Sailbox so it stops costing money. Use the
`sailbox_id` you got back from the create:

```bash theme={null}
curl -X POST https://sailbox-api.sailresearch.com/v1/sailboxes/sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d/terminate \
  -H "Authorization: Bearer $SAIL_API_KEY"
```

To reach a service running inside a Sailbox you publish a port, and something
has to be listening on it inside the Sailbox. Starting that process takes an
SDK or the CLI. [Networking](/sailboxes-networking) covers publishing ports and
restricting who can reach them.

## Errors

Failures come back with an HTTP status and a JSON body in one shape:

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

| Status        | `type`                  | What it means                                                                                                                                            |
| ------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400           | `invalid_request_error` | A field is missing, malformed, or out of range. Fix the request.                                                                                         |
| 401           | `authentication_error`  | The API key is missing or invalid.                                                                                                                       |
| 402           | `billing_error`         | The organization is out of credits. Retrying only helps once you add credits.                                                                            |
| 403           | `permission_error`      | The key cannot perform this operation, or the Sailbox is private and belongs to someone else.                                                            |
| 404           | `not_found_error`       | No such Sailbox, volume, or checkpoint in your organization.                                                                                             |
| 409           | `conflict_error`        | The request conflicts with current state. Deleting a volume a Sailbox still mounts, or reusing an `Idempotency-Key` for a different request, lands here. |
| 413           | `invalid_request_error` | The request body is too large.                                                                                                                           |
| 429           | `rate_limit_error`      | Too many requests in flight, or the organization is at a limit. Whether `Retry-After` is present tells you if the request ran.                           |
| 500, 503, 504 | `server_error`          | The operation failed or outran its deadline before it finished. Retry with backoff.                                                                      |

A URL that is not an endpoint returns 404, and an endpoint called with the wrong
method returns 405. Both use `invalid_request_error`.

Retry 500, 503, and 504 with exponential backoff. Send an `Idempotency-Key` on
every `POST` that creates or changes a Sailbox, a listener, or a volume, on the
first attempt and on every retry, so a retry after a timeout replays the first
answer instead of running the operation again. A key does not cover every case: a
server error, or a failure to record the answer, can leave the retry to run the
request again. Creating a Sailbox is where that matters. See
[Retrying safely](#retrying-safely).

The `Retry-After` header tells the two 429s apart. With one, the request never
started: wait that many seconds and retry, and the same `Idempotency-Key` is
fine. Without one, the request ran and hit the limit `message` names.

Some of those limits clear on their own, such as a shortage of public ports.
Others need you to free something up first. Once the cause is gone, send the
request again under a fresh `Idempotency-Key`, because the old key now replays
this answer.

Do not resend a 400, 401, 402, 403, 404, or 413 unchanged, because the same
request fails the same way. Fix it first. Registering a custom domain is the one
exception: it answers 400 until your DNS record resolves, and that request is
worth sending again once it does, as long as your DNS provider is not proxying
the record. A 409 is usually the same, but some
conflicts clear on their own: a volume you could not delete becomes deletable
once the Sailbox mounting it terminates, and an operation refused because another
one is already running on that Sailbox succeeds once that one finishes. `message`
tells you which you have.

A proxy between you and Sail can also return a 502 or a 504 with no JSON body at
all. Treat those the same way: retry with backoff.

Match on the HTTP status and on `type`. Treat `message` as text for humans: it is
written to be readable and can change.

## Retrying safely

Every `POST` that creates or changes a Sailbox, a listener, or a volume accepts
an `Idempotency-Key` header, and it is the safest way to handle an ambiguous
failure such as a timeout or a 502. A retry that sends the same key and the same
body gets the first request's response instead of starting a second operation.

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

Generate one key per logical operation, use any unique string up to 255 bytes,
and send it on the first attempt and every retry. A blank key is ignored, and
the request runs without idempotency.

* A replayed response carries `Idempotent-Replayed: true`.
* If the original request is still running when the retry arrives, the retry
  waits for it and returns its result. If the original is still going after 30
  seconds, the retry gets a 504 and you can retry again with the same key.

Sail remembers the answer it sent under a key, a 400 or a 409 included, and
replays it for a retry that repeats the key, the method, the path, and the body.
Changing any of those needs a new key: the old one returns 409 for a request it
has not seen. Bodies are compared byte for byte, so even reformatted JSON counts
as a different request. If a create comes back 400, fix the field it names and
send the corrected request under a fresh key.

Recording the answer can itself fail. The answer still reaches you, but a retry
under that key runs the request again instead of replaying it. A create whose
answer never reached you therefore needs the same care as a server error.

Server errors are the exception. 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, because the error can arrive once the Sailbox already exists. Some of
those errors are remembered, so the retry replays the error and does nothing;
others run again and leave you with a second Sailbox. List your Sailboxes to see
what you have, then continue under a fresh key.

A key is remembered for at least 24 hours and is scoped to the API key that sent
it. A retry from a different API key runs the request again, and so does a retry
long enough after the original that the key has been forgotten.

Some operations are already safe to repeat without a key. Terminating a
terminated Sailbox succeeds, creating a volume whose name already exists returns
the existing volume, and registering a custom domain that is already registered
the same way returns it.

Registering a custom domain takes no `Idempotency-Key`. A key sent there is
ignored, so retry it on its own terms.

## Listing and pagination

`GET /sailboxes` returns a page at a time.

```bash theme={null}
curl "https://sailbox-api.sailresearch.com/v1/sailboxes?limit=50&offset=50&status=running" \
  -H "Authorization: Bearer $SAIL_API_KEY"
```

```json theme={null}
{
  "data": [],
  "limit": 50,
  "offset": 50,
  "total": 128,
  "has_more": true
}
```

Page with `limit` and `offset`, and stop when `has_more` is `false`. `limit` goes
up to 100.

Filters combine: `app`, `status`, and `search` narrow the list, and
`manageable_by_caller=true` hides private Sailboxes someone else created,
whether or not you could operate them with an override.

## Checking a resume result

Resume returns 200 whether or not the Sailbox came back, and reports what
happened in `resume_state`:

* `running`: the Sailbox is ready now.
* `already_running`: it never stopped, so there was nothing to do.
* `terminal_unavailable`: it can never resume, and `error_message` says why.
  Create a new Sailbox instead of retrying.

Read the field before you use the Sailbox. A client that checks only the HTTP
status will treat the last case as a success.

## Handling fields you do not recognize

Both `status` and `resume_state` are open sets. New values get added as the
service grows, and a client that treats an unfamiliar value as an error breaks
the first time it sees one. Match the values you care about, and pass anything
else through untouched.

Responses also grow new fields over time. Ignore fields you do not recognize
rather than rejecting the response.

## Limits

Most endpoints that take a request body cap it at 64 KiB. `POST /sailboxes`
accepts up to 256 MiB, which is what lets a large image definition through.
`POST /apps/find` shares the larger cap.

For its first ten minutes, a new organization is also capped on how many
requests it can have in flight at once. The cap counts every request from the
organization, not just yours. Over it you get a 429 with a `Retry-After`
header, and it clears as those requests finish.
