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

# Publish and share surfaces (apps)

> List a conversation's surfaces, list the workspace app catalog, mint a time-limited share link, change visibility, and delete or restore an app.

When an HQ agent builds a live app, the platform records it as a **surface**: a persistent, browser-reachable application backed by the conversation's VM. Each surface has a stable id, a `slug`, a lifecycle `state`, a `declared_port` the in-VM server listens on, a `run_mode`, an `auth_mode`, and a ready-to-open `url`. This guide covers listing a conversation's surfaces, listing the whole workspace's app catalog, minting a time-limited share link, switching a surface between private and public, and the delete / restore lifecycle.

All requests go to `https://api.hq.zone` and authenticate with a personal access token:

```bash theme={null}
export HQ_TOKEN="hq_pat_..."
```

<Note>The endpoints in this guide carry these scopes: reads (listing a conversation's surfaces, listing apps) require `conversations:read`; every mutation (share, visibility, restore, delete) requires `conversations:write`.</Note>

## What a surface is

A surface is the live app row attached to a conversation. The list endpoints return these fields per surface:

* `id` — the surface id used by the share / visibility / restore / delete endpoints.
* `slug` — the URL slug for the app.
* `state` — the lifecycle state. Non-destroyed surfaces are listed; the states you will see include `booting`, `live`, `paused`, `archived`. A `destroyed` surface is filtered out of every listing.
* `declared_port` — the port the app's server is bound to inside the VM.
* `run_mode` — `dev` or `prod`.
* `auth_mode` — `signed_url` (private) or `public`. This is the underlying column; the Apps list also exposes it as a derived `visibility` of `private` or `public`.
* `url` — a ready-to-open, visitor-facing URL: the bare public URL when `auth_mode` is `public`, or a signed plain-host URL when it is `signed_url`.

<Note>There is no `visibility` column on a surface. Visibility is derived from `auth_mode`: `public` maps to visibility `public`, and `signed_url` maps to visibility `private`.</Note>

## List a conversation's surfaces

[List conversation surfaces](/api-reference/conversations/list-surfaces) returns the live application surfaces attached to one conversation, excluding destroyed ones, ordered by most recent activity.

<Steps>
  <Step title="Fetch the surfaces for a conversation">
    ```bash theme={null}
    curl https://api.hq.zone/v1/api/conversations/<CONVERSATION_ID>/surfaces \
      -H "Authorization: Bearer $HQ_TOKEN"
    ```

    The response wraps a `surfaces` array. Each entry carries `id`, `slug`, `state`, `declared_port`, `run_mode`, `auth_mode`, the visitor-facing `url`, and a separate owner-only `dev_url`:

    ```js theme={null}
    // → 200
    // {
    //   "surfaces": [
    //     {
    //       "id": "<SURFACE_ID>",
    //       "slug": "revenue-dashboard",
    //       "state": "live",
    //       "declared_port": 5173,
    //       "run_mode": "dev",
    //       "auth_mode": "signed_url",
    //       "url": "https://...",
    //       "dev_url": "https://..."
    //     }
    //   ]
    // }
    ```
  </Step>
</Steps>

<Tip>The `url` is the visitor-facing view that the Share modal hands out and "open in new tab" points at. The `dev_url` is a separate owner-only developer-embed URL (always signed) that loads the in-browser devtools runtime; it is an empty string when no signing secret is configured. If you are a workspace admin inspecting another member's conversation, add `?view=admin` — this admin read is audited.</Tip>

## List the workspace app catalog

[List apps](/api-reference/conversations/list-apps) returns the workspace-wide catalog of persistent app surfaces, visible to any member of the workspace, excluding destroyed apps and ordered by most recent activity.

```bash theme={null}
curl https://api.hq.zone/v1/api/apps \
  -H "Authorization: Bearer $HQ_TOKEN"
```

The response carries an `apps` array plus the workspace quota (`max_persistent`) and how many apps currently count against it (`used`). Each app includes `id`, `slug`, `state`, a derived `visibility` (`public` or `private`), `run_mode`, `declared_port`, `backing_conversation_id` (null if that conversation was deleted), a visitor-facing `url`, and `created_at` / `last_activity_at` as Unix-seconds timestamps:

```js theme={null}
// → 200
// {
//   "apps": [
//     {
//       "id": "<SURFACE_ID>",
//       "slug": "revenue-dashboard",
//       "state": "live",
//       "visibility": "private",
//       "run_mode": "dev",
//       "declared_port": 5173,
//       "backing_conversation_id": "<CONVERSATION_ID>",
//       "url": "https://...",
//       "created_at": 1749546840,
//       "last_activity_at": 1749550440
//     }
//   ],
//   "max_persistent": 5,
//   "used": 1
// }
```

<Note>The conversation-surfaces list is conversation-scoped (one thread's apps); the Apps catalog is workspace-scoped (every member sees the same apps). Use the conversation list to embed the app a thread just built; use the catalog for a workspace-wide management view.</Note>

## Mint a time-limited share link

[Share a surface](/api-reference/conversations/share-surface) mints a time-limited signed URL you can paste into chat, email, or a doc **without** making the surface publicly visible. The surface stays private; the URL carries the token that lets a recipient in.

<Steps>
  <Step title="Mint a link with the default TTL">
    Omit the body (or send an empty one) to get the default validity window of **7 days**:

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/api/surfaces/<SURFACE_ID>/share \
      -H "Authorization: Bearer $HQ_TOKEN" -H "Content-Type: application/json" \
      -d '{}'
    ```

    The response is the surface `id`, the tokenized `url`, and `exp` (the expiry as a Unix-seconds timestamp):

    ```js theme={null}
    // → 200
    // {
    //   "surface_id": "<SURFACE_ID>",
    //   "url": "https://...?token=...&exp=...",
    //   "exp": 1750155640
    // }
    ```
  </Step>

  <Step title="Mint a link with a custom TTL">
    Set `ttl_secs` (seconds) to control the window. It is clamped to the inclusive range **60 seconds (1 minute) to 2,592,000 seconds (30 days)**; anything outside that range is rejected with a `400`:

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/api/surfaces/<SURFACE_ID>/share \
      -H "Authorization: Bearer $HQ_TOKEN" -H "Content-Type: application/json" \
      -d '{"ttl_secs":86400}'
    ```
  </Step>
</Steps>

<Note>The share link keeps the surface private — it does not flip `auth_mode`. To open a surface to anyone with the bare URL, change its visibility instead (next section). A `404` from this endpoint means the surface was not found in your workspace.</Note>

## Change visibility

[Set surface visibility](/api-reference/conversations/set-visibility) flips a surface between private and public. The only accepted values are `private` and `public`; anything else returns a `400`.

```bash theme={null}
curl -X POST https://api.hq.zone/v1/api/surfaces/<SURFACE_ID>/visibility \
  -H "Authorization: Bearer $HQ_TOKEN" -H "Content-Type: application/json" \
  -d '{"visibility":"public"}'
```

The response echoes the surface `id`, the resulting `visibility`, and the underlying `auth_mode` it maps to (`private` to `signed_url`, `public` to `public`):

```js theme={null}
// → 200
// {
//   "surface_id": "<SURFACE_ID>",
//   "visibility": "public",
//   "auth_mode": "public"
// }
```

<Note>Visibility can only be changed on a surface in a non-terminal state (`live`, `paused`, or `booting`). If the surface is not found or is in a terminal state, the call returns `404`.</Note>

## Delete and restore

An app surface can be put to sleep and woken back up, or torn down for good.

<Steps>
  <Step title="Restore a paused app">
    [Restore a surface](/api-reference/conversations/restore-surface) wakes a paused app back to `live`. Restore is **app-only**.

    ```bash theme={null}
    curl -X POST https://api.hq.zone/v1/api/surfaces/<SURFACE_ID>/restore \
      -H "Authorization: Bearer $HQ_TOKEN"
    ```

    The response carries the surface `id` and an `outcome` describing what happened — `already_live`, `restored`, or `in_progress`:

    ```js theme={null}
    // → 200
    // { "surface_id": "<SURFACE_ID>", "outcome": "restored" }
    ```

    A `404` means the surface was not found in your workspace.
  </Step>

  <Step title="Delete an app">
    [Delete a surface](/api-reference/conversations/delete-surface) stops the app and releases its resources, flipping the surface to a destroyed state.

    ```bash theme={null}
    curl -X DELETE https://api.hq.zone/v1/api/surfaces/<SURFACE_ID> \
      -H "Authorization: Bearer $HQ_TOKEN"
    ```

    The response carries the surface `id` and an `outcome` — `destroyed` (this call did the teardown) or `already_destroyed` (it was already gone, so the call is a no-op):

    ```js theme={null}
    // → 200
    // { "surface_id": "<SURFACE_ID>", "outcome": "destroyed" }
    ```

    A `404` means the surface was not found in your workspace.
  </Step>
</Steps>

<Note>Delete is permanent: a destroyed surface is filtered out of every listing and cannot be restored — restore rejects surfaces in a terminal state (publish a fresh app instead). A non-admin caller must participate in the surface's backing conversation; a workspace admin may delete any app in the workspace.</Note>
