Skip to main content
Preview URLs expose one sandbox port through Nullspace ingress. The SDK returns signed HTTP and WebSocket URLs when edge ingress is active; use those returned URLs directly instead of constructing them by hand. For compatibility, the Python SDK still exposes helpers named sandbox.get_url(port) and sandbox.get_websocket_url(port). They return preview URLs. Older docs and scripts may call this surface “public URLs”; the current product name is Preview URLs.

Start a server

from nullspace import Sandbox

with Sandbox.create(template="base", timeout=300) as sandbox:
    sandbox.files.write("/workspace/index.html", "hello\n")
    server = sandbox.commands.run(
        "python3 -m http.server 8080 --bind 0.0.0.0",
        shell=True,
        cwd="/workspace",
        background=True,
    )
    try:
        print(f"Preview URL: {sandbox.get_url(8080)}")
        input("Open the URL, then press Enter to stop the server and destroy the sandbox...")
    finally:
        server.kill()

Host information

info = sandbox.get_host_info(8080)
print(info.host)
print(info.url)
print(info.websocket_url)
print(info.access_token_expires_at)
sandbox.get_url(port) returns info.url when a signed HTTP URL is present and otherwise falls back to the bare host with the API scheme. get_websocket_url does the same for WebSocket URLs.

WebSocket URL for exposed ports

ws_url = sandbox.get_websocket_url(8080)
print(ws_url)
Use this for WebSocket servers running inside the sandbox. Direct HTTP preview continuation cookies do not authorize WebSocket upgrades; use the signed websocket_url returned by the SDK or CLI. For SSH, use SSH Access instead of preview URL routing. Only older port-22 fallback deployments use SSH as an exposed WebSocket service.

Behavior

The returned URL includes a signed edge token. Treat it as a bearer credential and rotate by requesting a fresh preview URL when needed. After a browser opens the signed HTTP URL, Nullspace edge can use a scoped HTTP-only continuation cookie so normal navigation, refreshes, relative links, redirects, and assets do not need to preserve the query token. The service inside the sandbox must bind to 0.0.0.0, not only 127.0.0.1.

Self-hosted ingress

The single-host OSS appliance uses API-compatible ingress, not apps/edge. In localhost/no-domain mode, the appliance leaves NULLSPACE_PUBLIC_HOSTNAME unset. Caddy serves the console at /console and proxies API/WebSocket routes to the local API, but signed public preview hostnames are not available. Use sandbox.get_host(port) or sandbox.get_host_info(port) for direct local port mappings in private operator-only environments. In owned-domain mode, operators set NULLSPACE_PUBLIC_HOSTNAME to a DNS name they control and configure wildcard DNS so preview subdomains resolve to the single host. Signed preview URLs use https://{PORT}-{SANDBOX_ID}.${NULLSPACE_PUBLIC_HOSTNAME}/?edge_token=... for HTTP and the matching wss:// URL for WebSocket. Caddy obtains exact-host preview certificates on demand through a loopback-only API ask endpoint, then proxies those hosts to nullspace-api while preserving the original Host and forwarded client/proto headers so the API can parse the sandbox and port without apps/edge. The launch gate records the difference explicitly. In --mode localhost, it asserts direct get_host/get_host_info mappings and no signed URL metadata. In --mode owned-domain, it asserts signed HTTP and WebSocket preview URL metadata and Caddy routing without apps/edge.

Embedding

Direct preview links are not iframe targets. Their browser continuation cookie is host-scoped and uses SameSite=Lax. No SameSite=None direct preview cookie mode is available for third-party iframe embedding. Customer-run preview proxies are the supported path for embedding, custom browser sessions, and custom-domain presentation.

Warnings and CORS

Nullspace does not add a preview warning or interstitial before serving direct preview traffic. Treat signed preview URLs as bearer credentials and share them only with trusted recipients. Preview CORS and browser response headers come from the sandbox service or the customer-run proxy in front of it. Nullspace preview edge does not inject a separate CORS policy for direct preview links.

Expiry, inventory, and revocation

Use sandbox.create_signed_preview_url(port, expires_in_seconds=...) or nullspace sandbox preview-url create <id> <port> --expires 15m when you need a durable grant with an explicit expiry. Use sandbox.list_preview_urls() or nullspace sandbox preview-url list <id> to inspect active and recent grants, and sandbox.revoke_preview_url(grant_id) or nullspace sandbox preview-url revoke <id> <grant-id> to revoke a grant before it expires. Grant inventory is token-redacted. It includes status, expiry, first and last use, validation use count, HTTP request count, WebSocket connection count, byte-in and byte-out counters, last error code/time, and disabled time when an operator has temporarily disabled a grant. Operator audit records use the same safe identifiers and never store raw preview tokens. Direct preview URLs use query-token bootstrap auth for HTTP and WebSocket URLs. Custom preview proxy targets use header-token auth instead, so customer-run proxies can keep Nullspace bearer credentials out of browser-visible URLs. Agent service deployments reuse the same ingress path. nullspace agent url and the SDK service URL helper return a dynamic URL only after the service is ready and permissions.public_url is enabled. Edge environments return either a host-style or path-style URL depending on operator configuration; both forms can include a short-lived edge_token query parameter. Do not store these URLs in deployment config, logs, or source files. If service permissions disable public traffic, ingress may also require the x-nullspace-traffic-access-token header documented in Access control. The Console Preview tab exposes the same workflow for a sandbox: create a direct preview link, open it in a new browser tab, copy raw secret values only after confirmation, create a custom preview proxy target, inspect grant inventory, revoke grants, and run readiness diagnostics.

Port policy

Preview URLs support normal application ports in the 1-65535 range, including privileged HTTP ports such as 80 and 443. Generic preview URLs do not expose platform-owned guest ports:
PortUse
22SSH access; use SSH Access instead of preview URLs.
5900-5999Desktop/VNC traffic; use the managed desktop viewer.
Host service ports such as the API, host-agent, and edge listener ports are operator configuration, not guest preview policy. Common app ports such as 3000 and 8080 remain valid sandbox preview ports.

Readiness

Creating or resolving a preview URL confirms that routing exists; it does not wait for the server process inside the sandbox to listen. Use the SDK or CLI wait helpers when startup is asynchronous:
readiness = sandbox.wait_for_preview(8080, timeout_secs=30)
if not readiness.ready:
    print(readiness.error)
    print(readiness.suggested_action)
nullspace sandbox url sb_123 8080 --wait --json
nullspace sandbox preview-url create sb_123 8080 --expires 15m --wait --json
If readiness fails, confirm the service is running on the requested port and binds to 0.0.0.0 inside the sandbox rather than only 127.0.0.1. Use Custom Preview Proxy when you need your own domain, app session checks, or proxy middleware in front of a sandbox preview. That flow returns a marker-only upstream URL and a header token for your proxy to send to Nullspace edge, so browser-visible URLs do not contain Nullspace bearer credentials. A managed custom preview domain workflow is not part of the current launch. If the sandbox was created with on_timeout="pause", auto_resume=True, HTTP and WebSocket requests to signed preview URLs can wake a paused sandbox. When auto-resume is disabled or the wake cannot finish in time, callers receive 503 Service Unavailable with Retry-After: 5 and JSON fields for code, retryable, and suggested_action.

Troubleshooting

Code or symptomWhat to do
unsupported_preview_portUse an application port in 1-65535 except 22 and 5900-5999.
preview_service_not_readyConfirm the server is running on that port and binding to 0.0.0.0; use sandbox.wait_for_preview(port) while it starts.
edge_token_missingUse the full signed URL returned by sandbox.get_url(port) or request a fresh signed preview URL.
edge_token_expired, preview_grant_revokedRequest a fresh preview URL before retrying the direct browser request.
preview_grant_disabled, preview_proxy_token_disabled, preview_sandbox_disabledPreview access was disabled by an operator control. Use another route or ask an operator to re-enable preview traffic before retrying.
preview_proxy_token_missingForward the x-nullspace-preview-proxy-token header returned by create_preview_proxy_target.
preview_proxy_token_expired, preview_proxy_token_revokedCreate a fresh preview proxy target and update the proxy header token.
rate_limit_exceededWait for the Retry-After window before retrying. Repeated 429s usually mean too many requests from the same preview grant, port, transport, and client address.
edge_runtime_host_unreachableCheck readiness with sandbox.wait_for_preview(port), nullspace sandbox preview-url create <id> <port> --wait, or nullspace sandbox url <id> <port> --wait, then retry.
sandbox_paused_auto_resume_disabledResume the sandbox manually or recreate it with on_timeout="pause", auto_resume=True.
Localhost self-host returns no signed URL metadataExpected when NULLSPACE_PUBLIC_HOSTNAME is unset. Use direct get_host_info() mappings or configure owned-domain mode.
Owned-domain self-host returns direct mappingsConfirm NULLSPACE_PUBLIC_HOSTNAME, NULLSPACE_EDGE_PUBLIC_BASE_URL, NULLSPACE_EDGE_ACCESS_TOKEN_SIGNING_KEY, and NULLSPACE_PUBLIC_INGRESS_MODE=api_compat, then rerun nullspace-host launch-gate --mode owned-domain.
Auto-resume timeout or control-plane unreachableRetry after Retry-After; if it repeats, check runtime capacity and sandbox startup logs.

Create-time network controls

Sandbox create accepts a network dictionary for deployment-supported network policy controls and an internet_access flag for outbound internet access:
sandbox = Sandbox.create(
    template="base",
    internet_access=True,
    network={
        "allow_public_traffic": True,
        "mask_request_host": "localhost:${PORT}",
        "deny_out": ["0.0.0.0/0"],
        "allow_out": ["10.0.0.0/8"],
    },
)
Use Access control for private preview URLs, traffic tokens, outbound network policy, and Host header masking. Use Token model for the difference between API keys, edge preview tokens, preview continuation cookies, proxy tokens, and private traffic tokens.