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

# Sandbox Runtime URLs

> Understand runtime URLs, exposed ports, and authenticated browser access

Sandboxes have real URL endpoints when you start it. No extra configuration required. Pick your favorite framework and get started.

Sandboxes have two types of URLs:

* `runtime` - This is the URL you use to access the sandbox's runtime API. It is reserved on port 4001.
* `custom` - This is any other port you wish to expose within the sandbox that is not reserved. You can set up any process you want on the sandbox on the specified port.

## Runtime URL vs Custom Port URL

Every running sandbox includes a runtime base URL for API calls.

<CodeGroup>
  ```typescript Node.js theme={null}
  const sandbox = await client.sandboxes.create({
    imageName: "node",
  });

  console.log("Runtime URL:", sandbox.runtime.baseUrl);

  // The SDK sends this to sandbox.runtime.baseUrl + /sandbox/exec for you.
  const result = await sandbox.exec(`node -e 'console.log("hello from runtime")'`);
  console.log(result.stdout.trim());
  ```

  ```python Python theme={null}
  from hyperbrowser.models import CreateSandboxParams

  sandbox = client.sandboxes.create(
      CreateSandboxParams(
          image_name="python",
      )
  )

  print(f"Runtime URL: {sandbox.runtime.base_url}")

  # The SDK sends this to sandbox.runtime.base_url + /sandbox/exec for you.
  result = sandbox.exec("""python3 -c 'print("hello from runtime")'""")
  print(result.stdout.strip())
  ```

  ```bash cURL theme={null}
  create_response=$(
    curl -sS https://api.hyperbrowser.ai/api/sandbox \
      -H "x-api-key: $HYPERBROWSER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "imageName": "node"
      }'
  )

  runtime_base_url=$(jq -r '.runtime.baseUrl' <<<"$create_response")
  runtime_token=$(jq -r '.token' <<<"$create_response")

  echo "Runtime URL: $runtime_base_url"

  curl -sS \
    "$runtime_base_url/sandbox/exec" \
    -H "Authorization: Bearer $runtime_token" \
    -H "Content-Type: application/json" \
    -d '{"command":"printf \"%s\\n\" \"hello from runtime\""}' \
    | jq -r '.result.stdout'
  ```
</CodeGroup>

<Note>
  SDK handles the sandbox runtime layer for you URL for you when you use the runtime functions, see [Node SDK
  sandboxes](/sdks/node#sandboxes) and [Python SDK sandboxes](/sdks/python#sandboxes).
</Note>

To make a service reachable, start it inside the sandbox and expose its port.

For example:

* `runtime.baseUrl` looks like `https://<runtime-host>/`
* exposed port `3000` looks like `https://3000-<runtime-host>/`

The SDK can derive that URL locally:

<CodeGroup>
  ```typescript Node.js theme={null}
  sandbox.getExposedUrl(3000);
  ```

  ```python Python theme={null}
  sandbox.get_exposed_url(3000)
  ```
</CodeGroup>

## Example: Expose a Public HTTP Server

<Note>
  The example below provides a public URL which you can use for running your own services inside the sandbox.
  For sensitive workloads, treat this as any other publicly accessible endpoint and authenticate your requests properly.
</Note>

Start a simple HTTP server inside the sandbox, expose port `3000`, and call it from outside the sandbox.

<CodeGroup>
  ```typescript Node.js theme={null}
  const sandbox = await client.sandboxes.create({
    imageName: "node",
  });

  const server = await sandbox.processes.start(`node -e "const http = require('http');

  http
    .createServer((_, res) => {
      res.writeHead(200, { 'content-type': 'text/plain' });
      res.end('hello from sandbox');
    })
    .listen(3000, '0.0.0.0')"`);

  await new Promise((resolve) => setTimeout(resolve, 1000));

  const exposure = await sandbox.expose({
    port: 3000,
    auth: false,
  });

  console.log("Service URL:", exposure.url);

  const response = await fetch(exposure.url);
  console.log(await response.text());
  ```

  ```python Python theme={null}
  import time
  from urllib.request import urlopen

  from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams

  sandbox = client.sandboxes.create(
      CreateSandboxParams(
          image_name="python",
      )
  )

  server = sandbox.processes.start(
      '''python3 -c "from http.server import BaseHTTPRequestHandler, HTTPServer

  class Handler(BaseHTTPRequestHandler):
      def do_GET(self):
          body = b'hello from sandbox'
          self.send_response(200)
          self.send_header('Content-Type', 'text/plain')
          self.send_header('Content-Length', str(len(body)))
          self.end_headers()
          self.wfile.write(body)

  HTTPServer(('0.0.0.0', 3000), Handler).serve_forever()"'''
  )

  time.sleep(1)

  exposure = sandbox.expose(
      SandboxExposeParams(
          port=3000,
          auth=False,
      )
  )

  print(f"Service URL: {exposure.url}")

  with urlopen(exposure.url) as response:
      print(response.read().decode())
  ```
</CodeGroup>

## Auth Enabled (`auth: true`)

When `auth` is `true`, the exposed custom URL now requires the auth token to be passed on each request.

<AccordionGroup>
  <Accordion title="Backend Calls (`exposure.url`)">
    Call `exposure.url` programmatically and pass the sandbox bearer token in
    the `Authorization` header.
  </Accordion>

  <Accordion title="Browser Access (`exposure.browserUrl`)">
    Open `exposure.browserUrl` in a browser when you need a user-facing flow
    for an auth-protected endpoint.
  </Accordion>

  <Accordion title="`browserUrl` Bootstrap">
    `browserUrl` performs auth bootstrap first and then redirects to the path in
    `next`.
  </Accordion>

  <Accordion title="Public Endpoint (`auth: false`)">
    Use `auth: false` when you want open browser access and keep auth/authorization
    logic inside your own application.
  </Accordion>
</AccordionGroup>

### Backend Workflow (Bearer Token)

This flow is best for backend automation and API calls. Call the protected exposed URL with the sandbox bearer token.

<CodeGroup>
  ```typescript Node.js theme={null}
  const sandbox = await client.sandboxes.create({
    imageName: "node",
  });

  await sandbox.processes.start(`node -e "const http = require('http');

  http
    .createServer((req, res) => {
      if (req.url === '/api/status') {
        res.writeHead(200, { 'content-type': 'application/json' });
        res.end(JSON.stringify({ ok: true }));
        return;
      }
      res.writeHead(404);
      res.end('not found');
    })
    .listen(3000, '0.0.0.0')"`);

  await new Promise((resolve) => setTimeout(resolve, 1000));

  const exposure = await sandbox.expose({
    port: 3000,
    auth: true,
  });

  const detail = await sandbox.info();
  const apiUrl = new URL("/api/status", exposure.url);

  const response = await fetch(apiUrl, {
    headers: {
      Authorization: `Bearer ${detail.token}`,
    },
  });

  console.log(await response.text());
  ```

  ```python Python theme={null}
  import time
  from urllib.parse import urljoin
  from urllib.request import Request, urlopen

  from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams

  sandbox = client.sandboxes.create(
      CreateSandboxParams(
          image_name="python",
      )
  )

  sandbox.processes.start(
      '''python3 -c "from http.server import BaseHTTPRequestHandler, HTTPServer

  class Handler(BaseHTTPRequestHandler):
      def do_GET(self):
          if self.path == '/api/status':
              body = b'{"ok": true}'
              self.send_response(200)
              self.send_header('Content-Type', 'application/json')
              self.send_header('Content-Length', str(len(body)))
              self.end_headers()
              self.wfile.write(body)
              return

          self.send_response(404)
          self.end_headers()

      def log_message(self, format, *args):
          pass

  HTTPServer(('0.0.0.0', 3000), Handler).serve_forever()"'''
  )

  time.sleep(1)

  exposure = sandbox.expose(
      SandboxExposeParams(
          port=3000,
          auth=True,
      )
  )

  detail = sandbox.info()
  api_url = urljoin(exposure.url, "/api/status")

  request = Request(
      api_url,
      headers={"Authorization": f"Bearer {detail.token}"},
  )

  with urlopen(request) as response:
      print(response.read().decode())
  ```

  ```bash cURL theme={null}
  curl "$EXPOSED_URL/api/status" \
    -H "Authorization: Bearer $SANDBOX_TOKEN"
  ```
</CodeGroup>

### Browser Workflow (Preview URL Redirect)

Use the auth-enabled endpoint to generate a protected preview URL for browser access.

<CodeGroup>
  ```typescript Node.js theme={null}
  const sandbox = await client.sandboxes.create({
    imageName: "node",
  });

  await sandbox.processes.start(`node -e "const http = require('http');

  http
    .createServer((req, res) => {
      if (req.url === '/preview') {
        res.writeHead(200, { 'content-type': 'text/html' });
        res.end('<h1>Sandbox Preview</h1><p>Protected by sandbox auth.</p>');
        return;
      }
      res.writeHead(404);
      res.end('not found');
    })
    .listen(3000, '0.0.0.0')"`);

  await new Promise((resolve) => setTimeout(resolve, 1000));

  const exposure = await sandbox.expose({
    port: 3000,
    auth: true,
  });

  const browserUrl = new URL(exposure.browserUrl);
  browserUrl.searchParams.set("next", "/preview");

  console.log("Open preview URL:", browserUrl.toString());
  ```

  ```python Python theme={null}
  import time
  from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse

  from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams

  sandbox = client.sandboxes.create(
      CreateSandboxParams(
          image_name="python",
      )
  )

  sandbox.processes.start(
      '''python3 -c "from http.server import BaseHTTPRequestHandler, HTTPServer

  class Handler(BaseHTTPRequestHandler):
      def do_GET(self):
          if self.path == '/preview':
              body = b'<h1>Sandbox Preview</h1><p>Protected by sandbox auth.</p>'
              self.send_response(200)
              self.send_header('Content-Type', 'text/html')
              self.send_header('Content-Length', str(len(body)))
              self.end_headers()
              self.wfile.write(body)
              return

          self.send_response(404)
          self.end_headers()

      def log_message(self, format, *args):
          pass

  HTTPServer(('0.0.0.0', 3000), Handler).serve_forever()"'''
  )

  time.sleep(1)

  exposure = sandbox.expose(
      SandboxExposeParams(
          port=3000,
          auth=True,
      )
  )

  parsed = urlparse(exposure.browser_url)
  query = dict(parse_qsl(parsed.query))
  query["next"] = "/preview"
  browser_url = urlunparse(parsed._replace(query=urlencode(query)))

  print(f"Open preview URL: {browser_url}")
  ```
</CodeGroup>

<Note>
  `browserUrl` is not the raw port API URL. It is a bootstrap URL that sets the
  auth cookie and then redirects to `next`. Use `url` for programmatic clients,
  and use `browserUrl` when you want to land a browser on a protected page.
</Note>
