Ports and previews
Reach a web server running in a sandbox through a signed preview URL on usercontent.b3os.org, private by default or public, with base-path handling.
A server in a sandbox is reachable from outside only on ports you declare when you create the sandbox, through a preview URL on usercontent.b3os.org.
Declare ports
json{ "template": "base", "ports": [{ "port": 3000 }, { "port": 8080, "public": true }] }
Up to 8 ports. A port you did not declare answers port_not_exposed.
Private ports
A port is private by default: nobody reaches it without a URL you mint for it. Ask for one when you need it:
bashcurl -s -X POST "https://beta-api.b3os.org/v1/sandboxes/$SBX/ports/3000/url" \ -H "Authorization: Bearer $B3OS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ttl_s": 600}'
tsconst preview = await b3os.sandboxes.createPortUrl(sbx, 3000, { ttl_s: 600 });console.log(preview.url, preview.expires_at);
pythonpreview = b3os.sandboxes.create_port_url(sbx, 3000, {"ttl_s": 600})print(preview["url"], preview["expires_at"])
textStart the dev server in my sandbox on port 3000 in the background.
The agent starts the server with sandbox_exec. The MCP server has no tool that mints preview URLs; mint one from code, or with b3os sandbox url $SBX 3000.
json{ "object": "port_url", "port": 3000, "public": false, "url": "https://usercontent.b3os.org/sbx/sbx-01m5t68q1b590rj801nbzsqzkk/3000/pt1.n1.1792955460.tjsgjiRC3sSYEwneW62TSvNqd-Jo_zzFI5zFYkB_YQBNl0CiBzI9xgoV6EJD_5YEmh3HbzSmb3acL8mY1LmYCg/", "expires_at": "2026-10-25T19:11:00.000Z"}
The token in the path is signed by the node and expires after ttl_s (10 minutes by default, from one minute to 24 hours). Anyone with the URL reaches the port until then, with no cookie or other credential, so share it like a password.
Public ports
A port declared public: true gets a long-lived URL in the sandbox's ports[].url, ready as soon as the sandbox is. Its token lasts 7 days from the sandbox's creation, so the URL stays fixed; a sandbox that runs longer gets a new one.
What your server sees
The preview forwards GET (WebSocket upgrades included), HEAD, POST, PUT, PATCH, DELETE and OPTIONS; other methods get 405. Your server receives the request at the path after the token, with the query string unchanged, plus these headers:
| Header | Value |
|---|---|
X-Forwarded-Proto | https |
X-Forwarded-Host | usercontent.b3os.org |
X-Forwarded-Prefix | /sbx/<host_id>/<port>/<token>, the full base path of this request |
Cookies are removed in both directions, as everywhere on usercontent.b3os.org. Unless your server sets Referrer-Policy, the preview adds Referrer-Policy: strict-origin, so the token in the path does not leak to other sites. A response must start within 65 seconds; a WebSocket or a streamed body can stay open up to an hour.
Base paths
Your app lives under a path prefix, not at the root of a domain. Two rules keep links working:
- Emit relative URLs where you can. A URL that ends at the token redirects to the same URL plus
/, so relative links resolve under the prefix. - For absolute paths, build them from
X-Forwarded-Prefixon each request.B3OS_PORT_BASE_PATHin the sandbox's environment is only the fixed start,/sbx/<host_id>, because the port and the token follow it and the token differs for every minted URL.
In v0.1 every preview shares the usercontent.b3os.org origin. A page served from one preview can send requests to any other preview URL it knows. Keep secrets out of previews. Per-port hostnames are planned.
When it fails
| Code | Status | When |
|---|---|---|
signed_url_invalid | 403 | The token is malformed, expired, or for another sandbox or port |
port_not_exposed | 404 | The port was not declared when the sandbox was created |
sandbox_not_running | 409 | The sandbox is paused or stopped |