Sprites API
Programmatic Sprites usage for HTTP clients and Node code. This is the API-side companion to Sandbox containers, which is CLI-oriented.
Sprites expose three control surfaces:
- CLI:
sprite ... - HTTP / WebSocket API:
https://api.sprites.dev/v1 - JavaScript SDK:
@fly/sprites
The JavaScript SDK is Node-only. It is not browser-compatible as published today: the package declares node >=24 and imports Node builtins such as node:events and node:stream, so a browser-target bundle fails.
If you are writing code inside this repo, prefer @arbe/sandbox over importing @fly/sprites directly. @arbe/sandbox re-exports the SDK, resolves tokens from the places arbe supports, and adds openProxy() for local TCP tunnels. For code that must run in non-Node environments (Cloudflare Workers, browsers), import @arbe/sandbox/http instead — it provides execInSprite and syncSecretsToSandbox using only fetch and Web-standard APIs.
Mental model
- A Sprite is a persistent Linux sandbox with a durable filesystem.
- Idle Sprites sleep and wake on demand when you run a command or hit the Sprite URL.
- The control plane API lives at
api.sprites.dev. The Sprite’s own HTTP app lives at itssprites.appURL. - Commands run as exec sessions. One-off commands are easy HTTP POSTs. Long-running or interactive commands use WebSockets.
- TTY sessions can survive disconnects. You can list sessions, reattach later, and get scrollback.
- Checkpoints snapshot filesystem state. Network policy controls outbound access. Proxy tunnels local TCP into the Sprite.
Auth and endpoints
Arbe standardizes on SPRITES_TOKEN for the bearer token:
export SPRITES_TOKEN=your-tokenThe public Sprites examples sometimes use SPRITE_TOKEN and sometimes SPRITES_TOKEN. The token format is the same. Inside this repo, use SPRITES_TOKEN.
- REST base URL:
https://api.sprites.dev/v1 - WebSocket base URL:
wss://api.sprites.dev/v1 - Auth header:
Authorization: Bearer $SPRITES_TOKEN
Minimal HTTP helper:
const headers = { Authorization: `Bearer ${process.env.SPRITES_TOKEN!}`,}Minimal Node client:
import { SpritesClient } from '@fly/sprites'
const client = new SpritesClient(process.env.SPRITES_TOKEN!)Which surface to use
| Task | Node client | HTTP / WSS |
|---|---|---|
| Create, list, get, update, delete Sprites | createSprite, listSprites, getSprite, updateSprite, deleteSprite | POST/GET/PUT/DELETE /v1/sprites... |
| Run a one-off command | sprite.execFile(cmd, args) | POST /v1/sprites/{name}/exec |
| Start a long-running or interactive session | sprite.createSession(...) or sprite.spawn(...) | WSS /v1/sprites/{name}/exec |
| Reattach to an existing session | sprite.listSessions() + sprite.spawn('', [], { sessionId, tty: true }) | GET /exec then WSS /exec/{session_id} |
| Kill an exec session | no documented helper in the current Node examples | POST /exec/{session_id}/kill |
| Open a TCP tunnel to a port inside the Sprite | use openProxy() from @arbe/sandbox | WSS /v1/sprites/{name}/proxy |
| Read or update outbound network policy | getNetworkPolicy, updateNetworkPolicy | GET/POST /policy/network |
| Create or restore checkpoints | createCheckpoint, restoreCheckpoint | checkpoint endpoints returning NDJSON |
The kill-session row is based on the current Sprites Node examples, which show raw fetch() against the HTTP endpoint rather than an SDK helper.
What arbe uses
Today arbe uses a small subset of the full Sprites API:
client.getSprite(name)for reachability checksclient.createSprite(name)andclient.deleteSprite(name)for sandbox lifecycleclient.sprite(name).execFile(...)for non-interactive remote commands (CLI, via the Node SDK)POST /v1/sprites/{name}/execfor non-interactive remote commands (www dispatch, via@arbe/sandbox/http)wss://api.sprites.dev/v1/sprites/{name}/proxyviaopenProxy()for tunneling to opencode running inside the Sprite
Relevant code:
- packages/sandbox/src/sprite-client.ts — Node SDK wrapper, token resolution, TCP proxy
- packages/sandbox/src/sprites-http.ts — fetch-only exec and env sync for CF Workers
- packages/sandbox/src/sandbox.ts
Lifecycle
Create
HTTP:
curl -X POST "https://api.sprites.dev/v1/sprites" \ -H "Authorization: Bearer $SPRITES_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"my-sprite","url_settings":{"auth":"public"}}'Node:
import { SpritesClient } from '@fly/sprites'
const client = new SpritesClient(process.env.SPRITES_TOKEN!)await client.createSprite('my-sprite')The API docs show url_settings.auth as either "sprite" or "public". The response includes the Sprite URL plus runtime status such as "cold", "warm", or "running".
List and inspect
HTTP:
curl -H "Authorization: Bearer $SPRITES_TOKEN" \ "https://api.sprites.dev/v1/sprites"Node:
const sprites = await client.listSprites()const sprite = await client.getSprite('my-sprite')Use this when you need the Sprite URL, current status, or paging through an org’s Sprites.
Update
Updating is mainly for URL auth and labels.
HTTP:
curl -X PUT "https://api.sprites.dev/v1/sprites/my-sprite" \ -H "Authorization: Bearer $SPRITES_TOKEN" \ -H "Content-Type: application/json" \ -d '{"url_settings":{"auth":"public"}}'Node:
await client.updateSprite('my-sprite', { urlSettings: { auth: 'public' }, labels: ['prod'],})Delete
HTTP:
curl -X DELETE "https://api.sprites.dev/v1/sprites/my-sprite" \ -H "Authorization: Bearer $SPRITES_TOKEN"Node:
await client.deleteSprite('my-sprite')Deletion removes the Sprite and its associated state.
Running commands
Simplest path: non-interactive command
Use this when you want stdout, stderr, and exit status in one request.
Node:
const sprite = client.sprite('my-sprite')const result = await sprite.execFile('python', ['-c', 'print(2 + 2)'])
process.stdout.write(result.stdout)process.stderr.write(result.stderr)console.log(result.exitCode)HTTP:
curl -X POST \ "https://api.sprites.dev/v1/sprites/my-sprite/exec?cmd=python&cmd=-c&cmd=print(2%20%2B%202)" \ -H "Authorization: Bearer $SPRITES_TOKEN"The POST exec endpoint is non-TTY only. It accepts repeated cmd parameters, optional dir, optional repeated env=KEY=VALUE, and optional stdin from the request body. Despite the API docs listing application/json as the response type, the actual response is application/octet-stream using the same binary framing as the WebSocket path: each chunk is prefixed with a one-byte stream identifier (0x01 stdout, 0x02 stderr, 0x03 exit code). The exit frame is always last and carries a single byte — the process exit code. See parseExecResponse in sprites-http.ts for the parser.
Platform-agnostic (fetch-only):
import { execInSprite } from '@arbe/sandbox/http'
const result = await execInSprite(token, 'my-sprite', ['python', '-c', 'print(2 + 2)'])console.log(result.stdout, result.exitCode)Long-running and detachable sessions
Use a WebSocket-backed exec session when you need interactive IO, TTY behavior, or a process that survives disconnect.
Node:
const sprite = client.sprite('my-sprite')
const cmd = sprite.createSession('python', [ '-c', "import time; print('ready', flush=True); time.sleep(30)",])
cmd.stdout.on('data', (chunk: Buffer) => process.stdout.write(chunk))On the raw protocol, the WebSocket endpoint is:
wss://api.sprites.dev/v1/sprites/{name}/execKey query parameters from the reference:
cmd: repeated for command and argstty: TTY modestdin: whether stdin is enabledcolsandrows: initial terminal sizemax_run_after_disconnect: how long the process may continue after disconnectenv: repeatedKEY=VALUEid: attach to an existing session
The current reference says TTY sessions default to staying alive after disconnect, while non-TTY sessions default to 10s of post-disconnect runtime.
Reattach to an existing session
Node:
const sprite = client.sprite('my-sprite')const sessions = await sprite.listSessions()const target = sessions.find((session) => session.command.includes('python'))
if (target) { const cmd = sprite.spawn('', [], { sessionId: target.id, tty: true }) cmd.stdout.on('data', (chunk: Buffer) => process.stdout.write(chunk))}HTTP/WSS flow:
GET /v1/sprites/{name}/execWSS /v1/sprites/{name}/exec/{session_id}
The attach endpoint immediately sends scrollback, so you see output that happened while you were disconnected.
Kill a session
HTTP:
curl -X POST \ "https://api.sprites.dev/v1/sprites/my-sprite/exec/73/kill" \ -H "Authorization: Bearer $SPRITES_TOKEN"Node, following the current reference example:
const response = await fetch( 'https://api.sprites.dev/v1/sprites/my-sprite/exec/73/kill', { method: 'POST', headers: { Authorization: `Bearer ${process.env.SPRITES_TOKEN!}` }, },)The kill endpoint returns streaming NDJSON describing the signal, timeout, and exit state.
Proxying to ports inside a Sprite
This is the piece arbe relies on to reach services running inside a Sprite.
Raw endpoint:
wss://api.sprites.dev/v1/sprites/{name}/proxyHandshake:
{ "host": "localhost", "port": 8080 }After that initial JSON message, the socket becomes a raw TCP relay.
Inside this repo, you usually do not want to implement that yourself. Use openProxy() from @arbe/sandbox:
import { openProxy, requireSpritesClient } from '@arbe/sandbox'
const client = requireSpritesClient()const proxy = await openProxy(client, 'my-sprite', 'localhost', 8080)
console.log(proxy.url)await proxy.close()See packages/sandbox/src/sprite-client.ts for the local TCP-to-WebSocket bridge.
Network policy
Network policy controls outbound access from the Sprite. The policy model is DNS-based and supports allow, deny, wildcard domains, and preset includes.
Node:
const sprite = client.sprite('my-sprite')
await sprite.updateNetworkPolicy({ rules: [ { domain: 'api.github.com', action: 'allow' }, { domain: '*.npmjs.org', action: 'allow' }, { domain: '*', action: 'deny' }, ],})
const policy = await sprite.getNetworkPolicy()console.log(policy)HTTP:
curl -X POST "https://api.sprites.dev/v1/sprites/my-sprite/policy/network" \ -H "Authorization: Bearer $SPRITES_TOKEN" \ -H "Content-Type: application/json" \ -d '{"rules":[{"action":"allow","domain":"api.github.com"},{"action":"allow","domain":"*.npmjs.org"},{"action":"deny","domain":"*"}]}'The reference notes that policy changes apply immediately and can terminate existing connections to newly blocked domains.
The same API section also exposes privileges and resources policies when you need tighter runtime restrictions.
Checkpoints
Checkpoints are point-in-time filesystem snapshots. They are the right primitive for “before risky change” or “restore known-good state”.
Node:
const sprite = client.sprite('my-sprite')
const createStream = await sprite.createCheckpoint('before-upgrade')for await (const event of createStream) { console.log(event)}
const restoreStream = await sprite.restoreCheckpoint('v1')for await (const event of restoreStream) { console.log(event)}HTTP:
curl -X POST "https://api.sprites.dev/v1/sprites/my-sprite/checkpoint" \ -H "Authorization: Bearer $SPRITES_TOKEN" \ -H "Content-Type: application/json" \ -d '{"comment":"before-upgrade"}'Checkpoint creation and restore return NDJSON progress streams, not a single JSON object.
The higher-level Sprites guide describes checkpoints as filesystem snapshots you should take before risky changes. The API reference is more specific: create and restore are streamed operations with progress events.
Sprite URL versus API URL
Do not confuse these two:
https://api.sprites.dev/v1/...is the control plane for creating Sprites, running commands, checkpointing, and proxying.https://<name>-<suffix>.sprites.appis the Sprite’s own HTTP endpoint for whatever service is listening inside the Sprite.
When you start something like python -m http.server 8080, traffic to the Sprite URL is what wakes the Sprite and reaches that service.