Errors
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.
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:
$ 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
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
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.
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 | 400, 411, 422 | The request is not right. Put the call right as detail says. |
unauthorized | 401 | No key, or a key that does not work. Send the whole key as Authorization: Bearer ak_live_.... |
key_revoked | 401 | This key was revoked. Use another key, or make one with a key that holds cloud:keys:write. |
key_expired | 401 | This key has expired. Make a new key. |
forbidden | 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. |
not_found | 404 | Not found. Check the id (machines begin m_, projects prj_, keys key_) and the projects the key reaches: wm me shows them. |
conflict | 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 | 409 | The project still holds machines. Delete its machines first (wm machines ls --project <project>, then wm machines rm), then the project. |
payment_required | 402 | Nothing left to spend. Once the account's limit is raised, or the month turns, start the machines you need. |
rate_limited | 429 | Too many calls just now. Wait the seconds Retry-After says, then go on, more slowly. |
limit_reached | 429 | A limit is reached. Delete the machines you no longer need (wm machines ls, then wm machines rm), then try again. |
unavailable | 503 | No room just now. Try again after the seconds Retry-After says. |
timeout | 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 | 413 | Too large. Send less at once: a big file in parts, or packed (tar czf); free space in the machine. |
not_open | 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 | 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 | 410 | The sign-in's code is no longer good. Start the sign-in again (wm login). |
internal | 500 | Something went wrong on our side. Try again in a moment. |
invalid_request#
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 16orwait_s is between 0 and 600. - A body that is not one JSON object.
- A path in a machine that does not begin
home/orwork/, or that climbs out with... - 411: a file written without its length in
Content-Length. - 422: an
Idempotency-Keysent 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#
The call carried no key, or one the API does not know.
When it happens#
- No
Authorizationheader, or one that is notBearer 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#
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 revokeorDELETE /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#
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#
The key works, but it may not do this.
When it happens#
- The key does not hold the scope the call needs.
detailnames it, such asthis 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. For a held account, its owner can ask us why.
Send it again? Not with this key.
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#
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-Keyis 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#
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#
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#
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.
Send it again? Yes, after Retry-After. The libraries and wm wait and try again by themselves.
limit_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.
timeout#
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.
Send it again? A wait, yes. A command, only once you know it is not still running.
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#
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#
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#
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#
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.