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.
From inside a Sailbox
Code running inside a Sailbox can read that Sailbox, sleep it, schedule its wake, and manage its cron jobs without an API key. The SDKs do this for you: inside a Sailbox, a client built without an API key acts as the Sailbox itself. See guest identity. To call the API directly, send the Sailbox’s identity in two headers in place of the bearer token:X-Sail-Source-Sailbox-ID with the id from the
SAILBOX_ID environment variable, and X-Sail-Source-Sailbox-Auth with the
Sailbox’s auth token, which Sail sets in the Sailbox’s environment.
Authorization header is served with that
key’s authority, and the identity headers are ignored.
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. With an array, the program runs 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 include their byte offset, so an overlapping retry does not duplicate input. The response reportsaccepted_through. For interactive sessions, use the stdin stream. - 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, and up to 4 MiB after decoding. - 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. Clipboard writes do 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.
Stream standard input
The stdin stream carries writes to one exec over a single WebSocket, instead of onePUT .../stdin request per write. Use it for interactive sessions.
Start the exec with open_stdin or pty, then send
GET .../exec/$EXEC_ID/stdin/stream with the standard WebSocket upgrade
headers. A request refused before the upgrade gets the same JSON error a
PUT .../stdin would. A paused Sailbox answers 409, so resume it first. The
exec itself is checked on the first write: one that has ended, or that was
started without open_stdin or pty, gets a 409 reply.
Send each write as one binary message, then wait for its reply before you send
the next. A write sent before the previous reply ends the stream:
Offsets work the same way as on
PUT .../stdin: an overlapping retry does not
duplicate input. Each write gets one text message in reply:
{"accepted_through":N}when the write was applied. WhenNis lower than the end of your write, the Sailbox’s input buffer is full. Resend the rest from offsetNafter a short wait.{"status":S,"error":{...}}when the write failed.statusanderrorare the HTTP status and error body aPUT .../stdinwould get. Sail then closes the stream with close code 1011. A failed write may still have been applied, so after a retryable error, open a new stream and resend the same bytes at the same offset.
Reopening, pings, and limits
Reopening, pings, and limits
- A stream lasts at most 5 minutes. After that, the next write gets a
503with codeunavailableand nothing is written. Open a new stream and resend the same write. - If the Sailbox moves between machines, a later write on the stream fails with an error. Open a new stream and resend from the last accepted offset.
- Sail pings every 30 seconds and drops a stream that does not answer within 10 seconds. Keep reading the WebSocket while you are not writing, so your WebSocket library can answer the pings.
- A write carries at most 256 KiB of data. A larger message ends the stream
with close code 1009. A text message or a malformed write gets a
400reply and ends the stream.
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 contains
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.
Share a file by URL
Mint a URL that serves one file without an API key, then hand it to a browser,curl, or a CDN:
expires_at, an hour by
default and up to seven days, and serves whatever the file holds when it is
fetched. attachment: true serves it as a download instead of inline, and
immutable: true marks it cacheable for a year, for a path you never
overwrite. Fetch it with no header at all:
Content-Type comes from the file’s extension, Content-Length is its size,
ETag changes whenever the file is written or replaced, and Last-Modified
is its modification time.
- A byte range answers 206.
If-None-Matchanswers 304 while the file is unchanged. ARangesent withIf-Rangegets the whole file instead when the file has changed, so a resume either continues the same version or starts over.- An expired or altered URL answers 403. Expiry does not touch copies already downloaded or cached.
- A deleted file or a terminated Sailbox answers 404.
- A paused Sailbox answers 409 until it is resumed. A sleeping one is woken before the first byte.
Serving images through Cloudflare
Serving images through Cloudflare
Keep the API key and the mint call on your server; only the minted URL reaches
a browser or Cloudflare.A Worker that takes the minted URL as
?url= and returns a resized image:- The host check keeps the Worker from resizing arbitrary URLs, and the expiry check refuses an expired URL even when Cloudflare still holds a resized copy. Sail checks the signature on every fetch, so the Worker adds no authorization of its own and never holds your API key.
- Do not set a
cacheKeythat drops the query, or a cached image would be served to a request Sail would have refused. Cloudflare caches the resized image under the full source URL, signature included, and follows the source’s caching rules with a one hour minimum. Mint the source URL withimmutablefor a path you never overwrite so the resized copies stay cached. - Give the source URL a lifetime that covers how long the page will reference it. After it expires, the Worker answers 403.
- Cloudflare must be allowed to fetch from
sailbox-api.sailresearch.com: enable transformations on your zone and add that host under Images > Transformations > Sources.
fetch(src) without the cf.image
option and forward the client’s Range header, or point the client straight
at the minted URL.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 be 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 forwards 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. The interactive shell in the SDKs and the CLI uses tunnels this way
to forward 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 a failure in any of those steps
arrives as an ordinary JSON error. 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
- Data in either direction is sent as binary messages. 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 and drops a connection that does not answer within 10 seconds. The pings stop idle proxies from closing a tunnel that has no traffic.
- A tunnel ends with close code 1011 if the Sailbox moves between machines. Reconnect when that happens.
What needs an SDK
These tasks 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 a JSON body with the same structure every time: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 does not run
the request again. It gets the first response back, marked
Idempotent-Replayed: true.
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. Going over the cap returns a 429 with
Retry-After.