# Wheelmachine docs, whole > Every page of https://docs.wheelmachine.ai, one after another, for agents and tools that read a site at once. The API's contract is at https://docs.wheelmachine.ai/openapi.yaml. --- Source: https://docs.wheelmachine.ai/ # Wheelmachine docs > Machines for agents and people, by the second. Make one, run commands in it, copy files in and out, pause it and delete it, from a terminal, from Python or TypeScript, or from an agent. Wheelmachine docs Make a machine, usually in well under a second. Run commands in it, copy files in and out, pause it with its memory kept, and delete it when the work is done. From a terminal, from Python or TypeScript, or from an agent. [Start the quickstart](https://docs.wheelmachine.ai/quickstart.html)[API reference](https://docs.wheelmachine.ai/reference.html) Terminal: ```sh $ wm machines create --name box ID NAME STATE SIZE LOCATION AGE m_01jb2xk3n4p5q6r7s8t9v0w1x2 box running 2 CPU, 4 GB Helsinki, FI 0s $ wm exec box -- python3 -c "print(2+2)" 4 $ wm cp ./data.csv box:work/ $ wm machines pause box ID NAME STATE SIZE LOCATION AGE m_01jb2xk3n4p5q6r7s8t9v0w1x2 box paused 2 CPU, 4 GB Helsinki, FI 1m ``` ## Four steps 1. **Get a key.** It begins `ak_live_` and holds the scopes it may use. 2. **Log in.** `wm login` keeps it on your computer, readable by you only. 3. **Make a machine.** 2, 4, 8 or 16 CPUs, with 2 GB of memory each. 4. **Use it.** Run commands, copy files, pause it, copy it, delete it. ## Where to go [**Quickstart**A key, a machine, a command, a file, a pause. Five minutes.](https://docs.wheelmachine.ai/quickstart.html) [**Machines**States, sizes, sleeping when idle, copies and snapshots.](https://docs.wheelmachine.ai/machines.html) [**Commands and files**Run anything; copy files in and out.](https://docs.wheelmachine.ai/commands-and-files.html) [**Keys and scopes**What a key may do, and why one never makes a stronger one.](https://docs.wheelmachine.ai/keys.html) [**Command line**Every `wm` command and flag.](https://docs.wheelmachine.ai/cli.html) [**Libraries**Python and TypeScript, with retries that never make two.](https://docs.wheelmachine.ai/libraries.html) [**Agents**The `wm-mcp` server: machines as tools for an agent.](https://docs.wheelmachine.ai/agents.html) [**Errors**Every code the API answers with, and what to do.](https://docs.wheelmachine.ai/errors/index.html) [**API reference**Every route and field, made from the API's contract.](https://docs.wheelmachine.ai/reference.html) > **For agents that read.** The whole site is one file at [llms-full.txt](https://docs.wheelmachine.ai/llms-full.txt), with an index at [llms.txt](https://docs.wheelmachine.ai/llms.txt); every page is also there as Markdown (the same address, ending `.md`), and the API's contract is [openapi.yaml](https://docs.wheelmachine.ai/openapi.yaml). --- Source: https://docs.wheelmachine.ai/quickstart # Quickstart > From a key to a running machine: log in, make a machine, run a command in it, copy a file in and out, pause it and delete it. From a key to a machine you have used and put away, in about five minutes. You need a terminal and an API key. ## Get a key For now, keys are made in Wheelmachine's console by your account's owner: ask for one that holds `cloud:*` to follow this page. A key looks like `ak_live_` and 40 letters and digits. Keep it like a password: anyone who has it can use your machines. > **Coming:** A page where you make your own keys. ## Get wm `wm` is the command line. For now it is built from its source, with Go 1.27 or later, which writes `wm` and `wm-mcp` (the server for agents): Terminal: ```sh $ cd frontend/cli $ go build -o bin/ ./cmd/... $ export PATH="$PWD/bin:$PATH" ``` ## Log in Terminal: ```sh $ wm login To sign in, open https://app.wheelmachine.ai/device and enter this code: BCDF-GHJK Your browser is opening it. Sign in, type the code, then approve. Waiting for you to approve (the code works until 09:10; Ctrl-C to stop)... Logged in to account you@example.com. Signed in. A key of your own, "wm on laptop" (key_3f9a1c2b4d5e6f70), is kept in /home/you/.config/wheelmachine/config.json, readable by you only. ``` You sign in on the page with GitHub or Google, and `wm` checks the key it is given with the API before it keeps it. On a computer with no browser, open the address on any other device; to keep a key you already have, use `wm login --key`. To use a key without keeping it, put it in `WHEELMACHINE_API_KEY` instead. ## Make a machine Terminal: ```sh $ wm machines create --name box ID NAME STATE SIZE LOCATION AGE m_01jb2xk3n4p5q6r7s8t9v0w1x2 box running 2 CPU, 4 GB Helsinki, FI 0s ``` It answers once the machine runs, usually in well under a second. This one has 2 CPUs and 4 GB of memory; `--cpus 4`, `8` or `16` give it more. From here on, the machine is named by its id or by its name. ## Run a command Terminal: ```sh $ wm exec box -- uname -s Linux $ wm sh box 'echo hello > note.txt && ls -la | wc -l' 4 ``` `wm exec` runs a program with its words, without a shell; `wm sh` runs a line of shell, with pipes and redirections. Both print what the command wrote, and exit with its exit code. Commands start in the machine's work folder. ## Copy a file in and out Terminal: ```sh $ wm cp ./data.csv box:work/ $ wm exec box -- wc -l data.csv 1201 data.csv $ wm cp box:work/note.txt . ``` In a machine, a path begins `work/` (the work folder, where commands start) or `home/` (its home). A path that ends in `/` is a folder, and the file keeps its name in it. ## Pause it Terminal: ```sh $ wm machines pause box ID NAME STATE SIZE LOCATION AGE m_01jb2xk3n4p5q6r7s8t9v0w1x2 box paused 2 CPU, 4 GB Helsinki, FI 3m ``` Paused, it keeps its memory and no running time is charged. A command wakes it, and so does `wm machines start box`; it carries on where it was. A machine also pauses by itself once it has been idle for five minutes: see [Sleep when idle](https://docs.wheelmachine.ai/machines.html#sleep-when-idle). ## Delete it Terminal: ```sh $ wm machines rm box Delete m_01jb2xk3n4p5q6r7s8t9v0w1x2 and all its files? [y/N] y m_01jb2xk3n4p5q6r7s8t9v0w1x2 ``` Deleting takes its files with it, for good. In a script, add `--yes`. ## The same, in code The libraries and the API do the same steps. Each reads the key from `WHEELMACHINE_API_KEY`. Python: ```python from pathlib import Path from wheelmachine import Wheelmachine client = Wheelmachine() # reads WHEELMACHINE_API_KEY machine = client.machines.create(name="box") machine = client.machines.wait(machine.id, "running") # at once when it already runs result = client.machines.exec(machine.id, ["uname", "-s"]) print(result.exit_code, result.stdout) client.machines.write_file(machine.id, "work/data.csv", Path("data.csv")) client.machines.read_file(machine.id, "work/data.csv", to="data-back.csv") client.machines.pause(machine.id) client.machines.delete(machine.id) ``` TypeScript: ```ts import { Wheelmachine } from "wheelmachine"; const client = new Wheelmachine(); // reads WHEELMACHINE_API_KEY let machine = await client.machines.create({ name: "box" }); machine = await client.machines.wait(machine.id, "running"); // at once when it already runs const result = await client.machines.exec(machine.id, ["uname", "-s"]); console.log(result.exit_code, result.stdout); await client.machines.writeFile(machine.id, "work/data.csv", { path: "data.csv" }); await client.machines.readFile(machine.id, "work/data.csv", { to: "data-back.csv" }); await client.machines.pause(machine.id); await client.machines.delete(machine.id); ``` curl: ```sh API=https://api.wheelmachine.ai AUTH="Authorization: Bearer $WHEELMACHINE_API_KEY" # make a machine, and keep its id (jq reads it from the answer) ID=$(curl -s -X POST $API/v1/machines -H "$AUTH" -H "Content-Type: application/json" \ -d '{"name": "box"}' | jq -r .id) # run a command curl -s -X POST $API/v1/machines/$ID/exec -H "$AUTH" -H "Content-Type: application/json" \ -d '{"command": ["uname", "-s"]}' # copy a file in, and back out curl -s -T data.csv "$API/v1/machines/$ID/files?path=work/data.csv" -H "$AUTH" curl -s "$API/v1/machines/$ID/files?path=work/data.csv" -H "$AUTH" -o data-back.csv # pause it, then delete it curl -s -X POST $API/v1/machines/$ID/pause -H "$AUTH" curl -s -X DELETE $API/v1/machines/$ID -H "$AUTH" ``` ## Next - [Machines](https://docs.wheelmachine.ai/machines.html): sizes, states, sleeping when idle, copies and snapshots. - [Commands and files](https://docs.wheelmachine.ai/commands-and-files.html): long work, output, root, environment. - [Keys and scopes](https://docs.wheelmachine.ai/keys.html): a key of its own for each program and each agent. - [Agents](https://docs.wheelmachine.ai/agents.html): give an agent machines through `wm-mcp`. --- Source: https://docs.wheelmachine.ai/machines # Machines > What a machine is: its states, its sizes, how it sleeps when idle, copies, snapshots, names, labels, projects, where it runs and what it costs. A machine is a Linux computer of your own, reached through the API. It runs your commands and keeps your files, and it is charged by the second while it runs. wm: ```sh $ wm machines create --name trainer --cpus 8 --label team=ml --idle-after 30m ID NAME STATE SIZE LOCATION AGE m_01jb2xk3n4p5q6r7s8t9v0w1x2 trainer running 8 CPU, 16 GB Helsinki, FI 0s ``` Python: ```python machine = client.machines.create( name="trainer", cpus=8, labels={"team": "ml"}, idle_after_s=1800 ) print(machine.id, machine.state) ``` TypeScript: ```ts const machine = await client.machines.create({ name: "trainer", cpus: 8, labels: { team: "ml" }, idle_after_s: 1800, }); console.log(machine.id, machine.state); ``` curl: ```sh curl -X POST https://api.wheelmachine.ai/v1/machines \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "trainer", "cpus": 8, "labels": {"team": "ml"}, "idle_after_s": 1800}' ``` ## States Starting becomes running. From running, `pause`, `stop` and `archive` put a machine away, each keeping more or less of it; `start` brings it back. `delete` ends it from any state. **starting** Being made ready. When no server has room at once, one is made ready for it: the answer is `202`, and [a wait](https://docs.wheelmachine.ai/machines.html#wait-for-it) follows it until it runs. **running** It runs; commands and files work. Charged by the second. **paused** Its memory is kept and nothing runs; no running time is charged. A command wakes it, and so does `start`: it carries on where it was. **stopped** Its files are kept, its memory is not. `start` runs it again from its files. **archived** Its files are put away in storage: the cheapest way to keep a machine. `start` brings it back. **recovering** Its server was lost, and it is being brought back from its last moments. Wait for `running`. **deleted** Gone, with its files. Deleting cannot be undone. | To | wm | API | |---|---|---| | Pause it | `wm machines pause ` | `POST /v1/machines/{machine_id}/pause` | | Run it again | `wm machines start ` | `POST /v1/machines/{machine_id}/start` | | Stop it | `wm machines stop ` | `POST /v1/machines/{machine_id}/stop` | | Archive it | `wm machines archive ` | `POST /v1/machines/{machine_id}/archive` | | Delete it | `wm machines rm ` | `DELETE /v1/machines/{machine_id}` | A move its state does not allow, such as pausing a machine that is not running, answers [`409 conflict`](https://docs.wheelmachine.ai/errors/conflict.html). ## Sizes A machine has 2, 4, 8 or 16 CPUs, with 2 GB of memory for each. It is charged by the second at the 2-CPU price that many times over: a 16-CPU machine costs eight times a 2-CPU one. | CPUs | Memory | wm | API | |---|---|---|---| | 2 (when not said) | 4 GB | `--cpus 2` | `"cpus": 2` | | 4 | 8 GB | `--cpus 4` | `"cpus": 4` | | 8 | 16 GB | `--cpus 8` | `"cpus": 8` | | 16 | 32 GB | `--cpus 16` | `"cpus": 16` | The size is chosen when the machine is made, and a copy has its original's size. > **Coming:** Machines with a GPU. They are not open to every account yet; `wm gpus` shows what is coming. ## Sleep when idle A machine pauses by itself once it is idle: nothing has reached it for `idle_after_s` seconds (300, five minutes, when not said), and its programs have been quiet as long, under 5% of one CPU on average. A job left working keeps it awake. Paused, it keeps its memory and no running time is charged; the next command wakes it. wm: ```sh $ wm machines create --name nightly --idle-after 2h # 1m to 24h $ wm machines create --name server --no-sleep # never pauses by itself ``` API: ```json {"name": "nightly", "idle_after_s": 7200} {"name": "server", "sleep_when_idle": false} ``` `idle_after_s` is 60 to 86400 seconds. A machine made with `sleep_when_idle` false never pauses by itself: it runs, and is charged, until it is paused or stopped. Both are set when the machine is made; a copy sleeps as its original does. The machine says how it sleeps in `sleep_when_idle` and `idle_after_s` (null when it never does). ## Wait for it Making, starting or copying a machine answers once it runs, usually in well under a second. When no server has room just now, one is made ready for it; if that takes longer than `wait_s` (60 seconds when not said, at most 600), the answer is `202` with the machine `starting`, and a wait follows it: wm: ```sh $ wm machines wait box # until it runs, for up to 10 minutes $ wm machines wait box --state paused --timeout 2m ``` Python: ```python machine = client.machines.wait(machine.id, "running", timeout_s=600) ``` TypeScript: ```ts const ready = await client.machines.wait(machine.id, "running", { timeout_s: 600 }); ``` curl: ```sh curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/wait?state=running&timeout_s=120" \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" ``` One wait call lasts at most 600 seconds; a machine not there by then answers [`408 timeout`](https://docs.wheelmachine.ai/errors/timeout.html), and you can wait again. `wm` and the libraries wait for you; `wm machines create --no-wait` answers at once instead. ## Copies Terminal: ```sh $ wm machines copy box --name box-2 ID NAME STATE SIZE LOCATION AGE m_01jb2xm7c9d4e8f2g6h1j5k3n0 box-2 running 2 CPU, 4 GB Helsinki, FI 0s ``` A copy is a new machine, made from the original as it is now, files and all, beside it where there is room, so its files are copied in a moment. It has the original's name and labels unless you give others (`--label` gives the copy its own labels in place of the original's), and the original's size and sleep. From then on it is a machine of its own: charged, paused and deleted on its own. ## Snapshots Terminal: ```sh $ wm machines snapshot box --note "before the upgrade" SNAPSHOT MACHINE NOTE MADE 20261023T093500Z-3f2a0001 m_01jb2xk3n4p5q6r7s8t9v0w1x2 before the upgrade 2026-10-23 09:35 UTC $ wm machines create --name box-again --from-snapshot box:20261023T093500Z-3f2a0001 ``` A snapshot keeps the machine's files as they are now. A new machine made `from_snapshot` starts with those files; give the machine alone (`--from-snapshot box`) for its latest snapshot. Keep a snapshot's id when it is made: the API does not list them. ## Names and labels A machine's id is made by the API: `m_` and 26 letters and digits. Its name is for people, up to 80 characters. `wm` takes a name wherever it takes an id; when two machines share a name, give the id. Terminal: ```sh $ wm machines label box team=ml stage=test # set two labels $ wm machines label box stage- # take one off $ wm machines ls --label team=ml # machines with that label $ wm machines rename box trainer ``` A machine holds up to 32 labels. A label's name begins with a letter or a digit and goes on with letters, digits, `.`, `_`, `-` and `/`, up to 63 of them; its value is up to 200 characters. Through the API, labels given to `PATCH /v1/machines/{machine_id}` merge with the ones there, and a label given `null` is taken off. ## Projects Every machine is in a project. An account starts with one, and machines go there unless another is named. Terminal: ```sh $ wm projects create research $ wm machines create --name box --project research $ wm machines ls --project research ``` A key can be limited to some projects, so a program sees only its own machines: see [Keys for some projects](https://docs.wheelmachine.ai/keys.html#keys-for-some-projects). A project can be deleted only once it holds no machines. ## Where it runs `--region` (`region` in the API) asks for a machine near a place: an id from `wm regions`. It runs near there, wherever there is room soonest, and says where it actually runs in its `location`. ## Running time and money A machine is charged by the second while it runs. `running_seconds` is all the time it has run, and `wm usage` (`GET /v1/usage`) says what each machine ran for and cost in a month: Terminal: ```sh $ wm usage --month 2026-10 MACHINE PROJECT RAN COST m_01jb2xk3n4p5q6r7s8t9v0w1x2 prj_01jb2xk3n4p5q6r7s8t9v0w1x2 1h 38m $0.1312 total 2026-10 - - $0.1312 ``` When the account cannot spend - its limit for the month is reached, or the account is held - its running machines are paused within about half a minute, with their memory kept, and making or starting machines, running commands and reaching files answer [`402 payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html) until it can again. `wm me` says whether the account can spend. ## Delete `wm machines rm box` asks first; `--yes` does not. Deleting takes the machine's files with it and cannot be undone. Deleting a machine that is already deleted is not an error. ## What a machine says about itself GET /v1/machines/{machine_id}: ```json { "id": "m_01jb2xk3n4p5q6r7s8t9v0w1x2", "object": "machine", "name": "trainer", "project": "prj_01jb2xk3n4p5q6r7s8t9v0w1x2", "state": "running", "gpu": null, "size": {"cpus": 8, "memory_gb": 16}, "location": {"city": "Helsinki", "country": "FI"}, "cpus": 8, "memory_gb": 16, "sleep_when_idle": true, "idle_after_s": 1800, "labels": {"team": "ml"}, "network": {"internet": "open", "allow": []}, "files_sealed": true, "running_seconds": 5904.2, "cpu_cores": 6.4, "memory_bytes": 9126805504, "created_at": "2026-10-23T09:30:00Z", "last_used_at": "2026-10-23T11:08:24Z" } ``` Every field is in the reference: [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine). --- Source: https://docs.wheelmachine.ai/commands-and-files # Commands and files > Run any command in a machine and get what it wrote, with or without a shell, as its user or as root; copy files in and out of it. Run any command in a machine and get back what it wrote and how it ended. Copy files in and out of it, up to 5 GiB at a time. wm: ```sh $ wm exec box -- python3 -c "print(2+2)" 4 $ wm sh box 'ls -la | wc -l' 7 ``` Python: ```python result = client.machines.exec(machine.id, ["python3", "-c", "print(2+2)"]) print(result.exit_code, result.stdout) # 0 4 result = client.machines.exec(machine.id, shell="ls -la | wc -l") ``` TypeScript: ```ts const result = await client.machines.exec(machine.id, ["python3", "-c", "print(2+2)"]); console.log(result.exit_code, result.stdout); // 0 4 await client.machines.exec(machine.id, { shell: "ls -la | wc -l" }); ``` curl: ```sh curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/exec \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command": ["python3", "-c", "print(2+2)"]}' ``` ## Run a command Give a command one of two ways, never both: - `command`: the program and its words, run as they are, with no shell. Nothing in them is read as a pipe, a wildcard or a variable. `wm exec box -- [words...]`. - `shell`: one line for `sh -lc`, with its pipes, redirections and variables. `wm sh box ''`. When the command ends, the answer holds what it wrote and how it ended: Answer: ```json { "exit_code": 0, "stdout": "4\n", "stderr": "", "duration_ms": 38.2, "truncated": false } ``` A command that fails still gets a `200` answer: its `exit_code` is the command's own word, not a refusal of the API. `wm exec` and `wm sh` print stdout and stderr apart, and exit with the command's exit code. ## Where, and as whom A command starts in the machine's work folder, the one file paths call `work/`. `cwd` (`--cwd`) starts it elsewhere: an absolute path in the machine, beginning `/`. It runs as the machine's own user; `"user": "root"` (`--root`) runs it as root, inside the machine only, never on our servers. A paused machine is woken for it. Terminal: ```sh $ wm exec box --root -- id -un root $ wm exec box --cwd /tmp -- pwd /tmp ``` ## Environment `env` (`--env KEY=VALUE`, once for each) adds up to 256 variables to the command's environment. A name is letters, digits and `_`, and does not begin with a digit. wm: ```sh $ wm sh box --env STAGE=test --env WORKERS=4 -- 'echo $STAGE $WORKERS' test 4 ``` API: ```json {"shell": "echo $STAGE $WORKERS", "env": {"STAGE": "test", "WORKERS": "4"}} ``` ## Output stdout and stderr each come back whole up to 8 MiB. Past that they are cut, and `truncated` is true. For a program that writes more, send its output to a file and copy the file out. stdout comes as text. For bytes that are not text, ask for `"binary_output": true`: then stdout comes in `stdout_base64`, as base64. `wm` asks for it, so what it prints is the command's bytes exactly as they came. ## Long work A command may run 10 minutes. Past that the call answers [`504 timeout`](https://docs.wheelmachine.ai/errors/timeout.html), and the command may still be running in the machine. For longer work, start it in the background and read what it writes: Terminal: ```sh $ wm sh box 'nohup python3 train.py > train.log 2>&1 &' $ wm exec box -- tail -n 5 train.log epoch 3/20 loss 0.412 $ wm cp box:work/train.log . ``` While the job works, the machine is not idle, so it does not pause by itself under it. ## Run it once A command may change things, so it is never sent twice by itself: the libraries and `wm` try a call again after a dropped connection, but not a command once it went out. When the API answers [`503 unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) to a command, its result did not come back: it may or may not have run. Check before you run it again. ## Files wm: ```sh $ wm cp ./data.csv box:work/ # in: work/data.csv $ wm cp ./data.csv box:work/in/today.csv # in, under another name; folders are made $ wm cp box:work/results.csv . # out, into this folder $ wm cp box:work/results.csv - | head -3 # out, to standard output ``` Python: ```python from pathlib import Path client.machines.write_file(machine.id, "work/data.csv", Path("data.csv")) client.machines.write_file(machine.id, "work/hello.txt", b"hello\n") data = client.machines.read_file(machine.id, "work/results.csv") # bytes client.machines.read_file(machine.id, "work/results.csv", to="results.csv") # into a file ``` TypeScript: ```ts await client.machines.writeFile(machine.id, "work/data.csv", { path: "data.csv" }); await client.machines.writeFile(machine.id, "work/hello.txt", new TextEncoder().encode("hello\n")); const data = await client.machines.readFile(machine.id, "work/results.csv"); // Uint8Array await client.machines.readFile(machine.id, "work/results.csv", { to: "results.csv" }); // into a file ``` curl: ```sh # in: PUT the bytes, with their length (curl -T sends it) curl -T data.csv "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/data.csv" \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" # out: GET them curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/results.csv" \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" -o results.csv ``` A path in a machine begins `work/` (the work folder, where commands start) or `home/` (the machine's home). It names a file, not a folder, is at most 1024 characters, and never climbs out with `..`. - **In** (`PUT`): the file's bytes with their length in `Content-Length`, up to 5 GiB a call. The folders on the way are made, and a file already there is replaced. - **Out** (`GET`): the file's bytes, streamed. - **In `wm cp`**: one side is a machine (`box:work/...`), the other a file here. A machine path ending in `/` is a folder, and the file keeps its name in it. `-` is standard input or output. At a terminal it shows the bytes copied and the speed as it goes. `wm cp` copies one file. For a folder, pack it first: Terminal: ```sh $ tar czf - ./project | wm cp - box:work/project.tgz $ wm exec box -- tar xzf project.tgz $ wm exec box -- tar czf out.tgz results && wm cp box:work/out.tgz . ``` To copy from one machine to another, copy out to this computer and in again. A machine's files can be reached while it is on a server, running or paused; one that is not (stopped or archived, for example) answers [`409 conflict`](https://docs.wheelmachine.ai/errors/conflict.html): start it first. ## What can go wrong | When | Answer | |---|---| | A path that does not begin `home/` or `work/`, or climbs out with `..` | [`400 invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) | | Both `command` and `shell`, or neither | [`400 invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) | | A file written without its length | [`411 invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) | | No such file in the machine | [`404 not_found`](https://docs.wheelmachine.ai/errors/not_found.html) | | The machine's files cannot be reached in its state | [`409 conflict`](https://docs.wheelmachine.ai/errors/conflict.html) | | A file over 5 GiB, or more than the machine's disk takes | [`413 too_large`](https://docs.wheelmachine.ai/errors/too_large.html) | | The key lacks `cloud:machines:exec` | [`403 forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html) | | A command that ran longer than 10 minutes | [`504 timeout`](https://docs.wheelmachine.ai/errors/timeout.html) | | A command whose result did not come back | [`503 unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) | --- Source: https://docs.wheelmachine.ai/keys # Keys and scopes > What a key may do: the cloud scopes, keys limited to some projects, end times and rates, revoking, and why a key never makes a stronger one. A key says who is calling and what it may do. Give every program and every agent a key of its own, holding only what it needs. wm: ```sh $ wm keys create agent --scope machines:read,machines:write,machines:exec id: key_8c41d07e2b9a6f13 name: agent scopes: cloud:machines:read, cloud:machines:write, cloud:machines:exec projects: every project expires: never secret: ak_live_... wm: store the secret now: it is shown only this once, and cannot be shown again. ``` Python: ```python key = client.keys.create( name="agent", scopes=["cloud:machines:read", "cloud:machines:write", "cloud:machines:exec"], expires_at="2026-12-31T23:59:59Z", ) # key.secret is in this answer only: store it now ``` TypeScript: ```ts const key = await client.keys.create({ name: "agent", scopes: ["cloud:machines:read", "cloud:machines:write", "cloud:machines:exec"], expires_at: "2026-12-31T23:59:59Z", }); // key.secret is in this answer only: store it now ``` curl: ```sh curl -X POST https://api.wheelmachine.ai/v1/keys \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "agent", "scopes": ["cloud:machines:read", "cloud:machines:write", "cloud:machines:exec"]}' ``` ## A key A key is `ak_live_` and 40 letters and digits, sent with every call as `Authorization: Bearer ak_live_...`. Its secret is shown once, in the answer that makes it; after that the API shows only its first and last characters (`shown`), to know it by. It also has an id (`key_` and 16 letters and digits) and a name, for people. `wm me` (`GET /v1/me`) shows the key in use: its scopes, its projects, when it ends, and whether the account can spend. ## Scopes | Scope | In `wm` | What the key may do | |---|---|---| | `cloud:machines:read` | `machines:read` | See machines, wait for them, and see what they cost (`usage`). | | `cloud:machines:write` | `machines:write` | Make, rename, label, start, pause, stop, archive, copy, snapshot and delete machines, and see them. | | `cloud:machines:exec` | `machines:exec` | Run commands in machines and read and write their files, and see them. A command can read its environment, so such a key can read the secrets a machine was given. | | `cloud:gpus` | `gpus` | Make machines with a GPU, once they are open to the account. | | `cloud:projects:write` | `projects:write` | Make, rename and delete projects. | | `cloud:keys:write` | `keys:write` | List, make and revoke keys. | | `cloud:secrets:write` | `secrets:write` | Set and delete the account's secrets, and give them to machines (a key of the whole account). | | `cloud:*` | `all` | All of these. | Any key with one of these scopes can see who it is (`wm me`), its projects, the regions and the GPUs. A call the key's scopes do not allow answers [`403 forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html), naming the scope it needs. A key that holds none of these scopes is refused on every route here. ## A key never makes a stronger one A new key is made by a key that holds `cloud:keys:write`, and it can hold no more than that key: - only scopes the making key holds (`cloud:*` only from a key that holds `cloud:*`); - only projects the making key reaches; - an end no later than the making key's own; - no more calls a minute than the making key may make. Asking for more answers [`403 forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html). So a key handed to a program, or to an agent, cannot be used to make a key that does more than it. ## Keys for some projects Terminal: ```sh $ wm keys create ci --scope machines:write,machines:exec --project research ``` A key limited to some projects works inside them alone: - it sees and makes machines only in its projects; to it, a machine anywhere else does not exist ([`404 not_found`](https://docs.wheelmachine.ai/errors/not_found.html)); - when it reaches more than one project, it names the project of each machine it makes; - it cannot make projects; - it sees, makes and revokes only keys limited to its projects. A key that names no projects reaches every project of the account. ## End time and rate Terminal: ```sh $ wm keys create nightly --scope machines:write --expires 30d --rate 120 ``` `--expires` is a while (`30d`, `12h`) or a date (`2026-12-31`, or a time such as `2026-12-31T18:00:00Z`); in the API, `expires_at` is a time, at least a minute ahead. A key past its end answers [`401 key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html), and its end cannot be moved: make a new key. `--rate` (`rate_per_minute`) is the calls a minute the key may make, from 1 to 6000. Left out, the new key gets the making key's own rate, which is 600 for a key made without one. See [Calls a minute](https://docs.wheelmachine.ai/limits.html#calls-a-minute). ## Revoke Terminal: ```sh $ wm keys revoke agent Revoke key_8c41d07e2b9a6f13? Whatever uses it stops working. [y/N] y key_8c41d07e2b9a6f13 ``` A revoked key stops working within seconds, everywhere, and cannot be brought back; calls with it answer [`401 key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html). A key can revoke itself. `wm logout` is not the same: it only forgets the key on this computer, and the key still works until it is revoked. `wm keys ls` lists the account's machine keys, with their scopes, projects, state and when each was last used; never their secrets. ## Where keys come from For now, an account's first key is made in Wheelmachine's console by the account's owner. After that, a key that holds `cloud:keys:write` makes the others, with `wm keys create`, the libraries or `POST /v1/keys`. ## Keep keys safe - `wm login` keeps a key in `~/.config/wheelmachine/config.json` (under `$XDG_CONFIG_HOME` when that is set), in a file only you can read. `WHEELMACHINE_API_KEY`, when set, is used before it. - Never put a key in a web page: anyone who opens the page can read it. The TypeScript library refuses to run in a browser for this reason. Call the API from your server. - Give each program and each agent a key of its own, with only the scopes it needs and an end time, and revoke it when the work is done. - `wm`, `wm-mcp` and the libraries never log a key and keep it out of their errors. The one secret `wm` prints is a new key's, once, when it is made. --- Source: https://docs.wheelmachine.ai/errors/ # Errors > How the API refuses a call: application/problem+json with a stable code, a request id, and when to try again. Every code, and what to do about it. When the API refuses a call, it says why in the same shape every time: a code your program can rely on, and words for you. Answer: ```http HTTP/1.1 403 Forbidden Content-Type: application/problem+json X-Request-Id: req_01jb2xk3n4p5q6r7s8t9v0w1x2 { "type": "https://docs.wheelmachine.ai/errors/forbidden", "title": "This key may not do that", "status": 403, "detail": "this key does not hold the cloud:machines:exec scope", "code": "forbidden", "request_id": "req_01jb2xk3n4p5q6r7s8t9v0w1x2" } ``` ## Read a refusal A refusal is `application/problem+json` (RFC 9457): | Field | What it is | |---|---| | `code` | What went wrong, as a word that never changes. Let your program decide on this. | | `detail` | What went wrong this time, in words for people. They may change; do not match on them. | | `title` | The code's title, the same every time. | | `status` | The HTTP status, as in the answer's first line. | | `type` | A link to this site's page for the code. | | `request_id` | This call's id, also in the `X-Request-Id` header. Give it when you ask us about a call. | | `retry_after_s` | When there is one: the seconds to wait before trying again, also in the `Retry-After` header. | ## In wm and the libraries `wm` prints the refusal's words, its code, its status and its request id, and exits with 1: Terminal: ```sh $ wm exec box -- make test wm: this key does not hold the cloud:machines:exec scope (forbidden, 403, request req_01jb2xk3n4p5q6r7s8t9v0w1x2) ``` The libraries raise (or throw) a `WheelmachineError` with `status`, `code`, `detail`, the request id (`request_id` in Python, `requestId` in TypeScript) and the wait it asked for (`retry_after`, `retryAfter`). Each family of statuses has its own class, the same name in both: | Status | Class | |---|---| | 400, 411, 413 | `BadRequestError` | | 401 | `AuthenticationError` | | 402 | `PaymentRequiredError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 408 | `WaitTimeoutError` | | 409, 422 | `ConflictError` | | 429 | `RateLimitError` | | 500 | `InternalServerError` | | 502, 503, 504 | `UnavailableError` | | No answer came | `APIConnectionError`, and `APITimeoutError` when the time ran out | Python: ```python from wheelmachine import PermissionDeniedError, WheelmachineError try: client.machines.exec(machine.id, ["make", "test"]) except PermissionDeniedError as err: print("this key may not run commands:", err.detail) except WheelmachineError as err: print(err.status, err.code, err.request_id) ``` TypeScript: ```ts import { PermissionDeniedError, WheelmachineError } from "wheelmachine"; try { await client.machines.exec(machine.id, ["make", "test"]); } catch (err) { if (err instanceof PermissionDeniedError) console.log("this key may not run commands:", err.detail); else if (err instanceof WheelmachineError) console.log(err.status, err.code, err.requestId); else throw err; } ``` ## Try again, safely Some refusals pass: `rate_limited`, `unavailable`, and `conflict` while a first call with the same `Idempotency-Key` is still running. Wait the seconds `Retry-After` says, then send the call again. The libraries and `wm` do this by themselves, up to a few times, with a growing wait when none was said. The rest mean the call itself must change before it can work. A call that makes something (a machine, a copy, a snapshot, a project or a key) can carry an `Idempotency-Key` header: any string up to 255 printable characters, new for each thing you mean to make. Sent again with the same key and the same request within 24 hours, it gets the first answer again, with `Idempotent-Replayed: true`, and nothing is made twice. The same key with a different request answers `422 invalid_request`. Answers that pass (`429` and `5xx`) are not kept, so a retry of one runs again. The libraries put an `Idempotency-Key` on every call that makes something, the same on each of its tries. Terminal: ```sh curl -X POST https://api.wheelmachine.ai/v1/machines \ -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: nightly-build-2026-10-23" \ -d '{"name": "nightly"}' ``` A command is different: it may already have run, so neither the libraries nor `wm` ever send one again by themselves. Every answer carries `X-Request-Id`. Send your own `X-Request-Id` (letters, digits, `-` and `_`, up to 64) and the API uses it, so a call can be found by the same id in your logs and ours. ## Every code | Code | Status | What to do | |---|---|---| | [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) | 400, 411, 422 | The request is not right. Put the call right as `detail` says. | | [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) | 401 | No key, or a key that does not work. Send the whole key as `Authorization: Bearer ak_live_...`. | | [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) | 401 | This key was revoked. Use another key, or make one with a key that holds `cloud:keys:write`. | | [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html) | 401 | This key has expired. Make a new key. | | [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html) | 403 | This key may not do that. Use a key that holds the scope, or make one with a key that does: see [Keys and scopes](https://docs.wheelmachine.ai/keys.html#scopes). | | [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html) | 404 | Not found. Check the id (machines begin `m_`, projects `prj_`, keys `key_`) and the projects the key reaches: `wm me` shows them. | | [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) | 409 | Not possible in the state it is in. Look at the machine's `state` (`wm machines get `), bring it where it needs to be (`start` it), then try again. | | [`project_not_empty`](https://docs.wheelmachine.ai/errors/project_not_empty.html) | 409 | The project still holds machines. Delete its machines first (`wm machines ls --project `, then `wm machines rm`), then the project. | | [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html) | 402 | Nothing left to spend. Once the account's limit is raised, or the month turns, `start` the machines you need. | | [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) | 429 | Too many calls just now. Wait the seconds `Retry-After` says, then go on, more slowly. | | [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html) | 429 | A limit is reached. Delete the machines you no longer need (`wm machines ls`, then `wm machines rm`), then try again. | | [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) | 503 | No room just now. Try again after the seconds `Retry-After` says. | | [`timeout`](https://docs.wheelmachine.ai/errors/timeout.html) | 408, 504 | Not in time. For a wait, look at the machine's state, and wait again if it is still on its way. | | [`too_large`](https://docs.wheelmachine.ai/errors/too_large.html) | 413 | Too large. Send less at once: a big file in parts, or packed (`tar czf`); free space in the machine. | | [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html) | 403, 501, 503 | Not open yet. Make a machine without a GPU (`wm gpus` shows what is coming), sign in with a key you have (`wm login --key`), run commands with `wm exec` where a terminal cannot open, and leave a port's web address until web addresses are open. | | [`login_denied`](https://docs.wheelmachine.ai/errors/login_denied.html) | 403 | The sign-in was refused. Start the sign-in again (`wm login`), and approve it at the dashboard if it is yours. | | [`login_expired`](https://docs.wheelmachine.ai/errors/login_expired.html) | 410 | The sign-in's code is no longer good. Start the sign-in again (`wm login`). | | [`internal`](https://docs.wheelmachine.ai/errors/internal.html) | 500 | Something went wrong on our side. Try again in a moment. | ## `invalid_request` 400, 411, 422 **The request is not right** Something in the call is missing or wrong. `detail` says what, in plain words. #### When it happens - A field the API does not know, such as `no field "nmae" here`: a misspelt field is refused, never quietly ignored. - A value out of its range, such as `cpus is 2, 4, 8 or 16` or `wait_s is between 0 and 600`. - A body that is not one JSON object. - A path in a machine that does not begin `home/` or `work/`, or that climbs out with `..`. - 411: a file written without its length in `Content-Length`. - 422: an `Idempotency-Key` sent again with a different request. #### What to do Put the call right as `detail` says. For a 422, give the new request a new `Idempotency-Key`. **Send it again?** Not unchanged: the same call gets the same answer. ## `unauthorized` 401 **No key, or a key that does not work** The call carried no key, or one the API does not know. #### When it happens - No `Authorization` header, or one that is not `Bearer ak_live_...`. - A key that is not one of ours (keys begin `ak_live_`), or one cut short when it was pasted. #### What to do Send the whole key as `Authorization: Bearer ak_live_...`. With `wm`, run `wm login` again, and check that `WHEELMACHINE_API_KEY` does not hold an old key: it is used before the one `wm login` kept. **Send it again?** Not with the same key. An address that keeps calling with keys that do not work is slowed down (`429 rate_limited`). ## `key_revoked` 401 **This key was revoked** Someone revoked the key. It stopped working everywhere within seconds, and it cannot be brought back. #### When it happens - Any call made with a key after `wm keys revoke` or `DELETE /v1/keys/{key_id}`. #### What to do Use another key, or make one with a key that holds `cloud:keys:write`. **Send it again?** Not with this key. ## `key_expired` 401 **This key has expired** The key was made with an end time (`expires_at`), and that time has passed. #### When it happens - Any call made with the key after its end time. #### What to do Make a new key. A key's end time cannot be moved. **Send it again?** Not with this key. ## `forbidden` 403 **This key may not do that** The key works, but it may not do this. #### When it happens - The key does not hold the scope the call needs. `detail` names it, such as `this key does not hold the cloud:machines:exec scope`. - The key was made for other work and holds no machine scope at all. - A key limited to some projects tried to make a project. - A key tried to give a new key a scope, a project or a rate it does not hold itself. - The account is held. #### What to do Use a key that holds the scope, or make one with a key that does: see [Keys and scopes](https://docs.wheelmachine.ai/keys.html#scopes). For a held account, its owner can ask us why. **Send it again?** Not with this key. ## `not_found` 404 **Not found** Nothing of that id that this key can see. #### When it happens - A machine, project or key that is not the account's, that was deleted, or that is in a project the key is not limited to. The answer is the same whether it exists or not. - A file that is not in the machine. - A route that does not exist. #### What to do Check the id (machines begin `m_`, projects `prj_`, keys `key_`) and the projects the key reaches: `wm me` shows them. **Send it again?** Not unchanged. ## `conflict` 409 **Not possible in the state it is in** The machine, or the call, is not in a state where this can happen yet. #### When it happens - The machine cannot do it in its state, such as pausing a machine that is not running. - The machine's files cannot be reached just now, because it is not on a server (stopped or archived, for example): start it first. - The first call with the same `Idempotency-Key` is still running. #### What to do Look at the machine's `state` (`wm machines get `), bring it where it needs to be (`start` it), then try again. For an `Idempotency-Key` still in use, wait a second and send the same call again. **Send it again?** Yes, once the state is right; after `Retry-After` for an `Idempotency-Key` still in use. ## `project_not_empty` 409 **The project still holds machines** Only an empty project can be deleted. #### When it happens - Deleting a project that still holds machines. #### What to do Delete its machines first (`wm machines ls --project `, then `wm machines rm`), then the project. **Send it again?** Yes, once it is empty. ## `payment_required` 402 **Nothing left to spend** The account cannot spend just now: its limit for this month is reached, or it is held. `detail` says why. #### When it happens - Making, starting or copying a machine, running a command, or reading or writing a file, while the account cannot spend. - While it cannot spend, its running machines are paused within about half a minute, with their memory kept. #### What to do Once the account's limit is raised, or the month turns, `start` the machines you need. `wm me` says whether the account can spend. **Send it again?** Yes, once the account can spend. ## `rate_limited` 429 **Too many calls just now** Calls are coming faster than the key may make them. #### When it happens - The key made more calls than its limit: 600 a minute, unless it was made with another `rate_per_minute`. - An address kept calling with keys that do not work. #### What to do Wait the seconds `Retry-After` says, then go on, more slowly. The `RateLimit` header says how many calls can be made at once now: see [Limits](https://docs.wheelmachine.ai/limits.html#calls-a-minute). **Send it again?** Yes, after `Retry-After`. The libraries and `wm` wait and try again by themselves. ## `limit_reached` 429 **A limit is reached** The account holds as many machines as it may. #### When it happens - The account holds 100 machines through the API, deleted ones aside: `delete one first`. - The account has as many machines running or kept as it may: `pause, archive or delete one first`. #### What to do Delete the machines you no longer need (`wm machines ls`, then `wm machines rm`), then try again. **Send it again?** Yes, once there is room. ## `unavailable` 503 **No room just now** The API could not do it just now. It passes. #### When it happens - No server has room for the machine just now; one is being made ready for it. - A part of the service did not answer in time. - A command's result did not come back: it may or may not have run. #### What to do Try again after the seconds `Retry-After` says. For a command, check whether it ran before you run it again: the libraries and `wm` never send a command twice by themselves. **Send it again?** Yes, after `Retry-After`. ## `timeout` 408, 504 **Not in time** Something did not happen in the time it was given. #### When it happens - 408: the machine did not reach the state you waited for, or it was deleted meanwhile. - 504: a command ran longer than 10 minutes. It may still be running in the machine. - 504: the call took longer than it may. #### What to do For a wait, look at the machine's state, and wait again if it is still on its way. For long work, start it in the background and read its output from a file: see [Long work](https://docs.wheelmachine.ai/commands-and-files.html#long-work). **Send it again?** A wait, yes. A command, only once you know it is not still running. ## `too_large` 413 **Too large** The request, or the file, is larger than the API takes in one call. #### When it happens - A request's JSON over 1 MiB. - A file over 5 GiB in one call. - A file larger than the machine's disk can take. #### What to do Send less at once: a big file in parts, or packed (`tar czf`); free space in the machine. **Send it again?** Not unchanged. ## `not_open` 403, 501, 503 **Not open yet** What was asked for is not open yet: to every account, or here at all. #### When it happens - 403: making a machine with a GPU, while GPU machines are not open to every account; or a program starting its sign-in through the browser (`wm login`) where that is not open yet. - 501: opening a terminal in a machine whose server cannot open one, or asking for an SSH certificate where SSH is not offered. - 503: opening a machine's port at a web address of its own, or making a link to one, while machines' web addresses are not served yet. #### What to do Make a machine without a GPU (`wm gpus` shows what is coming), sign in with a key you have (`wm login --key`), run commands with `wm exec` where a terminal cannot open, and leave a port's web address until web addresses are open. **Send it again?** Not yet: waiting a moment does not change it. ## `login_denied` 403 **The sign-in was refused** The person refused the program's sign-in at the dashboard, or the dashboard sign-in that approved it has ended since. #### When it happens - A program asking for its key (`POST /v1/login/devices/token`) after the person pressed Deny at https://app.wheelmachine.ai/device. - The same, after the dashboard sign-in that approved it was signed out. #### What to do Start the sign-in again (`wm login`), and approve it at the dashboard if it is yours. **Send it again?** Not this sign-in: start a new one. ## `login_expired` 410 **The sign-in's code is no longer good** A program's sign-in ran out of time before it was approved, or its key was given already. #### When it happens - Asking for the key more than 10 minutes after the sign-in started. - Asking again after the key was given: it is given once. #### What to do Start the sign-in again (`wm login`). **Send it again?** Not this sign-in: start a new one. ## `internal` 500 **Something went wrong on our side** A fault of ours. It is logged under the call's request id. #### When it happens - Anything we did not expect. #### What to do Try again in a moment. If it keeps happening, tell us the `request_id`. **Send it again?** Yes, after a moment. --- Source: https://docs.wheelmachine.ai/limits # Limits > How many calls a key may make, how many machines an account may hold, the largest request, file, command and output, and what happens when the money runs out. What the API takes at once, and how fast. Every answer says how many calls the key has left. On every answer: ```http HTTP/1.1 200 OK RateLimit-Policy: "key";q=600;w=60 RateLimit: "key";r=59;t=1 ``` ## Calls a minute Each key may make 600 calls a minute, unless it was made with another `rate_per_minute` (1 to 6000, and never more than the key that made it). It may make up to a tenth of them at once, and at least 10; after that, calls are let through at the key's pace. Every answer carries two headers: - `RateLimit-Policy`: `q` is the key's calls a minute, over a window `w` of 60 seconds. - `RateLimit`: `r` is how many calls can be made at once now, and `t` the seconds until all of them are back. A call over the limit answers [`429 rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html), with `Retry-After`: the seconds until the next call is let through. The libraries and `wm` wait and try again by themselves. An address that keeps calling with keys that do not work is held to about 30 tries a minute. ## Machines An account holds up to 100 machines made through the API, deleted ones aside. One more answers [`429 limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): delete one first. An account may also have a limit on the machines it has running or kept at once; reaching it answers the same code, and pausing, archiving or deleting a machine makes room. ## Sizes | What | Limit | |---|---| | A request's JSON | 1 MiB | | A file written in one call | 5 GiB | | A command's run | 10 minutes | | A command's output | 8 MiB each for stdout and stderr; cut there, and `truncated` says so | | A command's words (`command`) | 4096 | | A line of shell (`shell`) | 131072 characters | | Environment variables | 256 a command | | A path in a machine | 1024 characters | | A name (machine, project or key) | 80 characters | | Labels | 32 a machine; a name up to 63 characters, a value up to 200 | | A snapshot's note | 200 characters | | A wait (`wait_s`, `timeout_s`) | 0 to 600 seconds a call | | Idle before a machine pauses (`idle_after_s`) | 60 to 86400 seconds | | A page of a list (`limit`) | 1 to 200; 50 when not said | | An `Idempotency-Key` | 255 printable characters; its answer is kept 24 hours | | A key's calls a minute (`rate_per_minute`) | 1 to 6000; 600 when not said | ## Money Machines run while the account can spend. When it cannot - its limit for the month is reached, or the account is held - its running machines are paused within about half a minute, with their memory kept, and making, starting or copying machines, running commands and reaching files answer [`402 payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html). Once it can again, `start` brings the machines back where they were. `wm me` says whether the account can spend, and what is left this month. --- Source: https://docs.wheelmachine.ai/cli # Command line > The wm command: install it, log in, and every command and flag for machines, commands, files, projects, keys and the catalogue. `wm` drives Wheelmachine from a terminal: machines, commands, files, projects and keys. Every command answers `-h` with its flags. Terminal: ```sh $ wm login $ wm machines create --name box --cpus 4 $ wm exec box -- make test $ wm cp box:work/report.html . $ wm machines rm --yes box ``` ## Install For now `wm` is built from its source, with Go 1.27 or later (an older `go` fetches 1.27 by itself). It is one file, with nothing else to install, and the same build gives `wm-mcp`, the server for [agents](https://docs.wheelmachine.ai/agents.html): Terminal: ```sh $ cd frontend/cli $ go build -o bin/ ./cmd/... # writes bin/wm and bin/wm-mcp $ go install ./cmd/... # or puts them in $(go env GOPATH)/bin $ wm --version wm 1.0.0 ``` ## Log in `wm login` signs you in through your browser, with GitHub or Google: it shows a short code and opens `https://app.wheelmachine.ai/device`, where you sign in, type the code and approve. `wm` is then given a key of its own, named for this computer (`"wm on laptop"`, say), checks it with the API and keeps it in `~/.config/wheelmachine/config.json` (under `$XDG_CONFIG_HOME` when that is set), in a file only you can read. On a computer with no browser, open the address on any other device. To keep a key you already have, `wm login --key` asks for it without showing it as you type, or reads it from standard input: `wm login --key < keyfile`. | Variable | What it does | |---|---| | `WHEELMACHINE_API_KEY` | The key to use. When set, it is used before the one `wm login` kept. | | `WHEELMACHINE_BASE_URL` | Where the API is; `https://api.wheelmachine.ai` when not set. | | `XDG_CONFIG_HOME` | Where `wm login` keeps the key, in `wheelmachine/config.json`. | ## The basics - **Names.** A machine is named by its id (`m_...`) or its name, a project by its id (`prj_...`) or its name, and a key by its id (`key_...`) or its name. When two share a name, give the id. - **Output.** Tables for people. `--json` prints the API's own JSON, and `-q` (`--quiet`) prints ids only, one a line, for scripts. Both go on any command. - **Asking first.** `wm machines rm` and `wm keys revoke` ask before they act; away from a terminal they need `--yes`. - **Trying again.** A call refused for a moment (too many calls, no room just now) is tried again after the wait the API asks for, and `wm` says so at a terminal. A command run in a machine is never sent twice. ## Exit codes | Code | When | |---|---| | 0 | Done. | | 1 | The API or the network refused. `wm` prints why, with the refusal's code and request id. | | 2 | The command was not right: a missing word, a flag that does not exist. | | Any | `wm exec` and `wm sh` exit with the command's own exit code (255 for one outside 0 to 255). | | 130 | Stopped with Ctrl-C. | ## Machines ### wm machines ls `wm machines ls [flags]` List machines, newest first. | Flag | What it does | |---|---| | `--project ` | Only this project's machines. | | `--state ` | Only machines in this state: `starting`, `running`, `paused`, `stopped`, `archived` or `recovering`. | | `--label key=value` | Only machines with this label; repeat for more. | | `--limit ` | Show at most this many (0, the default: all). | ### wm machines create `wm machines create [flags]` Make a machine. It answers once the machine runs, usually in well under a second; when a server has to be made ready for it first, `wm` waits for it, up to `--timeout`. | Flag | What it does | |---|---| | `--name ` | A name for people; ids are made by the API. | | `--project ` | Its project; the account's first when left out. | | `--cpus ` | Its CPUs: 2, 4, 8 or 16, with 2 GB of memory each; 2 when left out. | | `--region ` | A place to run near, from `wm regions`. | | `--label key=value` | A label; repeat for more. | | `--from-snapshot machine[:snapshot]` | Start from a snapshot's files: the machine's latest when no snapshot is named. | | `--idle-after ` | Pause by itself once nothing has reached it, and its programs have been quiet, this long: `1m` to `24h`; `5m` when left out. | | `--no-sleep` | Never pause by itself: it runs, and is charged, until it is paused or stopped. | | `--timeout ` | How long to wait for it to run; `10m` when left out. | | `--no-wait` | Answer at once, while it may still be starting. | | `--gpu ` | **Coming:** A GPU to give it, from `wm gpus`. Not open to every account yet. | ### wm machines get `wm machines get ` Show one machine: its state, size, where it runs, how it pauses, its labels, its running time and what it uses now. Terminal: ```sh $ wm machines get box id: m_01jb2xk3n4p5q6r7s8t9v0w1x2 name: box state: running project: prj_01jb2xk3n4p5q6r7s8t9v0w1x2 size: 2 CPU, 4 GB memory location: Helsinki, FI pauses: after 5m 00s idle (nothing reached it, its programs quiet) labels: team=ml files sealed: yes running time: 42m 10s processors used: 0.12 memory used: 612.4 MiB created: 2026-10-23 09:30 UTC (42m ago) last used: 2026-10-23 10:11 UTC (1m ago) ``` ### wm machines rm `wm machines rm [--yes] ...` Delete machines and all their files. This cannot be undone. | Flag | What it does | |---|---| | `-y`, `--yes` | Delete without being asked. | ### wm machines pause `wm machines pause ...` Pause machines with their memory kept: nothing runs and no running time is charged. `start` brings them back where they were. ### wm machines start `wm machines start ...` Run machines again, from paused, stopped or archived. | Flag | What it does | |---|---| | `--timeout ` | How long to wait for each to run; `10m` when left out. | | `--no-wait` | Answer at once, while it may still be starting. | ### wm machines stop `wm machines stop ...` Stop machines: their files stay, their memory does not. ### wm machines archive `wm machines archive ...` Put machines' files away in storage, the cheapest way to keep them. `start` brings them back. ### wm machines copy `wm machines copy [flags]` Make a new machine, a copy of this one as it is now, beside it where there is room. Its name and labels are the original's unless given. | Flag | What it does | |---|---| | `--name ` | The copy's name. | | `--label key=value` | A label for the copy, in place of the original's; repeat for more. | | `--timeout ` | How long to wait for it to run; `10m` when left out. | | `--no-wait` | Answer at once, while it may still be starting. | ### wm machines snapshot `wm machines snapshot [--note ]` Keep a machine's files as they are now. Make a machine from it with `wm machines create --from-snapshot :`. | Flag | What it does | |---|---| | `--note ` | A note to know it by, at most 200 characters. | ### wm machines wait `wm machines wait [--state ] [--timeout 10m]` Wait until a machine is in a state. | Flag | What it does | |---|---| | `--state ` | The state to wait for; `running` when left out. | | `--timeout ` | How long to wait; `10m` when left out. | ### wm machines label `wm machines label key=value... [key-]...` Set a machine's labels: `key=value` sets one, `key-` takes one off. Labels not named stay as they are. ### wm machines rename `wm machines rename ` Give a machine another name. ## Commands and files ### wm exec `wm exec [flags] -- [args...]` Run a command in a machine, without a shell. What it wrote is printed as it came, stdout and stderr apart, and `wm` exits with its exit code. A paused machine is woken for it. Flags go before the machine, or between the machine and `--`. | Flag | What it does | |---|---| | `--root` | Run it as root, inside the machine (never on our servers). | | `--cwd ` | Where it starts: an absolute path; the work folder when left out. | | `--env KEY=VALUE` | A variable for its environment; repeat for more. | ### wm sh `wm sh [flags] ''` Run a line of shell in a machine (`sh -lc`), with its pipes and redirections. What it wrote is printed as it came, and `wm` exits with its exit code. It takes the same flags as `wm exec`; with flags after the machine, put the line after `--`. ### wm cp `wm cp :work/`   or   `wm cp :home/ ` Copy a file to or from a machine, as a stream. In a machine, a path begins `home/` (its home) or `work/` (where commands start); a path ending in `/` is a folder, and the file keeps its name in it. `-` is standard input or output. At a terminal, the bytes copied and the speed are shown as it goes. One side is always this computer: `wm cp` copies one file, between here and a machine. ## Account ### wm login `wm login [--scope ...] [--name ] [--no-browser]` or `wm login --key` Sign in through your browser, with GitHub or Google, and keep a key of your own on this computer, for `wm` and `wm-mcp`: the key is made when you approve the code `wm` shows, checked with the API, and kept in a file only you can read. `--scope` asks for less than every scope (repeat it, or join scopes with commas), `--name` names the key, and `--no-browser` shows the address without opening a browser here. `--key` keeps a key you already have instead: asked for at a terminal and not shown as you type, otherwise read from standard input (`wm login --key < keyfile`). ### wm logout `wm logout` Forget the key kept on this computer. The key itself still works until it is revoked (`wm keys revoke`). ### wm me `wm me` The account and key in use: the key's scopes, projects, rate and end, and whether the account can spend and what is left. ### wm usage `wm usage [--month YYYY-MM] [--project ]` What each machine ran for and cost in a month. | Flag | What it does | |---|---| | `--month YYYY-MM` | The month; this month when left out. | | `--project ` | Only this project's machines. | ## Projects ### wm projects ls `wm projects ls` List the account's projects. ### wm projects create `wm projects create ` Make a project. ### wm projects rename `wm projects rename ` Give a project another name. ### wm projects rm `wm projects rm ...` Delete projects. Only an empty project can be deleted: delete its machines first. ## Keys ### wm keys ls `wm keys ls` List the account's machine keys: id, name, the characters it is known by, state, scopes, projects and when it was last used. A key's secret is never shown again after it is made. ### wm keys create `wm keys create --scope ... [flags]` Make a key. It can hold only scopes and projects the key in use holds. Its secret is shown this once: store it then. The scopes are `machines:read`, `machines:write`, `machines:exec`, `gpus`, `projects:write`, `keys:write`, or `all`: see [Scopes](https://docs.wheelmachine.ai/keys.html#scopes). | Flag | What it does | |---|---| | `--scope ` | What it may do; repeat, or join with commas. | | `--project ` | A project it is limited to; repeat for more. Every project when left out. | | `--expires