curl. Every endpoint is
listed under Reference → Sailbox → HTTP API;
this page covers what applies to all of them.
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:- 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, withstatusset tofailedanderror_messagesaying why. - The
Idempotency-Keymakes 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.
Run commands on a Sailbox
Sendcommand as a shell string or an argument array. A string supports
cwd and background; an array runs the program directly.
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.
Standard input, clipboard, reconnects, and limits
Standard input, clipboard, reconnects, and limits
- To write standard input, set
open_stdinorptywhen you start the exec, thenPUT .../exec/$EXEC_ID/stdin?offset=N&eof=truewith the raw bytes. Writes carry their byte offset, so an overlapping retry does not duplicate input; the response reportsaccepted_through. - To reconnect to a live exec, send the same
idempotency_keywith the highestseqyou 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 theexec_request_idquery 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 .../clipboardplaces the raw request body on the Sailbox’s clipboard, withContent-Typeas its media type:text/plainor animage/*type. Parameters such ascharsetare 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 wayssh -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.
Messages, half-close, and limits
Messages, half-close, and limits
- 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
imageblock. Asking for an unbuilt image returns 409. - Turning on SSH inside a Sailbox for the first time. After that the rest is HTTPS.
Troubleshooting
Failures come back as an HTTP status and one JSON shape:type; message is for people and can change.
Retrying safely
EveryPOST 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.
The details
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.
Listing, resume results, and unknown fields
Listing, resume results, and unknown fields
GET /sailboxespages withlimit(up to 100) andoffset; stop whenhas_moreis false.app,status, andsearchfilter, andmanageable_by_caller=truehides private Sailboxes you cannot operate.- Resume returns 200 either way and reports
resume_state:running,already_running, orterminal_unavailable, in which caseerror_messagesays why and you should create a new Sailbox. statusandresume_stateare 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.