Skip to main content
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. This page covers the parts that apply to all of them.

Base URL

The apps endpoints use a different base URL:
Your API key works on both.

Authentication

Send your API key as a bearer token on every request.
Create keys in the dashboard. 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.
  • 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.
Every Sailbox belongs to an app, which groups the Sailboxes that belong to one workload. Start by getting an app id.
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.
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. 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:
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 covers publishing ports and restricting who can reach them.

Errors

Failures come back with an HTTP status and a JSON body in one shape:
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. 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.
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.
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.