# Terminal API

> Open an interactive login shell in a running machine over a WebSocket. Browsers first mint a single-use ticket.

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

## Open a terminal

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

Opens an interactive login shell over a WebSocket (`Upgrade: websocket`). A browser session presents its cookie, an `Origin` the service serves the UI from, and `ticket` from `POST .../terminal/tickets`; the CLI presents `Authorization: Bearer` with a `use_existing` or higher key. After the upgrade the server sends `{"type":"opened"}`, then binary messages carry terminal output and the client's binary messages are stdin; text messages are JSON control (`TerminalClientMessage` in, `TerminalServerMessage` out). `exit` or `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 `ticket`, `cols`, and `rows` to the ticket's `url` and must connect from the console's origin. Messages, limits, and close reasons are in the [Web terminal guide](/docs/guides/web-terminal).

- **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. |
| `cols` | query | `integer` | No | Initial width, 1-1000 (default 80). |
| `rows` | query | `integer` | No | Initial height, 1-1000 (default 24). |

**Responses**

| Status | Description | Body |
| --- | --- | --- |
| `101` | Switching protocols: the terminal is open | — |
| `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 `terminal_ticket_invalid` (missing, expired, used, or for another machine or session) | [`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`, `machine_deleted`, or `terminal_refused` (the runtime refused it, for example at its host or machine limit) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `422` | Invalid `cols` or `rows` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `429` | `terminal_limit_reached` (per-user or per-organization concurrency) or `rate_limited` (opens per minute) | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `503` | `terminals_unavailable` or `runtime_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

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

## Create a terminal ticket

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

The ticket is valid once, for 30 seconds, for this machine and the session that minted it. API keys open the terminal 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}/terminal`, valid 30 s, redeemable only by the minting session | [`TerminalTicketDto`](/docs/api/schemas#terminalticketdto) |
| `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 terminals 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` | Too many requests; wait for `Retry-After`. | [`ProblemDetails`](/docs/api/schemas#problemdetails) |
| `503` | `terminals_unavailable` | [`ProblemDetails`](/docs/api/schemas#problemdetails) |

```sh title="Request"
curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/terminal/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",
  "url": "https://hooks.example.com/vmyard"
}
```
