VM API
HTTP reference for the GUI desktop surface. Base URL https://api.getsolari.com. Every route takes Authorization: Bearer slr_live_…. For the TypeScript client over these routes, see the VM SDK reference.
/desktops is the legacy GUI-VM surface. It runs the same pipeline that backs /sandboxes, so every desktop also gets a queryable sandbox record. That means every /sandboxes/:id/* route (timeout, metrics, exec, ports, files, snapshots, revert) works on a desktop id too. For new integrations, prefer POST /sandboxes with kind: "desktop".
Contents
Desktops
POST/desktops
Creates a GUI microVM and returns its stream + control URLs. Requires a paid plan and the desktop entitlement; otherwise 402.
Request body. All fields optional. A malformed or absent body is tolerated as {}. This route accepts no diskGb or envs; the host uses its desktop defaults.
| Field | Type | Required | Description |
|---|---|---|---|
template | string | No | Default "default". A built-in template name, or a tpl_… custom desktop template, which boots via restore rather than assign. |
resolution | string | No | Default "1280x720". |
cpu | integer | No | vCPUs, 1 to 16, default 2. Grown from a small warm clone via Cloud Hypervisor vm.resize (ACPI vCPU hot-add). |
memMb | integer | No | Guest RAM in MiB, 1 to 65536, default 2048. Grown via virtio-mem hotplug on assign. |
timeoutMs | integer | No | Rolling idle window in ms, reset by activity rather than a fixed lifetime. Wins over ttlSeconds. Clamped to the org's max. Default 30 minutes. |
ttlSeconds | integer | No | Deprecated. Legacy fixed TTL, clamped to the org max. Superseded by timeoutMs; when only this is set it becomes the rolling idle window. |
lifecycle | object | No | { onTimeout: "pause" | "kill", autoResume: boolean }. What happens when the idle window elapses: pause (default) snapshots RAM+disk and frees the slot, resumable; kill destroys the session. Any value other than the literal "kill" is treated as pause. autoResume defaults to false. |
metadata | object | No | Opaque string labels. Non-string values are dropped. |
record | boolean | No | Default false. Record the session server-side; the response then carries a presigned playback recordingUrl. Only a literal true enables it. See the warning below. |
volumes | array | No | Volumes to mount before the session starts. Each entry is { volumeId, path }; path must be absolute and unique within the request. See Volumes. |
Responses
| Status | Meaning |
|---|---|
201 | Desktop created. The VM is live and billable from this moment. Body carries sessionId, streamUrl, controlUrl, expiresAt, and recordingUrl when record: true. |
400 | Malformed request body. |
401 | Missing, malformed, or unknown bearer key. |
402 | FeatureRequiresPlan when the plan lacks desktops or the org lacks the desktop feature; InsufficientCredit when admission rejects on prepaid balance. |
403 | NotEntitled. Admission says the plan does not include desktops or sandboxes. |
404 | The named tpl_… template, or a referenced volumeId, does not exist in your org. |
409 | TemplateNotReady. The custom template exists but is still building, or failed. |
429 | ConcurrencyLimitExceeded. At the org's concurrency cap. Not retryable; pause or kill a session first. |
500 | Unexpected failure. The gateway defensively tears down any already-live VM, undoes partial store writes, and frees the reservation before returning this. |
501 | volumes[] was non-empty but VOLUMES_TABLE is unconfigured on this deployment. The gateway refuses rather than silently hand back a session with nothing mounted. |
502 | The host rejected the assign/restore for a non-capacity reason. Retryable. |
503 | No host with free capacity (often desired=0, where the gateway signals scale-from-zero), or admission itself was unreachable. Carries retryable: true. Back off and retry. |
Recording is wired only on the host's assign path. A record: true create that boots by restore (any tpl_… custom template) presigns a playback URL the guest never uploads to, so the link 404s forever. This route does not reject that combination. POST /sandboxes does, with 400 RecordingRequiresGoldenBoot. Until this is fixed, use a built-in template when you need recording here.
Example request
curl -s -X POST https://api.getsolari.com/desktops \
-H "Authorization: Bearer $SOLARI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"template": "default",
"resolution": "1920x1080",
"cpu": 4,
"memMb": 8192,
"timeoutMs": 3600000,
"lifecycle": { "onTimeout": "pause", "autoResume": true },
"metadata": { "project": "acme" }
}'Example response
{
"sessionId": "pool-desktop-sta:vm_7c1e:org_9f2a.YWJjZGVmZ2hpamts",
"streamUrl": "wss://api.getsolari.com/stream/pool-desktop-sta%3Avm_7c1e%3Aorg_9f2a.YWJjZGVmZ2hpamts",
"controlUrl": "wss://api.getsolari.com/control/pool-desktop-sta%3Avm_7c1e%3Aorg_9f2a.YWJjZGVmZ2hpamts",
"expiresAt": "2026-07-16T12:30:00.000Z",
"recordingUrl": "https://storage.solaribrowser.com/org_9f2a/res_1a2b.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=8f2b1c0d4e6a7b9c"
}Mounting a volume and recording the session:
curl -s -X POST https://api.getsolari.com/desktops \
-H "Authorization: Bearer $SOLARI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "default",
"record": true,
"volumes": [
{ "volumeId": "vol_4f3a9c1e77b24d5e8a1b0c9d2e3f4a5b", "path": "/data" }
]
}'GET/desktops/:id
Returns the routing record's status. An authorized call is activity: it re-stamps the rolling idle deadline, so polling this route keeps a session alive.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The signed session capability from POST /desktops. URL-encode it; it contains : and .. See Session ID format. |
Responses
| Status | Meaning |
|---|---|
200 | Session status. Body carries sessionId, status, expiresAt and orgId. status is one of starting, ready, paused, releasing, gone. ready here corresponds to running on the sandbox surface. |
401 | Missing, malformed, or unknown bearer key. |
404 | Unknown id, bad signature, or an id belonging to another org. All three are indistinguishable by design, so the API never confirms another org's ids. |
A session whose record is gone reports status: "gone" with expiresAt: null rather than 404ing. 404 is reserved for a bad signature or a cross-org id, so the two cases stay distinguishable.
Example request
ENC=$(jq -rn --arg v "$SESSION_ID" '$v|@uri')
curl -s "https://api.getsolari.com/desktops/$ENC" \
-H "Authorization: Bearer $SOLARI_API_KEY"Example response
{
"sessionId": "pool-desktop-sta:vm_7c1e:org_9f2a.YWJjZGVmZ2hpamts",
"status": "ready",
"expiresAt": "2026-07-16T12:30:00.000Z",
"orgId": "org_9f2a"
}DELETE/desktops/:id
Releases the host VM, frees the concurrency slot, and closes the billing segment.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The URL-encoded signed session id. |
Responses
| Status | Meaning |
|---|---|
200 | Released, or already gone. Idempotent: deleting an already-gone session still returns {"ok": true} and still releases any dangling reservation by session key, so a retry after a partial failure cannot leak a slot. |
401 | Missing, malformed, or unknown bearer key. |
404 | Unknown id, bad signature, or another org's id. |
Example request
curl -s -X DELETE "https://api.getsolari.com/desktops/$ENC" \
-H "Authorization: Bearer $SOLARI_API_KEY"Example response
{ "ok": true }POST/desktops/:id/pause
Saves full RAM+disk state. Billing stops (you are billed only for active runtime) and it stops counting against your concurrency limit. Paused means stopped; resume picks up where it left off.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The URL-encoded signed session id. |
Responses
| Status | Meaning |
|---|---|
200 | Paused. Body carries sessionId and status: "paused". Idempotent: pausing an already-paused session succeeds without a host call. Serialized per-session, so it cannot interleave with a concurrent resume/delete. |
401 | Missing, malformed, or unknown bearer key. |
404 | Unknown id, bad signature, or another org's id. |
409 | Not pausable. The host no longer knows this VM (e.g. it is still booting, or already released). |
502 | The host failed the pause. Retryable. |
Example request
curl -s -X POST "https://api.getsolari.com/desktops/$ENC/pause" \
-H "Authorization: Bearer $SOLARI_API_KEY"Example response
{
"sessionId": "pool-desktop-sta:vm_7c1e:org_9f2a.YWJjZGVmZ2hpamts",
"status": "paused"
}POST/desktops/:id/resume
Restores the RAM+disk snapshot and re-acquires a concurrency slot. A fresh billing segment opens.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The URL-encoded signed session id. |
Responses
| Status | Meaning |
|---|---|
200 | Running again. Body carries sessionId and status: "ready". |
401 | Missing, malformed, or unknown bearer key. |
402 | InsufficientCredit. Admission rejected on prepaid balance. |
403 | NotEntitled. The plan no longer includes desktops. |
404 | Unknown id, bad signature, or another org's id. |
409 | The session is not paused, or the host cannot restore it. |
429 | ConcurrencyLimitExceeded. The org filled its cap while this session was paused. Not retryable. |
502 | The host failed the resume. Retryable. |
503 | No host available to restore onto. Retryable. |
Resume prefers the same host for a fast local restore. If that host is gone and the snapshot was shipped to S3, the gateway picks a fresh host and restores there.
Admission is re-checked, so an out-of-credit org cannot accrue new billed runtime by pausing and resuming. But on an admission error, as opposed to a rejection, resume fails open onto the static per-key cap: a control-plane blip must not strand work you already own.
Example request
curl -s -X POST "https://api.getsolari.com/desktops/$ENC/resume" \
-H "Authorization: Bearer $SOLARI_API_KEY"Example response
{
"sessionId": "pool-desktop-sta:vm_7c1e:org_9f2a.YWJjZGVmZ2hpamts",
"status": "ready"
}Stream channel
WSwss://api.getsolari.com/stream/:id
Desktop-only. Serves plain RFB (VNC) bytes for embedding the live view. It is not JSON-RPC and has no REST equivalent. Returned as streamUrl from POST /desktops and from POST /sandboxes when kind: "desktop". Point any RFB client or noVNC at it.
Everything a desktop session does rides the control WebSocket instead: mouse and keyboard input, screenshots, clipboard, process and port listings. See the control channel, whose computer-use namespace is desktop-kind only.
Session ID format
The sessionId returned by POST /desktops is a signed, opaque capability:
<poolId>:<vmId>:<orgId>.<sig>
pool-desktop-sta:vm_7c1e:org_9f2a.YWJjZGVmZ2hpamtsThe gateway verifies the signature and that the embedded org matches the caller's org. A forged, malformed, or cross-org id is indistinguishable from a missing one: both return 404.
The id contains : and ., so it must be URL-encoded when interpolated into a path. Every example on this page does this via ENC=$(jq -rn --arg v "$SESSION_ID" '$v|@uri').
Desktop ids are signed identically to sandbox ids and are accepted by every /sandboxes/:id/* route.
