# Migrate from boat to Sandboxer

> 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.

Source: https://sandboxer.com/migrate/boat. Updated 2026-10-27.

**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](/docs/environments)) |
| Automatic snapshots, stop and resume | Automatic snapshots, stop and resume ([Snapshots](/docs/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](/docs/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](/docs/organizations)) |
| Regions: Germany, Finland and France ([boat FAQ](https://docs.boat.dev/faq)) | Two regions, `us-east` and `eu-central` ([regions](/docs/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](/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](/docs/sdk-python)).

## Before and after

From boat's docs: [https://docs.boat.dev/quickstart](https://docs.boat.dev/quickstart) (the curl tab, checked on 2026-10-27; the page sets `BOAT_API_BASE=https://boat.dev/api/v1`):

```bash
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:

```bash
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"}'
```

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

```bash
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:

```bash
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:

```bash
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 PATH
```

Use 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`:

```bash
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 --yes
```

An 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](https://docs.boat.dev/snapshots/download-and-delete)). Then upload it to Sandboxer:

```bash
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 project
```

For 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:

```bash
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 v1
```

On 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](/docs/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](/docs/webhooks)).

## What's different

What you gain:

- **Resume restores memory.** On Sandboxer, `sbx resume` brings back running processes with the same PIDs ([Snapshots](/docs/snapshots#resume)). 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 shows `memoryRestored: false`. Pass `--require-memory` to 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](https://docs.boat.dev/snapshots/whats-saved)).
- **Extra CLI commands**: `sbx handoff`, `sbx mcp`, `sbx idle` and `sbx 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](/docs/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](/docs/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](/docs/billing#storage)).
- **Start bursts.** boat lets you pay for starts above your plan's start limits, up to 3x. Sandboxer returns `429` at the limit ([Limits](/docs/limits)).
- **Hard-coded names.** Any code that checks for `bx_` ids, `on.boat.dev` URLs, `BOAT_*` variables or `boat_*` 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](/pricing). boat's prices are from its [pricing page](https://docs.boat.dev/pricing) and [free trial page](https://docs.boat.dev/free-trial), 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.

## Related

- [Quickstart](/docs/quickstart)
- [API reference](/docs/api)
- [CLI reference](/docs/cli)
- [Pause and resume a cloud sandbox with its memory](/guides/pause-and-resume-a-sandbox-with-memory)
