Skip to main content
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:
export HQ_TOKEN="hq_pat_..."
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.

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_modedev or prod.
  • auth_modesigned_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.
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.

List a conversation’s surfaces

List conversation surfaces returns the live application surfaces attached to one conversation, excluding destroyed ones, ordered by most recent activity.
1

Fetch the surfaces for a conversation

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:
// → 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://..."
//     }
//   ]
// }
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.

List the workspace app catalog

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.
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:
// → 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
// }
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.
Share a 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.
1

Mint a link with the default TTL

Omit the body (or send an empty one) to get the default validity window of 7 days:
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):
// → 200
// {
//   "surface_id": "<SURFACE_ID>",
//   "url": "https://...?token=...&exp=...",
//   "exp": 1750155640
// }
2

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:
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}'
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.

Change visibility

Set surface visibility flips a surface between private and public. The only accepted values are private and public; anything else returns a 400.
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):
// → 200
// {
//   "surface_id": "<SURFACE_ID>",
//   "visibility": "public",
//   "auth_mode": "public"
// }
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.

Delete and restore

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

Restore a paused app

Restore a surface wakes a paused app back to live. Restore is app-only.
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:
// → 200
// { "surface_id": "<SURFACE_ID>", "outcome": "restored" }
A 404 means the surface was not found in your workspace.
2

Delete an app

Delete a surface stops the app and releases its resources, flipping the surface to a destroyed state.
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 outcomedestroyed (this call did the teardown) or already_destroyed (it was already gone, so the call is a no-op):
// → 200
// { "surface_id": "<SURFACE_ID>", "outcome": "destroyed" }
A 404 means the surface was not found in your workspace.
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.