# Migrate from Daytona to Sandboxer

> For the covered calls, change the API URL and key. Set DAYTONA_API_URL to https://sandboxer.com/daytona/api and DAYTONA_API_KEY to a Sandboxer API key, and keep your Daytona TypeScript or Python code. Lifecycle, files, processes, sessions and previews work. Images, volumes, GPUs, git and PTY return not_supported.

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

**Short answer:** For the covered calls, change the API URL and key. Set DAYTONA_API_URL to https://sandboxer.com/daytona/api and DAYTONA_API_KEY to a Sandboxer API key, and keep your Daytona TypeScript or Python code. Lifecycle, files, processes, sessions and previews work. Images, volumes, GPUs, git and PTY return not_supported.

Sandboxer runs a Daytona-compatible API next to its own. It has two base URLs: the control API at `https://sandboxer.com/daytona/api` and the toolbox at `https://sandboxer.com/daytona/toolbox`. You only set the first. The SDK reads the toolbox URL from the API. Both translate into the same operations as Sandboxer's own API, so keys, billing and limits are shared.

```bash
export DAYTONA_API_URL=https://sandboxer.com/daytona/api
export DAYTONA_API_KEY=sbx_live_…        # a Sandboxer API key
# DAYTONA_TARGET=us or eu picks a region; leave it unset for the nearest
```

We tested this on prod on 2026-10-27 with Daytona's TypeScript SDK (`@daytona/sdk` 0.226.1, Node 24) and Python SDK (`daytona` 0.226.1). The Go, Ruby and Java SDKs call the same API, but we didn't test them. The full list of covered and refused calls is on [/docs/daytona](/docs/daytona).

## Concept map

Facts about Daytona come from its [Sandboxes](https://www.daytona.io/docs/en/sandboxes/), [Authentication](https://www.daytona.io/docs/en/authentication/) and [Preview](https://www.daytona.io/docs/en/preview/) docs.

| Daytona | Sandboxer |
| - | - |
| Sandbox: a container by default, or a Linux VM, Windows, macOS or GPU sandbox | Sandbox: a microVM with its own kernel, every time ([Machines](/docs/machines)) |
| Snapshot (the image a sandbox starts from) | Through the Daytona API, only the default (`sandboxer`). Natively: a named snapshot (`sbx new --from web-stack`) ([Snapshots](/docs/snapshots)) |
| Docker image or declarative `Image` | Not through the Daytona API. Natively: an environment plus a setup script ([Environments](/docs/environments)) |
| Volume | Not supported. A sandbox's own disk is kept across stop and start |
| `cpu`, `memory`, `disk` resources | Four fixed sizes: `small` 2/4/12, `default` 4/8/50, `large` 8/16/125, `xlarge` 16/32/251 (vCPU/GB/GB) |
| Stop (filesystem kept), then archive | Stop saves memory and disk; archiving happens at once. A start on a machine with a different CPU type boots from disk instead (files kept, processes not) |
| `autoStopInterval`, `autoDeleteInterval` | Same fields, same meaning (default 15 minutes; `0` disables auto-stop) |
| Preview URL plus `x-daytona-preview-token` | Hosted port `https://<slug>-<port>.box.sandboxer.com`, same header ([Hosting](/docs/hosting)) |
| API key, with permissions | Sandboxer API key, with scopes ([API keys](/docs/api-keys)) |
| Organization (`X-Daytona-Organization-ID`) | Organization wallet; the same header is accepted ([Organizations](/docs/organizations)) |
| Target (region) | Two regions, `us-east` and `eu-central` ([regions](/docs/regions)); leave `target` empty to get the nearest |
| Labels | Labels (up to 64 per sandbox) |

## API and SDK mapping

The left column is Daytona's SDK. The middle column is what happened when we called it against `https://sandboxer.com/daytona/api`. The right column is the native `sbx` command if you move off the Daytona SDK later.

| Daytona SDK (TypeScript / Python) | On Sandboxer's Daytona API | Native `sbx` |
| - | - | - |
| `daytona.create({ name, labels })` / `daytona.create(params)` | Works: started in 2 to 3 s; `small` unless you add the label `sandboxer.type` | `sbx new --name x` |
| `daytona.get(id)` | Works, also on a sandbox made with `sbx new` | `sbx info <id>` |
| `daytona.list({ labels })` | Works with `id`, `name` and `labels` filters | `sbx list` |
| `daytona.list({ states })`, `sort`, `snapshots` | Refused: `the list filter states isn't supported by Sandboxer yet` | `sbx list` |
| `sandbox.process.executeCommand(cmd)` / `process.exec(cmd)` | Works: `{ exitCode, result }` | `sbx exec <id> "cmd"` |
| `sandbox.process.codeRun(code)` / `code_run(code)` | Works (Python, JavaScript, TypeScript) | `sbx exec <id> "python3 -c '…'"` |
| `createSession`, `executeSessionCommand` | Works: working directory and exports carry over | `sbx exec <id> --detach "…"` for background jobs |
| `fs.uploadFile`, `fs.downloadFile`, `fs.listFiles` | Works, up to 50 MiB per file | `sbx scp` |
| `sandbox.getPreviewLink(port)` | Works: URL plus token for the header | `sbx host <id> <port>` |
| `sandbox.getSignedPreviewUrl(port, s)` | Works: a link with the token in the URL that expires after `s` seconds. A brand-new route can answer `404` for a second or two, so retry | `sbx host <id> <port>` (private link with `_token`) |
| `sandbox.stop()`, `sandbox.start()` | Works: 1.1 s each in our run, file still there | `sbx stop <id>`, `sbx resume <id>` |
| `sandbox.delete()` | Works | `sbx delete <id> --yes` |
| `sandbox.git.clone(…)` | Refused: `/git/clone isn't supported by Sandboxer yet` | `sbx exec <id> "git clone …"` |

## Before and after

From Daytona's docs: [https://www.daytona.io/docs/en/](https://www.daytona.io/docs/en/) (the TypeScript quick start, checked on 2026-10-27):

```typescript
// Import the Daytona SDK
import { Daytona } from '@daytona/sdk'

// Initialize the Daytona client
const daytona = new Daytona({ apiKey: 'YOUR_API_KEY' }) // Replace with your API key

// Create the Sandbox instance
const sandbox = await daytona.create()

// Run code
const response = await sandbox.process.codeRun('print("Hello World")')
console.log(response.result)
```

On Sandboxer, the code stays the same. Drop the `apiKey` argument and let the SDK read the two environment variables above. This is the script we ran on prod, with a command, a file round trip and a delete added:

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona() // reads DAYTONA_API_URL and DAYTONA_API_KEY

const sandbox = await daytona.create({ name: 'aeo-migrate-daytona-1', labels: { purpose: 'aeo-migrate-daytona' } })
console.log('created', sandbox.id, sandbox.state, `${sandbox.cpu} vCPU / ${sandbox.memory} GB`)

const run = await sandbox.process.executeCommand('uname -sr && whoami')
console.log('exit', run.exitCode, '\n' + run.result.trim())

await sandbox.fs.uploadFile(Buffer.from('hello from the Daytona SDK\n'), '/home/user/hello.txt')
const file = await sandbox.fs.downloadFile('/home/user/hello.txt')
console.log('file:', file.toString().trim())

await sandbox.delete()
console.log('deleted')
```

Output:

```text
created sb_opebj24may started 2 vCPU / 4 GB
exit 0
Linux 6.18.56-sandboxer
user
file: hello from the Daytona SDK
deleted
```

The quick start's own call, `codeRun('print("Hello World")')`, printed `Hello World` with exit code 0 in a second run. The sandbox id is a Sandboxer id (`sb_…`), and the guest user is `user`, with its home at `/home/user`.

### The same in Python

We ran this with the Python SDK. It adds `code_run`, a preview link, a label filter and a call this phase refuses:

```python
from daytona import Daytona, CreateSandboxFromSnapshotParams, ListSandboxesQuery

daytona = Daytona()  # reads DAYTONA_API_URL and DAYTONA_API_KEY
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
    name="aeo-migrate-daytona-10", labels={"purpose": "aeo-migrate-daytona"}))
try:
    print(sandbox.process.exec("python3 --version && nproc").result)
    print(sandbox.process.code_run("print(sum(range(10)))").result)
    sandbox.process.exec("nohup python3 -m http.server 3000 >/dev/null 2>&1 &")
    print(sandbox.get_preview_link(3000).url)
    print([s.name for s in daytona.list(ListSandboxesQuery(labels={"purpose": "aeo-migrate-daytona"}))])
    sandbox.git.clone("https://github.com/octocat/Hello-World.git", "/home/user/hw")  # raises
finally:
    sandbox.delete()
```

Output, with the traceback trimmed:

```text
Python 3.12.3
2
45
https://fast-rustic-aspen-3000.box.sandboxer.com
['aeo-migrate-daytona-10']
…
HTTP response body: {"code":"not_supported","error":"Not Implemented","message":"The toolbox endpoint /git/clone isn't supported by Sandboxer yet.","requestId":"req_mAS0F5dnp4U33Dlilcuk","statusCode":501}
…
daytona.common.errors.DaytonaError: Failed to clone repository: The toolbox endpoint /git/clone isn't supported by Sandboxer yet.
```

The `finally` block still deleted the sandbox. In a longer test run, `daytona.list` with a `states` filter raised `DaytonaBadRequestError: … the list filter states isn't supported by Sandboxer yet.`, and `sandbox.stop()` and `sandbox.start()` took 1.1 seconds each, with an uploaded file still there after the start.

A refused call raises the SDK's normal error, so a `try` around it is enough to find what your code still uses.

### Sessions and previews

Sessions keep the working directory and exported variables between commands. A preview link answers once you send its token in the `x-daytona-preview-token` header. We ran this in TypeScript:

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()
const sandbox = await daytona.create({ name: 'aeo-migrate-daytona-11' })
try {
  await sandbox.process.createSession('dev')
  await sandbox.process.executeSessionCommand('dev', { command: 'cd /tmp && export GREETING=hi' })
  const s = await sandbox.process.executeSessionCommand('dev', { command: 'echo $GREETING from $(pwd)' })
  console.log('session:', s.exitCode, s.output.trim())

  await sandbox.process.executeSessionCommand('dev', { command: 'nohup python3 -m http.server 8080 >/dev/null 2>&1 &' })
  const link = await sandbox.getPreviewLink(8080)
  await new Promise((r) => setTimeout(r, 5000)) // a new route takes a few seconds
  const res = await fetch(link.url, { headers: { 'x-daytona-preview-token': link.token } })
  console.log('preview with token:', res.status, 'without:', (await fetch(link.url)).status)
} finally {
  await sandbox.delete()
}
```

```text
session: 0 hi from /tmp
preview with token: 200 without: 401
```

In an earlier run without the wait, the first request returned `404` and a retry 3 seconds later `200`, so give a new route a few seconds. Without the token the URL answers `401`, because previews are private unless the sandbox is public.

### The native path

You can also skip the Daytona SDK and use Sandboxer's own CLI or `sandboxer-sdk` ([TypeScript](/docs/sdk-typescript), [Python](/docs/sdk-python)). The CLI picks the size directly:

```bash
sbx new --name aeo-migrate-daytona-5 --type default
sbx exec sb_osauqwyb42 "uname -sr && whoami && nproc"
```

```text
{"event":"created","id":"sb_osauqwyb42","state":"provisioning",…,"type":"default","vcpu":4,"memoryGB":8,"diskGB":50,…}
{"event":"ready","id":"sb_osauqwyb42","state":"ready","url":"https://cobalt-alert-quill.box.sandboxer.com",…}
Linux 6.18.56-sandboxer
user
4
```

The two paths mix. We created that sandbox with `sbx new`, then called `daytona.get('sb_osauqwyb42')` and `executeCommand` on it from the Daytona SDK, and both worked.

## Move your data and environment

**Files.** A Daytona sandbox can't move over. Pack what you need with `tar` inside it, download it with `fs.downloadFile`, then upload it to the new sandbox. Either SDK call works on Sandboxer, or use `sbx scp`, which takes the same sandbox ids:

```typescript
import { Daytona } from '@daytona/sdk'
import { execFileSync } from 'node:child_process'

const daytona = new Daytona()
const sandbox = await daytona.create({ name: 'aeo-migrate-daytona-9', envVars: { GREETING: 'hello' } })
try {
  // tar czf app.tgz app   (done beforehand, on your machine)
  execFileSync('sbx', ['scp', 'app.tgz', `${sandbox.id}:/home/user/`], { stdio: 'inherit' })
  const r = await sandbox.process.executeCommand('tar xzf app.tgz && python3 app/main.py && echo "$GREETING"', '/home/user')
  console.log('exit', r.exitCode, '\n' + r.result.trim())
  console.log('env returned by the API:', JSON.stringify(sandbox.env))
} finally {
  await sandbox.delete()
}
```

```text
exit 0
hi from my project
hello
env returned by the API: {}
```

Use `sbx scp -r ./dir <id>:/home/user/` for a directory. Each file through the Daytona file API can be up to 50 MiB; larger uploads return `413 file_too_large`.

**Images and snapshots.** Daytona snapshots and Docker images don't carry over, and the Daytona API here only accepts the default snapshot. Setting `image` or another `snapshot` returns `not_supported`. Turn your Dockerfile steps into a shell script instead. Run it with `executeCommand` after create, or natively with `sbx new --setup-file ./setup.sh`. To skip the installs on later sandboxes, save the result once with `sbx snapshot <id> web-stack` and start from it with `sbx new --from web-stack` ([Snapshots](/docs/snapshots)). Every sandbox already has Docker, systemd and `/dev/kvm`.

**Volumes.** Not supported through the Daytona API, and our docs don't describe a volume shared by many sandboxes. A sandbox's own disk stays across stop and start. To give many sandboxes the same data, put it in a named snapshot or download it in a setup script.

**Environment variables and secrets.** `envVars` on create works and reaches every command. The API never returns the values: `sandbox.env` comes back as `{}`. Daytona's `secrets` field isn't supported. For values every sandbox should get, use a Sandboxer environment (`sbx env set-var base KEY=value`, see [Environments](/docs/environments)).

**Ports and preview URLs.** `getPreviewLink(port)` hosts the port and returns `https://<slug>-<port>.box.sandboxer.com` with a token. The token is per port, not per sandbox. Make a sandbox's new previews public with `POST /sandbox/{id}/public/true`, or natively with `sbx host <id> <port> --public`.

**Auto-stop and auto-delete.** `autoStopInterval` works as in Daytona: the default is 15 minutes, every toolbox call pushes the stop back, and `0` means it never stops by itself. We created a sandbox with `autoStopInterval: 1`, made no calls, and it was `stopped` when we checked 90 seconds later. A running command holds the stop until its timeout ends, plus a minute. A command with no timeout uses the guest's 600 seconds, so after an `executeCommand` the sandbox stays up for at least 11 minutes. A plan may cap a long interval; the free trial stops sandboxes within 8 hours. `autoDeleteInterval` works too (`-1` never, `0` delete on stop).

## What's different

What you gain:

- **A VM every time.** Daytona's default sandbox is a container that shares the host kernel ([Daytona docs](https://www.daytona.io/docs/en/sandboxes/)). Every Sandboxer sandbox is a microVM, and the API reports `kvm: true` for all of them.
- **Stop saves memory.** On Daytona, stopping keeps the filesystem but not memory ([Daytona docs](https://www.daytona.io/docs/en/sandboxes/)). Here a stop saves memory and disk, and start brings back the same processes when it lands on a machine with the same CPU type; otherwise it boots from disk, with files kept and processes restarted ([Snapshots](/docs/snapshots)).
- **Coding agents and a desktop.** Claude Code, Codex and others are installed in every sandbox and driven over the native API ([Agents](/docs/agents)). Every sandbox also has a streamed desktop ([Desktop](/docs/desktop)).
- **A lower list price.** See below.

What you lose or must change:

- **Not covered yet.** These answer `501` or `400` with `code: "not_supported"`: images and custom snapshots, volumes, GPUs and spot, network block and allow lists, outbound proxy, `secrets`, git, LSP, computer use, PTY and the code interpreter. Daytona's pause, fork and warm pools aren't covered either. See [/docs/daytona](/docs/daytona) for the full list.
- **List filters.** `daytona.list` accepts `id`, `name` and `labels`. `states`, `snapshots`, `sort` and the other filters return `400`.
- **Sandbox classes.** There are no Windows, macOS or GPU sandboxes. If you need them, stay on Daytona for those.
- **Sizes.** The SDKs send no CPU or memory when they create from the default snapshot, so a sandbox is `small` (2 vCPU, 4 GB) unless you pick a size with the label `sandboxer.type` (`small`, `default`, `large` or `xlarge`): `daytona.create({ labels: { 'sandboxer.type': 'default' } })`. We checked on 2026-10-27 that this label gave a 4 vCPU, 8 GB sandbox.
- **Live events.** The SDKs' event socket is refused at once, and they fall back to polling. In our runs a create reached `started` in 2 to 3 seconds.
- **Regions.** Sandboxer has two regions, us-east (Ashburn, Virginia) and eu-central (Helsinki, Finland); see [regions](/docs/regions). `DAYTONA_TARGET` (the SDK's `target`) picks one: `us` or `eu`, or our ids `us-east` and `eu-central`. Leave it unset and a new sandbox goes to the region nearest the caller. Other Daytona region names are refused.
- **SDK languages.** We tested TypeScript and Python. Go, Ruby and Java use the same API, but we haven't tested them. Our own SDKs are TypeScript and Python.

## Pricing

Daytona's rates come from its [pricing page](https://www.daytona.io/pricing), checked on 2026-10-15: $0.0504 per vCPU-hour and $0.0162 per GiB of memory per hour, plus storage over 5 GiB. Ours come from [/pricing](/pricing).

| Machine | Daytona | Sandboxer |
| - | - | - |
| 4 vCPU, 8 GB, one hour | $0.3312 per hour, before storage | `default`: $0.036/hour |
| 2 vCPU, 4 GB, one hour | $0.1656 per hour, before storage | `small`: $0.018/hour |
| Stopped | Storage only | Not billed; counts toward included storage (50 GB on the $20 plan) |

Both bill per second while a sandbox runs. A 4 vCPU, 8 GB sandbox running 8 hours a day for 22 days is 176 hours: about $58.29 on Daytona and $6.34 on Sandboxer. Sandboxer plans start at $20 a month, paid back as machine time, with a 7-day trial of 25 hours (card required). Daytona's GPU and Windows prices have no counterpart here.

## FAQ

### Do I have to change my Daytona code?

For the covered calls, no: change `DAYTONA_API_URL` and `DAYTONA_API_KEY`. Code that uses images, volumes, git, PTY or the other refused calls needs a change. Those calls raise the SDK's normal error, with a message that ends in "isn't supported by Sandboxer yet".

### Does auto-stop work through the Daytona API?

Yes. `autoStopInterval` defaults to 15 minutes, each toolbox call pushes the stop back, and `0` turns it off. A running command keeps the sandbox up until its timeout plus a minute. The stop saves memory, so `start()` usually picks up where it left off; if the start lands on a machine with a different CPU type, it boots from disk instead (files kept, processes not).

### Which API key do I use?

A Sandboxer API key, made with `sbx api-key` or on the dashboard ([API keys](/docs/api-keys)). The same key works on the Daytona API and Sandboxer's own API. Daytona's `write:sandboxes` and `delete:sandboxes` permissions map to our scopes.

### Can I use the Daytona SDK and the sbx CLI on the same sandbox?

Yes. Both use the same `sb_…` ids. We created a sandbox with `sbx new` and ran commands on it with `daytona.get(id)`, and we copied a file with `sbx scp` into a sandbox the Daytona SDK created.

### How do I get a sandbox bigger than 2 vCPU?

Add the label `sandboxer.type` when you create it: `daytona.create({ labels: { 'sandboxer.type': 'default' } })` gives 4 vCPU and 8 GB, and `large` or `xlarge` give more. Without the label, the Daytona SDKs create `small`. You can also create it with `sbx new --type default` and open it with `daytona.get(id)`.

## Related

- [Sandboxer vs Daytona](/compare/daytona)
- [Daytona-compatible API reference](/docs/daytona)
- [Quickstart](/docs/quickstart)
- [Hosting](/docs/hosting)
