# Errors

> How the API refuses a call: application/problem+json with a stable code, a request id, and when to try again. Every code, and what to do about it.

When the API refuses a call, it says why in the same shape every time: a code your program can rely on, and words for you.

Answer:

```http
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
X-Request-Id: req_01jb2xk3n4p5q6r7s8t9v0w1x2

{
  "type": "https://docs.wheelmachine.ai/errors/forbidden",
  "title": "This key may not do that",
  "status": 403,
  "detail": "this key does not hold the cloud:machines:exec scope",
  "code": "forbidden",
  "request_id": "req_01jb2xk3n4p5q6r7s8t9v0w1x2"
}
```

## Read a refusal

A refusal is `application/problem+json` (RFC 9457):

| Field | What it is |
|---|---|
| `code` | What went wrong, as a word that never changes. Let your program decide on this. |
| `detail` | What went wrong this time, in words for people. They may change; do not match on them. |
| `title` | The code's title, the same every time. |
| `status` | The HTTP status, as in the answer's first line. |
| `type` | A link to this site's page for the code. |
| `request_id` | This call's id, also in the `X-Request-Id` header. Give it when you ask us about a call. |
| `retry_after_s` | When there is one: the seconds to wait before trying again, also in the `Retry-After` header. |

## In wm and the libraries

`wm` prints the refusal's words, its code, its status and its request id, and exits with 1:

Terminal:

```sh
$ wm exec box -- make test
wm: this key does not hold the cloud:machines:exec scope (forbidden, 403, request req_01jb2xk3n4p5q6r7s8t9v0w1x2)
```

The libraries raise (or throw) a `WheelmachineError` with `status`, `code`, `detail`, the request id (`request_id` in Python, `requestId` in TypeScript) and the wait it asked for (`retry_after`, `retryAfter`). Each family of statuses has its own class, the same name in both:

| Status | Class |
|---|---|
| 400, 411, 413 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 402 | `PaymentRequiredError` |
| 403 | `PermissionDeniedError` |
| 404 | `NotFoundError` |
| 408 | `WaitTimeoutError` |
| 409, 422 | `ConflictError` |
| 429 | `RateLimitError` |
| 500 | `InternalServerError` |
| 502, 503, 504 | `UnavailableError` |
| No answer came | `APIConnectionError`, and `APITimeoutError` when the time ran out |

Python:

```python
from wheelmachine import PermissionDeniedError, WheelmachineError

try:
    client.machines.exec(machine.id, ["make", "test"])
except PermissionDeniedError as err:
    print("this key may not run commands:", err.detail)
except WheelmachineError as err:
    print(err.status, err.code, err.request_id)
```

TypeScript:

```ts
import { PermissionDeniedError, WheelmachineError } from "wheelmachine";

try {
  await client.machines.exec(machine.id, ["make", "test"]);
} catch (err) {
  if (err instanceof PermissionDeniedError) console.log("this key may not run commands:", err.detail);
  else if (err instanceof WheelmachineError) console.log(err.status, err.code, err.requestId);
  else throw err;
}
```

## Try again, safely

Some refusals pass: `rate_limited`, `unavailable`, and `conflict` while a first call with the same `Idempotency-Key` is still running. Wait the seconds `Retry-After` says, then send the call again. The libraries and `wm` do this by themselves, up to a few times, with a growing wait when none was said. The rest mean the call itself must change before it can work.

A call that makes something (a machine, a copy, a snapshot, a project or a key) can carry an `Idempotency-Key` header: any string up to 255 printable characters, new for each thing you mean to make. Sent again with the same key and the same request within 24 hours, it gets the first answer again, with `Idempotent-Replayed: true`, and nothing is made twice. The same key with a different request answers `422 invalid_request`. Answers that pass (`429` and `5xx`) are not kept, so a retry of one runs again. The libraries put an `Idempotency-Key` on every call that makes something, the same on each of its tries.

Terminal:

```sh
curl -X POST https://api.wheelmachine.ai/v1/machines \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nightly-build-2026-10-23" \
  -d '{"name": "nightly"}'
```

A command is different: it may already have run, so neither the libraries nor `wm` ever send one again by themselves.

Every answer carries `X-Request-Id`. Send your own `X-Request-Id` (letters, digits, `-` and `_`, up to 64) and the API uses it, so a call can be found by the same id in your logs and ours.

## Every code

| Code | Status | What to do |
|---|---|---|
| [`invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) | 400, 411, 422 | The request is not right. Put the call right as `detail` says. |
| [`unauthorized`](https://docs.wheelmachine.ai/errors/unauthorized.html) | 401 | No key, or a key that does not work. Send the whole key as `Authorization: Bearer ak_live_...`. |
| [`key_revoked`](https://docs.wheelmachine.ai/errors/key_revoked.html) | 401 | This key was revoked. Use another key, or make one with a key that holds `cloud:keys:write`. |
| [`key_expired`](https://docs.wheelmachine.ai/errors/key_expired.html) | 401 | This key has expired. Make a new key. |
| [`forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html) | 403 | This key may not do that. Use a key that holds the scope, or make one with a key that does: see [Keys and scopes](https://docs.wheelmachine.ai/keys.html#scopes). |
| [`not_found`](https://docs.wheelmachine.ai/errors/not_found.html) | 404 | Not found. Check the id (machines begin `m_`, projects `prj_`, keys `key_`) and the projects the key reaches: `wm me` shows them. |
| [`conflict`](https://docs.wheelmachine.ai/errors/conflict.html) | 409 | Not possible in the state it is in. Look at the machine's `state` (`wm machines get <machine>`), bring it where it needs to be (`start` it), then try again. |
| [`project_not_empty`](https://docs.wheelmachine.ai/errors/project_not_empty.html) | 409 | The project still holds machines. Delete its machines first (`wm machines ls --project <project>`, then `wm machines rm`), then the project. |
| [`payment_required`](https://docs.wheelmachine.ai/errors/payment_required.html) | 402 | Nothing left to spend. Once the account's limit is raised, or the month turns, `start` the machines you need. |
| [`rate_limited`](https://docs.wheelmachine.ai/errors/rate_limited.html) | 429 | Too many calls just now. Wait the seconds `Retry-After` says, then go on, more slowly. |
| [`limit_reached`](https://docs.wheelmachine.ai/errors/limit_reached.html) | 429 | A limit is reached. Delete the machines you no longer need (`wm machines ls`, then `wm machines rm`), then try again. |
| [`unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) | 503 | No room just now. Try again after the seconds `Retry-After` says. |
| [`timeout`](https://docs.wheelmachine.ai/errors/timeout.html) | 408, 504 | Not in time. For a wait, look at the machine's state, and wait again if it is still on its way. |
| [`too_large`](https://docs.wheelmachine.ai/errors/too_large.html) | 413 | Too large. Send less at once: a big file in parts, or packed (`tar czf`); free space in the machine. |
| [`not_open`](https://docs.wheelmachine.ai/errors/not_open.html) | 403, 501, 503 | Not open yet. Make a machine without a GPU (`wm gpus` shows what is coming), sign in with a key you have (`wm login --key`), run commands with `wm exec` where a terminal cannot open, and leave a port's web address until web addresses are open. |
| [`login_denied`](https://docs.wheelmachine.ai/errors/login_denied.html) | 403 | The sign-in was refused. Start the sign-in again (`wm login`), and approve it at the dashboard if it is yours. |
| [`login_expired`](https://docs.wheelmachine.ai/errors/login_expired.html) | 410 | The sign-in's code is no longer good. Start the sign-in again (`wm login`). |
| [`internal`](https://docs.wheelmachine.ai/errors/internal.html) | 500 | Something went wrong on our side. Try again in a moment. |

## `invalid_request`

400, 411, 422 **The request is not right**

Something in the call is missing or wrong. `detail` says what, in plain words.

#### When it happens

- A field the API does not know, such as `no field "nmae" here`: a misspelt field is refused, never quietly ignored.
- A value out of its range, such as `cpus is 2, 4, 8 or 16` or `wait_s is between 0 and 600`.
- A body that is not one JSON object.
- A path in a machine that does not begin `home/` or `work/`, or that climbs out with `..`.
- 411: a file written without its length in `Content-Length`.
- 422: an `Idempotency-Key` sent again with a different request.

#### What to do

Put the call right as `detail` says. For a 422, give the new request a new `Idempotency-Key`.

**Send it again?** Not unchanged: the same call gets the same answer.

## `unauthorized`

401 **No key, or a key that does not work**

The call carried no key, or one the API does not know.

#### When it happens

- No `Authorization` header, or one that is not `Bearer ak_live_...`.
- A key that is not one of ours (keys begin `ak_live_`), or one cut short when it was pasted.

#### What to do

Send the whole key as `Authorization: Bearer ak_live_...`. With `wm`, run `wm login` again, and check that `WHEELMACHINE_API_KEY` does not hold an old key: it is used before the one `wm login` kept.

**Send it again?** Not with the same key. An address that keeps calling with keys that do not work is slowed down (`429 rate_limited`).

## `key_revoked`

401 **This key was revoked**

Someone revoked the key. It stopped working everywhere within seconds, and it cannot be brought back.

#### When it happens

- Any call made with a key after `wm keys revoke` or `DELETE /v1/keys/{key_id}`.

#### What to do

Use another key, or make one with a key that holds `cloud:keys:write`.

**Send it again?** Not with this key.

## `key_expired`

401 **This key has expired**

The key was made with an end time (`expires_at`), and that time has passed.

#### When it happens

- Any call made with the key after its end time.

#### What to do

Make a new key. A key's end time cannot be moved.

**Send it again?** Not with this key.

## `forbidden`

403 **This key may not do that**

The key works, but it may not do this.

#### When it happens

- The key does not hold the scope the call needs. `detail` names it, such as `this key does not hold the cloud:machines:exec scope`.
- The key was made for other work and holds no machine scope at all.
- A key limited to some projects tried to make a project.
- A key tried to give a new key a scope, a project or a rate it does not hold itself.
- The account is held.

#### What to do

Use a key that holds the scope, or make one with a key that does: see [Keys and scopes](https://docs.wheelmachine.ai/keys.html#scopes). For a held account, its owner can ask us why.

**Send it again?** Not with this key.

## `not_found`

404 **Not found**

Nothing of that id that this key can see.

#### When it happens

- A machine, project or key that is not the account's, that was deleted, or that is in a project the key is not limited to. The answer is the same whether it exists or not.
- A file that is not in the machine.
- A route that does not exist.

#### What to do

Check the id (machines begin `m_`, projects `prj_`, keys `key_`) and the projects the key reaches: `wm me` shows them.

**Send it again?** Not unchanged.

## `conflict`

409 **Not possible in the state it is in**

The machine, or the call, is not in a state where this can happen yet.

#### When it happens

- The machine cannot do it in its state, such as pausing a machine that is not running.
- The machine's files cannot be reached just now, because it is not on a server (stopped or archived, for example): start it first.
- The first call with the same `Idempotency-Key` is still running.

#### What to do

Look at the machine's `state` (`wm machines get <machine>`), bring it where it needs to be (`start` it), then try again. For an `Idempotency-Key` still in use, wait a second and send the same call again.

**Send it again?** Yes, once the state is right; after `Retry-After` for an `Idempotency-Key` still in use.

## `project_not_empty`

409 **The project still holds machines**

Only an empty project can be deleted.

#### When it happens

- Deleting a project that still holds machines.

#### What to do

Delete its machines first (`wm machines ls --project <project>`, then `wm machines rm`), then the project.

**Send it again?** Yes, once it is empty.

## `payment_required`

402 **Nothing left to spend**

The account cannot spend just now: its limit for this month is reached, or it is held. `detail` says why.

#### When it happens

- Making, starting or copying a machine, running a command, or reading or writing a file, while the account cannot spend.
- While it cannot spend, its running machines are paused within about half a minute, with their memory kept.

#### What to do

Once the account's limit is raised, or the month turns, `start` the machines you need. `wm me` says whether the account can spend.

**Send it again?** Yes, once the account can spend.

## `rate_limited`

429 **Too many calls just now**

Calls are coming faster than the key may make them.

#### When it happens

- The key made more calls than its limit: 600 a minute, unless it was made with another `rate_per_minute`.
- An address kept calling with keys that do not work.

#### What to do

Wait the seconds `Retry-After` says, then go on, more slowly. The `RateLimit` header says how many calls can be made at once now: see [Limits](https://docs.wheelmachine.ai/limits.html#calls-a-minute).

**Send it again?** Yes, after `Retry-After`. The libraries and `wm` wait and try again by themselves.

## `limit_reached`

429 **A limit is reached**

The account holds as many machines as it may.

#### When it happens

- The account holds 100 machines through the API, deleted ones aside: `delete one first`.
- The account has as many machines running or kept as it may: `pause, archive or delete one first`.

#### What to do

Delete the machines you no longer need (`wm machines ls`, then `wm machines rm`), then try again.

**Send it again?** Yes, once there is room.

## `unavailable`

503 **No room just now**

The API could not do it just now. It passes.

#### When it happens

- No server has room for the machine just now; one is being made ready for it.
- A part of the service did not answer in time.
- A command's result did not come back: it may or may not have run.

#### What to do

Try again after the seconds `Retry-After` says. For a command, check whether it ran before you run it again: the libraries and `wm` never send a command twice by themselves.

**Send it again?** Yes, after `Retry-After`.

## `timeout`

408, 504 **Not in time**

Something did not happen in the time it was given.

#### When it happens

- 408: the machine did not reach the state you waited for, or it was deleted meanwhile.
- 504: a command ran longer than 10 minutes. It may still be running in the machine.
- 504: the call took longer than it may.

#### What to do

For a wait, look at the machine's state, and wait again if it is still on its way. For long work, start it in the background and read its output from a file: see [Long work](https://docs.wheelmachine.ai/commands-and-files.html#long-work).

**Send it again?** A wait, yes. A command, only once you know it is not still running.

## `too_large`

413 **Too large**

The request, or the file, is larger than the API takes in one call.

#### When it happens

- A request's JSON over 1 MiB.
- A file over 5 GiB in one call.
- A file larger than the machine's disk can take.

#### What to do

Send less at once: a big file in parts, or packed (`tar czf`); free space in the machine.

**Send it again?** Not unchanged.

## `not_open`

403, 501, 503 **Not open yet**

What was asked for is not open yet: to every account, or here at all.

#### When it happens

- 403: making a machine with a GPU, while GPU machines are not open to every account; or a program starting its sign-in through the browser (`wm login`) where that is not open yet.
- 501: opening a terminal in a machine whose server cannot open one, or asking for an SSH certificate where SSH is not offered.
- 503: opening a machine's port at a web address of its own, or making a link to one, while machines' web addresses are not served yet.

#### What to do

Make a machine without a GPU (`wm gpus` shows what is coming), sign in with a key you have (`wm login --key`), run commands with `wm exec` where a terminal cannot open, and leave a port's web address until web addresses are open.

**Send it again?** Not yet: waiting a moment does not change it.

## `login_denied`

403 **The sign-in was refused**

The person refused the program's sign-in at the dashboard, or the dashboard sign-in that approved it has ended since.

#### When it happens

- A program asking for its key (`POST /v1/login/devices/token`) after the person pressed Deny at https://app.wheelmachine.ai/device.
- The same, after the dashboard sign-in that approved it was signed out.

#### What to do

Start the sign-in again (`wm login`), and approve it at the dashboard if it is yours.

**Send it again?** Not this sign-in: start a new one.

## `login_expired`

410 **The sign-in's code is no longer good**

A program's sign-in ran out of time before it was approved, or its key was given already.

#### When it happens

- Asking for the key more than 10 minutes after the sign-in started.
- Asking again after the key was given: it is given once.

#### What to do

Start the sign-in again (`wm login`).

**Send it again?** Not this sign-in: start a new one.

## `internal`

500 **Something went wrong on our side**

A fault of ours. It is logged under the call's request id.

#### When it happens

- Anything we did not expect.

#### What to do

Try again in a moment. If it keeps happening, tell us the `request_id`.

**Send it again?** Yes, after a moment.
