Solari

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.

These are thin adapters over the unified VM pipeline

/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.

FieldTypeRequiredDescription
templatestringNoDefault "default". A built-in template name, or a tpl_… custom desktop template, which boots via restore rather than assign.
resolutionstringNoDefault "1280x720".
cpuintegerNovCPUs, 1 to 16, default 2. Grown from a small warm clone via Cloud Hypervisor vm.resize (ACPI vCPU hot-add).
memMbintegerNoGuest RAM in MiB, 1 to 65536, default 2048. Grown via virtio-mem hotplug on assign.
timeoutMsintegerNoRolling idle window in ms, reset by activity rather than a fixed lifetime. Wins over ttlSeconds. Clamped to the org's max. Default 30 minutes.
ttlSecondsintegerNoDeprecated. Legacy fixed TTL, clamped to the org max. Superseded by timeoutMs; when only this is set it becomes the rolling idle window.
lifecycleobjectNo{ 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.
metadataobjectNoOpaque string labels. Non-string values are dropped.
recordbooleanNoDefault false. Record the session server-side; the response then carries a presigned playback recordingUrl. Only a literal true enables it. See the warning below.
volumesarrayNoVolumes to mount before the session starts. Each entry is { volumeId, path }; path must be absolute and unique within the request. See Volumes.

Responses

StatusMeaning
201Desktop created. The VM is live and billable from this moment. Body carries sessionId, streamUrl, controlUrl, expiresAt, and recordingUrl when record: true.
400Malformed request body.
401Missing, malformed, or unknown bearer key.
402FeatureRequiresPlan when the plan lacks desktops or the org lacks the desktop feature; InsufficientCredit when admission rejects on prepaid balance.
403NotEntitled. Admission says the plan does not include desktops or sandboxes.
404The named tpl_… template, or a referenced volumeId, does not exist in your org.
409TemplateNotReady. The custom template exists but is still building, or failed.
429ConcurrencyLimitExceeded. At the org's concurrency cap. Not retryable; pause or kill a session first.
500Unexpected failure. The gateway defensively tears down any already-live VM, undoes partial store writes, and frees the reservation before returning this.
501volumes[] was non-empty but VOLUMES_TABLE is unconfigured on this deployment. The gateway refuses rather than silently hand back a session with nothing mounted.
502The host rejected the assign/restore for a non-capacity reason. Retryable.
503No 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.
record: true on a restore boot returns a dead link

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

NameTypeRequiredDescription
idstringYesThe signed session capability from POST /desktops. URL-encode it; it contains : and .. See Session ID format.

Responses

StatusMeaning
200Session 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.
401Missing, malformed, or unknown bearer key.
404Unknown 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 vanished session reports gone, it does not 404

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

NameTypeRequiredDescription
idstringYesThe URL-encoded signed session id.

Responses

StatusMeaning
200Released, 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.
401Missing, malformed, or unknown bearer key.
404Unknown 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

NameTypeRequiredDescription
idstringYesThe URL-encoded signed session id.

Responses

StatusMeaning
200Paused. 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.
401Missing, malformed, or unknown bearer key.
404Unknown id, bad signature, or another org's id.
409Not pausable. The host no longer knows this VM (e.g. it is still booting, or already released).
502The 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

NameTypeRequiredDescription
idstringYesThe URL-encoded signed session id.

Responses

StatusMeaning
200Running again. Body carries sessionId and status: "ready".
401Missing, malformed, or unknown bearer key.
402InsufficientCredit. Admission rejected on prepaid balance.
403NotEntitled. The plan no longer includes desktops.
404Unknown id, bad signature, or another org's id.
409The session is not paused, or the host cannot restore it.
429ConcurrencyLimitExceeded. The org filled its cap while this session was paused. Not retryable.
502The host failed the resume. Retryable.
503No host available to restore onto. Retryable.
A paused session survives host replacement

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.YWJjZGVmZ2hpamts

The 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.

URL-encode the id

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.