--- title: Agent Sandboxes API | Sendblue Docs description: Give your AI agent an isolated cloud Linux machine — create, run commands, read/write files, and destroy sandboxes via the Sendblue API --- Every `free_api` and paid-plan Sendblue account can spin up **sandboxes**: isolated Linux machines in the cloud that your AI agent controls over HTTP. Run shell commands, install packages, clone repos, start dev servers — each sandbox is a real container with its own filesystem, isolated from every other account. Free API accounts get **$100 of sandbox compute free** once their phone number is verified. Paid plans get a larger allowance and more concurrent sandboxes. ## How it works ``` POST /v3/sandboxes create a sandbox POST /v3/sandboxes/{id}/exec run a shell command POST /v3/sandboxes/{id}/files write a file GET /v3/sandboxes/{id}/files?path= read a file GET /v3/sandboxes list sandboxes + usage + agent connect prompt DELETE /v3/sandboxes/{id} destroy a sandbox ``` Sandboxes **sleep automatically after 10 minutes idle** and wake transparently on the next `exec` or file operation — you’re only billed while a sandbox is awake. Destroying a sandbox refunds the unused portion of its final awake window. **Sandboxes are ephemeral across sleep.** Waking gives you the same sandbox id on a fresh filesystem — anything you wrote is gone. Treat every awake window as a complete unit of work: finish, then read your results out via the files API (or push them somewhere with `curl`) before going idle. Keeping a sandbox awake preserves state for as long as you keep working — any operation **on that sandbox** (`exec` or a file read/write) resets the idle timer. Listing sandboxes with `GET /v3/sandboxes` does not. All endpoints use your standard API credentials: ``` sb-api-key-id: YOUR_API_KEY_ID sb-api-secret-key: YOUR_API_SECRET_KEY ``` Both values are in your dashboard’s API settings. The same pair works for the whole Sendblue API. ## Pricing and the free tier | Plan | Compute allowance | Active sandboxes | | ---------- | ----------------- | ---------------- | | `free_api` | $100 (one-time) | 1 | | Paid plans | $500 | 5 | Compute is metered at **$0.33 per awake-hour**. A sandbox that runs a command and then sleeps 10 minutes later costs about $0.055. Sleeping sandboxes cost nothing. **Free-tier eligibility.** The $100 free tier is tied to your account’s **primary phone number** — the number you add during onboarding: 1. Your primary number must be **verified**: send any message from that phone to your Sendblue number. 2. Each phone number can claim the free tier **once, ever**, across all of Sendblue. A number that has already claimed on another account gets `403 sandbox_credit_claimed`. 3. The number must be a standard mobile line. VoIP, toll-free, and other virtual/non-mobile numbers aren’t eligible for free credits (`403 sandbox_line_type_ineligible`) — but work fine on paid plans. ### Phone verification during setup For a fresh free API account, the quickest path is CLI phone setup: Terminal window ``` npx -y @sendblue/cli setup --phone +15551234567 --company my-agent ``` The CLI requests a temporary Sendblue verification route and shows the human two values: - `sharedNumber`: the Sendblue number to text. - `challenge`: the exact message body to send, for example `SB SETUP 123456`. The human must send the `challenge` from the same phone passed with `--phone` to `sharedNumber`. If you render a convenience SMS link in an agent, build it from those response fields: ``` sms:?&body= ``` The `?&body=` form is deliberate, not a typo: iOS parses `&body` and Android parses `?body`, and the combined form works on both — don’t “fix” it to `?body=`. For example: `sms:+15559876543?&body=SB%20SETUP%20123456`. The `phoneNumber` is the sender being verified; it is not the SMS recipient. Wait for setup to finish and return API credentials before creating a sandbox. Phone setup verifies the primary phone for sandbox free-tier eligibility, so a fresh phone-onboarded account should not need a second verification text before its first sandbox request. When your balance runs out you’ll get `402 balance_exhausted`. You can still list and destroy your sandboxes; upgrade to a paid plan to keep computing. ## Create a sandbox ``` POST https://api.sendblue.co/v3/sandboxes ``` Terminal window ``` curl -X POST 'https://api.sendblue.co/v3/sandboxes' \ -H 'sb-api-key-id: YOUR_API_KEY_ID' \ -H 'sb-api-secret-key: YOUR_API_SECRET_KEY' ``` ``` { "data": { "id": "sbx_a9c7253d-8567-4fff-bf81-08a25076e50f", "object": "sandbox", "status": "running", "created_at": "2026-07-19T18:03:14.399Z" } } ``` Returns `201` once the sandbox has actually booted (typically a few seconds) — the `id` is immediately usable for `exec`, no polling needed. ## What’s inside Ubuntu 22.04 (x86\_64), running as `root`, with full outbound internet access. Preinstalled: `bash`, `git`, `curl`, `wget`, **Node 22** (`node`/`npm`/`npx`), `bun`, `jq`, `unzip`. **Python is not preinstalled** — install it in \~10 seconds when you need it: ``` {"command": "apt-get update -qq && apt-get install -y -qq python3 python3-pip", "timeout_ms": 120000} ``` Anything else is an `apt-get install -y` away. ## Run a command ``` POST https://api.sendblue.co/v3/sandboxes/{sandbox_id}/exec ``` | Parameter | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------------------------- | | `command` | string | Yes | Shell command to run (max 8,192 chars) | | `cwd` | string | No | Working directory (max 1,024 chars) | | `env` | object | No | Extra environment variables (max 64; keys ≤128 chars, values ≤4,096) | | `timeout_ms` | number | No | 1,000–120,000. Default 60,000 | Terminal window ``` curl -X POST 'https://api.sendblue.co/v3/sandboxes/sbx_YOUR_ID/exec' \ -H 'sb-api-key-id: YOUR_API_KEY_ID' \ -H 'sb-api-secret-key: YOUR_API_SECRET_KEY' \ -H 'Content-Type: application/json' \ -d '{"command": "git clone https://github.com/you/repo && cd repo && npm install && npm test", "timeout_ms": 120000}' ``` ``` { "data": { "object": "sandbox.command", "sandbox_id": "sbx_...", "command": "git clone https://github.com/you/repo && cd repo && npm install && npm test", "success": true, "exit_code": 0, "stdout": "...test output...\n", "stderr": "", "duration_ms": 48210 } } ``` **Execs share one persistent shell while the sandbox is awake.** `cd`, exported variables, and activated virtualenvs carry over between calls. That shell state resets along with the filesystem when the sandbox sleeps — don’t depend on it across awake windows. You can also pass `cwd`/`env` explicitly per call. **Waking is transparent.** If the sandbox is asleep, the first request simply takes a few extra seconds while it wakes — no polling or retry needed. **Long-running work:** commands are capped at 120 s. For anything longer, start a background process and poll it: ``` {"command": "nohup npm run build > /root/build.log 2>&1 & echo started"} ``` then check on it with `{"command": "tail -5 /root/build.log"}`. Background processes keep running while the sandbox is awake (each `exec` or file read/write on the sandbox resets the 10-minute idle timer — polling the log this way also keeps it awake); when the sandbox sleeps, processes stop and the filesystem resets — collect results before going idle. On a `408 command_timeout` the command is terminated, but work it completed first — files written, packages installed — remains on disk; check state before re-running. `stdout`/`stderr` are capped at 262,144 characters (\~256 KiB) each (`stdout_truncated: true` flags a cut). Commands that outlive `timeout_ms` return `408 command_timeout`. ## Read and write files ``` POST https://api.sendblue.co/v3/sandboxes/{sandbox_id}/files # write GET https://api.sendblue.co/v3/sandboxes/{sandbox_id}/files # read ``` Write (`path` + `content`, content up to 65,536 characters — fetch bigger payloads from inside the sandbox with `exec` + `curl`; parent directories are created automatically): Terminal window ``` curl -X POST 'https://api.sendblue.co/v3/sandboxes/sbx_YOUR_ID/files' \ -H 'sb-api-key-id: YOUR_API_KEY_ID' \ -H 'sb-api-secret-key: YOUR_API_SECRET_KEY' \ -H 'Content-Type: application/json' \ -d '{"path": "/root/script.py", "content": "print(\"hello\")"}' ``` ``` { "data": { "object": "sandbox.file", "sandbox_id": "sbx_...", "path": "/root/script.py", "size_bytes": 14 } } ``` Read (files up to 1 MiB; URL-encode the `path` query param): Terminal window ``` curl 'https://api.sendblue.co/v3/sandboxes/sbx_YOUR_ID/files?path=/root/script.py' \ -H 'sb-api-key-id: YOUR_API_KEY_ID' \ -H 'sb-api-secret-key: YOUR_API_SECRET_KEY' ``` ``` { "data": { "object": "sandbox.file", "sandbox_id": "sbx_...", "path": "/root/script.py", "size_bytes": 14, "content": "print(\"hello\")" } } ``` `content` is the raw UTF-8 text of the file. For binary files, base64 them inside the sandbox first (`exec` + `base64 file.bin`) and read that. ## List sandboxes, check usage, connect your agent ``` GET https://api.sendblue.co/v3/sandboxes ``` ``` { "data": { "object": "list", "sandboxes": [ { "id": "sbx_...", "object": "sandbox", "status": "sleeping", "created_at": "..." } ], "usage": { "object": "sandbox.usage", "plan": "free_api", "used_usd": 0.055, "cap_usd": 100, "remaining_usd": 99.945, "rate_usd_per_hour": 0.33, "max_active_sandboxes": 1 }, "connect": { "object": "sandbox.connect", "prompt": "You have access to a Sendblue sandbox: an isolated Linux container you control over HTTP. ..." } } } ``` **`connect.prompt` is the fastest way to put a sandbox in your agent’s hands**: it’s a ready-made system-prompt snippet that teaches any AI agent the full sandbox API. Substitute your real keys for the `YOUR_API_KEY_ID` / `YOUR_API_SECRET_KEY` placeholders, paste it into your agent’s instructions, and the agent can drive the sandbox on its own. (If your account lives on a non-default Sendblue host, also swap the base URL in the prompt.) ## Destroy a sandbox ``` DELETE https://api.sendblue.co/v3/sandboxes/{sandbox_id} ``` ``` { "data": { "id": "sbx_...", "object": "sandbox", "deleted": true } } ``` Destroys the container and its disk immediately, and **refunds the unused portion** of the current awake window — destroy sandboxes you’re done with to conserve your balance. Listing and destroying always work, even if your free-tier eligibility lapses. ## Errors Errors use the standard v3 envelope: `{ "error": { "code", "message", "request_id", "details?" } }` — `details` carries specifics like `used_usd`/`cap_usd` on a 402 or `active_sandboxes`/`max_active_sandboxes` on a 409. | Status | Code | Meaning | What your agent should do | | --------- | ----------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 401 | `unauthorized` | Missing or invalid API credentials | Stop; verify both headers and key values with the account owner | | 400 | `invalid_request` | Body/params failed validation | Fix the request per `details`; don’t retry as-is | | 403 | `sandbox_primary_phone_missing` | Account has no primary phone. Run phone setup or add one during onboarding | Stop and tell the human | | 403 | `sandbox_access_denied` | Plan doesn’t include sandboxes, or the primary number isn’t verified yet | If this is a fresh phone setup, wait for setup to finish before retrying. Otherwise ask the human to text their Sendblue number from the primary phone, wait a few seconds, then retry | | 403 | `sandbox_credit_claimed` | This phone number already claimed free credits on another account | Stop and tell the human | | 403 | `sandbox_line_type_ineligible` | VoIP/toll-free/virtual numbers can’t claim free credits | Stop and tell the human (paid plans aren’t screened) | | 402 | `balance_exhausted` | Compute balance used up; you can still list and destroy sandboxes | Stop and tell the human to upgrade | | 409 | `sandbox_limit_reached` | Too many active sandboxes for the plan | Destroy one, then retry | | 404 | `not_found` | Unknown or already-destroyed sandbox id | Don’t retry; create a new sandbox if needed | | 404 | `file_not_found` | Read of a nonexistent file | Check the path — use absolute paths, and remember the filesystem resets when the sandbox sleeps | | 413 | `file_too_large` | File read over 1 MiB | Split or compress it inside the sandbox first | | 408 | `command_timeout` | Command didn’t finish within `timeout_ms` | Raise `timeout_ms` (≤120000) or background the work; completed side effects persist | | 429 | `rate_limit_exceeded` | Over 60 requests / 10 s | Back off briefly and retry | | 429 | `free_tier_paused` | Free tier temporarily paused globally | Wait or tell the human to upgrade | | 503 / 504 | `service_unavailable` / `sandbox_timeout` | Transient sandbox-service issue | Retry once after a short wait, then stop | ## Limits | Limit | Value | | ----------------- | --------------------------------------------------------------------------------------- | | Machine | 2 vCPU / 8 GiB RAM / 16 GB disk (x86\_64 Linux) | | Idle sleep | 10 minutes with no API calls to that sandbox (billing stops; filesystem resets on wake) | | Command timeout | 120 s max per exec — run longer work in background processes | | File write / read | 65,536 chars / 1 MiB via API (unlimited inside the sandbox) | | Rate limit | 60 requests per 10 seconds |