# Desktop API

> View a running machine's graphical desktop, or only its web browser, over a WebSocket carrying VNC. Browsers first mint a single-use ticket.

- [Open a desktop](#open-a-desktop): `GET /v1/machines/{machine_id}/desktop`
- [Create a desktop ticket](#create-a-desktop-ticket): `POST /v1/machines/{machine_id}/desktop/tickets`

## Open a desktop

`GET /v1/machines/{machine_id}/desktop`

Opens a viewer of the machine's graphical desktop over a WebSocket (`Upgrade: websocket`), starting the desktop if needed. A browser session presents its cookie, an `Origin` the service serves the UI from, and `ticket` from `POST .../desktop/tickets`; the CLI presents `Authorization: Bearer` with a `use_existing` or higher key. `mode=browser` shows only a maximized web browser instead of the whole desktop. After the upgrade the server sends `{"type":"opened","width","height","mode"}`, then binary messages carry the VNC server's RFB bytes and the client's binary messages are RFB bytes for it (security type None: the WebSocket is the authentication). Text messages are JSON control (`DesktopClientMessage` in, `DesktopServerMessage` out); `{"type":"resize"}` is accepted and ignored, as clients resize in RFB (`SetDesktopSize`). `closed` is final and the server closes the WebSocket.

This is a WebSocket upgrade, not a JSON request: connect with a WebSocket client (`wss://`), not `curl`. A browser adds the ticket and must connect from the console's origin; `mode=browser` shows only the machine's web browser and is refused with `503 browser_unavailable` when the machine can't stream it. Messages and close reasons are in the [Desktop guide](/docs/guides/desktop).

- **API key:** `Authorization: Bearer vmy_…` with preset `use_existing`, `create_and_use`, `full_access`.
- **Browser session:** the `vmyard_session` cookie.
- **Who:** Owner, Admin, or Operator.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `machine_id` | path | `string (uuid)` | Yes |  |
| `ticket` | query | `string` | No | Required with a session cookie; refused with an API key. |
| `width` | query | `integer` | No | Starting width, 320-7680, with `height`. Applies only when this open starts the desktop (default 1920 x 1080); a running desktop keeps its size, which the `opened` message reports. |
| `height` | query | `integer` | No | Starting height, 240-4320, with `width`. |
| `mode` | query | [`DesktopModeDto`](/docs/api/schemas#desktopmodedto) | No | `desktop` (default) or `browser`: the machine's full desktop, or its separate browser-only desktop. A runtime refusing browser mode is `503 browser_unavailable`. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `101` | Switching protocols: the viewer is connected | — |
| `400` | Not a WebSocket upgrade request | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | `forbidden` (Viewer or Billing), `insufficient_scope` (a `read_only` key), `origin_not_allowed`, or `desktop_ticket_invalid` (missing, expired, used, or for another machine, session, or a terminal) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `machine_not_running`, `machine_not_created`, or `machine_deleted` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid `width`, `height`, or `mode` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `429` | `desktop_limit_reached` (viewers per machine, or per-user or per-organization concurrency) or `rate_limited` (opens per minute, with `Retry-After`) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `503` | `desktop_unavailable` (desktops are not configured, or the runtime refused: an image without a desktop, a host without desktops, a runtime limit), with `mode=browser` `browser_unavailable` (the runtime refused browser mode: an image without the browser session, or a runtime from before modes), or `runtime_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/desktop" \
  -H "Authorization: Bearer $VMYARD_API_KEY"
```

## Create a desktop ticket

`POST /v1/machines/{machine_id}/desktop/tickets`

The ticket is valid once, for 30 seconds, for this machine and the session that minted it. API keys open the desktop directly instead (`403 session_required`).

- **API key:** not accepted (`403 session_required`).
- **Browser session:** the `vmyard_session` cookie and the `x-csrf-token` header.
- **Who:** Owner, Admin, or Operator.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `machine_id` | path | `string (uuid)` | Yes |  |
| `X-Organization-ID` | header | `string (uuid)` | No | Required with a browser session. Optional with an API key, which fixes the organization; when sent it must match. |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `201` | A single-use ticket for `GET /v1/machines/{machine_id}/desktop`, valid 30 s, redeemable only by the minting session | [`DesktopTicketDto`](/docs/api/schemas#desktopticketdto) |
| `401` | Not signed in, or the API key is malformed, expired, or revoked (`unauthenticated`, `invalid_api_key`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `403` | Viewer or Billing (`forbidden`), a bad CSRF token, or an API key (`session_required`: keys open desktops directly) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `404` | Not found in this organization (`resource_not_found`). | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `409` | `machine_not_running`, `machine_not_created` (its create is pending or failed), or `machine_deleted` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `429` | `rate_limited`, with `Retry-After` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `503` | `desktop_unavailable` (desktops are not configured) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/desktop/tickets" \
  -b "vmyard_session=$VMYARD_SESSION" \
  -H "x-csrf-token: $VMYARD_CSRF" \
  -H "X-Organization-ID: $VMYARD_ORGANIZATION_ID"
```

```json title="Response 201"
{
  "expires_at": "string",
  "ticket": "string"
}
```
