sandbox_start

Starts a new box for a goal: the services your repo exposes, each under a name of its own, and optionally an environment, secrets, a size and toolchains. Returns the box id, its URLs and the services it received.

Input Output
name, goal, services[{name, port, targetPort, web, fromBox, fromPort, version, containerPort, env}], externalBaseUrl, environment, secrets (list of names), size, toolchains, orientation, idleTimeoutMin, restore, waitForCapacitySec (goal is required, the rest optional) id, arch, sceneUrl, webUrl, takeoverUrl, status, orientation, orientationNote (only on a landscape fallback), startedFrom (snapshot), size, vcpus, memoryGb, the services it received (each web service with its url), the secrets, toolchains and idleTimeoutMin it received, toolchainsNote (only when toolchains named a language that is always on), environment, boxUrlAccess when the account has an IP allowlist, next
  • name: what the box is for, in the person's language ("membercenter refund flow"). The app shows it as the box's title; without it the person sees only the box id.

  • goal (required): why the box exists and what done looks like, one or two sentences in the person's language ("Make the member center refund button post the refund to the ledger; done when a refund goes through end to end in the browser"). The app shows it on the box's card and page, and sandbox_status and sandbox_list return it, so another conversation can finish the work if this one is closed (Picking up a box another conversation left). At most 500 characters; a longer one is cut to 500. Without it, or blank, the call fails with goal is required: one or two sentences, in the language the person reads, on why this box exists and what done looks like. The person sees it on the box's card in their app, and whoever picks the box up if this conversation is closed reads it in sandbox_status. Call sandbox_start again with goal set. Over REST (POST /v1/boxes) goal is optional.

  • orientation: portrait (390 × 844) or landscape (1280 × 800) for the AI screen and review screenshots. Omit it to use the account preference, which defaults to portrait. The choice belongs to this box; changing the account preference later does not resize it (PUT /v1/boxes/{id}/orientation over REST switches a running box). Ask for landscape when what you show on the screen is a desktop layout; a Chromium window is at least 500 pixels wide, and the flags that fit it on the portrait screen are in sandbox_exec. When portrait is asked for (or is the account's preference) but the box's image cannot rotate its screen, the box starts in landscape instead and the result carries orientationNote saying so: pages render in a 1280 × 800 window, and a phone-sized screen needs a new box once the image is updated.

  • restore: the id of a box the idle rules stopped while its frozen copy is still kept, for 24 hours after it stopped (restorableUntil in sandbox_status, or in sandbox_list with all: true). It brings that same box back as it froze, with /work, its running programs, its id and its URLs, instead of starting a new one, and next says so. Pass goal as usual; the other settings are the box's own and are ignored. Attached phones are not kept; attach new ones. Only a box the idle rules stopped keeps a frozen copy: one stopped with sandbox_stop cannot be restored (409, with how to start it again). Restoring needs a free slot under the plan's box limit and credits, like a new box; if no host has room right then, the box comes back frozen and the next call that acts on it wakes it.

  • size: 1 (the default), 2, 4 or 8 units of 2 vCPU, 8 GB of memory and 40 GB of disk, fixed for the life of the box and billed proportionally. Every box is an x86_64 (amd64) Debian 12 machine, and the result says so as arch: "amd64"; for arm64 images see sandbox_exec. 1 fits web front ends and most Node, Python and Go work; use 2 or more for Android, Gradle or heavy docker compose, such as several services built as images in one box. toolchains: java-17, java-21, android, dotnet, rust, flutter (web and Android; implies android), esp32 (PlatformIO for ESP32 with Arduino and ESP-IDF, compile only), python-3.12 or python-3.13 (that Python first on PATH as python, python3 and pip; without them python3 is Debian's 3.11, which has Pillow, numpy and boto3), switched on for every command. Go, Node, Python 3.11, PHP with Composer, MySQL, Postgres and S3 test servers (psbx-testdb), psql, redis-cli, mysql, ImageMagick, PowerShell 7, the Azure and GitHub CLIs, clang and cmake are always on; naming one of the languages (go, node, python) in toolchains is ignored, and toolchainsNote in the result says so.

  • services: the services your repo exposes on the box, each with a name, the port callers dial, and optionally targetPort and web.

    • From the moment the box starts, each name has an address of its own inside the box, from 198.18.0.0/15, and the box's DNS answers the name with that address, for processes and containers alike; there is nothing to wire. name:port reaches your process on targetPort, which defaults to port: callers keep dialing name:port while the process listens on targetPort. Containers on the default bridge, on user-defined networks and in docker compose reach the names the same way: the box's rules apply to its own programs and to every container network, and Docker's DNS in the box forwards to the box's resolver.
    • The process must listen on 0.0.0.0: one bound to 127.0.0.1 is reachable through its URL (see sceneUrl below) but not by its name. A container must publish its targetPort (ports: in compose, -p <targetPort>:<container port> in docker run).
    • Names that share a targetPort are one process and switch together in sandbox_wire. Since every name has its own address, names on the same port with different targetPorts are separate services: api.acme.internal:8080 and billing.acme.internal:8080 can reach two processes listening on 8080 and 8081.
    • Ports 80 and 9095 in the box belong to boxd (80 is the scene entry, 9095 its API), and no process can listen on them. A service that callers reach on port 80 declares a targetPort: with { "name": "api.acme.internal", "port": 80, "targetPort": 8080 }, callers dial api.acme.internal:80 (or http://api.acme.internal/) and the process listens on 8080. Without targetPort the call fails with 400: port 80 in the box is boxd's scene entry; add targetPort, the port your process listens on — callers keep dialing <name>:80. Port 9095 works the same way, and targetPort itself cannot be 80 or 9095. Every other port is free, 443 and other privileged ports included (commands run as root). Only a service in the box needs targetPort for port 80: an environment address listed on port 80 (an internal load balancer) and a link declared on port 80 ({ "name": "api.test.internal", "port": 80, "fromBox": "<A>", "fromPort": 8080 }) work as they are, because every name has its own address in the box and never meets the box's own port 80. A name dialed on port 80 that is not declared or listed on 80 (a service or link declared on another port, or an environment host listed only on other ports) gets 502 nothing listens on <name>:80 in this box: that name is declared on another port. Dial the port it is declared on, or declare it on port 80 with targetPort, the port your process listens on, from the box's programs and containers alike; localhost:80 is still boxd's scene entry and answers with the first declared service, as sceneUrl does. A box started before 2026-09-24 20:30 UTC answers such a name on 80 with the first declared service instead. On other ports that nothing declares, the connection is refused, or reaches whatever listens on that port in the box; on port 9095 a name reaches boxd's API, which returns 404.
    • A name is a host name (lowercase letters, digits, - and .), or a private IPv4 address or range inside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 or 100.64.0.0/10 (such as 172.16.0.0/16): connections from the box, containers included, to that range on that port reach your service in the box on its targetPort (in external mode, the forwarder to externalBaseUrl). Ranges are for callers that dial addresses from their own registry instead of a host name, have no address of their own, and can only be declared here.
    • web: true marks each service a person can open in a browser to use the product; leave it off for APIs, databases and anything that would only show JSON. Each web service gets its own URL, and the app's Use it button opens the first of them (webUrl, below).
    • fromBox, with an optional fromPort, makes the entry a link to another of your boxes instead of a service in this one: name:port in this box reaches fromPort in box fromBox. See Links to other boxes.
    • version, with an optional containerPort, runs a published version under the name instead of your own process: the box pulls the image and starts it in the background. See Run a published version.
  • externalBaseUrl: where unchanged HTTP services live, http(s)://host[/path] without query or credentials, for example your staging gateway. When a box has one, every declared service starts in external mode: name:port, from processes and containers alike, forwards each HTTP request to externalBaseUrl, keeping the path and rewriting Host. That includes a service declared on port 80. Programs running on the box itself also reach the forwarder at localhost:<port> and 127.0.0.1:<port>, except on 80 and 9095 (boxd's own; localhost:80 is the scene entry) and on a port where a box-mode service's process listens. The box's own IP and 172.17.0.1 on that port do not reach it: dial the name. boxd does not hold the service's port, so you can start your own process on its targetPort at any time and then switch the name to it with sandbox_wire. Optional.

  • environment: one of your environments (see sandbox_environments). The box can reach its private addresses (databases, Redis, internal services, through the connector in your network), gets each service's settings as /work/.sbx/env/<service>.env and .sh, and uses its externalBaseUrl unless you pass one, with the same external start. A host name you declare in services that is also one of its addresses (on any port) points at the box instead and leaves reachable; so does an environment IP address inside a declared range on the same port. Each of its host names gets an address of its own in the box as well, so an address on port 80, such as an internal load balancer, works like any other, and a process of yours can listen on the same port number as one of its addresses. The result's environment lists, per service, keysCount and notable (the connection, mode and permission settings to check first; the full keys are in sandbox_environments and the env file), each connection's rttMs, and activeBoxes, your other live boxes on the same environment. Started without environment while the account has some, next names them, since a box cannot gain one later. See Environments.

  • secrets: names from sandbox_secrets to inject as environment variables when the box is claimed. Nothing is injected unless listed; more can be added later with sandbox_secrets.

  • idleTimeoutMin: stop the box for good (/work is gone) after this many minutes without use (a tool call that acts on the box, or a wake; see Box states), frozen or not; it replaces the default limits. Something that keeps the box awake (a command started with background: true, for up to 1 hour after the last use; a person's takeover; a link connection from another box) holds the stop off until it ends. The count runs from the last use: a person using the box's URLs does not reset it. Their requests keep the box up for at most 2 hours after the last use; then it freezes and, being past its limit, is stopped for good right away. A page load that wakes a frozen box counts as use and restarts the count. 0 or omitted: a frozen box is stopped 24 hours after its last use. sandbox_status and sandbox_list give stopsAt, when these rules will stop the box unless it is used before then; within 30 minutes of it, every tool result for the box ends with a warning (Box states). idleTimeoutMin is capped at 10080 minutes (seven days). Larger values are reduced to 10080 with an explanatory note in the result. A frozen box still counts toward the account's limit of concurrent boxes. Either way, a box freezes after 10 idle minutes (see Box states). A box that keeps being used has no maximum lifetime.

  • startedFrom: snapshot: the box was restored from a snapshot on a microVM host, in a few seconds. Every box runs on a microVM (placement: "microvm" in sandbox_status) and freezes when idle.

  • sceneUrl: https://<id>-<key>.box.parallelsandbox.com. It opens the first declared service, marked web or not: in box mode 127.0.0.1:<targetPort> in the box, in external mode externalBaseUrl; with no services declared it returns 503. Valid until the box stops; a freeze does not change it.

  • services[].url: every service declared with web: true gets a URL of its own, valid until the box stops, which opens that service the same way. When the first declared service is web, its url is sceneUrl itself, https://<id>-<key>.box.parallelsandbox.com; every other web service's is https://<id>-<key>-<targetPort>.box.parallelsandbox.com. Services not marked web have no url. sandbox_status and the REST box list (GET /v1/boxes) return the same URLs. A box whose boxd predates per-service URLs (started before they shipped) returns none; only its sceneUrl works.

  • webUrl: the url of the first service marked web, absent when there is none; valid until the box stops. So it is sceneUrl when the first declared service is web, and otherwise the -<targetPort> URL of the first web service. The app's Use it button opens it, so the order of services does not matter for it. A sandbox_review card opens what that call names in open, not webUrl.

  • Only the services marked web, plus the first declared service through sceneUrl, are reachable from outside the box. A -<port> host whose port belongs to a service not marked web gets 404, the same as a port with no service, so hidden services cannot be discovered by trying ports.

  • These URLs carry a random key, so they cannot be built from the box id: use them exactly as sandbox_start or sandbox_status returns them. A wrong key gets the same 404 as a box that does not exist.

  • There is no login in front of these URLs: the key in the URL is the only protection. Anyone who has one can use that service, and through it whatever the service reaches: a front end in a box with an environment calls your dev databases and internal services for whoever opens it. Share the URLs only with people who should have that. Chat apps fetch a URL pasted into a chat to build a preview: the preview service gets the page, the fetch counts as use of the box, and one that asks for HTML wakes a frozen box like a page load. They stop working when the box stops (503 box is terminated) and answer 503 while it is frozen.

  • Each box's URLs have a host of their own, so they cannot be registered with an identity provider in advance. For sign-in (SSO, OAuth) on a box, run the box's copy with the login your team uses locally, or add the box's exact URL to a dev client's allowed redirect URIs while the box runs. Never register a wildcard such as https://*.box.parallelsandbox.com/*: every account's boxes share that domain. See Sign-in on a box URL.

  • Requests through any of these URLs reach the process with Host: 127.0.0.1:<targetPort>, the public host in X-Forwarded-Host, the scheme (https) in X-Forwarded-Proto and the client in X-Forwarded-For. Relative redirects work as they are; an app that builds absolute URLs or cookie domains from Host must trust the forwarded headers instead (Express: app.set('trust proxy', true)).

  • A page opened through one of these URLs runs in the person's browser, outside the box. If its code calls an API on another origin, that request reaches the API in the box only if the API's service is marked web (the page must then be given that service's url) and the API answers CORS for the page's origin. The simpler way is a same-origin proxy in the front end's dev server to the API's name in the box, for example Vite's server: { proxy: { '/api': 'http://api.acme.internal:8080' } }: the browser calls /api/... on the page's own origin, and the dev server, running in the box, forwards it to api.acme.internal:8080, /api prefix included. Keep or strip the prefix the way dev does (Team setup).

  • next: a one-line hint for the next step.

Free accounts can run 3 boxes at once, Pro 5, Max 20. Frozen boxes count toward that limit, and it is the account's, shared by every connection. At the limit, sandbox_start fails with 429 plan <plan> allows <n> boxes at once and <m> are running or frozen; frozen boxes still hold a slot, so list them with sandbox_list (GET /v1/boxes) and sandbox_stop one you no longer need., followed by how many of them are frozen and which one the idle rules stop first, and when, if nobody uses it before then. sandbox_list shows which boxes hold the slots.

When no host has room for a new box, sandbox_start fails with 503 no box capacity right now: every host is full and more is being started. Try sandbox_start again in about 5 minutes; a box of size 4 or 8 can take a few minutes longer. No box was created by this call. Another host starts at once; you do not need a smaller size unless you want one. For about 3 minutes right after ParallelSandbox switches box images it says the hosts are switching to a new box image, which takes about 3 minutes instead.

  • waitForCapacitySec (0 to 1800) waits for room inside the call instead of failing at once: while every host is full, the hosts are switching images, or the plan's box limit is reached, sandbox_start tries again every 15 seconds, sends progress (No room for the box yet: … Waited 45s of 5m0s; trying again in 15s.) and returns the box as soon as one starts. No box exists until then. The wait is capped by how long your client says it can wait in one call: the stdio adapter declares its host's tool timeout (PSBX_TOOL_TIMEOUT_SEC, 60 seconds by default, which leaves 25 seconds of waiting; raising it is shown under sandbox_review), and a connection that declares nothing waits at most 25 seconds. When the wait runs out, the error says how long it waited and why it stopped.
  • When a call's result never reaches you (the connection dropped, or your client cancelled the call and shows it as interrupted), the stdio adapter looks for the box: a result that never came back says which box this conversation just started with that name and goal, or that none was started. After an interrupted call, the next sandbox_start with the same name and goal returns the box the interrupted one started instead of starting a second one (call it once more to start another anyway), and any other tool's result ends with a note naming that box.

Every tool and topic is listed in the tool reference.