# Errors

Every API error has the same shape, and `docs_url` points at the matching section of this page. The exception is a path that exists asked with a method it doesn't take: that's a 405 with no body, from before our code runs.

```json
{
  "error": {
    "code": "limit_reached",
    "message": "The Free plan runs 1 machine at a time, and dev is already running. Suspend one or upgrade.",
    "retryable": false,
    "retry_after_seconds": null,
    "docs_url": "https://tryquivvo.com/docs/errors#limit_reached",
    "request_id": "req_...",
    "owner_action": { "url": "https://tryquivvo.com/app/billing", "message": "Upgrade from the Free plan to run more machines at once." }
  }
}
```

| Field | What to do with it |
| --- | --- |
| `code` | Branch on this. The message can change |
| `retryable`, `retry_after_seconds` | When `retryable` is true, wait that long and send the same request again |
| `request_id` | Quote it when you report a problem. It's also the `x-request-id` header |
| `owner_action` | Something only the workspace's owner can do, signed in on the dashboard. Pass on `message` and `url`: an API key can't do it |

The CLI turns these into exit codes. Under `--json` every error is one document on stdout, with the same fields plus `exit_code`, `status` and `hint`. The MCP server returns the same fields as JSON text with `isError` set.

## `bad_request`

HTTP 400. CLI exit 2. Don't retry as it is.

The request is wrong: a bad field, a size that isn't allowed, a missing `X-Confirm-Delete` header, or an upload over 4 MB. The message says what to change.

## `unauthorized`

HTTP 401. CLI exit 3. Don't retry as it is.

There's no key, or it's invalid or was revoked. Sign in again with `quivvo login --agent --no-wait --json`, or check `QUIVVO_API_KEY`. See [agent sign-up](https://tryquivvo.com/docs/agents).

## `forbidden`

HTTP 403. CLI exit 3. Don't retry as it is.

This key doesn't have the scope, a read-only key asked for a path it can't read (`/proc`, `/sys`, `/dev`, `/run` or the daemon's own state), the route needs someone signed in on the dashboard, as billing does, or a signed-in browser's request came from another site. `GET /api/v1/whoami` lists the scopes a key has. `owner_action` says what the owner can do.

## `not_found`

HTTP 404. CLI exit 4. Don't retry as it is.

There's no such machine, snapshot, job or file in this workspace. `quivvo ls` shows the machines it has.

## `conflict`

HTTP 409. CLI exit 75 when `retryable`, otherwise 1. From `exec`, `run` and `sh`, 125.

The name is taken, the machine is between states or still being created, its CPU or memory can't change while it runs, a snapshot isn't ready, a disk is still being copied from what you're deleting, or an `Idempotency-Key` was reused for a different request, including the same route with a different body. When `retryable` is true, the machine is between states and will settle: try again. Otherwise the message says what to change.

## `limit_reached`

HTTP 403. CLI exit 5. Don't retry as it is.

A plan limit: machines running at once, machines kept, size, disk, snapshots, how long a machine may stay awake, active keys, webhooks, or published ports on a machine. The message names the limit and what's in the way. Make room with what you're allowed to do: suspend or delete a machine, or delete a snapshot. The CLI's hint names the commands. A request that's wrong in itself, such as a bad name or a name already taken, gets that error first and never this one. Upgrading is for the owner: pass on `owner_action`.

## `insufficient_credits`

HTTP 402. CLI exit 5. Don't retry as it is.

The credit balance is empty, so machines can't run. Only the owner can add credit, on the dashboard: pass on `owner_action`. API keys can't reach billing.

## `queued`

HTTP 403. CLI exit 5. Don't retry as it is.

This workspace is waiting in the queue, so it can't create or fork machines yet. The message gives its place. Only the owner can move it: pass on `owner_action`, which holds their invite link. Each person who signs up with the link moves them up 10 places, and a paid plan skips the queue. `quivvo queue` and [`GET /queue`](https://tryquivvo.com/docs/api#the-queue) show the same.

## `machine_not_running`

HTTP 409. CLI exit 75 when `retryable`, otherwise 1, or 125 from `exec`, `run` and `sh`.

The machine isn't running, and nothing ran. With `retry_after_seconds` it's on its way up: try again, as the CLI does by itself. Without it, it stays down until asked: run `quivvo start` or `quivvo resume`, or for a machine in `failed`, `quivvo restart`. The MCP server's read tools answer this for any machine that isn't running, because they don't wake machines, and so does the REST API for a key with no `machines:write`.

## `provider_error`

HTTP 502. CLI exit 75 when `retryable`, otherwise 1. From `exec`, `run` and `sh`, 125.

The machine's host, or its daemon, didn't do what was asked. The command may or may not have run, so check before repeating anything that changes things. `quivvo logs` shows what's known about a machine, whatever its state.

A snapshot or fork that the provider won't copy ends here too. In that case nothing was created, and the source is untouched.

## `capacity`

HTTP 503. CLI exit 75. Retry after `retry_after_seconds`.

There's no room right now, on our side or the provider's. On a create, nothing was created: try another region with `--region`, or try again in a few minutes. On a start or resume, the machine and its disk are where they were, and no other host in its region had room either: try again in a few minutes.

## `rate_limited`

HTTP 429. CLI exit 75. Retry after `retry_after_seconds`.

Too many requests, such as more than 30 feedback reports in an hour, more than 600 commands or 1,200 changes of any kind a minute in a workspace, more machines created in an hour than a plan allows (60 on Free), or too many sign-ins started from one address. Nothing happened. Wait and send it again. A sign-in that's already pending doesn't count: run the same `login` again and it reuses its code.

## `internal`

HTTP 500. CLI exit 75 when `retryable`, otherwise 1. From `exec`, `run` and `sh`, 125.

Something went wrong on our side. The request may or may not have taken effect, so check first if repeating it would matter. If it keeps happening, report it with the `request_id`: `quivvo feedback "what happened"`.

## `authorization_pending`

HTTP 400, while polling `POST /api/v1/auth/device/token`. `quivvo login --agent --no-wait` exits 75.

The owner hasn't decided yet. Try again later, no sooner than every `interval` seconds. An agent should end its turn rather than wait.

## `access_denied`

HTTP 400, while polling. `quivvo login` exits 77.

The owner refused this sign-in. Ask them before trying again. Nothing was created.

## `expired_token`

HTTP 400, while polling. `quivvo login` exits 76.

The code expired before anyone approved it, or this sign-in was already completed. Agent codes last 24 hours and CLI codes 15 minutes. Run the command again for a new code.

## What other codes can the CLI report?

Under `--json`, a failure that never reached the API has a code of its own:

| Code | Exit | Meaning |
| --- | --- | --- |
| `usage` | 2 | The command line was wrong. `quivvo help --json` lists every command and flag |
| `not_signed_in` | 3 | There's no key. The hint names the sign-in command |
| `no_terminal` | 3 | `quivvo login` needs a person at a terminal. The hint says what to run instead |
| `network_error` | 75, 1 or 125 | The API couldn't be reached. 75 means nothing happened; 1 or 125 means it may have |
| `timeout` | 124 | A wait ran out. What you were waiting for keeps going |
| `agent_not_installed`, `agent_not_signed_in` | 3 | `quivvo agent prompt`: the message gives the exact fix |
| `confirmation_required` | 2 | Something destructive needs `--yes` when there's no terminal to ask on |
| `setup_failed` | 1 | `create --wait-setup`: the script failed, and the machine is still there and billing. The hint has its log and how to delete it |
| `machine_failed` | 1 | A machine being waited for went to `failed`. `quivvo logs` shows what's known |
| `unexpected_state` | 1 | A machine being waited for settled in a state nobody asked for |
| `snapshot_failed` | 1 | `snapshot save`: the copy didn't finish |
| `stream_lost` | 125 | `exec`: the stream ended before the command did, so its result is unknown. `quivvo history` shows what's known |
| `command_lost` | 125 | `run`: the machine restarted while the command ran, so its result is unknown |
| `nothing_to_push` | 2 | `push`: every file in the directory is left out. The hint says how to send them |
| `tar_failed` | 1 | `cp` or `push`: the local `tar` failed |
| `no_desktop` | 1 | `desktop`: the machine's image has none. A new machine on the `universal` template has one |
| `bad_config` | 1 | `mcp init`: an existing config file can't be read, and it wasn't touched |
| `client_failed` | 1 | `mcp init`: the client's own command failed. The hint has the command to run yourself |
| `no_session` | 1 | `agent prompt --continue`: that run has no session to carry on from |
| `preflight_failed` | 1 | `agent prompt`: the check before starting failed. The message says why |

`exec`, `run`, `ssh`, `jobs wait` and `jobs logs -f` pass on the remote command's exit code, which can be any of these numbers too. Under `--json`, the `error` key is what tells them apart.
