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

# Instance and preview URLs

> Reach the software running inside an instance over HTTPS, at instance and preview URLs you can derive from the instance id.

Every instance with a default port is reachable at `https://{instanceId}.agent37.app`. The instance id is the DNS label, so instance `ab12cd34ef` lives at `https://ab12cd34ef.agent37.app`: you can construct the URL from the id alone, with no lookup step.

<Info>
  Instance URLs are the Agent API plane: you reach what runs *inside* an instance here, authenticated by the same `sk_live_` key as the hosting API, sent as the `X-Agent37-Key` header. See [Core concepts](/docs/agents-api/concepts).
</Info>

## Every port has a URL

Nothing is declared and nothing is looked up — both URL forms are derivable from the instance id alone:

* **Instance URL** — `https://{instanceId}.agent37.app`, the bare id. It routes to the template's [`default_port`](/docs/agents-api/templates#register-a-workspace-template) (`3737`, the gateway, unless the template says otherwise), and it is the `url` field on every instance object.
* **Preview URL** — `https://{instanceId}-{port}.agent37.app`, for **any** port, no registration needed. Preview URLs serve your own services plus the agent's built-in dashboard, terminal, and file browser.

A port nothing listens on answers with a transport error (`502`) rather than a `404`: the URL always routes; whether something serves is up to the instance.

You can also serve these on your own domain: register a [custom domain](/docs/agents-api/domains) and every URL here — the instance URL, preview URLs, and every public-port hostname — is mirrored at `{label}.yourdomain.com` with the same authentication.

### What `agent37-hermes` serves

| Port   | URL                                        | Serves                                                                                                              |
| ------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `3737` | `https://ab12cd34ef.agent37.app` (default) | The gateway: [chat](/docs/agents-api/chat) at `/v1/responses`, plus sessions, [files](/docs/agents-api/files), models, health |
| `9119` | `https://ab12cd34ef-9119.agent37.app`      | Hermes dashboard (browser)                                                                                          |
| `7681` | `https://ab12cd34ef-7681.agent37.app`      | A shell in the container (browser)                                                                                  |
| `8080` | `https://ab12cd34ef-8080.agent37.app`      | File browser for the workspace (browser)                                                                            |

So for the default [template](/docs/agents-api/templates), chat is just the bare URL plus a path: `POST https://ab12cd34ef.agent37.app/v1/responses`. The dashboard, terminal, and file browser live on preview URLs; mint a [signed URL](#browser-access-with-signed-urls) to open one in a browser.

### What `agent37-openclaw` serves

| Port    | URL                                        | Serves                                                                                                              |
| ------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `3737`  | `https://ab12cd34ef.agent37.app` (default) | The gateway: [chat](/docs/agents-api/chat) at `/v1/responses`, plus sessions, [files](/docs/agents-api/files), models, health |
| `18789` | `https://ab12cd34ef-18789.agent37.app`     | OpenClaw dashboard, its Control UI (browser)                                                                        |
| `7681`  | `https://ab12cd34ef-7681.agent37.app`      | A shell in the container (browser)                                                                                  |
| `8080`  | `https://ab12cd34ef-8080.agent37.app`      | File browser for the workspace (browser)                                                                            |

Same shape as Hermes: chat is the bare URL plus a path, and the dashboard, terminal, and file browser sit on preview URLs. The dashboard here is OpenClaw's own Control UI, served on `18789` instead of Hermes's `9119`.

## Authentication

A port accepts either credential, so you can reach the same port two ways:

* **API key header** (`X-Agent37-Key`) for API calls.
* **Signed URL** for handing a browser tab to a person.

There is a third option for callers that can't send either — webhooks, most commonly: give the port its own permanent unauthenticated URL. See [Public ports](/docs/agents-api/public-ports).

### API key header (programmatic)

Every request to an instance URL can carry the same workspace API key as the hosting API, sent raw in the `X-Agent37-Key` header:

```
X-Agent37-Key: sk_live_...
```

The platform edge authenticates the key, checks that the instance belongs to your workspace, and forwards the request to the instance's port. A request without a credential gets `401`; a key from another workspace gets `404`.

Requests are forwarded to your instance with `Authorization` untouched, so software inside the instance can run its own Bearer authentication; the platform never forwards an `sk_live_` key into an instance. Earlier versions authenticated instance URLs with `Authorization: Bearer sk_live_...`; that form is deprecated, still accepted for existing integrations, and is never forwarded to your instance. Use `X-Agent37-Key`.

The key travels in a header. To open a preview URL like the dashboard, terminal, or file browser in a browser, mint a signed URL instead.

### Browser access with signed URLs

A signed URL is a time-boxed link a browser can open with no header, usually a preview URL. Mint one for any exposed port on the hosting API:

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.agent37.com/v1/instances/ab12cd34ef/signed-url \
    -H "Authorization: Bearer sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{ "port": 9119 }'
  ```

  ```python python theme={null}
  import requests

  r = requests.post(
      "https://api.agent37.com/v1/instances/ab12cd34ef/signed-url",
      headers={"Authorization": "Bearer sk_live_..."},
      json={"port": 9119},
  )
  print(r.json()["url"])
  ```

  ```javascript node theme={null}
  const res = await fetch("https://api.agent37.com/v1/instances/ab12cd34ef/signed-url", {
    method: "POST",
    headers: { Authorization: "Bearer sk_live_...", "Content-Type": "application/json" },
    body: JSON.stringify({ port: 9119 }),
  });
  console.log((await res.json()).url);
  ```

  ```json response theme={null}
  {
    "url": "https://ab12cd34ef-9119.agent37.app/?a37_token=6a2b...e1f0",
    "domain_urls": [],
    "port": 9119,
    "expires_at": 1717999200
  }
  ```
</CodeGroup>

<ResponseField name="url" type="string">
  The browser-openable URL. The first request promotes the token to a short-lived cookie, so the page's own assets and WebSocket connections authenticate without it.
</ResponseField>

<ResponseField name="domain_urls" type="string[]">
  The same link on each of the workspace's active [custom domains](/docs/agents-api/domains), oldest first — the token is bound to the label, not the host, so every entry works. Empty without one.
</ResponseField>

<ResponseField name="port" type="integer">
  The port the URL routes to.
</ResponseField>

<ResponseField name="expires_at" type="integer">
  Unix seconds when the URL stops working. Mint a fresh one when it expires.
</ResponseField>

The instance must be running or sleeping (opening the link is exactly the kind of request that [wakes it](#sleeping-instances-wake-on-request)); any `port` from 1 to 65535 mints. An optional `ttl_seconds` sets how long the URL lives, default `3600` (one hour), clamped to `[60, 86400]` (one minute to one day); pass a short value for a quick preview link or the max for a day-long share link. A missing or invalid port, or a `ttl_seconds` that is not a positive integer, returns `400`; an unknown or cross-workspace instance returns `404`.

The token rides in the `a37_token` query param. The edge consumes it (promoting it to the cookie) and strips it before forwarding, so it never reaches the instance and never collides with a query param your own app uses.

For user-facing browser access, mint the signed URL on demand and hand the link to the browser. The link is the only credential it carries, and it expires.

## Call an instance

Hit the bare URL directly. The gateway's health endpoint is a quick reachability check:

<CodeGroup>
  ```bash curl theme={null}
  curl https://ab12cd34ef.agent37.app/v1/health \
    -H "X-Agent37-Key: sk_live_..."
  ```

  ```python python theme={null}
  import requests

  r = requests.get(
      "https://ab12cd34ef.agent37.app/v1/health",
      headers={"X-Agent37-Key": "sk_live_..."},
  )
  print(r.json())
  ```

  ```javascript node theme={null}
  const res = await fetch("https://ab12cd34ef.agent37.app/v1/health", {
    headers: { "X-Agent37-Key": "sk_live_..." },
  });
  console.log(await res.json());
  ```

  ```json response theme={null}
  { "ok": true, "agent": "hermes", "healthy": true, "hermes": true }
  ```
</CodeGroup>

<Note>
  Use the gateway's `/v1/health` for this, not `/health`: the bare path `/health` is reserved by the platform edge, which answers `{ "ok": true }` itself — without authentication and without reaching your instance.
</Note>

## Sleeping instances wake on request

An [auto-sleep](/docs/agents-api/instances#auto-sleep) instance that has gone to sleep keeps every one of its URLs. A request to any of them wakes it transparently: the edge holds the request while the instance restores, then forwards it, usually well under a second (a few seconds when the wake falls back to a fresh boot). No status polling, no retry logic; the response you get is the response to the request you sent.

A wake can be refused or fail, and those answers use the same flat error shape as the other [transport errors](/docs/agents-api/errors#transport-errors):

| Code                 | HTTP | When                                                                                                                                                                 |
| -------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `instance_suspended` | 402  | The workspace balance is negative and the instance is suspended. No wake is attempted; top up and retry. See [Billing](/docs/agents-api/billing#past-due-and-suspension). |
| `wake_timeout`       | 503  | The wake did not complete within about 3 minutes. The instance keeps waking in the background; retry.                                                                |
| `wake_failed`        | 503  | The wake failed. The instance stays `sleeping` and the next request tries again.                                                                                     |

## HTTP, SSE, and WebSocket

Plain HTTP requests, SSE streams, and WebSocket connections all pass end to end. [Streaming chat](/docs/agents-api/streaming) with `stream: true` works on the bare URL, and the connection stays open until the server closes it after the terminal event. WebSocket upgrades pass through too: that is what the browser terminal uses for its interactive shell.

## Private sandboxes

A [template](/docs/agents-api/templates) whose image listens on nothing still runs fine: with `default_port` omitted, boot is not probed, and every URL simply answers a transport error because nothing serves. Drive the instance from the hosting API with [exec](/docs/agents-api/exec), which runs shell commands inside it without touching any port.
