Skip to content

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 its sprites.app URL.
  • 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:

Terminal window
export SPRITES_TOKEN=your-token

The 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

TaskNode clientHTTP / WSS
Create, list, get, update, delete SpritescreateSprite, listSprites, getSprite, updateSprite, deleteSpritePOST/GET/PUT/DELETE /v1/sprites...
Run a one-off commandsprite.execFile(cmd, args)POST /v1/sprites/{name}/exec
Start a long-running or interactive sessionsprite.createSession(...) or sprite.spawn(...)WSS /v1/sprites/{name}/exec
Reattach to an existing sessionsprite.listSessions() + sprite.spawn('', [], { sessionId, tty: true })GET /exec then WSS /exec/{session_id}
Kill an exec sessionno documented helper in the current Node examplesPOST /exec/{session_id}/kill
Open a TCP tunnel to a port inside the Spriteuse openProxy() from @arbe/sandboxWSS /v1/sprites/{name}/proxy
Read or update outbound network policygetNetworkPolicy, updateNetworkPolicyGET/POST /policy/network
Create or restore checkpointscreateCheckpoint, restoreCheckpointcheckpoint 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 checks
  • client.createSprite(name) and client.deleteSprite(name) for sandbox lifecycle
  • client.sprite(name).execFile(...) for non-interactive remote commands (CLI, via the Node SDK)
  • POST /v1/sprites/{name}/exec for non-interactive remote commands (www dispatch, via @arbe/sandbox/http)
  • wss://api.sprites.dev/v1/sprites/{name}/proxy via openProxy() for tunneling to opencode running inside the Sprite

Relevant code:

Lifecycle

Create

HTTP:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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}/exec

Key query parameters from the reference:

  • cmd: repeated for command and args
  • tty: TTY mode
  • stdin: whether stdin is enabled
  • cols and rows: initial terminal size
  • max_run_after_disconnect: how long the process may continue after disconnect
  • env: repeated KEY=VALUE
  • id: 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:

  1. GET /v1/sprites/{name}/exec
  2. WSS /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:

Terminal window
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}/proxy

Handshake:

{ "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:

Terminal window
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:

Terminal window
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.app is 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.

Sources