Skip to main content
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; this page covers what applies to all of them.
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; a key belongs to one organization and only ever sees that organization’s Sailboxes.
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.

Example

Every Sailbox belongs to an app. Get an app id, then create a Sailbox in it:
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.
  • 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.
Terminate it when you are done:

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.
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:
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.
  • 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.
  • PUT .../clipboard places the raw request body on the Sailbox’s clipboard, with Content-Type as its media type: text/plain or an image/* type. Parameters such as charset are ignored, so send text as UTF-8. The body, its media type, the Sailbox id, and a few bytes of framing together fit in 4 MiB, so a body only a little under 4 MiB may not. A write does not wake the Sailbox. A paused Sailbox answers 409, so resume it first. One that is asleep, mid-transition, or still starting its clipboard answers 503, so retry with backoff. A Sailbox whose image has no clipboard answers 501.

Move files

Files stream in both directions without being buffered whole.
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.

Connect to a port without publishing it

Publishing a port with a listener gives it a public hostname that any client can reach, subject to the access controls you set. A tunnel is for the other case: a server that should stay private to you, such as a database, a debugger, or a web app you are still working on. Nothing is published. Your own connection is carried into the Sailbox over this API, with the same key and TLS as every other call, and it ends when you close it. Each tunnel carries one TCP connection, so the natural use is a local port forwarder, the way ssh -L works: listen on a port on your machine and, for each connection you accept, open a tunnel and copy bytes both ways. Then psql -h localhost or a browser at http://localhost:3000 reaches the Sailbox. This is how the interactive shell in the SDKs and the CLI forwards the Sailbox’s local servers and browser opens to your machine. To open a tunnel, send GET .../tunnel?port=N with the standard WebSocket upgrade headers. The program must be listening on 127.0.0.1 or on all interfaces. Sail authenticates the request, wakes a sleeping Sailbox, and connects to the port before answering 101, so those failures arrive as ordinary JSON errors. A paused Sailbox answers 409, so resume it first. A port with nothing listening is a 503 with code unavailable and the connection error as its message. Once the WebSocket is open, a failure ends it with close code 1011 and a reason of the form code: detail. Close code 1000 means the program in the Sailbox closed the connection. A Sailbox whose guest_schema_version is below 231 connects to the port only after answering 101, so a port with nothing listening there closes with code 1011 and that same unavailable reason. Upgrading the Sailbox changes that for every tunnel opened once the upgraded Sailbox is running.
  • Binary messages carry bytes in either direction. Messages you send are limited to 1 MiB. A larger one ends the tunnel with close code 1009.
  • The text message {"type":"eof"} closes the sender’s direction while the peer can keep sending, the way a TCP half-close does. A close frame ends the tunnel.
  • Sail pings every 30 seconds, so a quiet tunnel stays open through idle proxies, and drops a connection that does not answer within 10 seconds.
  • A tunnel ends with close code 1011 if the Sailbox moves between machines; reconnect when that happens.

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:
Match on the status and type; message is for people and can change.

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