API reference
Every route of the API, made from its contract, openapi.yaml (OpenAPI 3.1; also as JSON), each time these pages are built.
$ curl https://api.wheelmachine.ai/v1/me -H "Authorization: Bearer $WHEELMACHINE_API_KEY"The basics#
- Where.
https://api.wheelmachine.ai, over HTTPS only. - Keys. Every call carries one:
Authorization: Bearer ak_live_.... Each route below says the scope it needs. - JSON. Bodies are JSON, one object at most 1 MiB, except a file's bytes. A field the API does not know is refused, never quietly ignored.
- Ids. Machines are
m_, projectsprj_, both with 26 letters and digits; keys arekey_with 16. - Lists.
{"object": "list", "data": [...], "next": ...}: givenextasafterfor the page after it, up tolimit(1 to 200, 50 when not said) at a time;nextisnullon the last page. Machines come newest first, projects oldest first. - Times are RFC 3339, in UTC.
- Refusals are
application/problem+jsonwith a stablecode: see Errors. Any call can also answer403 forbidden(a key made for other work, or an account that is held),429 rate_limited,500 internal,503 unavailableand504 timeout. - Making things safely. Calls that make something take an
Idempotency-Key: see Try again, safely. - Every answer carries
X-Request-Idand the key's rate limit.
Answers marked also are given by the API on that route, though its contract does not list them yet.
Account#
The account and key this call is made with#
GET/v1/me
Any key with a machine scope.
curl https://api.wheelmachine.ai/v1/me \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"{
"object": "me",
"account": {"id": "you@example.com"},
"key": {
"id": "key_3f9a1c2b4d5e6f70",
"object": "key",
"name": "laptop",
"scopes": ["cloud:*"],
"projects": [],
"shown": "ak_live_abcd…wxyz",
"state": "active",
"rate_per_minute": 600,
"created_at": "2026-10-23T09:00:00Z",
"expires_at": null,
"last_used_at": "2026-10-23T09:29:58Z"
},
"standing": {"can_spend": true, "balance_usd": 42.5}
}Answers#
- 200 Who is calling: a Me
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work
What each machine ran for and cost in a month#
GET/v1/usage
Needs cloud:machines:read.
curl "https://api.wheelmachine.ai/v1/usage?month=2026-10" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"{
"object": "usage",
"month": "2026-10",
"total_usd": 0.1312,
"machines": [
{
"machine": "m_01jb2xk3n4p5q6r7s8t9v0w1x2",
"project": "prj_01jb2xk3n4p5q6r7s8t9v0w1x2",
"running_seconds": 5904,
"usd": 0.1312
}
]
}Parameters#
| Parameter | Type | What it is |
|---|---|---|
monthin the query | string | YYYY-MM; this month when left out. Matches ^[0-9]{4}-[0-9]{2}$. |
projectin the query | string |
Answers#
- 200 The month's usage: a Usage
- 400
invalid_request:monthis notYYYY-MM - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Theprojectis not one this key reaches
Sign-in#
Start signing a program in through the browser#
POST/v1/login/devices
No key.
For a program on a computer with a browser (wm login): no key is needed. The answer holds device_code, which the program keeps to itself, and user_code, which the person types at verification_uri, signed in to the dashboard, and approves. The code is never carried in an address (there is no verification_uri_complete): a link with someone else's code in it is how such sign-ins are stolen. The codes last 10 minutes and are used once. Then ask for the key every interval seconds (/v1/login/devices/token).
curl -X POST https://api.wheelmachine.ai/v1/login/devices \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Body (it may be left out)#
| Field | Type | What it is |
|---|---|---|
name | string | What the key will be called, such as wm on laptop. At most 80 characters. |
scopes | array of Scope | What the program asks for; cloud:* when left out. The person may give it less. |
Answers#
- 200 The sign-in, waiting for the person: object
- 400
invalid_request: The request is not right - 403
not_open: The key does not hold the scope or project this needs - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again
The program's key, once the person approved#
POST/v1/login/devices/token
No key.
202 while the person has not decided; 201 with the new key, its secret in this answer only, once they approved - named as the program asked ("wm on laptop") and holding the scopes they allowed; 403 login_denied when they refused (or signed out since); 410 login_expired when the code ran out or was used. Asked more often than interval: 429 with Retry-After.
curl -X POST https://api.wheelmachine.ai/v1/login/devices/token \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Body#
| Field | Type | What it is |
|---|---|---|
device_code required | string |
Answers#
- 201 The new key: a KeyCreated
- 202 Still waiting for the person: object
- 400
invalid_request: The request is not right - 403
login_denied: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 410
login_expired: The sign-in's code ran out, or was used already - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again
What a program asks, by the code the person typed#
POST/v1/login/devices/lookup
The dashboard's sign-in, not a key.
The dashboard's own, signed in. The code goes in the body, never in an address.
curl -X POST https://api.wheelmachine.ai/v1/login/devices/lookup \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Body#
| Field | Type | What it is |
|---|---|---|
user_code required | string | As the person typed it; case, spaces and dashes do not matter. |
scopes | array of Scope | Approving only - what the program is given, within what it asked for. |
Answers#
- 200 The program's sign-in, waiting: object
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again
Let a program in#
POST/v1/login/devices/approve
The dashboard's sign-in, not a key.
The dashboard's own, signed in. scopes narrows what the program asked for; it never widens it.
curl -X POST https://api.wheelmachine.ai/v1/login/devices/approve \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Body#
| Field | Type | What it is |
|---|---|---|
user_code required | string | As the person typed it; case, spaces and dashes do not matter. |
scopes | array of Scope | Approving only - what the program is given, within what it asked for. |
Answers#
- 200 Approved; the program is given its key when it next asks: object
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: Not possible in the state it is in - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again
Refuse a program#
POST/v1/login/devices/deny
The dashboard's sign-in, not a key.
curl -X POST https://api.wheelmachine.ai/v1/login/devices/deny \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Body#
| Field | Type | What it is |
|---|---|---|
user_code required | string | As the person typed it; case, spaces and dashes do not matter. |
scopes | array of Scope | Approving only - what the program is given, within what it asked for. |
Answers#
- 200 Refused; the program is told so: object
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: Not possible in the state it is in
Sign out of the dashboard here#
DELETE/v1/session
The dashboard's sign-in, not a key.
curl -X DELETE https://api.wheelmachine.ai/v1/session \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 204 Signed out, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs
Where the account is signed in to the dashboard#
GET/v1/sessions
The dashboard's sign-in, not a key.
curl https://api.wheelmachine.ai/v1/sessions \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 Every sign-in that still works, this one marked
current: object - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs
Sign out of the dashboard everywhere#
DELETE/v1/sessions
The dashboard's sign-in, not a key.
Every sign-in of the account ends, this one too. Keys are not touched; revoke those on their own.
curl -X DELETE https://api.wheelmachine.ai/v1/sessions \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 204 Signed out everywhere, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs
Sign one browser out of the dashboard#
DELETE/v1/sessions/{session_id}
The dashboard's sign-in, not a key.
That sign-in ends, and no other; this one too when it is this one. Another account's sign-in, or one that has ended, is not found.
curl -X DELETE https://api.wheelmachine.ai/v1/sessions/ses_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
session_id requiredin the path | string | Matches ^ses_[0-9a-z]{26}$. |
Answers#
- 204 Signed out, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
Which ways of signing in are open#
GET/auth/providers
No key.
A way that is not set up yet says false, and its door sends the browser back with signin=not_set_up.
curl https://api.wheelmachine.ai/auth/providers \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 The doors: object
Continue with GitHub#
GET/auth/github/start
No key.
Sends the browser to GitHub with a state, PKCE and the scopes read:user user:email. then is a place on the dashboard to come back to (/device, /keys); anything else is dropped.
curl https://api.wheelmachine.ai/auth/github/start \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
thenin the query | string | Where on the dashboard to go once signed in, such as /device. |
Answers#
- 302 : On to GitHub, or back to the dashboard with
signin=saying why not
Back from GitHub#
GET/auth/github/callback
No key.
Signs the person in when the state is the one this browser set out with, in time, and GitHub verified an e-mail address of theirs; the account is found - by the GitHub account's number, else by that address - or made. Otherwise back to the dashboard with signin= one of state, cancelled, unverified, held, young, unavailable, failed.
curl https://api.wheelmachine.ai/auth/github/callback \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 302 : Signed in and on to the dashboard, or back to it with
signin=saying why not
Continue with Google#
GET/auth/google/start
No key.
Sends the browser to Google with a state, PKCE, a nonce and the scopes openid email profile.
curl https://api.wheelmachine.ai/auth/google/start \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
thenin the query | string | Where on the dashboard to go once signed in, such as /device. |
Answers#
- 302 : On to Google, or back to the dashboard with
signin=saying why not
Back from Google#
GET/auth/google/callback
No key.
As GitHub's way back; the ID token must be Google's, for this application, in time, this sign-in's (nonce), with email_verified.
curl https://api.wheelmachine.ai/auth/google/callback \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 302 : Signed in and on to the dashboard, or back to it with
signin=saying why not
Projects#
The account's projects#
GET/v1/projects
Any key with a machine scope.
curl "https://api.wheelmachine.ai/v1/projects?limit=50" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
limitin the query | integer | From 1 to 200. Left out, it is 50. |
afterin the query | string | The next of the page before. |
Answers#
- 200 A page of projects: a list of Project
- 400
invalid_request:limitorafteris not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work
Make a project#
POST/v1/projects
Needs cloud:projects:write.
Needs the cloud:projects:write scope. A key limited to some projects cannot make new ones.
curl -X POST https://api.wheelmachine.ai/v1/projects \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "research"}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
Idempotency-Keya header | string | Any string up to 255 characters, unique per attempt. The same key with the same request within 24 hours gets the first answer again (with Idempotent-Replayed: true); with a different request it is refused. At most 255 characters. |
Body#
| Field | Type | What it is |
|---|---|---|
name required | string | 1 to 80 characters. |
Answers#
- 201 The new project: a Project
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 409
conflict: Not possible in the state it is in - 422
invalid_request: ThisIdempotency-Keywas used with a different request - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again
One project#
GET/v1/projects/{project_id}
Any key with a machine scope.
curl https://api.wheelmachine.ai/v1/projects/prj_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
project_id requiredin the path | string | Matches ^prj_[0-9a-z]{26}$. |
Answers#
- 200 The project: a Project
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 404
not_found: Nothing of that id that this key can see
Rename a project#
PATCH/v1/projects/{project_id}
Needs cloud:projects:write.
curl -X PATCH https://api.wheelmachine.ai/v1/projects/prj_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "research-2026"}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
project_id requiredin the path | string | Matches ^prj_[0-9a-z]{26}$. |
Body#
| Field | Type | What it is |
|---|---|---|
name required | string | 1 to 80 characters. |
Answers#
- 200 The project as it is now: a Project
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
Delete an empty project#
DELETE/v1/projects/{project_id}
Needs cloud:projects:write.
A project that still holds machines is refused with 409 project_not_empty.
curl -X DELETE https://api.wheelmachine.ai/v1/projects/prj_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
project_id requiredin the path | string | Matches ^prj_[0-9a-z]{26}$. |
Answers#
- 204 Deleted, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
project_not_empty: Not possible in the state it is in
Keys#
The account's machine keys#
GET/v1/keys
Needs cloud:keys:write.
Keys that hold at least one cloud: scope. Secrets are never shown again after a key is made.
curl https://api.wheelmachine.ai/v1/keys \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 The keys: a list of Key
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs
Make a key#
POST/v1/keys
Needs cloud:keys:write.
Needs the cloud:keys:write scope. A new key can hold only scopes and projects the calling key holds, so a key can never make a stronger one. The secret is in this answer only.
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"], "expires_at": "2026-12-31T23:59:59Z"}'{
"id": "key_8c41d07e2b9a6f13",
"object": "key",
"name": "agent",
"scopes": ["cloud:machines:read", "cloud:machines:write", "cloud:machines:exec"],
"projects": [],
"shown": "ak_live_q7rm…h2xk",
"state": "active",
"rate_per_minute": 600,
"created_at": "2026-10-23T09:31:00Z",
"expires_at": "2026-12-31T23:59:59Z",
"last_used_at": null,
"secret": "ak_live_..."
}Parameters#
| Parameter | Type | What it is |
|---|---|---|
Idempotency-Keya header | string | Any string up to 255 characters, unique per attempt. The same key with the same request within 24 hours gets the first answer again (with Idempotent-Replayed: true); with a different request it is refused. At most 255 characters. |
Body#
| Field | Type | What it is |
|---|---|---|
name required | string | 1 to 80 characters. |
scopes required | array of Scope | At least 1 item. |
projects | array of string | |
expires_at | time | |
rate_per_minute | integer | From 1 to 6000. |
Answers#
- 201 The new key, with its secret shown this once: a KeyCreated
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: A project inprojectsis not one this key reaches - 409
conflict: The first call with thisIdempotency-Keyis still running - 422
invalid_request: ThisIdempotency-Keywas used with a different request - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again
Revoke a key#
DELETE/v1/keys/{key_id}
Needs cloud:keys:write.
The key stops working within seconds, everywhere. A key can revoke itself.
curl -X DELETE https://api.wheelmachine.ai/v1/keys/key_0000000000000001 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
key_id requiredin the path | string | Matches ^key_[0-9a-f]{16}$. |
Answers#
- 204 Revoked, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
Machines#
The account's snapshots, newest first#
GET/v1/snapshots
Needs cloud:machines:read.
Needs the cloud:machines:read scope. Every snapshot taken through the API of a machine that is not deleted, in the projects this key reaches - each one a new machine can start from with from_snapshot. A deleted machine's snapshots go with it.
curl https://api.wheelmachine.ai/v1/snapshots \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
limitin the query | integer | From 1 to 200. Left out, it is 100. |
Answers#
- 200 The snapshots: object
- 400
invalid_request:limitis not between 1 and 200 - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs
Machines, newest first#
GET/v1/machines
Needs cloud:machines:read.
curl "https://api.wheelmachine.ai/v1/machines?state=running&label=team=ml&limit=20" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
projectin the query | string | Only this project's machines. |
statein the query | MachineState | One of "starting", "running", "paused", "stopped", "archived", "recovering" or "deleted". |
labelin the query | array of string | key=value; repeat to ask for every one of several labels. |
limitin the query | integer | From 1 to 200. Left out, it is 50. |
afterin the query | string | The next of the page before. |
Answers#
- 200 A page of machines: a list of Machine
- 400
invalid_request: Astate,label,limitorafterthat is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Theprojectis not one this key reaches
Make a machine#
POST/v1/machines
Needs cloud:machines:write and cloud:gpus for a machine with a GPU, and cloud:secrets:write with a key of the whole account to give it secrets.
Answers when the machine is running (201), usually in well under a second. When no server has room just now one is started for it; if that takes longer than wait_s, the answer is 202 with the machine starting, and GET /v1/machines/{machine_id}/wait follows it.
curl -X POST https://api.wheelmachine.ai/v1/machines \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "trainer", "cpus": 4, "labels": {"team": "ml"}, "idle_after_s": 900}'{
"id": "m_01jb2xk3n4p5q6r7s8t9v0w1x2",
"object": "machine",
"name": "trainer",
"project": "prj_01jb2xk3n4p5q6r7s8t9v0w1x2",
"state": "running",
"gpu": null,
"size": {"cpus": 4, "memory_gb": 8},
"location": {"city": "Helsinki", "country": "FI"},
"cpus": 4,
"memory_gb": 8,
"sleep_when_idle": true,
"idle_after_s": 900,
"labels": {"team": "ml"},
"network": {
"internet": "open",
"allow": []
},
"files_sealed": true,
"running_seconds": 0.4,
"created_at": "2026-10-23T09:30:00Z",
"last_used_at": "2026-10-23T09:30:00Z",
"ready_ms": 412.6
}Parameters#
| Parameter | Type | What it is |
|---|---|---|
Idempotency-Keya header | string | Any string up to 255 characters, unique per attempt. The same key with the same request within 24 hours gets the first answer again (with Idempotent-Replayed: true); with a different request it is refused. At most 255 characters. |
Body#
| Field | Type | What it is |
|---|---|---|
name | string | For people; ids are made by us. At most 80 characters. |
project | string | Its project; the account's first project when left out. |
region | string | A region id from GET /v1/regions to run near. Where it runs is said in location. |
gpu | string | A GPU id from GET /v1/gpus. Needs the cloud:gpus scope. Coming Not open to every account yet: such a call answers 403 not_open. |
cpus | integer | Its processors, with 2 GB of memory each: 16 is sixteen processors and 32 GB. Charged by the second at the 2-processor price that many times over (16 is 8 times). Not with gpu: a GPU machine's size comes with it. One of 2, 4, 8 or 16. Left out, it is 2. |
labels | object of strings | At most 32. |
secrets | array of string | Names of the account's secrets that every command run in the machine gets as environment variables. Giving them needs the cloud:secrets:write scope too, and a key of the whole account; each must be set already. A command can read its environment: any key that may run commands in the machine can read what it was given. |
network | object | Its internet policy, the outbound gate. Left out, the machine is open - it reaches the internet as every machine always could. A machine made from a snapshot keeps the source machine's policy unless this sets its own. |
from_snapshot | object | |
from_snapshot.machine required | string | |
from_snapshot.snapshot | string | A snapshot's id; its latest when left out. |
sleep_when_idle | boolean | True: it pauses by itself once it is idle - no call or connection has reached it for idle_after_s and its programs have been quiet as long (under 5% of one CPU on average), so a job left running keeps it going. Paused, it keeps its memory and no running time is charged. False: it never pauses by itself; it runs, and is charged, until it is paused or stopped. Left out, it is true. |
idle_after_s | integer | How long it must be idle before it pauses by itself. Only with sleep_when_idle true. From 60 to 86400. Left out, it is 300. |
wait_s | number | From 0 to 600. Left out, it is 60. |
Answers#
- 201 Running: a Machine
- 202 Starting; follow it with the wait call: a Machine
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbiddenornot_open: The key does not hold the scope or project this needs - 404
not_found: Theproject, or the machine infrom_snapshot, is not one this key reaches - 409
conflictornot_open: The first call with thisIdempotency-Keyis still running; or its internet is held back and no server here can hold back a machine's internet yet (not_open) - 422
invalid_request: ThisIdempotency-Keywas used with a different request - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again - 503
unavailable: No room just now;Retry-Aftersays when to try again
One machine#
GET/v1/machines/{machine_id}
Needs cloud:machines:read.
curl https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Answers#
- 200 The machine: a Machine
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 404
not_found: Nothing of that id that this key can see
Rename a machine, move it to another project, or change its labels, secrets or internet#
PATCH/v1/machines/{machine_id}
Needs cloud:machines:write and cloud:secrets:write with a key of the whole account to give it secrets.
Labels merge: a label given null is taken off. A machine moves only to a project this key reaches, and keeps everything of its own as it goes. Everything asked is changed at once, or nothing is. What it was made as - its size and when it sleeps by itself - stays as it was made: a new machine started from a snapshot of it can be made otherwise.
curl -X PATCH https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "trainer-2", "labels": {"stage": "done", "team": null}}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Body#
| Field | Type | What it is |
|---|---|---|
name | string | At most 80 characters. |
project | string | Another of the account's projects to move it to, one this key reaches. |
labels | object of strings or nulls | |
secrets | object | Every secret it is to get, said whole: the list takes the place of the one it had, and [] takes them all away (which needs no cloud:secrets:write: it gives nothing). |
network | object | Its internet policy, said whole: it takes the place of the one it had. A running machine has its policy changed on its host at once, without a restart. |
Answers#
- 200 The machine as it is now: a Machine
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: The machine, or theprojectit is moved to, is not one this key reaches - 409
conflictornot_open: Its internet was held back while it runs on a server that cannot hold it back (not_open): the rule is kept, and takes effect when it next starts on a server that can
Delete a machine and its files#
DELETE/v1/machines/{machine_id}
Needs cloud:machines:write.
curl -X DELETE https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Answers#
- 204 Deleted, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
Run it again, from paused, stopped or archived#
POST/v1/machines/{machine_id}/start
Needs cloud:machines:write.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/start \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"wait_s": 60}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Body (it may be left out)#
| Field | Type | What it is |
|---|---|---|
wait_s | number | From 0 to 600. Left out, it is 60. |
Answers#
- 200 Running: a Machine
- 202 Starting: a Machine
- 400
invalid_request:wait_sis not between 0 and 600 - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: The machine cannot start in the state it is in; or its internet is held back and no server here can hold back a machine's internet yet (not_open) - 429
rate_limitedorlimit_reached: Too many calls just now, or the account has as many machines running or kept as it may - 503
unavailable: No room just now;Retry-Aftersays when to try again
Pause it with its memory kept#
POST/v1/machines/{machine_id}/pause
Needs cloud:machines:write.
Nothing runs and no running time is charged; start brings it back where it was.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/pause \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Answers#
- 200 Paused: a Machine
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: Not possible in the state it is in
Stop it; its files stay#
POST/v1/machines/{machine_id}/stop
Needs cloud:machines:write.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/stop \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Answers#
- 200 Stopped: a Machine
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: The machine cannot stop in the state it is in
Put its files away in storage, the cheapest way to keep it#
POST/v1/machines/{machine_id}/archive
Needs cloud:machines:write.
Sealed with the account's key when the machine's files are sealed. start brings it back.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/archive \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Answers#
- 200 Archived: a Machine
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: The machine cannot be archived in the state it is in
A new machine, a copy of this one as it is now#
POST/v1/machines/{machine_id}/copy
Needs cloud:machines:write.
Made beside the original where there is room, so the files are copied in a moment. The copy's internet is held back as the original's is: when it is and no server here can hold back a machine's internet yet, the answer is 409 not_open.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/copy \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "trainer-copy"}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Idempotency-Keya header | string | Any string up to 255 characters, unique per attempt. The same key with the same request within 24 hours gets the first answer again (with Idempotent-Replayed: true); with a different request it is refused. At most 255 characters. |
Body (it may be left out)#
| Field | Type | What it is |
|---|---|---|
name | string | At most 80 characters. |
labels | object of strings | At most 32. |
wait_s | number | From 0 to 600. Left out, it is 60. |
Answers#
- 201 The copy, running: a Machine
- 400
invalid_request: A name, label orwait_sthat is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: The first call with thisIdempotency-Keyis still running - 422
invalid_request: ThisIdempotency-Keywas used with a different request - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again - 503
unavailable: No room just now;Retry-Aftersays when to try again
The machine's snapshots, newest first#
GET/v1/machines/{machine_id}/snapshots
Needs cloud:machines:read.
Needs the cloud:machines:read scope. Those taken through the API; a deleted machine has none.
curl https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/snapshots \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
limitin the query | integer | From 1 to 200. Left out, it is 100. |
Answers#
- 200 Its snapshots: object
- 400
invalid_request:limitis not between 1 and 200 - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
Keep its files as they are now#
POST/v1/machines/{machine_id}/snapshots
Needs cloud:machines:write.
A new machine can be made from a snapshot with from_snapshot.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/snapshots \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"note": "before the upgrade"}'{
"id": "20261023T093500Z-3f2a0001",
"object": "snapshot",
"machine": "m_01jb2xk3n4p5q6r7s8t9v0w1x2",
"note": "before the upgrade",
"created_at": "2026-10-23T09:35:00Z"
}Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Idempotency-Keya header | string | Any string up to 255 characters, unique per attempt. The same key with the same request within 24 hours gets the first answer again (with Idempotent-Replayed: true); with a different request it is refused. At most 255 characters. |
Body (it may be left out)#
| Field | Type | What it is |
|---|---|---|
note | string | At most 200 characters. |
Answers#
- 201 The snapshot: a Snapshot
- 400
invalid_request: The note is longer than 200 characters - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: Not possible in the state it is in - 422
invalid_request: ThisIdempotency-Keywas used with a different request
Answer when the machine reaches a state#
GET/v1/machines/{machine_id}/wait
Needs cloud:machines:read.
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/wait?state=running&timeout_s=120" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
state requiredin the query | MachineState | One of "starting", "running", "paused", "stopped", "archived", "recovering" or "deleted". |
timeout_sin the query | number | From 0 to 600. Left out, it is 60. |
Answers#
- 200 It is in that state: a Machine
- 400
invalid_request: Nostate, or atimeout_sthat is not between 0 and 600 - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 404
not_found: Nothing of that id that this key can see - 408
timeout: The state was not reached in time
What the machine used, step by step, over the last day#
GET/v1/machines/{machine_id}/metrics
Needs cloud:machines:read.
Needs the cloud:machines:read scope. Its processors as a share of its own size, the most memory it held and the most it may hold, and the bytes a second it read and wrote on its disk and received and sent on its network - a point for each step from since on. Its server reads it every few seconds while it runs and keeps a point a minute for the last day, in memory: a step it was paused or stopped through has no point, and the numbers begin again when the machine moves to another server or its server's program starts anew (kept_since says how far back they go). A machine that is not on a server just now has no points.
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/metrics?since=6h&step=5m" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
sincein the query | string | From when: a time (RFC 3339), or how long ago (90s, 15m, 6h, 1d); the last hour when left out, and at most a day back. For example "6h". |
stepin the query | string | How long each point covers: whole minutes from 1 to 60, in seconds (300) or as a span (5m). Left out, it is "60". For example "60". |
Answers#
- 200 Its points, the oldest first: object
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 501
not_open: It runs on a server that does not keep its numbers yet: none come while it runs there - 503
unavailable: No room just now;Retry-Aftersays when to try again
Files and commands#
Sign a short-lived SSH certificate for a machine#
POST/v1/ssh/certificates
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope. Give a public key and a machine; get back an SSH certificate good for at most an hour, for that machine alone (its one principal is the machine's id, which is also the SSH user to connect as). Present it to the SSH server (ssh_host) to open a real SSH session - for scp, sftp and editors' remote-SSH. There is no password or bare public-key login. wm ssh-config prints the matching ~/.ssh/config block. A certificate works only while the key or the sign-in that asked for it does: revoking that key, or signing out, ends the SSH sessions it opened, and it opens no more.
curl -X POST https://api.wheelmachine.ai/v1/ssh/certificates \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... you@laptop", "machine": "m_01jb2xk3n4p5q6r7s8t9v0w1x2"}'Body#
| Field | Type | What it is |
|---|---|---|
public_key required | string | One line of an OpenSSH public key (ssh-ed25519 AAAA..., or ecdsa/rsa). |
machine required | string | The machine's id; the certificate opens that machine alone. |
ttl_s | integer | How long it lives, in seconds; one hour at most, and the default. From 1 to 3600. |
Answers#
- 201 The signed certificate: object
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflict: The first call with thisIdempotency-Keyis still running - 422
invalid_request: ThisIdempotency-Keywas used with a different request - 501
not_open: Not open for this machine
Whether real SSH is open here, and how to reach and trust it#
GET/v1/ssh
Any key with a machine scope.
Open when SSH certificates are signed here and the SSH server's address is set: what wm ssh-config needs to point your own ssh, scp and sftp at a machine. Public keys only.
curl https://api.wheelmachine.ai/v1/ssh \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 The SSH door: object
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work
Run a command in the machine and get what it wrote#
POST/v1/machines/{machine_id}/exec
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope. A paused machine is woken for it. Commands run as the machine's user unless root is asked for; root is inside the machine only, never on our servers.
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)"]}'{
"exit_code": 0,
"stdout": "4\n",
"stderr": "",
"duration_ms": 38.2,
"truncated": false
}Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Body#
Give command (the words, no shell) or shell (a line for sh -lc), not both.
| Field | Type | What it is |
|---|---|---|
command | array of string | 1 to 4096 items. For example ["python3", "-c", "print(2+2)"]. |
shell | string | At most 131072 characters. For example "ls -la | wc -l". |
env | object of strings | Environment variables for this command, beside the machine's secrets, which it gets as variables too. Of a name in both, the one given here wins: the secret is left out of this command, and the answer does not say so. The command, its variables and the machine's secrets are at most 1 MiB together (413). At most 256. |
cwd | string | Where it starts; the work folder when left out. |
user | string | One of "user" or "root". Left out, it is "user". |
binary_output | boolean | Give stdout as base64 in stdout_base64. Left out, it is false. |
Answers#
- 200 It ran; its exit code says how it ended: a ExecResult
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: The machine cannot run a command in the state it is in; or its internet is held back and no server here can hold back a machine's internet yet (not_open) - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again - 503
unavailable: The command's result did not come back: it may or may not have run, so check before running it again - 504
timeout: The command ran longer than 10 minutes; it may still be running in the machine
Open an interactive terminal in the machine (WebSocket)#
GET/v1/machines/{machine_id}/terminal
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope. This route is a WebSocket upgrade, not a plain request: connect with Upgrade: websocket (version 13) and the usual Authorization: Bearer header. A paused machine is woken for it; the terminal is a login shell unless a command is given, as the machine's user unless root is asked for.
Over the socket, each message is one event. From you: a **binary** message is typed in (keystrokes, bytes as they are); a **text** message {"resize":{"cols":C,"rows":R}} sets the window. From us: a **binary** message is what the program wrote on its terminal; a final **text** message {"exit_code":N} (or {"reason":..., "message":...} when it did not end by itself) says how it ended, just before the socket closes.
A caller may ask for the protocol wm.terminal on the socket, and is answered with it; the dashboard's page does, beside its sign-in's token (wm.csrf.<token>), from its own address. Such a caller - a browser is never shown the answer to a handshake it was refused - is told a terminal that cannot open over the socket instead: it is switched, sent one text message {"reason": <the code>, "status": <the status>, "message": ...}, and closed with the code 4000 and that status (4501: not open for this machine). Any other caller is answered as below, before anything is switched.
An open terminal stays open only while its caller still may hold it. When the sign-in it was opened with ends (signed out, here or from another browser), the key is revoked or expires, or the account is held or can no longer spend, it is ended - at once, or at the latest at the next half-minute ping - and every caller is told why the same way: one text message {"reason": ..., "status": ..., "message": ...} (4401 signed out or the key revoked, 4403 the account held, 4402 it cannot spend), then a close with the code 4000 and that status.
We never log what is typed or shown. The audit records only that a terminal opened and closed.
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/terminal?cols=120&rows=40" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
colsin the query | integer | From 1 to 4096. Left out, it is 80. |
rowsin the query | integer | From 1 to 4096. Left out, it is 24. |
userin the query | string | One of "user" or "root". Left out, it is "user". |
termin the query | string | The terminal's kind, set as TERM inside (default xterm-256color). At most 64 characters. |
cwdin the query | string | Where it starts, an absolute path; the work folder when left out. At most 1024 characters. |
cmdin the query | string | A word of the command to run; repeat for each word. Left out, a login shell opens. |
Answers#
- 101 Switching to the terminal's WebSocket
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: Not possible in the state it is in - 426
invalid_request: The request is not right - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again - 501
not_open: Not open for this machine
Read a file#
GET/v1/machines/{machine_id}/files
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope.
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/results.csv" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-o results.csvParameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
path requiredin the query | string | Inside the machine's home (home/...) or its work folder, where commands start (work/...). At most 1024 characters. |
Answers#
- 200 The file's bytes: the file's bytes
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: Not possible in the state it is in
Write a file, making the folders on the way#
PUT/v1/machines/{machine_id}/files
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope and a Content-Length.
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/data.csv" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-T data.csvParameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
path requiredin the query | string | Inside the machine's home (home/...) or its work folder, where commands start (work/...). At most 1024 characters. |
Body#
The file's bytes as they are, with their length in Content-Length.
Answers#
- 204 Written, with nothing in the answer
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: Not possible in the state it is in - 411
invalid_request: The request is not right - 413
too_large: Too large
Delete a file, or a folder#
DELETE/v1/machines/{machine_id}/files
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope. A folder goes only when it is empty, unless recursive is true: then everything in it goes with it. A link is taken away as itself, never what it points to. The home and the work folder themselves cannot be deleted.
curl -X DELETE "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/old-runs&recursive=true" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
path requiredin the query | string | Inside the machine's home (home/...) or its work folder, where commands start (work/...). At most 1024 characters. |
recursivein the query | boolean | With a folder, everything in it too. Left out, it is false. |
Answers#
- 204 Deleted, with nothing in the answer
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: The folder is not empty andrecursiveis not true, it belongs to another user of the machine, or the machine cannot reach its files in the state it is in
What a folder holds#
GET/v1/machines/{machine_id}/folder
Needs cloud:machines:exec.
Needs the cloud:machines:exec scope. Its own entries, folders first and then by name, each with its kind, its size and when it last changed; at most 1,000, and truncated says when there were more. A paused machine is woken for it.
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/folder?path=home/notes" \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
path requiredin the query | string | A folder of the machine's home (home, home/...) or of its work folder, where commands start (work, work/...). At most 1024 characters. |
Answers#
- 200 The folder: object
- 400
invalid_request: The path is not one of the machine's home or work folder, or it names a file - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 402
payment_required: The account cannot spend now - its limit for this month is reached, or the account is held - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 409
conflictornot_open: The machine cannot reach its files in the state it is in - 501
not_open: The machine has nofindthat can say what each file is
Ports#
The machine's ports open at web addresses#
GET/v1/machines/{machine_id}/ports
Needs cloud:machines:read.
Needs the cloud:machines:read scope.
curl https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/ports \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Answers#
- 200 Its open ports, lowest first: object
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
Open a port of the machine at a web address of its own#
POST/v1/machines/{machine_id}/ports
Needs cloud:machines:write.
Needs the cloud:machines:write scope. Private unless public is true. A port that is open already is answered 200, made public or private as asked (left out, public is false). The ports the machine itself uses (5900, 6080, 7070, 9222, 9223 and 9224) cannot be opened (400), and a machine has at most 20 open (429 with the code limit_reached). Until web addresses are served, the answer is 503 with the code not_open, which waiting a moment does not change.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/ports \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"port": 8000}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
Body#
| Field | Type | What it is |
|---|---|---|
port required | integer | From 1 to 65535. |
public | boolean | Left out, it is false. |
Answers#
- 200 It was open already; it is public or private as asked: object
- 201 Open: object
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again - 503
unavailableornot_open: No room just now;Retry-Aftersays when to try again
Close a port's web address#
DELETE/v1/machines/{machine_id}/ports/{port}
Needs cloud:machines:write.
Needs the cloud:machines:write scope. The address answers 404 from then on, and links made for it no longer open it. Closing a port that is not open is answered 204 all the same.
curl -X DELETE https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/ports/8000 \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
port requiredin the path | integer | From 1 to 65535. |
Answers#
- 204 Closed, with nothing in the answer
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see
A link that lets one browser into a private address for a while#
POST/v1/machines/{machine_id}/ports/{port}/link
Needs cloud:machines:read.
Needs the cloud:machines:read scope. The link opens once, within five minutes. The browser that opens it is given access to that one address for access_s seconds (a cookie for that address alone) and is taken to path on it; a page that only previews links does not use it up. The link is shown in this answer only, and each call makes a new one: it is not sent again once it may have reached the API. A port that is not open is answered 404; until web addresses are served, 503 with the code not_open.
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/ports/8000/link \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"path": "/", "access_s": 1800}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
machine_id requiredin the path | string | Matches ^m_[0-9a-z]{26}$. |
port requiredin the path | integer | From 1 to 65535. |
Body (it may be left out)#
| Field | Type | What it is |
|---|---|---|
access_s | integer | How long the browser that opens the link may reach the address. From 60 to 3600. Left out, it is 1800. |
path | string | Where on the address the link lands: a path beginning with a single / (not // or /\), with no space or backslash in it; a query may follow it. At most 2048 characters. Left out, it is "/". |
Answers#
- 201 The link: object
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 503
unavailableornot_open: No room just now;Retry-Aftersays when to try again
Secrets#
The account's secrets, by name, and the machines that get each#
GET/v1/secrets
Needs cloud:machines:read.
Needs the cloud:machines:read scope. A secret's value is never shown again once it is set: not here, not in any other answer, not in the audit. A key limited to some projects lists only the secrets its projects' machines get, and sees only those machines.
curl https://api.wheelmachine.ai/v1/secrets \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 The secrets: object
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 503
unavailable: Secrets are not set up on this API yet (unavailable, with aRetry-Afterof an hour)
Set a secret, or set it anew#
PUT/v1/secrets/{name}
Needs cloud:secrets:write.
Needs the cloud:secrets:write scope and a key of the whole account: secrets belong to the account, and a key limited to some projects cannot set, delete or give them (403). The value is sealed at once with a key of the account's own and kept only so; the machines given the secret get the new value from their next command. An account keeps at most 200 secrets.
curl -X PUT https://api.wheelmachine.ai/v1/secrets/DATABASE_URL \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value": "postgres://app@db.example.com:5432/app"}'Parameters#
| Parameter | Type | What it is |
|---|---|---|
name requiredin the path | string | Written as an environment variable's name is, which is what a machine's commands get it as. Matches ^[A-Z_][A-Z0-9_]{0,63}$. For example "DATABASE_URL". |
Body#
| Field | Type | What it is |
|---|---|---|
value required | string | The value, up to 32 KiB of text with no NUL in it. It is never shown again. 1 to 32768 characters. |
outside | object | Keep the secret outside the machine: its value never enters the machine - not its environment (which holds only a placeholder wm_secret_<NAME>_<hex>), its files, its process memory or any answer. The outbound gate on the host, which is given the value and the machine is not, puts it into the header of requests going to one of hosts. Left out, the value reaches the machine's commands as an environment variable. |
outside.hosts required | array of string | Where its value may be sent (exact, or a leading *.). When the machine is not open these must also be allowed by its network policy, or the gate could never reach them. 1 to 32 items. |
outside.header required | string | The request header its value is written into, such as Authorization. At most 64 characters. |
outside.format | string | How the value is written, with {value} exactly once; {value} (the value alone) by default. At most 256 characters. |
Answers#
- 200 Set anew: object
- 201 Set for the first time: object
- 400
invalid_request: The request is not right - 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 413
too_large: Too large - 429
rate_limitedorlimit_reached: A limit is reached;Retry-Aftersays when to try again - 503
unavailable: Secrets are not set up on this API yet (unavailable, with aRetry-Afterof an hour)
Delete a secret#
DELETE/v1/secrets/{name}
Needs cloud:secrets:write.
Needs the cloud:secrets:write scope and a key of the whole account. The secret is taken off every machine that was given it: one set again later under the same name goes to none of them until it is given anew.
curl -X DELETE https://api.wheelmachine.ai/v1/secrets/DATABASE_URL \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Parameters#
| Parameter | Type | What it is |
|---|---|---|
name requiredin the path | string | Written as an environment variable's name is, which is what a machine's commands get it as. Matches ^[A-Z_][A-Z0-9_]{0,63}$. For example "DATABASE_URL". |
Answers#
- 204 Deleted, with nothing in the answer
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work - 403
forbidden: The key does not hold the scope or project this needs - 404
not_found: Nothing of that id that this key can see - 503
unavailable: Secrets are not set up on this API yet (unavailable, with aRetry-Afterof an hour)
Catalogue#
The sizes a machine without a GPU comes in, and what an hour of each costs#
GET/v1/sizes
Any key with a machine scope.
What an hour of a machine of each size running comes to - the price its running time is counted at. A paused machine runs no time.
curl https://api.wheelmachine.ai/v1/sizes \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 The sizes, the smallest first: object
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work
Places a machine can be asked to run near#
GET/v1/regions
Any key with a machine scope.
The machine says where it actually runs in location.
curl https://api.wheelmachine.ai/v1/regions \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"{
"object": "list",
"data": [
{"id": "helsinki", "city": "Helsinki", "country": "FI"},
{"id": "mumbai", "city": "Mumbai", "country": "IN"}
]
}Answers#
- 200 The places: a list of Region
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work
GPUs, where they are and what an hour costs#
GET/v1/gpus
Coming GPU machines are not open to every account yet: for_sale is false, and the list shows what is coming.
Any key with a machine scope.
curl https://api.wheelmachine.ai/v1/gpus \
-H "Authorization: Bearer $WHEELMACHINE_API_KEY"Answers#
- 200 The GPUs on offer now: a GpuList
- 401
unauthorizedorkey_revokedorkey_expired: No key, or a key that does not work
Objects#
What the answers are made of. A list comes as {"object": "list", "data": [...], "next": ...}: give next as after to read the page after it; it is null on the last page.
Me#
| Field | Type | What it is |
|---|---|---|
object required | "me" | |
account required | object | |
account.id required | string | |
account.name | string | With the dashboard's sign-in: the person's name, as their door gave it. |
account.email | string | With the dashboard's sign-in: the e-mail address GitHub or Google verified. |
key required | Key or null | The key this call is made with; null for a call made with the dashboard's sign-in. |
session | object | |
standing required | Standing |
Standing#
| Field | Type | What it is |
|---|---|---|
can_spend required | boolean | False when the account's limit for this month is reached or the account is held. |
balance_usd | number or null | What is left to spend this month under the account's limit; null for an account with no money limit. |
reason | string | Why it cannot spend, in words a person reads. |
Project#
| Field | Type | What it is |
|---|---|---|
id required | string | For example "prj_01jb2xk3n4p5q6r7s8t9v0w1x2". |
object required | "project" | |
name required | string | |
created_at required | time |
Key#
| Field | Type | What it is |
|---|---|---|
id required | string | For example "key_3f9a1c2b4d5e6f70". |
object required | "key" | |
name required | string | |
scopes required | array of Scope | |
projects required | array of string | The projects it is limited to; empty for every project of the account. |
shown required | string | Its first and last characters, to recognise it by. For example "ak_live_abcd…wxyz". |
state required | string | One of "active", "revoked" or "expired". |
rate_per_minute | integer or null | |
created_at required | time | |
expires_at | time or null | |
last_used_at | time or null |
KeyCreated#
A Key, with its secret: the only answer that ever holds it.
| Field | Type | What it is |
|---|---|---|
id required | string | For example "key_3f9a1c2b4d5e6f70". |
object required | "key" | |
name required | string | |
scopes required | array of Scope | |
projects required | array of string | The projects it is limited to; empty for every project of the account. |
shown required | string | Its first and last characters, to recognise it by. For example "ak_live_abcd…wxyz". |
state required | string | One of "active", "revoked" or "expired". |
rate_per_minute | integer or null | |
created_at required | time | |
expires_at | time or null | |
last_used_at | time or null | |
secret required | string | The key itself. Shown in this answer only. |
Scope#
cloud:machines:read see machines, and the names of secrets; cloud:machines:write make, change, pause, copy and delete them; cloud:machines:exec run commands and read or write files in them; cloud:gpus make GPU machines; cloud:projects:write make and change projects; cloud:keys:write make and revoke keys; cloud:secrets:write set and delete secrets, and give them to machines; cloud:* all of these.
One of "cloud:*", "cloud:machines:read", "cloud:machines:write", "cloud:machines:exec", "cloud:gpus", "cloud:projects:write", "cloud:keys:write" or "cloud:secrets:write".
Machine#
| Field | Type | What it is |
|---|---|---|
id required | string | For example "m_01jb2xk3n4p5q6r7s8t9v0w1x2". |
object required | "machine" | |
name required | string | |
project required | string | |
state required | MachineState | One of "starting", "running", "paused", "stopped", "archived", "recovering" or "deleted". |
gpu | GpuRef or null | |
size | object or null | |
size.cpus | integer | |
size.memory_gb | number | |
size.disk_gb | number | |
location | object or null | Where it runs now. |
location.city | string | |
location.country | string | ISO 3166-1 alpha-2 |
cpus | integer or null | Its processors, with 2 GB of memory each; null for a GPU machine, whose size comes with its GPU. One of 2, 4, 8, 16 or null. |
memory_gb | number or null | Its memory; null for a GPU machine. |
sleep_when_idle | boolean | True: it pauses by itself once it is idle - no call or connection has reached it for idle_after_s and its programs have been quiet as long. False: it never pauses by itself. |
idle_after_s | integer or null | How long it must be idle before it pauses by itself; null when it never does. |
labels required | object of strings | |
secrets | array of string | The names of the secrets every command run in it gets as environment variables; never their values. |
network required | object | A machine's internet policy - the outbound gate. |
files_sealed required | boolean | Its files in storage are sealed with the account's own key. |
running_seconds | number | All the time it has run, which is what is charged. |
disk_bytes | integer | |
cpu_cores | number | Processors it used, about a minute's average, when running. |
memory_bytes | integer | |
created_at required | time | |
last_used_at | time or null | |
ready_ms | number | How long making it ready took, when this answer made it ready (create, start, copy). |
MachineState#
starting being made ready; running; paused memory kept, nothing running; stopped files kept, memory not; archived files in storage; recovering its server was lost and it is being brought back from its last moments; deleted.
One of "starting", "running", "paused", "stopped", "archived", "recovering" or "deleted".
GpuRef#
| Field | Type | What it is |
|---|---|---|
id required | string | An id from GET /v1/gpus. |
model | string | |
count | integer | |
memory_gb | integer |
Snapshot#
| Field | Type | What it is |
|---|---|---|
id required | string | |
object required | "snapshot" | |
machine required | string | |
note | string | |
created_at required | time |
ExecResult#
| Field | Type | What it is |
|---|---|---|
exit_code required | integer | |
stdout | string | |
stdout_base64 | string | |
stderr required | string | |
duration_ms required | number | |
truncated required | boolean | True when the output was longer than 8 MiB and was cut there. |
Region#
| Field | Type | What it is |
|---|---|---|
id required | string | For example "mumbai". |
city required | string | |
country required | string |
GpuList#
| Field | Type | What it is |
|---|---|---|
object required | "list" | |
for_sale required | boolean | False while GPU machines are not yet open to every account; the list shows what is coming. |
data required | array of GpuOffer |
GpuOffer#
| Field | Type | What it is |
|---|---|---|
id required | string | |
model required | string | For example "H100 80GB". |
count required | integer | |
memory_gb required | integer | |
cpus | integer | |
ram_gb | number | |
disk_gb | number | |
location | object | |
location.city | string | |
location.country | string | |
price_per_hour_usd required | number | |
interruptible | boolean | May be taken back at any time; cheaper for it. |
available required | boolean |
Usage#
| Field | Type | What it is |
|---|---|---|
object required | "usage" | |
month required | string | |
total_usd required | number | |
machines required | array of object | |
machines[].machine required | string | |
machines[].project | string | |
machines[].running_seconds required | number | |
machines[].usd required | number |
Problem#
| Field | Type | What it is |
|---|---|---|
type required | string | A link to the page that explains this code. |
title required | string | |
status required | integer | |
detail | string | |
code required | string | One of "invalid_request", "unauthorized", "key_revoked", "key_expired", "forbidden", "not_found", "conflict", "project_not_empty", "payment_required", "rate_limited", "limit_reached", "unavailable", "timeout", "too_large", "not_open", "internal", "login_denied" or "login_expired". |
request_id | string | |
retry_after_s | number |