Port ingress
Expose a guest TCP port at a stable public URL, token-gated or public, plus the dashboard's in-browser noVNC viewer.
Any TCP service listening inside an awake desktop -- a dev server, an
internal dashboard, anything the guest binds a port to -- can be published
at a stable public URL with
POST /v1/desktops/{id}/ports.
The URL scheme
An exposed port gets a URL of the form:
https://{port}-{desktopId}.canto.hostThe port and desktop id are both baked into the hostname, so each exposure gets its own stable, distinct URL -- no path prefixing, no collisions between desktops exposing the same port number. The URL never changes for the lifetime of the exposure, including across hibernate/wake cycles.
Private vs public
Exposures are private by default: creating one mints a
canto_pt_... token, shown exactly once in the create response, required
at the ingress via ?canto_token= or an X-Canto-Token header. Without
it, the URL answers 401.
Pass {"public": true} to skip the token entirely and serve the port to
the whole internet -- the create response then carries no token field at
all. Only the desktop's UUID in the hostname obscures a public exposure;
treat it as unauthenticated the moment you create it.
The token deliberately doesn't ride Authorization -- that header is
forwarded to your guest app untouched, and the ingress strips its own
credential before the request reaches the guest. That means a guest app
with its own auth scheme built on Authorization keeps working unmodified
behind a private exposure.
Because the token is hash-stored and unrecoverable once shown, re-exposing
an already-exposed port is a 409, not an idempotent success. Rotate a
token by deleting the exposure and creating a new one -- which also means
a new URL for the same guest port, since a fresh exposure mints a fresh
one.
Hibernation semantics
The exposure itself is desired state; the live host-side binding is
not. Hibernating a desktop makes its exposed URLs answer 503 without
deleting anything -- waking the desktop rebinds automatically, and the URL
resumes working with no client-side action needed. GET .../ports
reports each exposure's status as "bound" (live) or "unbound"
(asleep, or a rebind hasn't succeeded yet).
In short: creating and deleting exposures is a durable, desktop-level operation; whether the URL currently answers is tied to whether the desktop is awake, same as exec and files.
Requires an awake desktop to create
POST .../ports itself requires the desktop to be awake -- you can't
pre-provision an exposure on a desktop that's never woken. Once created,
though, the exposure persists across hibernate/wake; you don't need to
re-create it on every wake.
Browser access: the dashboard's noVNC embed
Separately from port ingress, the dashboard's desktop-detail page renders
the live screen in-browser using noVNC, reusing the
same stream-ticket flow the API exposes: a short-lived
ticket is requested, then a WebSocket bridge streams the framebuffer to
the browser. Opening a desktop's detail page and clicking Connect
requests a control claim with takeover: true -- an owner viewing their
own desktop expects their clicks and keystrokes to work -- so viewing a
desktop you also intend to drive by API at the same time is expected to
interleave (see Concepts: Computer use
on why API input is never gated by this claim). Tickets are single-use
with a short TTL; the panel automatically re-requests a fresh one on
reconnect.
The noVNC embed and port ingress are independent features that happen to share the visual result (a live view of the desktop) through different paths -- the embed rides the existing stream-ticket API, while port ingress publishes an arbitrary guest TCP service, VNC or otherwise, at a public URL.