# API reference

> Every route of the Wheelmachine API, with its scope, fields, answers and an example, made from the API's contract (OpenAPI 3.1).

Every route of the API, made from its contract, [openapi.yaml](https://docs.wheelmachine.ai/openapi.yaml) (OpenAPI 3.1; also as [JSON](https://docs.wheelmachine.ai/openapi.json)), each time these pages are built.

Terminal:

```sh
$ 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](https://docs.wheelmachine.ai/keys.html#scopes) 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_`, projects `prj_`, both with 26 letters and digits; keys are `key_` with 16.
- **Lists.** `{"object": "list", "data": [...], "next": ...}`: give `next` as `after` for the page after it, up to `limit` (1 to 200, 50 when not said) at a time; `next` is `null` on the last page. Machines come newest first, projects oldest first.
- **Times** are RFC 3339, in UTC.
- **Refusals** are `application/problem+json` with a stable `code`: see [Errors](https://docs.wheelmachine.ai/errors/index.html). Any call can also answer `403 forbidden` (a key made for other work, or an account that is held), `429 rate_limited`, `500 internal`, `503 unavailable` and `504 timeout`.
- **Making things safely.** Calls that make something take an `Idempotency-Key`: see [Try again, safely](https://docs.wheelmachine.ai/errors/index.html#try-again-safely).
- **Every answer** carries `X-Request-Id` and the key's [rate limit](https://docs.wheelmachine.ai/limits.html#calls-a-minute).

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.

Request:

```sh
curl https://api.wheelmachine.ai/v1/me \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

Answer 200:

```json
{
  "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](https://docs.wheelmachine.ai/reference.html#obj-Me)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
curl "https://api.wheelmachine.ai/v1/usage?month=2026-10" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

Answer 200:

```json
{
  "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 |
|---|---|---|
| `month` (in the query) | string | `YYYY-MM`; this month when left out. Matches `^[0-9]{4}-[0-9]{2}$`. |
| `project` (in the query) | string |  |

#### Answers

- 200 The month's usage: a [Usage](https://docs.wheelmachine.ai/reference.html#obj-Usage)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): `month` is not `YYYY-MM`
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): The `project` is 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`).

Request:

```sh
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](https://docs.wheelmachine.ai/reference.html#obj-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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 403 [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The key does not hold the scope or project this needs
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says 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`.

Request:

```sh
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](https://docs.wheelmachine.ai/reference.html#obj-KeyCreated)
- 202 Still waiting for the person: object
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 403 [`login_denied`](https://docs.wheelmachine.ai/errors/login_denied.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 410 [`login_expired`](https://docs.wheelmachine.ai/errors/login_expired.html): The sign-in's code ran out, or was used already
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says 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.

Request:

```sh
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](https://docs.wheelmachine.ai/reference.html#obj-Scope) | Approving only - what the program is given, within what it asked for. |

#### Answers

- 200 The program's sign-in, waiting: object
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says 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.

Request:

```sh
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](https://docs.wheelmachine.ai/reference.html#obj-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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): Not possible in the state it is in
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again

### Refuse a program

POST `/v1/login/devices/deny`

The dashboard's sign-in, not a key.

Request:

```sh
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](https://docs.wheelmachine.ai/reference.html#obj-Scope) | Approving only - what the program is given, within what it asked for. |

#### Answers

- 200 Refused; the program is told so: object
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): 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.

Request:

```sh
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 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): 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.

Request:

```sh
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 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): 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.

Request:

```sh
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 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): 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.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^ses_[0-9a-z]{26}$`. |

#### Answers

- 204 Signed out, with nothing in the answer
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): 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`.

Request:

```sh
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.

Request:

```sh
curl https://api.wheelmachine.ai/auth/github/start \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `then` (in 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`.

Request:

```sh
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`.

Request:

```sh
curl https://api.wheelmachine.ai/auth/google/start \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `then` (in 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`.

Request:

```sh
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.

Request:

```sh
curl "https://api.wheelmachine.ai/v1/projects?limit=50" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `limit` (in the query) | integer | From 1 to 200. Left out, it is `50`. |
| `after` (in the query) | string | The `next` of the page before. |

#### Answers

- 200 A page of projects: a list of [Project](https://docs.wheelmachine.ai/reference.html#obj-Project)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): `limit` or `after` is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work

### Make a project

POST `/v1/projects`

Needs [`cloud:projects:write`](https://docs.wheelmachine.ai/keys.html#scopes).

Needs the `cloud:projects:write` scope. A key limited to some projects cannot make new ones.

Request:

```sh
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-Key` (a 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](https://docs.wheelmachine.ai/reference.html#obj-Project)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): Not possible in the state it is in
- 422 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): This `Idempotency-Key` was used with a different request
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again

### One project

GET `/v1/projects/{project_id}`

Any key with a machine scope.

Request:

```sh
curl https://api.wheelmachine.ai/v1/projects/prj_01jb2xk3n4p5q6r7s8t9v0w1x2 \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `project_id` *(required)* (in the path) | string | Matches `^prj_[0-9a-z]{26}$`. |

#### Answers

- 200 The project: a [Project](https://docs.wheelmachine.ai/reference.html#obj-Project)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see

### Rename a project

PATCH `/v1/projects/{project_id}`

Needs [`cloud:projects:write`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
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` *(required)* (in 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](https://docs.wheelmachine.ai/reference.html#obj-Project)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see

### Delete an empty project

DELETE `/v1/projects/{project_id}`

Needs [`cloud:projects:write`](https://docs.wheelmachine.ai/keys.html#scopes).

A project that still holds machines is refused with `409 project_not_empty`.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^prj_[0-9a-z]{26}$`. |

#### Answers

- 204 Deleted, with nothing in the answer
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`project_not_empty`](https://docs.wheelmachine.ai/errors/project_not_empty.html): Not possible in the state it is in

## Keys

### The account's machine keys

GET `/v1/keys`

Needs [`cloud:keys:write`](https://docs.wheelmachine.ai/keys.html#scopes).

Keys that hold at least one `cloud:` scope. Secrets are never shown again after a key is made.

Request:

```sh
curl https://api.wheelmachine.ai/v1/keys \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Answers

- 200 The keys: a list of [Key](https://docs.wheelmachine.ai/reference.html#obj-Key)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs

### Make a key

POST `/v1/keys`

Needs [`cloud:keys:write`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```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"], "expires_at": "2026-12-31T23:59:59Z"}'
```

Answer 201:

```json
{
  "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-Key` (a 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](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-KeyCreated)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): A project in `projects` is not one this key reaches
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): The first call with this `Idempotency-Key` is still running
- 422 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): This `Idempotency-Key` was used with a different request
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again

### Revoke a key

DELETE `/v1/keys/{key_id}`

Needs [`cloud:keys:write`](https://docs.wheelmachine.ai/keys.html#scopes).

The key stops working within seconds, everywhere. A key can revoke itself.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^key_[0-9a-f]{16}$`. |

#### Answers

- 204 Revoked, with nothing in the answer
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see

## Machines

### The account's snapshots, newest first

GET `/v1/snapshots`

Needs [`cloud:machines:read`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
curl https://api.wheelmachine.ai/v1/snapshots \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `limit` (in the query) | integer | From 1 to 200. Left out, it is `100`. |

#### Answers

- 200 The snapshots: object
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): `limit` is not between 1 and 200
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs

### Machines, newest first

GET `/v1/machines`

Needs [`cloud:machines:read`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
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 |
|---|---|---|
| `project` (in the query) | string | Only this project's machines. |
| `state` (in the query) | [MachineState](https://docs.wheelmachine.ai/reference.html#obj-MachineState) | One of `"starting"`, `"running"`, `"paused"`, `"stopped"`, `"archived"`, `"recovering"` or `"deleted"`. |
| `label` (in the query) | array of string | `key=value`; repeat to ask for every one of several labels. |
| `limit` (in the query) | integer | From 1 to 200. Left out, it is `50`. |
| `after` (in the query) | string | The `next` of the page before. |

#### Answers

- 200 A page of machines: a list of [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): A `state`, `label`, `limit` or `after` that is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): The `project` is not one this key reaches

### Make a machine

POST `/v1/machines`

Needs [`cloud:machines:write`](https://docs.wheelmachine.ai/keys.html#scopes) 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.

Request:

```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": 4, "labels": {"team": "ml"}, "idle_after_s": 900}'
```

Answer 201:

```json
{
  "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-Key` (a 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`](https://docs.wheelmachine.ai/errors/not_open.html). |
| `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](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 202 Starting; follow it with the wait call: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): The `project`, or the machine in `from_snapshot`, is not one this key reaches
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The first call with this `Idempotency-Key` is 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): This `Idempotency-Key` was used with a different request
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): No room just now; `Retry-After` says when to try again

### One machine

GET `/v1/machines/{machine_id}`

Needs [`cloud:machines:read`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
curl https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2 \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `machine_id` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |

#### Answers

- 200 The machine: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes) 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.

Request:

```sh
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` *(required)* (in 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](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): The machine, or the `project` it is moved to, is not one this key reaches
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |

#### Answers

- 204 Deleted, with nothing in the answer
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
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` *(required)* (in 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](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 202 Starting: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): `wait_s` is not between 0 and 600
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): 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_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): Too many calls just now, or the account has as many machines running or kept as it may
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): No room just now; `Retry-After` says when to try again

### Pause it with its memory kept

POST `/v1/machines/{machine_id}/pause`

Needs [`cloud:machines:write`](https://docs.wheelmachine.ai/keys.html#scopes).

Nothing runs and no running time is charged; `start` brings it back where it was.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |

#### Answers

- 200 Paused: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): Not possible in the state it is in

### Stop it; its files stay

POST `/v1/machines/{machine_id}/stop`

Needs [`cloud:machines:write`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |

#### Answers

- 200 Stopped: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

Sealed with the account's key when the machine's files are sealed. `start` brings it back.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |

#### Answers

- 200 Archived: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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`.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `Idempotency-Key` (a 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](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): A name, label or `wait_s` that is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The first call with this `Idempotency-Key` is still running
- 422 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): This `Idempotency-Key` was used with a different request
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): No room just now; `Retry-After` says when to try again

### The machine's snapshots, newest first

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

Needs [`cloud:machines:read`](https://docs.wheelmachine.ai/keys.html#scopes).

Needs the `cloud:machines:read` scope. Those taken through the API; a deleted machine has none.

Request:

```sh
curl https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/snapshots \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `machine_id` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `limit` (in the query) | integer | From 1 to 200. Left out, it is `100`. |

#### Answers

- 200 Its snapshots: object
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): `limit` is not between 1 and 200
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

A new machine can be made from a snapshot with `from_snapshot`.

Request:

```sh
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"}'
```

Answer 201:

```json
{
  "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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `Idempotency-Key` (a 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](https://docs.wheelmachine.ai/reference.html#obj-Snapshot)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The note is longer than 200 characters
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): Not possible in the state it is in
- 422 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): This `Idempotency-Key` was used with a different request

### Answer when the machine reaches a state

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

Needs [`cloud:machines:read`](https://docs.wheelmachine.ai/keys.html#scopes).

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `state` *(required)* (in the query) | [MachineState](https://docs.wheelmachine.ai/reference.html#obj-MachineState) | One of `"starting"`, `"running"`, `"paused"`, `"stopped"`, `"archived"`, `"recovering"` or `"deleted"`. |
| `timeout_s` (in the query) | number | From 0 to 600. Left out, it is `60`. |

#### Answers

- 200 It is in that state: a [Machine](https://docs.wheelmachine.ai/reference.html#obj-Machine)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): No `state`, or a `timeout_s` that is not between 0 and 600
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 408 [`timeout`](https://docs.wheelmachine.ai/errors/timeout.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `since` (in 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"`. |
| `step` (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 501 [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): It runs on a server that does not keep its numbers yet: none come while it runs there
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): No room just now; `Retry-After` says when to try again

## Files and commands

### Sign a short-lived SSH certificate for a machine

POST `/v1/ssh/certificates`

Needs [`cloud:machines:exec`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html): The first call with this `Idempotency-Key` is still running
- 422 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): This `Idempotency-Key` was used with a different request
- 501 [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): 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.

Request:

```sh
curl https://api.wheelmachine.ai/v1/ssh \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Answers

- 200 The SSH door: object
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```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)"]}'
```

Answer 200:

```json
{
  "exit_code": 0,
  "stdout": "4\n",
  "stderr": "",
  "duration_ms": 38.2,
  "truncated": false
}
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `machine_id` *(required)* (in 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](https://docs.wheelmachine.ai/reference.html#obj-ExecResult)
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): 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_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): The command's result did not come back: it may or may not have run, so check before running it again
- 504 [`timeout`](https://docs.wheelmachine.ai/errors/timeout.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `cols` (in the query) | integer | From 1 to 4096. Left out, it is `80`. |
| `rows` (in the query) | integer | From 1 to 4096. Left out, it is `24`. |
| `user` (in the query) | string | One of `"user"` or `"root"`. Left out, it is `"user"`. |
| `term` (in the query) | string | The terminal's kind, set as `TERM` inside (default `xterm-256color`). At most 64 characters. |
| `cwd` (in the query) | string | Where it starts, an absolute path; the work folder when left out. At most 1024 characters. |
| `cmd` (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): Not possible in the state it is in
- 426 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again
- 501 [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): Not open for this machine

### Read a file

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

Needs [`cloud:machines:exec`](https://docs.wheelmachine.ai/keys.html#scopes).

Needs the `cloud:machines:exec` scope.

Request:

```sh
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/results.csv" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
  -o results.csv
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `machine_id` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `path` *(required)* (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

Needs the `cloud:machines:exec` scope and a `Content-Length`.

Request:

```sh
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/data.csv" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
  -T data.csv
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `machine_id` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `path` *(required)* (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): Not possible in the state it is in
- 411 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 413 [`too_large`](https://docs.wheelmachine.ai/errors/too_large.html): Too large

### Delete a file, or a folder

DELETE `/v1/machines/{machine_id}/files`

Needs [`cloud:machines:exec`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `path` *(required)* (in the query) | string | Inside the machine's home (`home/...`) or its work folder, where commands start (`work/...`). At most 1024 characters. |
| `recursive` (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The folder is not empty and `recursive` is 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `path` *(required)* (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The path is not one of the machine's home or work folder, or it names a file
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 402 [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html): The account cannot spend now - its limit for this month is reached, or the account is held
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 409 [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The machine cannot reach its files in the state it is in
- 501 [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): The machine has no `find` that 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`](https://docs.wheelmachine.ai/keys.html#scopes).

Needs the `cloud:machines:read` scope.

Request:

```sh
curl https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/ports \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `machine_id` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |

#### Answers

- 200 Its open ports, lowest first: object
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): No room just now; `Retry-After` says when to try again

### Close a port's web address

DELETE `/v1/machines/{machine_id}/ports/{port}`

Needs [`cloud:machines:write`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `port` *(required)* (in the path) | integer | From 1 to 65535. |

#### Answers

- 204 Closed, with nothing in the answer
- 400 [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): 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`](https://docs.wheelmachine.ai/keys.html#scopes).

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`.

Request:

```sh
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` *(required)* (in the path) | string | Matches `^m_[0-9a-z]{26}$`. |
| `port` *(required)* (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) or [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html): No room just now; `Retry-After` says when to try again

## Secrets

### The account's secrets, by name, and the machines that get each

GET `/v1/secrets`

Needs [`cloud:machines:read`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
curl https://api.wheelmachine.ai/v1/secrets \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Answers

- 200 The secrets: object
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): Secrets are not set up on this API yet (`unavailable`, with a `Retry-After` of an hour)

### Set a secret, or set it anew

PUT `/v1/secrets/{name}`

Needs [`cloud:secrets:write`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
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` *(required)* (in 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`](https://docs.wheelmachine.ai/errors/invalid_request.html): The request is not right
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 413 [`too_large`](https://docs.wheelmachine.ai/errors/too_large.html): Too large
- 429 [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) or [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html): A limit is reached; `Retry-After` says when to try again
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): Secrets are not set up on this API yet (`unavailable`, with a `Retry-After` of an hour)

### Delete a secret

DELETE `/v1/secrets/{name}`

Needs [`cloud:secrets:write`](https://docs.wheelmachine.ai/keys.html#scopes).

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.

Request:

```sh
curl -X DELETE https://api.wheelmachine.ai/v1/secrets/DATABASE_URL \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Parameters

| Parameter | Type | What it is |
|---|---|---|
| `name` *(required)* (in 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 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): No key, or a key that does not work
- 403 [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html): The key does not hold the scope or project this needs
- 404 [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html): Nothing of that id that this key can see
- 503 [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html): Secrets are not set up on this API yet (`unavailable`, with a `Retry-After` of 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.

Request:

```sh
curl https://api.wheelmachine.ai/v1/sizes \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Answers

- 200 The sizes, the smallest first: object
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): 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`.

Request:

```sh
curl https://api.wheelmachine.ai/v1/regions \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

Answer 200:

```json
{
  "object": "list",
  "data": [
    {"id": "helsinki", "city": "Helsinki", "country": "FI"},
    {"id": "mumbai", "city": "Mumbai", "country": "IN"}
  ]
}
```

#### Answers

- 200 The places: a list of [Region](https://docs.wheelmachine.ai/reference.html#obj-Region)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): 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.

Request:

```sh
curl https://api.wheelmachine.ai/v1/gpus \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"
```

#### Answers

- 200 The GPUs on offer now: a [GpuList](https://docs.wheelmachine.ai/reference.html#obj-GpuList)
- 401 [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) or [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) or [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html): 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](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-MachineState) | One of `"starting"`, `"running"`, `"paused"`, `"stopped"`, `"archived"`, `"recovering"` or `"deleted"`. |
| `gpu` | [GpuRef](https://docs.wheelmachine.ai/reference.html#obj-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](https://docs.wheelmachine.ai/reference.html#obj-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 |  |
