# Errors

GPU Forge uses standard HTTP status codes. Every refusal says why, and most say what to do next. A refusal of an action that costs money means **nothing was rented and nothing was charged**, unless the answer explicitly says otherwise (see `502` below).

## Error format

Most errors have a `detail` field. It is either a sentence:

```json
{"detail": "no job 'job_000000000000'"}
```

or an object with more fields:

```json
{
  "detail": {
    "error": "another rental for this capability is already running",
    "capability_id": "cap_ab12cd34",
    "nothing_rented": true,
    "help": "A scale is renting for this capability right now. ... Try again in a moment."
  }
}
```

Fields you may find in an object `detail`:

| Field | Meaning |
|---|---|
| `error` | What went wrong |
| `help` | What to do next |
| `nothing_rented` | `true`: no machine was rented, nothing is billing |
| `balance_empty` | The refusal is because your balance is $0 |
| `daily_budget_reached` | The capability's daily budget is spent |
| `capability_inactive` | The capability is switched off |
| `quote_usd`, `max_price_usd` | A job's quote and your cap |
| `indeterminate`, `machine_exists`, `pod_id` | See `502` |

**Invalid request bodies** (`422`) use a different shape that names each problem:

```json
{
  "error": "the request body was not accepted",
  "problems": [
    {"field": "spec.min_vram_gb", "problem": "Input should be greater than or equal to 0", "you_sent": -1},
    {"field": "spec.min_vram_gbb", "problem": "Extra inputs are not permitted", "you_sent": 48,
     "expected": "Unknown field — check the spelling. GET /v1/help lists every accepted field."}
  ],
  "help": "GET /v1/help for every field, its units, and examples."
}
```

Values that may carry a secret (anything under `secrets`, SSH keys) are never echoed back in `you_sent`.

A few refusals are returned as a top-level `{"error": "...", ...}` object without `detail` (for example a balance refusal raised at the moment of renting). Read `detail` first, then `error`.

## Status codes

| Status | Meaning | What to do |
|---|---|---|
| `200`, `201`, `202` | Success. `201` = created, `202` = accepted and running in the background (jobs) | |
| `400` | The request cannot work as sent: an expired or hand-made `offer_id`, no `max_price_per_hour` on an automatic deploy, `min_pods` above `desired_pods` | Read the message. For an offer, search again and copy the new `offer_id` exactly |
| `401` | No bearer token | Send `Authorization: Bearer <key>` |
| `402` | Payment needed: balance empty or too low to start, a job's quote above your `max_price_usd`, a capability's daily budget reached, or a card declined on top-up | Top up (console or `POST /v1/billing/top-up`), raise the cap or budget, or wait for 00:00 UTC |
| `403` | Not allowed: invalid or revoked key, an action only the person signed in on the console can do (cards, automatic top-up, the daily cap, API keys), a suspended account, a field you may not change on a ready program, or (on `POST /v1/deploy`) a balance too low to cover the machine | Use the console, or a valid key |
| `404` | Not found: the id does not exist, **or belongs to another account** | Check the id with a list call (`GET /v1/fleet`, `/v1/capabilities`, `/v1/jobs`, `/v1/catalog`) |
| `409` | Conflict with the current state: an offer above your price limit, another rental for the same capability in progress, the capability switched off, a machine that cannot be parked, a capability with machines still running (on delete), a program already added, no saved card, or the daily cap | Read the message. For "in progress", try again in a moment |
| `410` | A CLI login code expired or was already used | Run `gpuforge login` again |
| `422` | The body or a parameter failed validation, a route that is not a job route, or `update-capability` on files without `sha256` | Fix the field named in `problems` or the message |
| `429` | Too many CLI logins waiting for approval | Try again in a few minutes |
| `502` | A provider failed or did not confirm | See below |
| `503` | A part of the service is temporarily unavailable | Retry after a short wait |

## `502` and machines that may be billing

A `502` from a call that rents or ends a machine needs care:

- **On a rental** (`deploy`, `scale`, `POST /v1/deploy`): if the body has `"indeterminate": true`, a machine **may** have been created and may be billing; `"machine_exists": true` means one certainly was. **Do not retry**: a retry can rent a second machine. Find it by the `pod_id` in the body with `GET /v1/fleet/{pod_id}`, and destroy it if you do not want it. Without `indeterminate`, the provider refused and nothing was rented.
- **On a destroy:** the provider has not yet confirmed the machine is gone. The request is saved and retried automatically, and the machine stays in your fleet until absence is confirmed. You can call destroy again safely.
- **On a search:** a `502` means every provider failed. If only some failed, you get `200` with their errors in `provider_errors`.

## Errors from ready programs

A **job** reports the program's own errors inside the job, not on `POST /v1/jobs`:

```json
{"status": "failed", "status_code": 422, "error": "the program answered 422", "price_usd": 0.0,
 "result": {"error": "..."}}
```

A failed job is never charged. Common program answers:

| Status | Meaning |
|---|---|
| `400` | A field is invalid; the message names it |
| `401` | Missing or wrong program key (own machine only) |
| `422` | The request was refused; the message says why (for example an image of a child, or a prompt that cannot be drawn) |
| `503` | The machine is busy (see `Retry-After`) or its program key is not set |
| `500` | The render failed; try again, perhaps with another `seed` |

A job that could not get a machine within 20 minutes fails with `"error": "no machine of this program could be started in time; nothing was charged"`.

## Retrying safely

| Call | Safe to retry? |
|---|---|
| Any `GET` | Yes |
| `POST /v1/capabilities/{id}/scale` | Yes: it is a target, not an increment |
| `POST /v1/fleet/{pod_id}/destroy`, `/release`, `/park` | Yes |
| `POST /v1/jobs` | Each call is a new job, billed separately. Retry only after the first one failed |
| `POST /v1/capabilities/{id}/deploy`, `POST /v1/deploy` | Only after a clear refusal (`4xx`, or `5xx` without `indeterminate`). Never after `indeterminate` |
| `POST /v1/billing/top-up` | Check `GET /v1/billing/top-ups` first: a `pending` top-up may still succeed |
