Migrate from boat to Sandboxer
Updated October 27, 2026 .md
Short answer: Sandboxer's API at https://sandboxer.com/api/v1 is wire-compatible with boat's /api/v1, so most code moves by changing the base URL and the API key. Sandbox ids, hosted URLs, reserved env names and the CLI name change. boat's org and confirm-delete headers still work. Install sbx and link boat to it for scripts.
Concept map#
| boat | Sandboxer |
|---|---|
| Sandbox (a full Linux VM) | Sandbox (a full Linux VM) |
Sizes small, default, large, xlarge |
The same four names, with the same vCPU, RAM and disk |
| Environment (repos, secrets, credentials, versions) | Environment: sbx env, the same model (Environments) |
| Automatic snapshots, stop and resume | Automatic snapshots, stop and resume (Snapshots) |
Saved snapshot (called a named snapshot in its older docs), --from |
Named snapshot (template), --from |
| Hosted URL per port | Hosted URL per port (Hosting) |
| API key, per-sandbox key | API key (sbx_live_…), per-sandbox key (SANDBOXER_TOKEN) |
boat-sdk, @boatdev/sdk |
sandboxer-sdk on PyPI and npm |
| Organization wallet | Organization wallet (Organizations) |
| Regions: Germany, Finland and France (boat FAQ) | Two regions, us-east and eu-central (regions) |
API and SDK mapping#
The paths, request bodies and response envelopes are the same. Every operationId in boat's v1 OpenAPI file as we mirrored it on 2026-10-11 (84 operations) also exists in ours, at /openapi.yaml, so SDK method names carry over. boat has added operations since. Its live file, checked on 2026-10-27, lists 34 that Sandboxer doesn't have: file browsing and search (listFiles, searchFiles, moveFile), saved snapshots (saveSavedSnapshot and related), credential profiles, an AI proxy, unshare and updateOrganization. If you call those, they have no Sandboxer equivalent yet.
These are the names that change. We tested the rows marked "tested" on prod and checked the rest in our code.
| boat | Sandboxer | Notes |
|---|---|---|
https://boat.dev/api/v1 |
https://sandboxer.com/api/v1 |
https://api.sandboxer.com/v1 is an alias |
Header X-Boat-Org |
X-Sandboxer-Org |
X-Boat-Org is still accepted (tested) |
Sandbox id bx_… |
sb_ + 10 characters [a-z0-9] |
Update any regex that checks ids |
| API key secret | sbx_live_ + 40 characters; per-sandbox keys sbx_sbk_… |
|
https://<slug>-<port>.on.boat.dev |
https://<slug>-<port>.box.sandboxer.com |
|
Reserved env BOAT_ID, ASCII_TOKEN, … |
SANDBOXER_ID, SANDBOXER_TOKEN, SANDBOXER_API_URL, SANDBOXER_SUBDOMAIN |
Set in every sandbox (tested) |
boat CLI |
sbx CLI |
Same subcommands and flags (see below) |
boat-sdk / @boatdev/sdk |
sandboxer-sdk (imports as sandboxer in Python) |
Constructor changes, method names don't |
Header X-Ascii-Confirm-Delete |
X-Sandboxer-Confirm-Delete |
boat's name is still accepted (tested) |
Webhook headers X-Ascii-Event, -Delivery, -Timestamp, -Signature, -Attempt |
X-Sandboxer-* |
Sent under both names, so a boat receiver should verify unchanged (checked in code) |
Error codes boat_starting, boat_direct_failed |
sandbox_starting, sandbox_direct_failed |
Update code that branches on them |
Process logs in ~/.ascii/processes/ |
~/.sandboxer/processes/ |
|
CLI env BOAT_API_KEY, BOAT_API_URL, BOAT_ORG |
SANDBOXER_API_KEY, SANDBOXER_API_URL, SANDBOXER_ORG |
sbx does not read the BOAT_ names |
SDK calls map one to one. In TypeScript, new BoatApi(new Configuration({ basePath, accessToken })) becomes new Sandboxer({ baseUrl, apiKey }), and the low-level calls move to sandboxer.api: create, command, hostPort, resume and the rest. We ran sandboxer.api.sandboxes() with sandboxer-sdk 0.1.1 against prod. In Python, client.api has the same methods in snake_case (Python SDK).
Before and after#
From boat's docs: https://docs.boat.dev/quickstart (the curl tab, checked on 2026-10-27; the page sets BOAT_API_BASE=https://boat.dev/api/v1):
id=$(curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttlSeconds":3600}' | jq -r .sandbox.id)
curl -sS -X POST "$BOAT_API_BASE/sandboxes/$id/commands" \
-H "Authorization: Bearer $BOAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command":"echo hello"}'The same call on Sandboxer. We added type and name, which are optional. Only the host and the key change:
curl -sS -X POST "https://sandboxer.com/api/v1/sandboxes" \
-H "Authorization: Bearer $SANDBOXER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ttlSeconds":3600,"type":"small","name":"aeo-migrate-boat-1"}'{"ok":true,"type":"sandbox.created","sandbox":{"id":"sb_n0b34c8zbq","name":"aeo-migrate-boat-1","state":"provisioning","type":"small","vcpu":2,"memoryGB":4,"diskGB":12,"region":"eu-central","subdomain":"dense-easy-citrus","ttlSeconds":3600,…},"status":"provisioning","ttlSeconds":3600}The status was 202. Poll GET /sandboxes/{id} until state is ready, then run a command, write a file and host a port. The calls are the same as on boat:
B=https://sandboxer.com/api/v1/sandboxes/sb_n0b34c8zbq
H="Authorization: Bearer $SANDBOXER_API_KEY"
curl -sS -X POST "$B/commands" -H "$H" -H "Content-Type: application/json" \
-d '{"command":"echo $SANDBOXER_ID; uname -sr","timeoutSeconds":30}'
# {"ok":true,"type":"command.finished","exitCode":0,"stdout":"sb_n0b34c8zbq\nLinux 6.18.56-sandboxer\n",…}
curl -sS -X PUT "$B/files" -H "$H" -H "Content-Type: application/json" \
-d '{"path":"site/index.html","content":"hello from sandboxer"}'
# {"ok":true,"type":"file.written","encoding":"utf8","path":"/home/user/site/index.html","size":20,"success":true}
curl -sS -X POST "$B/commands" -H "$H" -H "Content-Type: application/json" \
-d '{"command":"cd site && python3 -m http.server 3000","detached":true}'
curl -sS -X POST "$B/host" -H "$H" -H "Content-Type: application/json" \
-d '{"port":3000,"public":true}'
# {"ok":true,"type":"port.hosted","access":"public","isProtected":false,"localhost":false,"port":3000,"success":true,"title":null,"url":"https://dense-easy-citrus-3000.box.sandboxer.com"}curl https://dense-easy-citrus-3000.box.sandboxer.com/ returned hello from sandboxer. A delete with boat's confirm header also worked:
curl -sS -X DELETE "$B" -H "$H" -H "X-Ascii-Confirm-Delete: sb_n0b34c8zbq"
# {"ok":true,"type":"sandbox.deleting","operation":{"id":"bdop_089b…","kind":"sandbox","status":"pending",…}}The CLI#
sbx has every top-level command in boat's CLI reference as we mirrored it on 2026-10-11, and the new and exec flags we compared match. sbx completions works too, as an alias of sbx completion. boat's live reference, checked on 2026-10-27, adds boat run, boat push, boat ai, boat wallet and boat feedback. sbx doesn't have those. Install sbx, sign in, and point the boat name at it:
curl -fsSL https://sandboxer.com/install | sh # installs sbx, then runs sbx onboard
ln -s "$(command -v sbx)" ~/.local/bin/boat # any directory on your PATHUse a symlink, not just alias boat=sbx. Bash doesn't expand aliases in scripts: our test script failed with boat: command not found (exit 127). The alias is fine in an interactive shell. We ran these on prod with boat as a symlink to sbx on the PATH:
boat new --type small --name aeo-migrate-boat-4 --ttl 1800 --setup-file ./setup.sh # {"event":"ready","id":"sb_ov9gp6dwsb",…}
boat exec sb_ov9gp6dwsb --cwd /tmp --timeout 60 -- pwd # /tmp
boat scp ./proj.tgz sb_ov9gp6dwsb:/home/user/ # exit 0
boat host sb_ov9gp6dwsb 8080 # https://sterling-frugal-walrus-8080.box.sandboxer.com?_token=…
boat stop sb_ov9gp6dwsb # {"type":"sandbox.stopping",…} at once; archiving, then archived about 3 s later
boat resume sb_ov9gp6dwsb -e APP_MODE=staging # {"event":"ready",…}
boat new --type small --name aeo-migrate-boat-5 --from aeo-migrate-boat-stack
boat exec current 'echo $SANDBOXER_ID' # sb_hb7nvdujp1: current = the last sandbox this shell created
boat delete current --yesAn earlier run of the same commands through shopt -s expand_aliases; alias boat=sbx gave the same results.
sbx also has commands with no boat equivalent: handoff, mcp, idle, proxy and ssh-config. In CI, sign in with sbx login --key-stdin <<< "$SANDBOXER_API_KEY".
Move your data and environment#
Files. Sandboxes don't move between providers. Copy the files you need. On boat, download a snapshot with boat snapshot pull <snapshotId> -o ./restore (boat docs). Then upload it to Sandboxer:
tar czf proj.tgz proj
sbx scp ./proj.tgz sb_o7a8ua4fwk:/home/user/
sbx exec sb_o7a8ua4fwk "tar xzf proj.tgz && python3 proj/main.py" # hi from my projectFor a whole directory, use sbx scp -r. The files API (PUT /files) takes files up to 5 MiB.
Templates. Saved or named snapshots and share codes (bsh_…) don't carry across. Rebuild the stack once, using the same setup script if you have one, and save it under the same name. Your --from calls then keep working. The setup script runs in the background after the sandbox is ready, so wait for it to finish before you run sbx snapshot. Otherwise the template captures a half-installed stack:
sbx new --type small --setup-file ./setup.sh # ready while setupStatus is still "running"
until [ "$(sbx info sb_ov9gp6dwsb --json | jq -r .sandbox.setupStatus)" = done ]; do sleep 2; done
sbx snapshot sb_ov9gp6dwsb aeo-migrate-boat-stack # {"type":"snapshot.named.info",…,"status":"ready",…}
sbx new --type small --from aeo-migrate-boat-stack
sbx exec current "cat stack/VERSION" # stack v1On prod, setup finished about 8 seconds after ready. The sandbox made from the template already had the script's stack/VERSION and Python venv. sbx snapshot rm <name> deletes a template.
Env vars and secrets. Re-enter them. Use sbx env set-var base KEY=value and sbx env set-file base path/in/home --from ./local-file, or POST /secrets (Environments). Per-sandbox values still use --env KEY=VALUE or -e. After a resume with -e APP_MODE=staging, the sandbox printed APP_MODE=staging. Rename any code that reads BOAT_ID to SANDBOXER_ID.
Ports and preview URLs. sbx host <id> <port> gives https://<slug>-<port>.box.sandboxer.com. The URL is private with a _token by default; add --public to open it. For Vite, add .box.sandboxer.com to server.allowedHosts in place of boat's domain.
Webhooks and keys. Create new API keys with sbx api-key create and register webhooks again with sbx webhook create. Signature verification code written for boat's X-Ascii-Signature should keep working, because Sandboxer sends those headers too (checked in code, not with a live delivery; Webhooks).
What's different#
What you gain:
- Resume restores memory. On Sandboxer,
sbx resumebrings back running processes with the same PIDs (Snapshots). This needs a machine with the same CPU type as the one the sandbox stopped on. If none is free within 2 minutes, the resume boots from disk instead: files stay, processes don't, and the sandbox showsmemoryRestored: false. Pass--require-memoryto wait for memory instead. boat's docs list "running programs, memory and open ports" as not saved, and describe a restore as "like a reboot" (boat docs). - Extra CLI commands:
sbx handoff,sbx mcp,sbx idleandsbx ssh-config.
What you lose or must change:
- Regions. boat runs in Germany, Finland and France. Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); new sandboxes go to the nearest unless you set
region(regions). - Windows apps. boat documents running Windows programs through Wine. Our docs don't cover it.
- Integrated agents. boat lists seven harnesses. Sandboxer installs six (Claude Code, Codex, OpenCode, Pi, Kimi Code and Mistral Vibe), and Prime Agent is coming (Agents).
- Storage. boat's pricing page (checked 2026-10-27) says a stopped sandbox keeps its disk for 14 days, then boat deletes it. Each plan includes 10 saved snapshots per $20, and each one over that costs $0.056 a day. Sandboxer keeps a stopped sandbox until you delete it and doesn't bill storage. Instead, each plan includes an amount (50 GB on the $20 plan). At the cap, new sandboxes, forks and snapshot saves return
402 storage_limit_reached(Billing). - Start bursts. boat lets you pay for starts above your plan's start limits, up to 3x. Sandboxer returns
429at the limit (Limits). - Hard-coded names. Any code that checks for
bx_ids,on.boat.devURLs,BOAT_*variables orboat_*error codes must change. - GPUs. Neither side offers them. boat's machine page lists "GPU: None", and our docs list no GPU sizes.
Pricing#
Sandboxer prices are as of 2026-10-27, from /pricing. boat's prices are from its pricing page and free trial page, checked live on 2026-10-27. Both bill per second while a sandbox runs.
| boat | Sandboxer | |
|---|---|---|
small, 2 vCPU / 4 GB |
$0.018/hour | $0.018/hour |
default, 4 vCPU / 8 GB |
$0.036/hour | $0.036/hour |
large, 8 vCPU / 16 GB |
$0.072/hour | $0.072/hour |
| Plans | $20, $100, $500, $2000 a month, paid back as machine time | The same four plans, with the same concurrency (100 to 2,000) and start limits |
| Organizations | Plan price per seat | Plan price per seat |
| Trial | 7 days, card required, 25 hours, 2 at once, small and default, 2-hour auto-stop |
7 days, card required, 25 hours, 2 at once, small and default, auto-stop of at most 8 hours |
| Storage | Stopped disks kept 14 days; saved snapshots over the included count cost $0.056 a day | Not billed; capped per plan |
One default sandbox running 8 hours a day for 22 days costs 176 h × $0.036 = $6.34 on either. The differences are in storage and start bursts, covered above.
FAQ#
Do I have to rewrite my boat API client?#
No. Change the base URL to https://sandboxer.com/api/v1 and use a Sandboxer key. The paths, bodies and the ok/type envelope are the same. Then fix any code that hard-codes bx_ ids, on.boat.dev URLs or boat_* error codes.
Does alias boat=sbx work?#
In an interactive shell, yes. Bash doesn't expand aliases in scripts, so for CI and scripts symlink sbx as boat on your PATH. Then the same commands and flags work.
Can I move a running boat sandbox over directly?#
No. Download a snapshot from boat or tar the files you need, copy them in with sbx scp, and rebuild templates with a setup script and sbx snapshot.
Do my boat webhook receivers need changes?#
You need to register them again. Sandboxer signs deliveries the same way and sends boat's X-Ascii-* headers next to X-Sandboxer-*, so existing verification code should work. We checked this in the code, not with a live delivery.
Is the Python package called sandboxer?#
Install sandboxer-sdk from PyPI. It imports as sandboxer. The PyPI package named sandboxer is an unrelated project.