> Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

---
title: "Preview URLs"
description: "Expose sandbox ports to the internet"
---

A preview URL is a public HTTPS endpoint that routes traffic to a port inside your sandbox. Start a web server and access it immediately via the sandbox's domain.

## Quick Access

Every sandbox has a built-in `domain` property that gives you instant access to port 80 without creating a preview URL explicitly:

```typescript
const sandbox = await Sandbox.create();
await sandbox.exec.run("python3 -m http.server 80 &");

console.log(sandbox.domain);
// → sb-abc123-p80.workers.opencomputer.dev

// For other ports:
console.log(sandbox.getPreviewDomain(3000));
// → sb-abc123-p3000.workers.opencomputer.dev
```

The `domain` and `getPreviewDomain(port)` properties construct the preview hostname directly — no API call needed. Traffic is routed through the control plane proxy to the correct sandbox.

## Authentication

Preview URLs are public by default — anyone who can guess or learn the hostname can hit your sandbox's port. To require a bearer token in front of every request, opt in at create time with `previewAuth`.

When enabled, the control plane validates an `Authorization: Bearer <token>` (or `X-OC-Preview-Token: <token>`) header on every preview-URL request. Missing or wrong → 401. The check happens before traffic ever reaches your sandbox, so a brute-force run also can't wake a hibernated sandbox.

### Enable at create time

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    const sandbox = await Sandbox.create({
      previewAuth: { scheme: "bearer", token: "auto" },
    });

    // previewAuthToken is returned exactly once — store it somewhere durable.
    console.log(sandbox.previewAuthToken);
    // → "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI"
    ```
  </Tab>

  <Tab title="Python">
    ```python
    sandbox = await Sandbox.create(
        preview_auth={"scheme": "bearer", "token": "auto"},
    )

    # preview_auth_token is returned exactly once — store it somewhere durable.
    print(sandbox.preview_auth_token)
    # → "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI"
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox create --preview-auth
    # Created sandbox sb-abc123 (status: running)
    # Preview auth token (shown once): qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI
    ```
  </Tab>
</Tabs>

`token: "auto"` (or omitting the field) tells the server to generate a 256-bit random token. To bring your own — useful when your gateway already has a secret it can share with both ends — pass an explicit string of at least 16 characters:

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    await Sandbox.create({
      previewAuth: { scheme: "bearer", token: process.env.GATEWAY_TOKEN! },
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await Sandbox.create(
        preview_auth={"scheme": "bearer", "token": os.environ["GATEWAY_TOKEN"]},
    )
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc sandbox create --preview-auth-token "$GATEWAY_TOKEN"
    ```
  </Tab>
</Tabs>

### Making authenticated requests

Send the token on every request to the preview URL. Two header forms are accepted; pick whichever fits your client:

```bash
curl -H "Authorization: Bearer $TOKEN" https://sb-abc123-p3000.workers.opencomputer.dev/
curl -H "X-OC-Preview-Token: $TOKEN"   https://sb-abc123-p3000.workers.opencomputer.dev/
```

The token gates **every port** on the sandbox — there's one token per sandbox, shared across all preview URLs you create on it.

### Rotation

When you need to roll the token — exposure, scheduled rotation, employee offboarding — call `rotate`. The old token stops working immediately and the new plaintext is returned exactly once:

<Tabs items={["TypeScript", "Python", "CLI"]}>
  <Tab title="TypeScript">
    ```typescript
    const newToken = await sandbox.rotatePreviewAuthToken();
    // sandbox.previewAuthToken is also updated in place.
    ```
  </Tab>

  <Tab title="Python">
    ```python
    new_token = await sandbox.rotate_preview_auth_token()
    # sandbox.preview_auth_token is also updated in place.
    ```
  </Tab>

  <Tab title="CLI">
    ```bash
    oc preview rotate-auth sb-abc123
    # New preview auth token (shown once): <new token>
    ```
  </Tab>
</Tabs>

There is no zero-downtime dual-token mode in v1 — roll out the new token to your caller before discarding the old one, or expect a brief window of 401s during the swap.

### What's stored and what's not

* Only the **SHA-256 hash** of the token is persisted. The server never sees the plaintext after the create or rotate response returns.
* There is no GET endpoint that returns the token. If you lose it, your only options are to rotate (mint a new one) or recreate the sandbox.
* A sandbox created **without** `previewAuth` has open preview URLs — exactly the same as before this feature existed. The feature is fully opt-in and backwards compatible.

### Where the check happens

Authentication is enforced by the cell's control plane immediately before the request is proxied to the worker. That means:

* **Hibernated sandboxes** stay hibernated on missing/wrong tokens — bad tokens can't burn wake capacity.
* **Self-hosted deployments** without the Cloudflare edge get the same gate for free — the check follows the sandbox, not the front door.

## Explicit Preview URLs

For tracking, custom domains, or auth configuration, use the preview URL API:

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    import { Sandbox } from "@opencomputer/sdk";

    const sandbox = await Sandbox.create();
    await sandbox.exec.run("npx create-next-app@latest /app --yes", { timeout: 120 });
    sandbox.exec.start("npm run dev -- --port 3000", { cwd: "/app" });

    const preview = await sandbox.createPreviewURL({ port: 3000 });
    console.log(preview.hostname); // sb-abc123-p3000.workers.opencomputer.dev
    ```
  </Tab>

  <Tab title="Python">
    ```python
    from opencomputer import Sandbox

    sandbox = await Sandbox.create()
    await sandbox.exec.run("npx create-next-app@latest /app --yes", timeout=120)
    await sandbox.exec.start("npm run dev -- --port 3000", cwd="/app")

    preview = await sandbox.create_preview_url(port=3000)
    print(preview["hostname"])
    ```
  </Tab>
</Tabs>

## API Reference

### Create Preview URL

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const preview = await sandbox.createPreviewURL({
      port: 3000,
      domain: "preview.myapp.com", // optional custom domain
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python
    preview = await sandbox.create_preview_url(
        port=3000,
        domain="preview.myapp.com",
    )
    ```
  </Tab>
</Tabs>

| Parameter    | Type   | Required | Description                        |
| ------------ | ------ | -------- | ---------------------------------- |
| `port`       | number | Yes      | Container port to expose (1–65535) |
| `domain`     | string | No       | Custom domain                      |
| `authConfig` | object | No       | Authentication configuration       |

### List Preview URLs

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    const previews = await sandbox.listPreviewURLs();
    for (const p of previews) {
      console.log(`Port ${p.port}: https://${p.hostname}`);
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python
    previews = await sandbox.list_preview_urls()
    for p in previews:
        print(f"Port {p['port']}: https://{p['hostname']}")
    ```
  </Tab>
</Tabs>

### Delete Preview URL

<Tabs items={["TypeScript", "Python"]}>
  <Tab title="TypeScript">
    ```typescript
    await sandbox.deletePreviewURL(3000);
    ```
  </Tab>

  <Tab title="Python">
    ```python
    await sandbox.delete_preview_url(3000)
    ```
  </Tab>
</Tabs>

## Custom Domains

Pass a `domain` when creating a preview URL. SSL is provisioned automatically via Cloudflare. Custom domain verification is configured at the organization level through the dashboard — not through the SDK.

## Persistence

Preview URLs persist across hibernation and wake cycles. If you hibernate a sandbox and wake it later, the preview URLs are still active.

## Multiple Ports

Expose multiple services from the same sandbox:

```typescript
// Frontend
await sandbox.createPreviewURL({ port: 3000 });
// API server
await sandbox.createPreviewURL({ port: 8080 });
// Database admin
await sandbox.createPreviewURL({ port: 5432 });
```

<Tip>
  CLI equivalent: [`oc preview`](/cli/preview). Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#preview-urls).
</Tip>
