# Error codes

Every error the API returns — one shape, a fixed code list, and explicit guidance on what is retryable.

Every non-2xx response has the same shape, and every code on this page is in
the `enum` in [/openapi.json](/openapi.json) — there are no undocumented
errors.

```json
{
  "error": {
    "code": "budget_exceeded",
    "message": "Run halted at max_budget_usd=500 after 74 episodes.",
    "run_id": "run_8843",
    "doc_url": "https://roborama.com/docs/errors/#budget_exceeded"
  }
}
```

`run_id` appears when the error concerns a specific run. SDKs raise these as
typed exceptions carrying the same fields.

## Auth and access

| Code | HTTP | Meaning | Retry? |
| --- | --- | --- | --- |
| `invalid_api_key` | 401 | key missing, malformed, or revoked; live keys use the `rbr_live_` prefix | no — fix the key |
| `insufficient_scope` | 403 | key is valid but lacks the needed scope, e.g. `runs:write` | no — issue a key with the scope |
| `not_found` | 404 | no such run, robot, environment, suite, or stream | no |
| `publish_forbidden` | 403 | `publish="leaderboard"` requested without leaderboard opt-in on the account | no — opt in first |
| `rate_limited` | 429 | request rate exceeded; `Retry-After` header is set | yes — after the header's delay |

## Validation (submission rejected, nothing ran)

These fire before any motor moves; a rejected submission costs nothing.

| Code | HTTP | Meaning | Retry? |
| --- | --- | --- | --- |
| `contract_validation_failed` | 422 | declared `action_space` or `observation_contract` doesn't match the pinned robot and scene ([policies](/docs/concepts/policies/)) | no — fix the declaration or the pin |
| `firmware_pin_unavailable` | 422 | requested `model@firmware` isn't installed on any cell; check `robots.list()` firmwares | no — pin an available revision |
| `environment_unavailable` | 422 | environment id or revision not in the catalogue, or offline for rebuild | no — check `environments.list()` |
| `task_unknown` | 422 | task id or version not recognized, e.g. a typo in `load_dishwasher@v2` | no |
| `scenario_format_invalid` | 422 | `verify()` scenarios parse as none of: PolaRiS/Isaac/world-model exports, layout-replay specs | no — fix the file |
| `episodes_invalid` | 422 | `episodes` is neither a positive int nor a well-formed `auto(ci=..., moe=...)` | no |
| `quote_expired` | 409 | submission referenced a quote past its validity window | yes — re-quote and resubmit |

## Runtime (the run itself)

| Code | HTTP | Meaning | Retry? |
| --- | --- | --- | --- |
| `budget_exceeded` | 402 | `max_budget_usd` hard stop fired mid-run; partial results kept | resubmit with a higher cap if the interval is too wide |
| `monthly_budget_exceeded` | 402 | account-level hard budget reached; submissions rejected, in-flight runs halted | no — raise the budget |
| `queue_timeout` | 408 | job couldn't be scheduled within its priority tier's window | yes — resubmit, or use `burst` |
| `policy_endpoint_timeout` | 408 | endpoint policy persistently exceeded `latency_budget_ms`; run halted | fix the endpoint, then resubmit |
| `estop_triggered` | 409 | hardware or safety emergency stop during the run; also emitted as the `estop.triggered` webhook | contact support if unexpected |
| `retention_expired` | 410 | artifacts past their `data.retention` window (or purged) | no — the data is gone, by design |

## Handling the important one

`budget_exceeded` is the code every integration should handle deliberately,
because it carries a partial result with honest statistics — n and the
interval describe the episodes that ran.

*Example: handling budget_exceeded*

**Python**

```python
import roborama  # reads ROBORAMA_API_KEY from the environment

try:
    run = roborama.run(
        robot="g1-edu-pro@fw2.3",
        environment="kitchen-std@v1.2",
        task="pick_place@v1",
        episodes=600,
        max_budget_usd=500,   # deliberately too small for 600 episodes
    )
    print(run.result())
except roborama.BudgetExceeded as err:
    # budget_exceeded: the hard stop fired mid-run; partial results are kept
    print(err.code, err.run_id)
    partial = err.partial_result
    print(partial.n, partial.success_rate, partial.ci95)
```

**TypeScript**

```typescript
// auth: reads ROBORAMA_API_KEY from the environment
import Roborama, { RoboramaError } from "@roborama/sdk";

const roborama = new Roborama();

try {
  const run = await roborama.runs.create({
    robot: "g1-edu-pro@fw2.3",
    environment: "kitchen-std@v1.2",
    task: "pick_place@v1",
    episodes: 600,
    max_budget_usd: 500, // deliberately too small for 600 episodes
  });
  console.log(await run.result());
} catch (err) {
  if (err instanceof RoboramaError && err.code === "budget_exceeded") {
    // the hard stop fired mid-run; partial results are kept
    console.log(err.code, err.run_id);
    console.log(err.partial_result);
  } else {
    throw err;
  }
}
```

**cURL**

```bash
# A run that hits its max_budget_usd hard stop returns 402 with
# code=budget_exceeded and the partial result attached.
curl -i https://api.roborama.com/v1/runs \
  -H "Authorization: Bearer $ROBORAMA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "run",
    "robot": "g1-edu-pro@fw2.3",
    "environment": "kitchen-std@v1.2",
    "task": "pick_place@v1",
    "episodes": 600,
    "max_budget_usd": 500
  }'
```

**Agent (tool-use payload)**

```json
{
  "type": "tool_use",
  "name": "roborama_run",
  "input": {
    "robot": "g1-edu-pro@fw2.3",
    "environment": "kitchen-std@v1.2",
    "task": "pick_place@v1",
    "episodes": 600,
    "max_budget_usd": 500
  }
}
```

> **Retry submissions with an idempotency key:** `POST /v1/runs` schedules physical work. Always send `Idempotency-Key` so a
> retried submission returns the original run instead of buying the episodes
> twice — the SDKs do this for you ([REST conventions](/docs/sdks/rest/)).
