> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superserve.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Preview URLs

> Publish sandbox ports with public access or per-port authentication.

Preview URLs route a hostname such as
`https://3000-{sandbox-id}.sandbox.superserve.ai` to a process listening inside
your sandbox. Use an explicit preview policy and publish only the ports you
intend to expose.

## Access policies

The sandbox's `previewAccess` / `preview_access` selects strict publication and
sets the default access for each newly published port:

| Sandbox policy  | Which ports are reachable?      | Default for a new port       |
| --------------- | ------------------------------- | ---------------------------- |
| `public`        | Explicitly published ports only | Public, no credential        |
| `private`       | Explicitly published ports only | Private, credential required |
| `legacy_public` | Every listening port            | Not applicable               |

Every published port stores its own `public` or `private` access mode. Changing
the sandbox default does not rewrite existing ports, so public and private ports
can coexist in one sandbox.

`legacy_public` exists only for sandboxes created before the publication
migration. New sandboxes default to strict `public` when the field is omitted.
The compatibility mode cannot be selected through create or update APIs.

<Warning>
  Moving a legacy sandbox to `public` or `private` immediately closes every port
  that has not been published. Publish the intended ports before changing the
  policy if you need a seamless transition.
</Warning>

## Choose the sandbox default

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  const sandbox = await Sandbox.create({
    name: "web-app",
    previewAccess: "private",
  })
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  sandbox = Sandbox.create(
      name="web-app",
      preview_access="private",
  )
  ```
</CodeGroup>

Use `public` when new ports should be internet-accessible by default. Use
`private` when new ports should require a credential by default. You can change
the default later with `update`; already-published ports keep their own mode:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  await sandbox.update({ previewAccess: "private" })
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  sandbox.update(preview_access="private")
  ```
</CodeGroup>

## Publish a port

Start the service, then publish its port. Omit the access option to use the
sandbox default for a new port. An omitted option preserves the mode of an
already-published port; pass an explicit mode to override just that port.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  await sandbox.commands.spawn("npm run dev -- --host 0.0.0.0")
  await sandbox.publishPreviewPort(3000, { access: "private" })

  const url = sandbox.getPreviewUrl(3000)

  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  # AsyncSandbox shown because spawn is an async session API.
  await sandbox.commands.spawn("npm run dev -- --host 0.0.0.0")
  await sandbox.publish_preview_port(3000, access="private")

  url = sandbox.get_preview_url(3000)
  ```
</CodeGroup>

`getPreviewUrl` / `get_preview_url` only constructs the hostname. It does not
publish the port or add credentials. Ports must be integers from `1024` through
`65535`; port `49983` is reserved for sandbox control traffic and cannot be
published.

List the current server-side publication state with `listPreviewPorts()` /
`list_preview_ports()`. The result includes the sandbox default and every
published port's `access`, but never includes tokens.

## Authenticate a private port

Private preview credentials are scoped to one published port. A token for port
`3000` cannot open port `8080` on the same sandbox.

### Machine clients and reverse proxies

Mint a token and send it using the header name returned by the API:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  const credential = await sandbox.getPreviewToken(3000)

  await fetch(sandbox.getPreviewUrl(3000), {
  headers: { [credential.header]: credential.token },
  })

  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  credential = sandbox.get_preview_token(3000)

  response = httpx.get(
      sandbox.get_preview_url(3000),
      headers={credential.header: credential.token},
  )
  ```
</CodeGroup>

Omit the expiry to keep a token valid until that port is rotated or
unpublished. For limited-lived machine credentials, pass `expiresInSeconds` /
`expires_in_seconds` from `1` to `604800` seconds.

### Browsers, iframes, and WebSockets

Browsers cannot attach a custom header to a navigation. Request a signed URL
instead:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  const url = await sandbox.getSignedPreviewUrl(3000, {
    expiresInSeconds: 300,
  })
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  url = sandbox.get_signed_preview_url(3000, expires_in_seconds=300)
  ```
</CodeGroup>

The SDK defaults signed links to 60 seconds. On first navigation, the edge
proxy verifies the query token, stores it in a secure host-scoped cookie, and
redirects to the same URL without the token. Asset requests and WebSocket
upgrades then use the cookie automatically.

<Note>
  Treat signed URLs as secrets until the initial redirect removes their query
  token. Avoid logging them or placing them in analytics and referrer data.
</Note>

## Revoke access

Rotate a port to invalidate all of its existing header tokens, signed links,
and browser cookies without affecting other ports:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  const fresh = await sandbox.rotatePreviewToken(3000)
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  fresh = sandbox.rotate_preview_token(3000)
  ```
</CodeGroup>

Unpublishing closes the port at the edge and revokes its credentials:

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  await sandbox.unpublishPreviewPort(3000)
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vitesse-dark"}}
  sandbox.unpublish_preview_port(3000)
  ```
</CodeGroup>

Unpublish is idempotent and retry-safe. Under a strict policy the edge returns
`404` for an unpublished port before revealing sandbox status or checking a
credential.

## MCP and console

The console's Preview panel uses the same server-backed published-port list. It
mints a signed browser URL for each private port without displaying the
credential; public sibling ports keep clean URLs.

The MCP `sandbox_preview_url` tool publishes the requested port. It returns a
clean URL when that port is public and a one-hour signed URL by default when it
is private. Re-publishing without an access override preserves an existing
port's mode. Set `expires_in_seconds` to choose another private-link lifetime.
