# VMYard documentation > VMYard runs isolated Linux machines for agents and developers. You create a machine from a plan, image, and region; run commands in it with streamed output; move files in and out of /work; expose ports as HTTPS URLs; reach it over SSH; and stop, start, or delete it. Every change is an idempotent operation you can follow to its observed result. Use the vy CLI, the TypeScript or Python SDK, or the HTTP API. --- # VMYard documentation > Isolated Linux machines you create, run commands in, and reach over HTTPS and SSH, from the CLI, the SDKs, or the HTTP API. VMYard gives you isolated Linux machines on demand. Create a machine from a plan, an image, and a region; run commands in it and stream their output; move files in and out of its `/work` directory; open its ports as HTTPS URLs; reach it over SSH; and stop, start, or delete it when you are done. You pay per second while a machine runs. Every change is an **operation**. The API accepts it, a worker carries it out, and the operation only reports success once VMYard has observed the result on the machine's host. Each change carries an idempotency key, so a retry after a dropped connection never creates a second machine or runs a command twice. ## Start here - [Quickstart](/docs/quickstart): sign in, get an API key, and run your first machine in about five minutes. - [Machines](/docs/concepts/machines): what a machine is and what it is made of. - [Operations and idempotency](/docs/concepts/operations): how changes are accepted, tracked, and retried. - [API reference](/docs/api): every route, generated from the OpenAPI document. ## Ways to use VMYard | Tool | Use it for | | --- | --- | | [Console](/) | Signing in, API keys, webhooks, billing, and day-to-day machine work in the browser. | | [`vy` CLI](/docs/cli) | Terminal and scripting: `vy new`, `vy exec`, `vy cp`, `vy usage`. | | [TypeScript SDK](/docs/sdk/typescript) | Node.js 20+ applications and agents (`@vmyard/sdk`). | | [Python SDK](/docs/sdk/python) | Python 3.9+ applications and agents (`vmyard-sdk`). | | [HTTP API](/docs/api) | Anything else. JSON over HTTPS with `Authorization: Bearer vmy_…`. | ## What you can do today - Create, list, rename, start, stop, restart, and delete machines; choose plan, image, and region from a versioned [catalog](/docs/concepts/catalog). - [Run commands](/docs/guides/commands) with streamed output, and [background commands](/docs/guides/background-commands) with logs. - [Write and read files](/docs/guides/files) up to 32 MiB. - [Expose ports](/docs/guides/ports) as public or token-protected HTTPS URLs, and connect over [SSH](/docs/guides/ssh). - Run a [setup script](/docs/guides/setup-scripts) once when a machine first starts. - Keep variables and secret files in encrypted, versioned [environments](/docs/guides/environments) that VMYard installs in machines. - Push stopped machines as [snapshots](/docs/guides/snapshots), save [templates](/docs/guides/templates), and [fork or restore](/docs/guides/fork-and-restore) machines into new ones. - Stop idle spend with [auto-stop](/docs/concepts/auto-stop) and opt-in [idle sleep](/docs/concepts/idle-sleep). - Receive signed [webhooks](/docs/guides/webhooks) when lifecycle operations finish. - Track [usage, balances, and limits](/docs/guides/billing). ## Waking a machine VMYard does not wake a stopped machine when a request arrives for it; start it to wake it. See [Sleep and wake](/docs/concepts/sleep-and-wake). ## For agents and LLMs Every page is also available as Markdown: add `.md` to its URL, or use **Copy page** at the top. [/llms.txt](/llms.txt) lists every page, and [/llms-full.txt](/llms-full.txt) holds the whole documentation in one file. --- # Quickstart > Sign in, get an API key, and create, use, reach, stop, and delete your first machine with the CLI, TypeScript, or Python. This guide takes you from a new account to a running machine and back. Each step shows the `vy` CLI, the TypeScript SDK, and the Python SDK; pick a tab once and every example on the page follows it. Port and SSH steps use the HTTP API so every client can follow them; the CLI also has `vy ports` and `vy ssh`. ## 1. Sign in Open the VMYard console and sign in with one of the methods it offers, such as GitHub, Google, or a 6-digit code sent to your email. Your first sign-in creates your VMYard user. See [Sign-in methods](/docs/guides/sign-in). > [!NOTE] > Organizations are set up by your VMYard operator. If the console says **No organizations available**, ask them to add you; machines, keys, and billing all belong to an organization. ## 2. Install the tools The CLI and SDKs are in private preview and are not published to npm or PyPI. Get access to the [erphq/vmyard-sdk](https://github.com/erphq/vmyard-sdk) repository from your VMYard contact, then build from a checkout: ```sh tab="CLI" git clone https://github.com/erphq/vmyard-sdk.git cd vmyard-sdk && npm ci && npm run build npm install -g ./cli ``` ```sh tab="TypeScript" git clone https://github.com/erphq/vmyard-sdk.git cd vmyard-sdk && npm ci && npm run build cd your-project && npm install ../vmyard-sdk/typescript ``` ```sh tab="Python" git clone https://github.com/erphq/vmyard-sdk.git pip install ./vmyard-sdk/python ``` The CLI needs Node.js 20 or newer, the TypeScript SDK Node.js 20 or newer, and the Python SDK Python 3.9 or newer. None of them has a default server: set `VMYARD_API_URL` to your API origin. ```sh export VMYARD_API_URL=https://api.example.com # your VMYard API origin ``` ## 3. Get an API key With the CLI, `vy login` opens the console, you approve the request, and the CLI stores a new API key. See [CLI device login](/docs/guides/cli-login). ```sh tab="CLI" vy login vy whoami ``` ```ts tab="TypeScript" // Create a key in the console: API keys → Create key → preset "Create and use". // The SDK reads VMYARD_API_KEY and VMYARD_API_URL from the environment. import { VMYard } from "@vmyard/sdk"; const vy = new VMYard(); console.log(await vy.me()); ``` ```python tab="Python" # Create a key in the console: API keys -> Create key -> preset "Create and use". # The SDK reads VMYARD_API_KEY and VMYARD_API_URL from the environment. from vmyard import VMYard vy = VMYard() print(vy.me()) ``` For the SDKs, create a key in the console under **API keys** with the **Create and use** preset and export it as `VMYARD_API_KEY`. The secret (`vmy_…`) is shown once. See [API keys and presets](/docs/guides/api-keys). ## 4. Create a machine Pick a plan from the catalog (`vy plans`), or let the client use the first one. `--auto-stop 2` stops the machine two hours after it starts so a forgotten machine doesn't keep billing. ```sh tab="CLI" vy plans vy new --plan small --name dev --auto-stop 2 --wait ``` ```ts tab="TypeScript" const machine = await vy.machines.create({ name: "dev", plan: "small", autoStopAfterSeconds: 2 * 3600, }); // resolves when the create operation succeeds console.log(machine.data.id, machine.data.observed_state); ``` ```python tab="Python" machine = vy.machines.create(name="dev", plan="small", auto_stop_after_seconds=2 * 3600) print(machine.id, machine.state) ``` Creating returns an operation; the CLI (with `--wait`) and the SDKs wait until it succeeds, which means VMYard observed the machine running. See [Operations and idempotency](/docs/concepts/operations). ## 5. Run a command ```sh tab="CLI" vy exec dev -- uname -a vy exec dev -c 'cd /work && echo hello > hello.txt && ls -l' ``` ```ts tab="TypeScript" const result = await machine.exec("uname -a"); console.log(result.exitCode, result.stdout); for await (const frame of machine.exec(["ls", "-l", "/work"], { stream: true })) { if (frame.type === "stdout") process.stdout.write(frame.data); } ``` ```python tab="Python" result = machine.exec("uname -a") print(result.exit_code, result.stdout) with machine.exec(["ls", "-l", "/work"], stream=True) as stream: for frame in stream: if frame.type == "stdout": print(frame.data.decode(), end="") ``` The CLI streams output as it arrives and exits with the command's exit code. See [Running commands](/docs/guides/commands). ## 6. Open a port Start a web server as a [background command](/docs/guides/background-commands), then expose its port. Private ports (the default) need a short-lived token. ```ts tab="TypeScript" await machine.commands.start("python3 -m http.server 8080"); ``` ```python tab="Python" machine.commands.start("python3 -m http.server 8080") ``` ```sh tab="curl" MACHINE_ID=$(vy get dev --json | jq -r .id) curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"command": "python3 -m http.server 8080"}' ``` ```sh title="Expose port 8080 and get a link (HTTP API)" MACHINE_ID=$(vy get dev --json | jq -r .id) # or machine.data.id in the SDKs curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ports" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"port": 8080}' curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ports/8080/token" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"ttl_seconds": 900}' ``` Exposing a port is an operation: follow the returned `operation_url` until it succeeds. Open `url_with_token` from the token response in a browser. The example assumes the image has `python3`; use any server your image provides. See [Ports and private URLs](/docs/guides/ports). > [!NOTE] > When you signed in with `vy login`, the key lives in the CLI's config file rather than in `VMYARD_API_KEY`. For the HTTP examples, export a key you created in the console. ## 7. Connect over SSH Install your public key; VMYard allocates an SSH gateway port and reports how to connect in the machine's `ssh` field. ```sh title="Install a key and connect (HTTP API)" curl -X PUT "$VMYARD_API_URL/v1/machines/$MACHINE_ID/ssh-key" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d "{\"public_key\": \"$(cat ~/.ssh/id_ed25519.pub)\"}" # After the operation succeeds: vy get dev --json | jq .ssh # {"host": "...", "port": 2201, "user": "vmyard"} ssh -p 2201 vmyard@ ``` See [SSH](/docs/guides/ssh). ## 8. Stop and start Stopping ends metering. Files in `/work` are kept, and starting brings the same machine back. ```sh tab="CLI" vy stop dev --wait vy start dev --wait ``` ```ts tab="TypeScript" await machine.stop(); await machine.start(); ``` ```python tab="Python" machine.stop() machine.start() ``` ## 9. Delete the machine ```sh tab="CLI" vy rm dev --wait ``` ```ts tab="TypeScript" await machine.delete(); ``` ```python tab="Python" machine.delete() ``` Deleting is permanent. The delete operation succeeds once VMYard confirms the machine is gone from its host. ## Next steps - Check what you spent with `vy usage` and read [Usage, billing, and limits](/docs/guides/billing). - Get notified when machines become ready or fail with [webhooks](/docs/guides/webhooks). - Browse the [CLI reference](/docs/cli) and the [API reference](/docs/api). --- # Machines > What a VMYard machine is, how it is identified, what it is made of, and who may change it. A **machine** is an isolated Linux environment that belongs to one organization. You create it from a plan, an image, and a region, then run commands in it, move files in and out of `/work`, expose its ports, and reach it over SSH. ## Identity | Field | What it is | | --- | --- | | `id` | The machine's UUID. Use it in every API path. | | `tenant_alias` | A name fixed at creation (1–63 lowercase letters, digits, and `-`, starting with a letter). It never changes and is part of how VMYard correlates the machine with its host. | | `name` | The display name. It follows the same rules, defaults to the tenant alias, and can be changed with `PATCH /v1/machines/{id}` or `vy rename`. | The CLI and SDK `find` accept a machine ID, a tenant alias, or a unique display name. The HTTP API takes IDs only. ## What a machine is made of | Field | Meaning | | --- | --- | | `plan` | `{id, version}` of the [plan](/docs/concepts/catalog): size, price, and backend. | | `image` | `{id, version}` of the image the disk starts from. | | `region` | `{id, version}` of the region it runs in. | | `resources` | `cpu`, `memory_mib`, and `disk_gib`. | | `backend` | `kvm` (virtual machines), `docker` (development only), or `mock` (tests). | | `desired_state` / `observed_state` | What you asked for and what VMYard last saw. See [Lifecycle states](/docs/concepts/lifecycle). | | `auto_stop_after_seconds` / `auto_stop_at` | The [auto-stop](/docs/concepts/auto-stop) duration and the current deadline. | | `idle_sleep_after_seconds` / `idle_since` / `idle_sleep_at` / `last_active_at` | The [idle-sleep](/docs/concepts/idle-sleep) window (`null` is off), the current idle window and when it ends, and the last observed activity. | | `setup_operation_id` | The [setup script](/docs/guides/setup-scripts) operation, when one was given. | | `environment` | The [environment](/docs/guides/environments) version the machine should have and whether it is installed; `null` without one. | | `ssh` | `{host, port, user}` once an SSH key is installed. See [SSH](/docs/guides/ssh). | Selections are pinned by **exact version**. Renaming a plan or publishing a new image version never changes a machine that already exists. ## The `/work` directory Files you write through the API live under `/work`, and file paths are relative to it. Background-command logs are written there too. Files in `/work` are kept while the machine is stopped and are removed when it is deleted. ## Networking Nothing reaches a machine from outside unless you [expose a port](/docs/guides/ports) or [install an SSH key](/docs/guides/ssh); exposed ports are served only through VMYard's HTTPS edge. What a machine may reach outbound is its [egress policy](/docs/guides/networking), `internet` or `none`, chosen when you create it and changeable later. Machines on the `docker` development backend have no network, so ports and SSH are not available there. ## Roles Organization members have one role each: | Role | Read machines | Create, change, use, and delete machines | Webhooks | Manage billing | | --- | --- | --- | --- | --- | | Owner | Yes | Yes | Yes | Yes | | Admin | Yes | Yes | Yes | No | | Operator | Yes | Yes | No | No | | Billing | Yes | No | No | Yes | | Viewer | Yes | No | No | No | "Use" includes running commands, reading and writing files, ports, and SSH keys. An API key is further limited by its [preset](/docs/guides/api-keys) and always by its creator's current role. ## Copies of a machine A stopped machine can be pushed as a [snapshot](/docs/guides/snapshots) or saved as a [template](/docs/guides/templates); a running machine can be [forked](/docs/guides/fork-and-restore), and a snapshot restored. Each copy is a new machine with its own ID and guest identity that keeps its source's tenant alias, so two machines can share a machine name. Its display name tells them apart, and `forked_from_machine_id` or `restored_from_snapshot_id` says where it came from. --- # Operations and idempotency > How VMYard accepts a change, carries it out, reports the truth about the result, and makes retries safe. Every change to a machine (create, start, stop, restart, delete, running a command, writing a file, exposing a port, installing an SSH key, changing internet access) is an **operation**. The API answers `202 Accepted` right away and a worker carries out the change in the background. ```json title="202 Accepted" { "operation_id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f", "machine_id": "0b6f6c55-2f1d-4f0e-9a51-6f0f4cba7a10", "status": "pending", "operation_url": "/v1/operations/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f", "requested_action": "create" } ``` ## Following an operation Poll `GET /v1/operations/{id}` until `status` is terminal, or stream `GET /v1/operations/{id}/events` as server-sent events. The event stream replays the persisted history first, then live events; send `Last-Event-ID` to resume after a disconnect. | Status | Meaning | | --- | --- | | `pending` | Accepted and queued. | | `running` | A worker is carrying it out. | | `succeeded` | Done, and VMYard observed the requested result on the machine's host. | | `failed` | It definitely did not happen. `error_code` says why. | | `uncertain` | The outcome could not be confirmed yet. VMYard retries it with the exact same request until it settles. | | `correlation_miss` | The work finished but the observed machine did not match what was asked for. Treat it as not done. | `succeeded`, `failed`, and `correlation_miss` are terminal. The SDKs and `vy … --wait` poll for you (250 ms, growing to 2 s, for up to 10 minutes) and raise an error for anything but `succeeded`. They raise on `uncertain` too; replaying the same request with the same idempotency key is safe and picks the operation up again. A wait that times out does not cancel the operation. > [!IMPORTANT] > A worker finishing is not the same as the change happening. An operation only succeeds when VMYard sees the expected state on the host: a running machine with a new boot after a restart, a machine that is gone after a delete. If `completed_but_unchanged` is `true`, the machine did not change; don't report it as done. ## Ordering Operations on one machine run one at a time, in the order they were accepted. A long command on one machine never delays another machine. ## Idempotency keys Send a unique `Idempotency-Key` header with every request that creates an operation. The key is scoped to your organization. - Repeating a request with the **same key and the same body** returns the original operation instead of doing the work again. This is how you recover from a timeout or a dropped connection: retry with the same key. - Using a key again with a **different body** is refused with `409 idempotency_conflict`. For creates this includes the name, auto-stop, idle sleep, and setup script. - A new key is a new request. ```sh KEY=$(uuidgen) curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/stop" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $KEY" \ -H "Content-Type: application/json" -d '{}' # Safe to repeat with the same $KEY: you get the same operation back. ``` The SDKs generate a UUID key for every mutation and reuse it when they retry. Pass your own (`idempotencyKey` / `idempotency_key`) when you need to retry across process restarts. Settings changes (`PATCH /v1/machines/{id}`), port access tokens, environments, webhooks, API keys, billing, and sign-in routes are not operations and take no idempotency key; they answer synchronously. Upgrading an environment creates operations, but its keys are derived from the version and the machine, so repeating the request replays them. ## Retries - Retry reads freely. - Retry a mutation only with the same idempotency key and body. - The SDKs retry reads on connection errors and `408`, `429`, `502`, `503`, and `504`; they retry mutations only on `429` and `503`, always with the same key, and honour `Retry-After`. ## Operation results A terminal operation keeps its result. Exec operations carry `exec_result` (exit code, base64 output, and `timed_out`, `cancelled`, `truncated`). A completed file read carries `file_ranges`. A `stop` carries `stop_reason`: `api` when a caller asked for it, or `auto_stop`, `billing`, `idle_sleep`, `suspension`, or `abuse` when VMYard stopped the machine itself ([auto-stop](/docs/concepts/auto-stop), an exhausted balance, [idle sleep](/docs/concepts/idle-sleep), a [suspension](/docs/guides/billing#suspended-organizations) of the organization, or the guard against sustained full CPU use on trials); other kinds have `null`. See [Running commands](/docs/guides/commands) and [Files](/docs/guides/files). --- # Lifecycle states > Desired and observed machine states, the actions that move between them, and how to read a machine that is in between. A machine has two states. `desired_state` is what you asked for most recently. `observed_state` is what VMYard last confirmed on the machine's host. They differ while an operation is in flight, and the observed state is the one to trust. ## Desired state | Value | Set by | | --- | --- | | `running` | Create, start, or restart. | | `stopped` | Stop, including an [auto-stop](/docs/concepts/auto-stop), [idle sleep](/docs/concepts/idle-sleep), or a stop because the balance ran out. | | `deleted` | Delete. | ## Observed state `observed_state` is an object with a `state` field: | `state` | Meaning | | --- | --- | | `absent` | No machine exists on a host yet, or it has been removed. | | `provisioning` | The machine is being created. | | `running` | It is up. Also carries `boot_id`, which changes on every boot. | | `stopped` | It is stopped. Nothing is metered. | | `degraded` | The host reported a problem with the machine. | | `unknown` | VMYard could not confirm the machine's state. | ```json { "desired_state": "running", "observed_state": { "state": "running", "boot_id": "c1a4e7f0-…" } } ``` ## Actions | Action | Route | CLI | Result when it succeeds | | --- | --- | --- | --- | | Create | `POST /v1/machines` | `vy new` | Observed `running`. Emits `machine.ready`. | | Stop | `POST /v1/machines/{id}/stop` | `vy stop` | Observed `stopped`. Emits `machine.stopped`. | | Start | `POST /v1/machines/{id}/start` | `vy start` | Observed `running`. Emits `machine.started`. | | Restart | `POST /v1/machines/{id}/restart` | `vy restart` | Observed `running` with a new `boot_id`. Emits `machine.started`. See [Restart](#restart). | | Delete | `POST /v1/machines/{id}/delete` | `vy rm` | The machine is confirmed gone. Emits `machine.deleted`. | Each action is an [operation](/docs/concepts/operations) and needs an `Idempotency-Key`. When a lifecycle operation ends `failed` or `correlation_miss`, VMYard emits `machine.error`; see [Webhooks](/docs/guides/webhooks). Creates, starts, and restarts also pass billing admission: the balance, the spend cap, the trial rules, and the concurrency and start-rate limits. See [Usage, billing, and limits](/docs/guides/billing#limits). ## Restart A restart is one operation that reboots a running machine in place: every process in it ends, `/work` is kept, and it comes back ready. On VM hosts the VM reboots from its own disk; on Docker hosts the workload's processes end. Exposed ports, the SSH port, and the [egress policy](/docs/guides/networking) stay. The machine never stops: its metered run stays open, no `machine.stopped` is sent, and its [auto-stop](/docs/concepts/auto-stop) deadline starts again. A retry with the same `Idempotency-Key` never reboots twice. It succeeds only when the machine is running and ready with a `boot_id` different from the one before. A restart that kept the old `boot_id` ends `correlation_miss`; the machine stays running and usable. A host that can't restart its machines yet ends the operation `failed` with `restart_unsupported`: nothing changed and the machine kept running. ## Reading a machine in between - **Desired `running`, observed `provisioning`:** a create is in progress. Wait for its operation. - **Desired `stopped`, observed `running`:** a stop is queued or running. - **Observed `degraded` or `unknown`:** don't assume the machine works. You can still delete it. Check its latest operation's `error_code`. - **An operation `succeeded` but the state did not change:** look at `completed_but_unchanged`; treat the change as not made. Guest commands, file operations, port changes, and SSH key changes all need an observed `running` machine. --- # Auto-stop > Stop machines automatically a fixed time after they start, so forgotten machines don't keep billing. Auto-stop stops a machine a set time after it starts running. New machines stop after **1 hour** unless you choose another duration or turn auto-stop off. The deadline counts from when the machine becomes running; activity inside the machine does not extend it. To stop a machine once it has done nothing for a while instead, turn on [idle sleep](/docs/concepts/idle-sleep); with both on, whichever comes first stops it. ## Settings | Field | Meaning | | --- | --- | | `auto_stop_after_seconds` | 60 to 2,592,000 seconds (1 minute to 30 days). `null` means never. Creates default to 3600. | | `never_auto_stop` | `true` turns auto-stop off. It can't be combined with `auto_stop_after_seconds`. | | `auto_stop_at` | The current deadline (UTC, RFC 3339), present only while the machine is meant to run with a deadline. | ## Set it when you create a machine ```sh tab="CLI" vy new --name dev --auto-stop 2 # hours; also 30m, 1d, or never ``` ```ts tab="TypeScript" await vy.machines.create({ name: "dev", autoStopAfterSeconds: 2 * 3600 }); await vy.machines.create({ name: "always-on", neverAutoStop: true }); ``` ```python tab="Python" vy.machines.create(name="dev", auto_stop_after_seconds=2 * 3600) vy.machines.create(name="always-on", never_auto_stop=True) ``` ```sh tab="curl" # In the create body: # "auto_stop_after_seconds": 7200 or "never_auto_stop": true ``` ## Change it later `PATCH /v1/machines/{id}` changes the setting and returns the updated machine right away. Changing auto-stop on a running machine restarts its deadline from now; renaming alone leaves the deadline as it is. ```sh tab="CLI" vy autostop dev 30m vy autostop dev never ``` ```ts tab="TypeScript" await machine.update({ autoStopAfterSeconds: 30 * 60 }); await machine.update({ neverAutoStop: true }); ``` ```python tab="Python" machine.update(auto_stop_after_seconds=30 * 60) machine.update(auto_stop_after_seconds=None) # never ``` ```sh tab="curl" curl -X PATCH "$VMYARD_API_URL/v1/machines/$MACHINE_ID" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"auto_stop_after_seconds": 1800}' ``` ## How the stop happens VMYard checks deadlines every second. When one passes, it requests an ordinary stop operation for that machine, exactly once per deadline, even if you change the setting or stop the machine at the same moment. The stop emits `machine.stopped` like any other stop, and its operation has `stop_reason: "auto_stop"`. A start or restart sets a new deadline. A stop or delete clears it. --- # Idle sleep > Put a running machine to sleep once it has done nothing for a while, how VMYard decides a machine is idle, and how idle sleep differs from auto-stop. Idle sleep stops a running machine once it has shown no activity for a window you choose, from 5 minutes to 24 hours. It is **off by default** for every machine: you turn it on per machine when you create it or later. The stop is an ordinary stop. The machine keeps its disk, and on hosts with snapshot sleep (KVM) also its memory and running processes, so `start` wakes it where it left off. Nothing is billed while it sleeps. ## Idle sleep and auto-stop | | [Auto-stop](/docs/concepts/auto-stop) | Idle sleep | | --- | --- | --- | | Ends a run | a fixed time after the machine starts, whatever it does | once the machine has been idle for the whole window | | Default | on, 1 hour | off | | Range | 1 minute to 30 days | 5 minutes to 24 hours | | Activity extends it | no | yes: any activity restarts the window | Use both: auto-stop bounds a machine you forgot, and idle sleep ends a run as soon as the work is done. Whichever comes first stops the machine. The console's machine list shows the sooner one under **Stops / sleeps in**. ## How VMYard decides a machine is idle The control plane checks running machines with idle sleep on about every 30 seconds. A machine is idle only while **every** signal stays quiet, without a break, for the whole window: - No operation of the machine is pending, running, or uncertain, and none was requested or finished inside the window. Commands, background commands, file transfers, ports, SSH keys, lifecycle changes, environment installs, and snapshots all count. - No [web terminal](/docs/guides/web-terminal) is open to it, and none closed inside the window. - Its latest [metrics](/docs/guides/machine-metrics) are current and CPU, disk I/O, and network use are at or below small thresholds, with no minute inside the window above them. Port and SSH traffic shows up as network use. The thresholds are set by the control plane, not by you, and may change. **Missing data never counts as idle.** When metrics are missing, stale, or overdue, or the host can't measure a value, the window ends and starts again only once current, quiet metrics return. A machine whose host stopped reporting is never put to sleep because of it. ## Turn it on In the console, choose **Sleep when idle** when you create a machine, or open the machine's overview and change **Idle sleep**. With the API, send `idle_sleep_after_seconds`: ```sh # In the create body: # "idle_sleep_after_seconds": 900 curl -X PATCH "$VMYARD_API_URL/v1/machines/$MACHINE_ID" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"idle_sleep_after_seconds": 900}' ``` `null` (or leaving the field out when you create) turns it off. Values outside 300 to 86,400 seconds answer `422` with a field error. Changing the setting restarts the idle window. ## What a machine reports | Field | Meaning | | --- | --- | | `idle_sleep_after_seconds` | The window in seconds, or `null` when idle sleep is off. | | `idle_since` | Start of the running machine's current unbroken idle window; `null` while it is active, its metrics are unknown, or idle sleep is off. | | `idle_sleep_at` | When the machine sleeps if it stays idle (`idle_since` plus the window); `null` without an open window. The console counts down to it as **Sleeps in**. | | `last_active_at` | The last activity VMYard observed; `null` until it saw any. The console shows it as **Last active**. | ## After it sleeps An idle-sleep stop is a normal `stop` operation. `GET /v1/operations/{id}` and the `machine.stopped` [webhook](/docs/guides/webhooks) carry `stop_reason: "idle_sleep"`, and members who turned on the `idle_sleep` [email alert](/docs/guides/email-alerts) get an email naming the machine and the operation. In the console, open the machine's overview with `?operation=` to see it as **Stopped: idle**. Start the machine to wake it. A start, like any activity, begins a new idle window once the machine is quiet again. --- # Sleep and wake > What happens to a machine when it stops and starts, how idle sleep puts it to sleep, how long stops move down the snapshot tiers, and how to wake it. > [!NOTE] > VMYard does not wake a machine when a request arrives for it. The API, CLI, and SDKs have no separate sleep or wake routes; sleep is a stop and waking is a start. ## Stop and start A machine runs until you stop it, [auto-stop](/docs/concepts/auto-stop) stops it, [idle sleep](/docs/concepts/idle-sleep) finds it idle, or the organization's balance runs out. - Stopping ends metering. Nothing is billed while a machine is stopped. - Files in `/work` are kept while the machine is stopped. - Starting brings the same machine back with the same ID and name. Its port URLs keep their label, so a URL never points at another machine. ```sh vy stop dev --wait vy start dev --wait ``` On KVM hosts a stop is a hot snapshot: memory, device state, and disk stay on the machine's host, and start resumes its processes where they were. Docker development hosts pause the machine instead. Even so, write what you need to keep to `/work`, and be ready to start services again with a [background command](/docs/guides/background-commands): a [restore from a cold snapshot](/docs/guides/snapshots#tiers) boots fresh from the disk. A [setup script](/docs/guides/setup-scripts) runs only once, after the first start. ## Long stops move down the tiers A background policy pushes machines that stay stopped to the snapshot store: a warm snapshot after 6 hours and a cold one after 7 days, by default, on KVM machines. It changes nothing about how you start the machine; it makes a long-stopped machine restorable on any host. See [Snapshots and tiers](/docs/guides/snapshots#stopped-machines-move-down-the-tiers). ## Idle cost A running machine is billed whether it is busy or idle. Turn on [idle sleep](/docs/concepts/idle-sleep) to stop a machine once it has done nothing for a while, set a short auto-stop for development machines, and stop machines when your agent or job finishes. --- # Regions, plans, and images > The versioned catalog a machine is created from, how to read it, and how sizes are priced. A machine is created from three catalog entries: a **plan** (size, price, and backend), an **image** (what the disk starts from), and a **region** (where it runs). Each entry has an `id` and a `version`, and a create request names both, so a later change to the catalog never changes an existing machine or a request you already sent. ## Reading the catalog ```sh tab="CLI" vy plans vy images vy regions ``` ```ts tab="TypeScript" const [plans, images, regions] = await Promise.all([ vy.catalog.plans(), vy.catalog.images(), vy.catalog.regions(), ]); ``` ```python tab="Python" plans, images, regions = vy.catalog.plans(), vy.catalog.images(), vy.catalog.regions() ``` ```sh tab="curl" curl "$VMYARD_API_URL/v1/plans" -H "Authorization: Bearer $VMYARD_API_KEY" curl "$VMYARD_API_URL/v1/images" -H "Authorization: Bearer $VMYARD_API_KEY" curl "$VMYARD_API_URL/v1/regions" -H "Authorization: Bearer $VMYARD_API_KEY" ``` Any member and any API key preset can read the catalog. The exact sizes, images, and regions on offer come from your VMYard deployment; always read them from the catalog rather than hard-coding them. ## Plans A plan has a `name`, `version`, `cpu`, `memory_mib`, `disk_gib`, `backend`, and a `price` with `hourly` and `monthly` USD amounts. Machines are billed per second at the plan's hourly price while they run. Sizes are priced like this: | Size | Price per hour | Multiplier | | --- | --- | --- | | `small` | $0.015 | 0.5 | | `default` | $0.030 | 1 | | `large` | $0.060 | 2 | | `xlarge` | $0.170 | about 5.67 | The multiplier is a size's hourly price relative to `default`. Plan allowances and the trial are counted in `default`-size hours, so one hour of `small` uses half an hour of allowance. Trials can use only `small` and `default`. See [Usage, billing, and limits](/docs/guides/billing). ## Images An image has a `name`, a `version`, and a content `digest`. A machine's disk starts from the exact image version you choose. ## Regions A region has a `name`, a `version`, a `backend`, and a `capacity_status`. Choose a region whose backend matches the plan's backend. ## Choosing entries The SDKs and CLI accept names or IDs and resolve them to the latest version for you. They default to the first plan, the first image, and the first region that matches the plan's backend. ```sh tab="CLI" vy new --plan small --image ubuntu-24.04 --region eu-central --name dev ``` ```ts tab="TypeScript" const request = await vy.machines.buildCreateRequest({ plan: "small", name: "dev" }); console.log(request.plan, request.image, request.region); // exact {id, version} pairs ``` ```python tab="Python" request = vy.machines.build_create_request(plan="small", name="dev") print(request["plan"], request["image"], request["region"]) ``` The image and region names above are examples; use the names your catalog lists. With the HTTP API, send `plan`, `image`, and `region` as `{id, version}`, plus the plan's `resources` and `backend`. See [Create a machine](/docs/api/machines#create-a-machine). --- # API keys and presets > Create organization API keys, choose a preset that limits what each key can do, and rotate or revoke keys. API keys let programs call VMYard without a browser. A key belongs to one organization, has a **preset** that limits which routes it reaches, and acts with its creator's current role. ## Create a key In the console, open **API keys** and choose **Create key**. Give it a name (1–64 characters), a preset, and a lifetime of 1 to 365 days (90 by default). The secret is shown **once**; copy it then. You can also get a key from the CLI with [`vy login`](/docs/guides/cli-login). Secrets look like `vmy_` followed by 43 characters. VMYard stores only a hash, the first 12 characters (`prefix`), and the last four, so it can show you which key is which but can never show the secret again. > [!NOTE] > Creating, listing, rotating, and revoking keys needs a signed-in browser session. An API key can't manage keys: those routes answer `403 session_required`. ## Use a key Send it as a bearer token. The key already names its organization, so `X-Organization-ID` is optional; if you send it, it must match (`403 organization_mismatch` otherwise). Bearer requests don't need a CSRF token. ```sh tab="curl" curl "$VMYARD_API_URL/v1/machines" \ -H "Authorization: Bearer $VMYARD_API_KEY" ``` ```sh tab="CLI" export VMYARD_API_KEY=vmy_... # always wins over a key stored by vy login vy ls ``` ```ts tab="TypeScript" const vy = new VMYard({ apiKey: process.env.VMYARD_API_KEY }); ``` ```python tab="Python" vy = VMYard(api_key=os.environ["VMYARD_API_KEY"]) ``` Keep keys out of source control and command lines. The CLI accepts keys only on stdin or from the device flow. ## Presets Each preset includes everything in the presets above it. | Preset | Adds | | --- | --- | | `read_only` | Reads: machines, operations, the catalog, `/v1/me`, billing, limits, usage, port lists, and environments (without values). | | `use_existing` | Using machines that exist: start, stop, restart, settings, commands and background commands, file reads and writes, ports, port tokens, SSH keys, and environment upgrades. | | `create_and_use` | Creating and deleting machines, and choosing or changing a machine's [internet access](/docs/guides/networking). | | `full_access` | Webhooks, environment changes and reveals, and billing management (spend cap, checkout, payment portal). | A key with too small a preset gets `403 insufficient_scope`. A `read_only` key gets it on every route that reaches inside a machine, including file reads and command status. ## Roles still apply A key never does more than its creator could. If the creator is a Viewer, the key can only read, whatever its preset; webhooks and environment changes also need the creator to be an Owner or Admin. If the creator leaves the organization, the key stops working. ## Rotate and revoke The key's creator, an Owner, or an Admin can: - **Rotate** a key: VMYard issues a new secret with the same name, preset, and lifetime and revokes the old one at once. The person who rotates becomes the new key's creator. - **Revoke** a key: it stops working at once. Revoking twice is harmless. Owners and Admins see every key in the organization; other members see the keys they created. A key that is malformed, unknown, expired, or revoked gets `401 invalid_api_key`. --- # Sign-in methods > Sign in to the console with GitHub, Google, or an emailed code, and link or remove sign-in methods. The console uses browser sign-in. Which methods appear depends on your deployment; the usual set is **GitHub**, **Google**, and **email**. Your first sign-in creates your VMYard user. ## GitHub and Google Choosing a provider sends you to it and back. VMYard identifies you by the provider's account, not by email address. GitHub sign-in needs a **verified primary email** on your GitHub account. ## Email codes and magic links 1. Enter your address. VMYard always answers the same way, whether or not an account exists. 2. Check your email for a **6-digit code** and a sign-in link. They are one credential: valid for 10 minutes, usable once, and replaced by any newer request. Five wrong codes use it up. 3. Type the code, or click the link. The link works only in the browser that asked for it; on another device, type the code. You can request 5 emails per address and 20 per network address per hour, and verify 30 codes per network address per 10 minutes. Beyond that, VMYard answers `429 rate_limited` with a `Retry-After` delay. ## One person, several methods Sign-in methods never merge on their own: a GitHub account and an email address with the same address are two different users until you link one to the other. To link a method, sign in, open **Account**, and choose the method to add. For email, VMYard sends a code to the new address. Linking and removing need a sign-in within the last **15 minutes**; if yours is older, the console asks you to confirm it's you first. - You can't remove your last sign-in method (`409 last_login_method`). - A method that already belongs to another user can't be linked (`identity_in_use`). - Removing a method doesn't end sessions that are already signed in. ## Sign-in errors When sign-in fails, the console shows one of these codes in the address (`/sign-in?error=…`) with an explanation: | Code | Meaning | | --- | --- | | `access_denied` | You declined at the provider. | | `identity_in_use` | That identity is already linked to another user. | | `oidc_failed` | The provider's answer couldn't be verified, or GitHub had no verified primary email. | | `oidc_unavailable` | The provider couldn't be reached. Try again later. | | `session_expired` | The sign-in attempt expired or was already used. Start again. | | `unsupported_provider` | That method isn't configured on this deployment. | ## Signing out **Sign out** in the console ends the browser session. API keys aren't affected; revoke a key to stop it working. --- # CLI device login > Sign the vy CLI in through the browser with the device authorization grant, or give it a key on stdin. `vy login` signs the CLI in without copying secrets around. It uses the OAuth 2.0 device authorization grant (RFC 8628): the CLI shows a code, you approve it in the console, and the CLI receives an ordinary [API key](/docs/guides/api-keys). ## Sign in ```sh export VMYARD_API_URL=https://api.example.com vy login ``` 1. The CLI prints a URL and a code such as `BCDF-GHJK`, and opens the console's **/device** page (pass `--no-browser` to open it yourself, for example over SSH). 2. In the console, check that the code and the device name (`vy on `) match what your terminal shows. **Approve only a request you started.** 3. Choose the organization, the key's preset, its lifetime (1–90 days, 90 by default), and its name, then approve. 4. The CLI receives the key and stores it with its organization. `--scope` asks for a preset: `read_only`, `use_existing`, `create_and_use` (the default), or `full_access` (needed for webhooks). The person approving can choose a different one. Any member of the organization can approve; the key is limited by their role like any key. ```sh vy login --scope full_access vy whoami ``` ## Other ways to sign in ```sh vy login --key-stdin < ~/vmyard-key.txt # a key you created in the console export VMYARD_API_KEY=vmy_... # or set it in the environment ``` `VMYARD_API_KEY` always wins over a stored key. `--key-stdin` checks the key with `/v1/me` before storing it (`--no-verify` skips the check). The CLI never accepts a key as a command-line argument and never prints a key. ## Where the key is stored | System | Path | | --- | --- | | Linux and others | `$XDG_CONFIG_HOME/vmyard/config.json` (default `~/.config/vmyard/`) | | macOS | `~/Library/Application Support/vmyard/config.json` | | Windows | `%APPDATA%\vmyard\config.json` | `VMYARD_CONFIG_DIR` overrides the directory. The file is written with mode `0600` in a `0700` directory, and `vy` warns if other users can read it. `vy logout` deletes the stored key from this computer only. The key itself stays valid until it expires; revoke it in the console under **API keys** if you no longer need it. Each `vy login` creates a new key. ## The protocol Other tools can use the same flow; the SDKs ship it as `DeviceAuthorization`. 1. `POST /v1/auth/device/code` with optional `client_name` and `scope`. No authentication. The answer has `device_code` (keep it secret), `user_code`, `verification_uri`, `verification_uri_complete`, `expires_in` (600 seconds), and `interval` (5 seconds). 2. Show the user `user_code` and `verification_uri`. 3. Poll `POST /v1/auth/device/token` with `grant_type` `urn:ietf:params:oauth:grant-type:device_code` and the `device_code`, waiting `interval` seconds between polls. ```sh curl -X POST "$VMYARD_API_URL/v1/auth/device/token" \ -H "Content-Type: application/json" \ -d '{"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "'"$DEVICE_CODE"'"}' ``` Until the user decides, polls answer `400` with an RFC 8628 error: | `error` | What to do | | --- | --- | | `authorization_pending` | Keep polling. | | `slow_down` | You polled too fast. Add 5 seconds to the interval. | | `access_denied` | The user denied it, or the approver left the organization. Stop. | | `expired_token` | The code expired. Start again. | | `invalid_grant` | Unknown or already used device code. Start again. | | `invalid_request` | The request is malformed. | | `unsupported_grant_type` | Wrong `grant_type`. | After approval, the next poll returns the key once: `access_token` (the `vmy_…` secret), `token_type: "Bearer"`, `expires_in` (seconds until the key expires), `scope` (the preset), and `api_key` (its details). Requesting codes is limited to 30 per network address per 10 minutes. --- # Running commands > Run a command in a machine, wait for its result or stream its output as it arrives, and handle exit codes, timeouts, and retries. `POST /v1/machines/{id}/exec` runs one command in a running machine and returns its exit code and output. The command is an [operation](/docs/concepts/operations), so it gets an idempotency key and runs after any earlier operation on the same machine. Past commands and their output stay in the machine's [command history](/docs/guides/command-history). ## Run and wait ```sh tab="CLI" vy exec dev -- uname -a # argv, run directly vy exec dev -c 'cd /work && make test' # a shell command ``` ```ts tab="TypeScript" const { exitCode, stdout, stderr } = await machine.exec("cd /work && make test"); const listing = await machine.exec(["ls", "-l", "/work"]); ``` ```python tab="Python" result = machine.exec("cd /work && make test") print(result.exit_code, result.stdout, result.stderr) listing = machine.exec(["ls", "-l", "/work"]) ``` ```sh tab="curl" curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/exec" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"command": "uname -a", "timeout_ms": 30000}' # 202 Accepted: poll operation_url until it is terminal, then read exec_result. ``` Send exactly one of: - `argv`: the program and its arguments, run directly without a shell. - `command`: a string run as `/bin/sh -c `, at most 8 KiB. In the SDKs a string becomes `command` and an array becomes `argv`. In the CLI, words after `--` are `argv` and `-c` is `command`. On a machine with an [environment](/docs/guides/environments), commands run with its variables loaded. ## The result The finished operation's `exec_result` holds: | Field | Meaning | | --- | --- | | `exit_code` | The command's exit status; `null` if it didn't exit normally. | | `stdout_base64`, `stderr_base64` | The output, base64-encoded. | | `timed_out` | It hit `timeout_ms` and was killed. | | `cancelled` | It was cancelled. | | `truncated` | Output went over 1 MiB and was cut. | The operation **succeeds** only when the command exits with code 0. Otherwise it ends `failed`, still carries `exec_result` with the output, and its `error_code` says why: | `error_code` | Meaning | | --- | --- | | `exec_nonzero` | The command exited with a non-zero code. | | `exec_timed_out` | It ran past `timeout_ms`. | | `exec_cancelled` | It was cancelled. | | `exec_no_exit_status` | It ended without an exit status, for example killed by a signal. | The SDKs raise `OperationError` for a failed exec; its `operation.exec_result` holds the output. The CLI exits with the remote exit code, `124` if the command timed out, and `125` if VMYard couldn't run it. A failed exec also emits the `operation.failed` [webhook](/docs/guides/webhooks). ```ts tab="TypeScript" import { OperationError } from "@vmyard/sdk"; try { await machine.exec("make test"); } catch (error) { if (error instanceof OperationError && error.errorCode === "exec_nonzero") { console.log("tests failed", error.operation.exec_result?.exit_code); } else throw error; } ``` ```python tab="Python" from vmyard import OperationError try: machine.exec("make test") except OperationError as error: if error.error_code != "exec_nonzero": raise print("tests failed", error.operation["exec_result"]["exit_code"]) ``` ## Timeouts `timeout_ms` is 1 to 600,000 (10 minutes) and defaults to the maximum. A larger value is refused with `422 timeout_exceeds_runtime_limit`. When the timeout passes, the command's process group is killed. For anything longer, use a [background command](/docs/guides/background-commands). ## Stream output Set `stream: true` to get output while the command runs. The response is `200 application/x-ndjson`: one JSON object per line. ```sh tab="CLI" vy exec dev -- npm test # streams by default; --no-stream waits instead ``` ```ts tab="TypeScript" for await (const frame of machine.exec(["npm", "test"], { stream: true })) { if (frame.type === "stdout") process.stdout.write(frame.data); if (frame.type === "stderr") process.stderr.write(frame.data); if (frame.type === "exit") console.log("exit", frame.exitCode); } ``` ```python tab="Python" with machine.exec(["npm", "test"], stream=True) as stream: for frame in stream: if frame.type in ("stdout", "stderr"): print(frame.data.decode(), end="") elif frame.type == "exit": print("exit", frame.exit_code) ``` ```sh tab="curl" curl -N -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/exec" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"argv": ["npm", "test"], "stream": true}' ``` ```json title="Frames" {"type":"started","operation_id":"6f1c…","operation_url":"/v1/operations/6f1c…"} {"type":"stdout","data_base64":"PiB0ZXN0Cg=="} {"type":"stderr","data_base64":"d2FybmluZwo="} {"type":"exit","operation_id":"6f1c…","status":"succeeded","exit_code":0,"timed_out":false,"cancelled":false,"truncated":false} ``` The stream starts with `started` and ends with exactly one `exit` or `error` frame. `stdout` and `stderr` frames are a preview; the `exit` frame and the operation's `exec_result` are the authoritative result. - If the connection drops, the command keeps running. Send the **same idempotency key and body** again to stream it from the start, or follow the operation from the `started` frame. - Output is kept with the operation, up to 1 MiB per operation, and also appears as `stdout`/`stderr` events on `GET /v1/operations/{id}/events`. - An `error` frame (`code`, `message`) means VMYard couldn't run or finish the command. ## Retries and ordering Retrying with the same idempotency key and body returns the original operation; it never runs the command twice. A different body with the same key is `409 idempotency_conflict`. Commands on one machine run one at a time in the order they were accepted. ## Who can run commands Owners, Admins, and Operators, or an API key with at least the `use_existing` preset. The machine must be running. --- # Background commands > Start long-running commands that outlive the request, page through their logs, and kill them. A background command runs detached: the request returns as soon as it starts, there is no timeout, and its combined output goes to a log you can page through. Use it for servers, watchers, and jobs longer than the 10-minute [exec](/docs/guides/commands) limit. Each start is also recorded in the machine's [command history](/docs/guides/command-history). ## Start one ```ts tab="TypeScript" const command = await machine.commands.start("npm run dev"); console.log(command.command_id); ``` ```python tab="Python" command = machine.commands.start("npm run dev") print(command["command_id"]) ``` ```sh tab="curl" curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"command": "npm run dev"}' ``` The body takes exactly one of `argv` or `command` (a `/bin/sh -c` string of at most 8 KiB). The answer is `202` with a `command_id`, which is also the ID of the operation that starts it, and a `command_url`. The CLI has no background-command subcommand yet; the console's **Terminal** tab can start and watch them. ## Status and logs ```ts tab="TypeScript" let offset = 0; const status = await machine.commands.status(command.command_id, { logOffset: offset }); if (status.log) { process.stdout.write(Buffer.from(status.log.data_base64, "base64")); offset = status.log.next_offset; } ``` ```python tab="Python" import base64 status = machine.commands.status(command["command_id"], log_offset=0) if status.get("log"): print(base64.b64decode(status["log"]["data_base64"]).decode(), end="") next_offset = status["log"]["next_offset"] ``` ```sh tab="curl" curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands/$COMMAND_ID?log_offset=0&log_limit=65536" \ -H "Authorization: Bearer $VMYARD_API_KEY" ``` `GET /v1/machines/{id}/commands/{command_id}` returns the status and a window of the log starting at `log_offset` (default 0), at most `log_limit` bytes (1 to 1,048,576; default 65,536). Pass the returned `next_offset` as the next `log_offset`. If the read is still queued, the answer is `202` with an operation to follow; the SDKs do that for you. | `status` | Meaning | | --- | --- | | `starting` | Being started. | | `failed_to_start` | It could not start; see `error_code`. | | `running` | Running. | | `exited` | Finished; `exit_code` holds its status. | | `killed` | Killed; `signal` holds the signal when known. | | `unknown` | The machine has no record of it, for example after a reboot. Stop polling. | VMYard asks for the status before reading the log, so once a command reports `exited`, its log is complete. Logs are bytes: decode them as a stream so a character split across two pages stays whole. `machine.commands.wait(id)` in the SDKs polls until the command is `exited`, `killed`, or `failed_to_start`. ## Kill one ```ts tab="TypeScript" await machine.commands.kill(command.command_id); ``` ```python tab="Python" machine.commands.kill(command["command_id"]) ``` ```sh tab="curl" curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands/$COMMAND_ID/kill" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" -d '{}' ``` Killing is an operation. Read the status afterwards: a command is only `killed` once it reports so. ## Limits Background commands, like everything inside a machine, need a running machine and Owner, Admin, or Operator access (or a `use_existing` key). Reading the status of a command reaches into the machine, so even reads need `use_existing`; a `read_only` key gets `403 insufficient_scope`. --- # Command history > Search a machine's past commands, check how each one ended, and page through its stored stdout and stderr. Every command run on a machine is kept in its history: [foreground commands](/docs/guides/commands), [setup scripts](/docs/guides/setup-scripts), and the starts of [background commands](/docs/guides/background-commands). Lifecycle and file operations are not. In the console, open a machine and choose **History**. History shows command text and output, which can include values an [environment](/docs/guides/environments) exported, so it needs the same role as running commands: Owner, Admin, or Operator, or an API key with at least the `use_existing` preset. Viewers and Billing members get `403 forbidden`, and a `read_only` key gets `403 insufficient_scope`. ## List and search ```sh curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands/history?q=npm&mode=sync&limit=25" \ -H "Authorization: Bearer $VMYARD_API_KEY" ``` Entries come newest first. | Parameter | Meaning | | --- | --- | | `q` | Text the command contains, ignoring case. At most 128 characters, trimmed; blank means no search. | | `mode` | `all` (default), `sync` for foreground commands and setup scripts (the console's **Commands**), or `detached` for background command starts (**Processes**). | | `limit` | 1 to 100 entries (default 50). | | `cursor` | The `next_cursor` of the previous page. It is opaque and continues strictly after the last entry, so commands started meanwhile never repeat or shift what you haven't read. The last page has no `next_cursor`. | Invalid values answer `422` with the field in `field_errors`. `GET /v1/machines/{id}/commands/history/{operation_id}` returns one entry. ## What an entry says | Field | Meaning | | --- | --- | | `operation_id` | The exec operation. For a background command it is also the `command_id`. | | `mode` | `sync` or `detached`. | | `command`, `argv` | The `/bin/sh -c` string, or the arguments joined by spaces, and the exact arguments. Search matches `command`. | | `status`, `error_code` | The operation's status and error code, for example `failed` with `exec_nonzero` or `exec_timed_out`. See [Operations](/docs/concepts/operations). | | `exit_code` | The exit status of a finished foreground command. | | `timed_out`, `cancelled` | How the command ended when it didn't exit on its own. | | `truncated` | The machine capped the captured output. | | `stdout_bytes`, `stderr_bytes` | The size of each stored stream. | | `output_purged` | Retention removed the stored output. The sizes stay. | | `created_at`, `started_at`, `finished_at`, `duration_ms` | When it was requested, first started, and finished, and how long it ran across retries. Entries recorded before history existed have no `started_at`. | | `logs_url` | Where to read the output. | A `detached` entry describes only the start. The process's exit status and live log come from `GET /v1/machines/{id}/commands/{command_id}`, and its stored stdout and stderr are empty. In the console, **Open live status** follows it on the Terminal tab. ## Read the output ```sh curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/commands/history/$OPERATION_ID/logs?stream=stderr&offset=0&limit=65536" \ -H "Authorization: Bearer $VMYARD_API_KEY" ``` `stream` is `stdout` (default) or `stderr`. The answer holds bytes `[offset, offset + limit)` of that stream: `{offset, next_offset, size, data_base64, complete, truncated}`. `limit` is 1 to 1,048,576 (default 65,536). Pass `next_offset` as the next `offset`; an offset at or past `size` returns no data. Output is bytes, so decode it as a stream to keep a character that two pages split whole. While a streamed foreground command is still running, the answer serves the output relayed so far, `size` grows, and `complete` is `false`. Keep reading from `next_offset` until `complete` is `true`, then re-read the entry for its exit code. ## Retention Stored output is removed a set number of days after a command finishes (30 by default; the server's operator chooses). The entry, its exit code, flags, timing, and sizes stay, `output_purged` becomes `true`, and reading the output answers `410 output_expired`. --- # Web terminal > Open an interactive login shell in a running machine from the console or any WebSocket client, and handle its messages, limits, and close reasons. The web terminal is an interactive login shell in a running machine, carried over a WebSocket. It is full machine access, so it is allowed exactly like [running a command](/docs/guides/commands): Owner, Admin, or Operator members, and API keys with the `use_existing` preset or higher. Viewer and Billing members get `403 forbidden`; a `read_only` key gets `403 insufficient_scope`. Each connection starts a new shell. Closing the tab, reconnecting, or losing the network ends that shell and whatever it was running in the foreground; run `tmux` or `screen` in the machine for sessions that should outlive a connection, or start long jobs as [background commands](/docs/guides/background-commands). ## In the console Open a machine, choose the **Terminal** tab, and pick **Shell**. **Connect** opens a terminal sized to the panel; the status line shows who you are signed in as on the machine. The terminal follows the light or dark theme, resizes with the window, and has **Copy**, **Paste**, and font-size controls. **Disconnect** ends the shell; **Reconnect** opens a new one. If the machine isn't running, the panel offers to start it. The console never records what you type or what the shell prints. ## Connect from a browser Browsers can't put headers on a WebSocket, so a session first mints a single-use ticket: ```sh tab="curl" 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" { "ticket": "vyt_…", "expires_at": "2026-10-12T12:00:30Z", "url": "/v1/machines/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/terminal?ticket=vyt_…" } ``` The ticket works once, within 30 seconds, for that machine and the session that minted it. Add the initial size to `url` and connect with the session cookie: ```js tab="JavaScript" const socket = new WebSocket( `wss://api.example.com${ticket.url}&cols=${cols}&rows=${rows}`, ); socket.binaryType = "arraybuffer"; ``` The `Origin` of the page must be the console's configured address (`ui_base_url`), or the upgrade is refused with `403 origin_not_allowed`. An expired, used, or foreign ticket is `403 terminal_ticket_invalid`; mint a new one. ## Connect with an API key Scripts skip the ticket and send the key on the upgrade request. Any WebSocket client works, for example [websocat](https://github.com/vi/websocat): ```sh tab="websocat" websocat --binary \ -H "Authorization: Bearer $VMYARD_API_KEY" \ "wss://api.example.com/v1/machines/$MACHINE_ID/terminal?cols=120&rows=40" ``` An `Origin` header, if sent, must still be allowed. `cols` and `rows` are 1–1000 and default to 80×24; other values are `422`. ## Messages | Direction | Kind | Content | | --- | --- | --- | | Server → client | Text | `{"type":"opened","terminal_id","user"}`, sent first; `user` is the login on the machine. | | Server → client | Binary | Terminal output, at most 64 KiB per message. | | Client → server | Binary | Keystrokes and pasted text (UTF-8), at most 64 KiB per message. | | Client → server | Text | `{"type":"resize","cols","rows"}` (1–1000 each). | | Client → server | Text | `{"type":"signal","signal"}`: `hup`, `int`, `quit`, `kill`, `term`, `usr1`, `usr2`, `cont`, `stop`, `tstp`, or `winch`, sent to the foreground process group. | | Client → server | Text | `{"type":"close"}` ends the shell. | | Server → client | Text | `{"type":"exit","exit_code","signal"}`: the shell ended with that status, or was killed by that signal. | | Server → client | Text | `{"type":"closed","reason"}`: the terminal ended without the shell's status. | `exit` and `closed` are final: the server then closes the WebSocket with code 1000 (1008 after an invalid message, 1001 when the server is shutting down). Any other text message from the client ends the terminal as `invalid_message`. Send a resize when the visible size changes, not on every pixel: the console waits for the size to settle for 150 ms. Send keystrokes as binary messages; a text message is always parsed as control. ## Keepalive and idle time The server pings every 15 seconds and ends a terminal whose client sent nothing, not even a pong, for 45 seconds (`client_lost`). Browsers answer pings on their own. A terminal with no input and no output for the deployment's idle timeout (30 minutes by default) ends as `idle_timeout`. Output is acknowledged to the machine only as it is delivered, so a slow reader pauses the program in the machine rather than losing output. ## Limits | Limit | Default | Answer | | --- | --- | --- | | Open terminals per user in an organization | 4 | `429 terminal_limit_reached` | | Open terminals per organization | 8 | `429 terminal_limit_reached` | | Opens per user per minute | 20 | `429 rate_limited` with `Retry-After` | | Terminals per machine, and per host | 16, 64 | `409 terminal_refused` | Operators can change the first three. Close terminals you no longer use; a tab left open keeps its terminal counted until it closes. ## Close reasons | `reason` | Meaning | | --- | --- | | `exit` | The shell exited. | | `client_closed` | The client sent `close` or closed the WebSocket. | | `client_lost` | The client sent nothing for 45 seconds. | | `idle_timeout` | No input or output for the idle timeout. | | `invalid_message` | The client sent a text message that isn't a control message. | | `server_shutdown`, `replica_lost` | The API server restarted or stopped. Reconnect. | | `channel_lost`, `host_lost`, `peer_lost`, `shutdown` | The machine's host became unreachable or restarted. | | `sandbox_sleeping`, `sandbox_destroyed` | The machine stopped or was deleted. | | `revoked` | The organization was [suspended](/docs/guides/billing#suspended-organizations). Reconnecting is refused with `403 organization_suspended` until the suspension is lifted. | | `replaced`, `fenced`, `identity_reset`, `invalid_frame`, `upgrade_failed` | The connection to the machine was reset. Reconnect. | Every open and close is recorded in the machine's audit log with who opened it, its size, how long it lasted, and why it closed. Keystrokes and output are never stored. ## Errors | Status | Code | What to do | | --- | --- | --- | | 403 | `forbidden` | Ask an Owner or Admin for the Operator role. | | 403 | `insufficient_scope` | Use a key with the `use_existing` preset or higher. | | 403 | `session_required` | API keys don't mint tickets; connect with the key directly. | | 403 | `origin_not_allowed` | Open the console from its configured address. | | 403 | `terminal_ticket_invalid` | Mint a new ticket and connect within 30 seconds. | | 409 | `machine_not_running` | Start the machine and wait until it is running. | | 409 | `terminal_refused` | The machine or its host has too many terminals. Close some. | | 429 | `terminal_limit_reached` | Close another terminal first. | | 429 | `rate_limited` | Wait `Retry-After` seconds. | | 503 | `terminals_unavailable` | This deployment has no runtime for terminals. | | 503 | `runtime_unavailable` | The machine's host can't be reached. Retry shortly. | See [Errors](/docs/guides/errors#web-terminal) for the problem format. --- # Desktop > See and use a running machine's graphical desktop over VNC in the browser, with its security model, latency expectations, and close reasons. The **Desktop** tab shows a running machine's graphical desktop in the console. It is a VNC session carried over a WebSocket and drawn in the page by [noVNC](https://github.com/novnc/noVNC): you see the screen, and your mouse, touch, and keyboard drive it. The desktop is VNC first. Machines whose image or host has no graphical desktop yet answer `503 desktop_unavailable`, and the tab says so; use the [web terminal](/docs/guides/web-terminal) for those. ## Who can open it A desktop is full machine access, so it is allowed like the web terminal: Owner, Admin, and Operator members. Viewer and Billing members get `403 forbidden`. Up to 4 viewers can watch one machine at a time. ## In the console Open a machine and choose **Desktop**, then **Connect**. The status line shows the remote screen size once the VNC session starts, and focus moves to the screen. - **Disconnect** ends the session; **Reconnect** opens a new one. Nothing on the machine stops when you disconnect. - **View only** stops sending your mouse and keyboard; you only watch. - **Scale to fit** shrinks the remote screen into the panel. Turn it off to see it at 1:1 and scroll. - **Resize remote to window** asks the machine's VNC server to match the panel's size, so nothing is scaled. The machine applies it if its desktop allows resizing. - **Full screen** shows only the desktop; press Escape to leave. - **Send keys** sends combinations your computer keeps for itself: Ctrl+Alt+Del, Ctrl+Esc, Alt+Tab, Alt+F4, the Super (Windows) key, Escape, Tab, and Print Screen. Under **Workspaces** it also sends the desktop's own shortcuts: **Workspace 1** to **Workspace 4** (Ctrl+Alt+1 to 4), **Next workspace** and **Previous workspace** (Ctrl+Alt+Right and Left), and **Open launcher** (Super). Use these when pressing the shortcut yourself does nothing; on a Mac, for example, Ctrl+Option+2 reaches the machine as a plain "2". While the desktop has focus, every key goes to the machine. **Shift+Escape** moves focus back to the controls. ### Full page in its own tab **Open in new tab** on the Desktop tab, **Desktop** in a machine's row menu on the Machines page, and **Desktop** at the top of a machine's overview all open the desktop full page in a new browser tab: only the remote screen, edge to edge, with no console around it. It connects as soon as it opens and scales the screen to fit. The tab is titled with the machine's name, such as "agent-primary — Desktop". A small handle on the left edge opens the controls: **Send keys**, **Clipboard** (send text or your clipboard to the machine, and copy what the machine copied), **Full screen**, **View only**, **Reconnect**, **Disconnect**, and **Back to machine**. Shift+Escape moves focus from the remote screen to the handle. While it connects you see "Connecting to the desktop…"; if that takes more than about 10 seconds the page says why it may be slow and offers **Reconnect** and **Back to machine**. A dropped connection reconnects on its own, as on the tab. The Desktop tab keeps working as before; both can be open at once. ### Browser mode **Browser** (next to **Open in new tab**, in the row menu, and at the top of the overview) opens the same full page showing only the machine's web browser, with no desktop shell around it. The page asks for it with `mode=browser` on the desktop WebSocket, and opens only when the server confirms browser mode. A machine whose image or host can't stream the browser on its own (`503 browser_unavailable`, or a host that ignores the mode) shows "Browser mode isn't available on this machine" with a link to the full desktop, whose web browser is in the app launcher. ### Opening apps Applications start from the desktop itself: click the apps-grid icon at the left of the top bar, or press the Super (Windows) key (or **Send keys** › **Open launcher** if your computer keeps that key), then type to search, for example "chrom" for the web browser, and press Enter. Arrow keys move through the list and Escape closes it. Apps open maximized under the top bar. Machine images whose desktop has no launcher yet open apps from the right-click menu instead. ### From the Machines page The row menu (⋯) of each machine offers **Desktop** and **Browser** (each in a new tab), **Terminal**, **Files**, and **Copy SSH**, next to Open overview, Run command, Start or Stop, Copy ID, and Delete. Desktop and Browser are disabled with "(Machine is not up)" while the machine isn't running, and for roles that can't open a desktop. Copy SSH copies the `ssh` command once an SSH key is installed and the machine's region has an SSH gateway; otherwise it says which is missing. ### Clipboard and typing - **Text for the remote machine**: **Type it** sends the text as keystrokes, which works in any application and from a phone keyboard. **Send to remote clipboard** puts it on the machine's clipboard to paste there; VNC clipboards carry Latin-1 text unless the server supports more. **Paste my clipboard** sends your clipboard directly, if the browser lets the page read it. - **Remote clipboard** shows text copied on the machine; **Copy to my clipboard** copies it to yours. ### On phones The panel keeps the remote screen's shape at narrow widths. Tap to click, and use **Type it** for text. **View only** is useful for watching a long job. ## Latency Every frame and keystroke travels from your browser to the control plane, on to the machine's host, and back, so expect a delay comparable to the round trip to the machine's region plus a few frames of encoding. Typing and pointing are fine for administration, installers, and checking a GUI test; video and fast animation are not smooth. Scaling a large remote screen down costs your browser some work; **Resize remote to window** avoids it. ## Security model - **A ticket, no VNC password.** The console mints a single-use ticket with your browser session and CSRF token (`POST /v1/machines/{machine_id}/desktop/tickets`). It works once, within 30 seconds, for that machine and that session. The WebSocket (`GET /v1/machines/{machine_id}/desktop?ticket=…`) also checks your session cookie and that the page is on the console's configured address. The VNC server on the machine is reached only through this bridge, so it has no password of its own; if one ever asks for a password, the console refuses and reports it. - **The console records nothing.** It relays the screen and your input to the machine without storing or logging them. - **Your role decides.** Role changes apply to the next connection. ## Status and errors | What you see | Why | What to do | | ------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------- | | This machine has no desktop yet | `503 desktop_unavailable`: the image or host has no desktop support | Use the terminal | | The machine isn't running | `409 machine_not_running` | **Start** it, then connect | | The machine stopped | The machine slept or was stopped during the session | **Start** it, then connect | | Too many desktops opened | `429 rate_limited`; the panel counts down `Retry-After` | Reconnect when the countdown ends | | Your role can't open a desktop | `403 forbidden` | Ask an Owner or Admin | | Closed: the machine already has 4 desktop viewers | `limit` | Close a viewer elsewhere, then reconnect | | Another viewer took over this desktop | `replaced` | Reconnect to take it back | | Closed after no input for the idle timeout | `idle_timeout` | Reconnect | | Connection lost / host became unreachable | The network or the machine's host dropped | The panel reconnects on its own, waiting 1, 2, 4, 8, then 16 seconds | | Browser mode isn't available on this machine | `503 browser_unavailable`, or the host ignores `mode=browser` | Open the full desktop and start the browser from the launcher | ## Protocol After the upgrade the server sends one text message, `{"type":"opened","width":1280,"height":800}`. Add `mode=browser` to the WebSocket query (`GET /v1/machines/{machine_id}/desktop?ticket=…&mode=browser`) for browser mode; the server confirms it with `"mode":"browser"` in `opened`, and the default `desktop` needs no parameter. From then on binary messages are raw RFB (VNC protocol) bytes in both directions. Other text messages are JSON control from the server: `{"type":"closed","reason":"…"}` is final (`idle_timeout`, `machine_stopped`, `deleted`, `replaced`, `runtime_unavailable`, `shutdown`, `limit`, `normal`, or `revoked` when the organization was [suspended](/docs/guides/billing#suspended-organizations)), and `{"type":"error","code":"…"}` reports a problem. The remote screen is resized inside RFB (SetDesktopSize), not with a control message. The desktop routes are not in the [API reference](/docs/api) yet; they follow the web terminal's ticket flow and errors. --- # Coding agents > Run Claude Code, Codex, or Pi inside a machine, follow what it does, steer or interrupt it, and pick up a conversation later. A coding agent runs inside a running machine, in its `/work` directory, with the machine's tools. You send it a prompt, follow its events as they happen, add instructions while it works, and stop it. Each machine keeps several conversations, and you can resume any of them. Prompts, attachments, and the agent's output stay on the machine's disk. In the console, open a machine and choose **Agents**. ## Provider keys The agent uses the provider keys in the machine's [environment](/docs/guides/environments): for example `ANTHROPIC_API_KEY` for Claude Code or `OPENAI_API_KEY` for Codex. A machine created without an environment gives the agent no keys, so add one to an environment and create the machine with it. Keys never appear in requests, events, or VMYard's records. ## Start or resume a conversation ```sh curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE/agents/prompt" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"provider": "claude", "model": "sonnet", "reasoning_effort": "medium", "text": "Add a health check endpoint and run the tests."}' ``` The answer (`202`) is the turn, with its `conversation_id`. Send that `conversation_id` with the next prompt to continue the conversation; a conversation keeps its agent. `GET /v1/provider-models` lists each agent's models and reasoning efforts. Up to 16 attachments (16 MiB together) are written to the machine for the agent to read. ## Follow the events `GET /v1/machines/{id}/agents/events?conversation_id=…` answers JSON by `cursor`, waiting up to `wait_seconds` (at most 30) for an event, or a server-sent event stream with `Accept: text/event-stream`. Each SSE event's `id` is the cursor after it, so a client that reconnects with `Last-Event-ID` resumes where it stopped, without repeats. The stream ends with an `idle` event when no turn is running. | `type` | Meaning | | --- | --- | | `turn_started` | The agent started, with its model and reasoning effort. | | `input_accepted` | The agent took an input (`kind`: `prompt`, `steer`, or `interrupt`) with this `input_id`. | | `text_delta`, `message` | Text the agent wrote, piece by piece or whole. | | `thinking_delta` | The agent's reasoning, when it shares it. | | `tool_call`, `tool_result` | A tool the agent ran (`id`, `name`, `input`) and what came back (`output`, `is_error`). | | `usage` | Tokens and the agent's own cost estimate. | | `error` | Something went wrong (`code`, `message`). | | `turn_end` | The turn ended: `outcome` is `completed`, `failed`, or `interrupted`. | ## Steer and interrupt While a turn runs, `POST /v1/machines/{id}/agents/steer` with the `conversation_id` and `text` adds instructions; the agent confirms when it takes them with an `input_accepted` event carrying the steer's `input_id`. `POST /v1/machines/{id}/agents/interrupt` asks the turn to stop, and `force: true` stops it at once. Both need an `Idempotency-Key`. See [Errors](/docs/guides/errors#coding-agents). --- # Files > Browse a machine's /work as a live tree, and write, read, upload, and download files up to 32 MiB. Files move in and out of a machine through `/work`. Paths are relative to `/work` (`notes/todo.txt`) or written in full (`/work/notes/todo.txt`). Paths can't contain `.` or `..` segments, empty segments, or NUL, and can't point anywhere else. ## Browse files in the console A machine's **Files** tab lists `/work` live while the machine runs: - The header shows how much data `/work` holds and how many files (**Your data 173 MiB · 301 files**). **Refresh** lists it again. - Each row shows its size (a folder counts everything inside it) and its share of the total, with a small bar. Hover a row for its full path and modified time; the selected row shows the time inline. - Folders open with a click or the arrow keys. The first listing goes two levels deep; deeper folders are listed when you open them. - **Fuzzy search files…** matches names and paths in the folders listed so far (`claudeset` finds `.claude/settings.json`), opens the folders on the way to each match, and highlights the matched letters. - **Name / Size** sorts the tree. Press the active one again to reverse it: names start A to Z with folders first, sizes start largest first. - Keyboard: ↑ ↓ move, → opens a folder or steps into it, ← closes it or steps out, Home and End jump, Enter opens a folder or a file. The bar under the tree acts on the selected row (or on `/work` when nothing is selected): | Action | What it does | | --- | --- | | **Open** | Shows a text file under **Read a file**. | | **Download** | Saves a file (up to 32 MiB) to this device. | | **Download folder** | Saves a folder as a `.tar.gz` archive. Very large folders answer `413 archive_too_large`; download smaller folders or copy over [SSH](/docs/guides/ssh). | | **Upload here** | Picks a local file and uploads it into the folder, then confirms it by reading it back. | | **New file** | Fills **Write a file** with the folder's path. | | **Copy path** | Copies the absolute path. | Above the tree, **Latest snapshot** shows when the newest ready [snapshot](/docs/guides/snapshots) was taken and how much data it holds; restore or fork from it on the **Snapshots** tab. Docker development machines never push snapshots, so they show **No snapshots yet**. A stopped machine shows **Start** in place of the tree. If a listing stops early (very large folders), the tree says so and marks the folder **Partial list**. The listing comes from `GET /v1/machines/{id}/files/tree?path=/work&depth=2` (`path` may be `/work`, `/root`, or `/home/`, or a folder inside one; `depth` is 1 to 4), which answers `{path, total_bytes, file_count, truncated, entries}`; each entry has `name`, `path`, `type` (`file`, `dir`, or `symlink`), `size_bytes`, `modified_at`, and, within the depth, `children`. Folders download from `GET /v1/machines/{id}/files/archive?path=`, and the snapshot strip reads `GET /v1/machines/{id}/snapshots/latest` (`{id, created_at, completed_at, source, tier, size_bytes}`, or `404 no_snapshot`). ## Write a file ```sh tab="CLI" vy cp ./build.tar.gz dev:build.tar.gz echo 'debug=true' | vy cp - dev:app/config.env ``` ```ts tab="TypeScript" await machine.files.write("app/config.json", JSON.stringify({ debug: true })); await machine.files.write("bin/tool", new Uint8Array(bytes)); // bytes go as base64 ``` ```python tab="Python" machine.files.write("app/config.json", '{"debug": true}') machine.files.write("bin/tool", open("tool", "rb").read()) # bytes go as base64 ``` ```sh tab="curl" curl -X PUT "$VMYARD_API_URL/v1/machines/$MACHINE_ID/files?path=app/config.json" \ -H "Authorization: Bearer $VMYARD_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"encoding": "text", "content": "{\"debug\": true}"}' ``` `PUT /v1/machines/{id}/files?path=` takes `{encoding, content}` where `encoding` is `text` or `base64`. It replaces the whole file. Writing is an [operation](/docs/concepts/operations): the answer is `202`, and the file is written once the operation succeeds. VMYard moves files in 512 KiB chunks and checks the size the machine reports against the bytes it sent, so a partial write fails rather than passing silently. ## Read a file ```sh tab="CLI" vy cp dev:out/report.txt . vy cp dev:out/report.txt - # to stdout ``` ```ts tab="TypeScript" const text = await machine.files.read("out/report.txt"); const bytes = await machine.files.readBytes("out/image.png"); ``` ```python tab="Python" text = machine.files.read("out/report.txt") data = machine.files.read_bytes("out/image.png") ``` ```sh tab="curl" curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/files?path=out/report.txt&encoding=text" \ -H "Authorization: Bearer $VMYARD_API_KEY" ``` `GET /v1/machines/{id}/files?path=&encoding=` returns `{path, size, encoding, content}`. Reads wait up to 30 seconds; if the machine hasn't answered by then, the response is `202` with an operation to follow, and the finished operation carries the content in `file_ranges`. The SDKs and CLI handle that for you. Read with `encoding=base64` for binary files. Reading a file that isn't valid UTF-8 as `text` fails with `422 file_not_text`. ## Errors | Status | Code | Meaning | | --- | --- | --- | | 404 | `file_not_found` | No file at that path. | | 413 | `file_too_large` | The file is larger than 32 MiB. | | 422 | `file_not_text` | Not UTF-8; read it as `base64`. | | 422 | `invalid_request` | The path is not allowed. | | 409 | `machine_not_running` | Listing needs a running machine. | | 413 | `archive_too_large` | The folder is too large for one archive. | ## Access Reading and writing files needs a running machine and Owner, Admin, or Operator access, or an API key with at least the `use_existing` preset. File content is whatever the machine holds: treat it as untrusted input. --- # Machine metrics > CPU, memory, disk, disk I/O, and network use for each machine, with one-minute history, live samples, and disk warnings. Every machine's host samples it every 5 seconds. VMYard keeps the latest values and one-minute history for the last 24 hours. Every member of the organization can read them, and so can every API key preset. ## What is measured | Value | Meaning | | --- | --- | | CPU | Millicores used over the host's last sampling interval (1000 = one core), as a share of the machine's CPU limit. | | Memory | Memory charged to the machine (RAM in use, page cache, and the virtual machine monitor), as a share of its limit. | | Disk | Blocks the machine's disk image holds, as a share of its size. It is an upper bound: blocks the machine freed stay allocated. | | Disk I/O | Bytes per second read from and written to the disk. | | Network | Bytes per second received (`rx`) and sent (`tx`) by the machine. | A value the host didn't measure is absent, never zero: CPU and memory while the machine is asleep, and Docker machines on macOS. Metrics describe resource use only. They are not used for billing. ## In the console The machine list has a **CPU / RAM / Disk / IO** column with a bar and a number for each value. Hover or focus it for the used amount, the limit, and the I/O and network rates. On a phone the same values appear in each machine's card. A machine that hasn't been sampled yet shows "—". The machine's **Overview** has a **Metrics** section with charts for CPU, memory, disk, disk I/O, and network over the last hour, 6 hours, or 24 hours. Hover or focus a chart (then use the arrow keys) to read the values at one minute; the table under the charts gives the latest, average, and peak of each value. **Live** follows the host's own samples, about 5 seconds apart, for as long as it stays on. Live needs a running or sleeping machine. When it isn't available the console says why and goes back to one-minute history. ### Stale values When VMYard couldn't read a machine recently, its values are kept and marked **Stale** with the reason. They are the last values known, not zero. | Reason | Meaning | | --- | --- | | Host unreachable | The machine's host couldn't be read. | | Not reported | The host no longer reports this machine, for example after it stopped. | | Poll overdue | No reading confirmed the values in the last three polling intervals. | ## Disk warnings | Flag | Meaning | | --- | --- | | `disk_almost_full` | At least 90% of the machine's disk is used. | | `disk_full` | At least 98% is used. Writes in the machine may fail. | | `host_disk_low` | The host the machine runs on is low on disk. This is not your machine's disk; nothing for you to clean up. | The disk flags come from the machine's own filesystem report while it is fresh, otherwise from the disk image, so treat them as advisory: VMYard enforces nothing on them. To free space, find large directories from the terminal (`du -sh /work/* | sort -h`), then delete what you don't need, or read files you want to keep from the [Files](/docs/guides/files) tab first. Owners, admins, and operators also get the `disk_almost_full` and `disk_full` [email alerts](/docs/guides/email-alerts) by default, at most once an hour per machine. ## Guest-reported values The response may include `guest_reported`: memory and filesystem totals that the machine's own agent reports. Code inside the machine can change them, so the console shows them in a separate, labelled block. They never reach the history, billing, or enforcement. ## Read metrics with the API The latest values come with every machine as `metrics`, or `null` until it is first sampled: ```json "metrics": { "sampled_at": "2026-10-12T11:59:30Z", "stale": false, "stale_reason": null, "state": "running", "cpu_percent": 37.5, "cpu_millis": 1500, "cpu_limit_millis": 4000, "memory_percent": 62.0, "memory_bytes": 5325918208, "memory_limit_bytes": 8589934592, "disk_percent": 91.2, "disk_used_bytes": 78383153152, "disk_size_bytes": 85899345920, "io_read_bps": 1250000, "io_write_bps": 420000, "net_rx_bps": 96000, "net_tx_bps": 18000, "flags": ["disk_almost_full"] } ``` History is one request per machine: ```sh tab="curl" curl "$VMYARD_API_URL/v1/machines/$MACHINE_ID/metrics?resolution=1m&since=2026-10-12T11:00:00Z" \ -H "Authorization: Bearer $VMYARD_API_KEY" ``` - `resolution=1m` (the default) returns stored minutes, oldest first, from `since` (default: the last 24 hours). Each point averages the minute, with `cpu_millis_max` and `memory_bytes_max` for its peak. A response holds at most 1440 points; when `next_since` is set, ask again with it as `since`. - `resolution=live` returns the host's retained samples (about 5 seconds apart, for about 15 minutes) newer than `since`. Poll with the last point's `t` as `since`. A machine that isn't running or sleeping answers `409 machine_not_running`; an unreachable host `503 runtime_unavailable`; a server without a runtime `503 metrics_unavailable`. On any of these, read `1m` instead. Rates are bytes per second. The console shows them in decimal units (1 kB/s = 1,000 B/s) and sizes in binary units (1 GiB = 1,073,741,824 bytes). --- # Ports and private URLs > Expose a machine's ports as HTTPS URLs, keep them private with short-lived tokens, or make them public. Any TCP port in a running machine can be served at its own HTTPS address: ```text https://