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, andsandbox_statusandsandbox_listreturn 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 withgoal 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)goalis optional.orientation:portrait(390 × 844) orlandscape(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}/orientationover REST switches a running box). Ask forlandscapewhen 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 insandbox_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 carriesorientationNotesaying 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 (restorableUntilinsandbox_status, or insandbox_listwithall: 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, andnextsays so. Passgoalas 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 withsandbox_stopcannot 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 asarch: "amd64"; for arm64 images seesandbox_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; impliesandroid),esp32(PlatformIO for ESP32 with Arduino and ESP-IDF, compile only),python-3.12orpython-3.13(that Python first on PATH aspython,python3andpip; without thempython3is 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) intoolchainsis ignored, andtoolchainsNotein the result says so.services: the services your repo exposes on the box, each with aname, theportcallers dial, and optionallytargetPortandweb.- 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:portreaches your process ontargetPort, which defaults toport: callers keep dialingname:portwhile the process listens ontargetPort. Containers on the default bridge, on user-defined networks and indocker composereach 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 to127.0.0.1is reachable through its URL (seesceneUrlbelow) but not by its name. A container must publish itstargetPort(ports:in compose,-p <targetPort>:<container port>indocker run). - Names that share a
targetPortare one process and switch together insandbox_wire. Since every name has its own address, names on the sameportwith differenttargetPorts are separate services:api.acme.internal:8080andbilling.acme.internal:8080can 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 dialapi.acme.internal:80(orhttp://api.acme.internal/) and the process listens on 8080. WithouttargetPortthe 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, andtargetPortitself 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 needstargetPortfor 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 502nothing 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:80is still boxd's scene entry and answers with the first declared service, assceneUrldoes. 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 inside10.0.0.0/8,172.16.0.0/12,192.168.0.0/16or100.64.0.0/10(such as172.16.0.0/16): connections from the box, containers included, to that range on that port reach your service in the box on itstargetPort(inexternalmode, the forwarder toexternalBaseUrl). 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: truemarks 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 optionalfromPort, makes the entry a link to another of your boxes instead of a service in this one:name:portin this box reachesfromPortin boxfromBox. See Links to other boxes.version, with an optionalcontainerPort, 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.
- From the moment the box starts, each name has an address of its own inside the box, from
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 inexternalmode:name:port, from processes and containers alike, forwards each HTTP request toexternalBaseUrl, keeping the path and rewriting Host. That includes a service declared on port 80. Programs running on the box itself also reach the forwarder atlocalhost:<port>and127.0.0.1:<port>, except on 80 and 9095 (boxd's own;localhost:80is the scene entry) and on a port where abox-mode service's process listens. The box's own IP and172.17.0.1on 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 itstargetPortat any time and then switch the name to it withsandbox_wire. Optional.environment: one of your environments (seesandbox_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>.envand.sh, and uses itsexternalBaseUrlunless you pass one, with the sameexternalstart. A host name you declare inservicesthat is also one of its addresses (on any port) points at the box instead and leavesreachable; 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'senvironmentlists, per service,keysCountandnotable(the connection, mode and permission settings to check first; the fullkeysare insandbox_environmentsand the env file), each connection'srttMs, andactiveBoxes, your other live boxes on the same environment. Started withoutenvironmentwhile the account has some,nextnames them, since a box cannot gain one later. See Environments.secrets: names fromsandbox_secretsto inject as environment variables when the box is claimed. Nothing is injected unless listed; more can be added later withsandbox_secrets.idleTimeoutMin: stop the box for good (/workis 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 withbackground: 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_statusandsandbox_listgivestopsAt, 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).idleTimeoutMinis 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"insandbox_status) and freezes when idle.sceneUrl:https://<id>-<key>.box.parallelsandbox.com. It opens the first declared service, markedwebor not: inboxmode127.0.0.1:<targetPort>in the box, inexternalmodeexternalBaseUrl; with no services declared it returns 503. Valid until the box stops; a freeze does not change it.services[].url: every service declared withweb: truegets a URL of its own, valid until the box stops, which opens that service the same way. When the first declared service isweb, itsurlissceneUrlitself,https://<id>-<key>.box.parallelsandbox.com; every other web service's ishttps://<id>-<key>-<targetPort>.box.parallelsandbox.com. Services not markedwebhave nourl.sandbox_statusand 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 itssceneUrlworks.webUrl: theurlof the first service markedweb, absent when there is none; valid until the box stops. So it issceneUrlwhen the first declared service isweb, and otherwise the-<targetPort>URL of the firstwebservice. The app's Use it button opens it, so the order ofservicesdoes not matter for it. Asandbox_reviewcard opens what that call names inopen, notwebUrl.Only the services marked
web, plus the first declared service throughsceneUrl, are reachable from outside the box. A-<port>host whose port belongs to a service not markedwebgets 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_startorsandbox_statusreturns 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 inX-Forwarded-Host, the scheme (https) inX-Forwarded-Protoand the client inX-Forwarded-For. Relative redirects work as they are; an app that builds absolute URLs or cookie domains fromHostmust 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'surl) 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'sserver: { 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 toapi.acme.internal:8080,/apiprefix 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_starttries 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 undersandbox_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
nameandgoal, or that none was started. After an interrupted call, the nextsandbox_startwith the samenameandgoalreturns 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.