---
title: REST errors
description: Every HTTP status and code the looot REST API answers with, and the fix for each.
sidebar:
  label: REST API
---

Every REST error is a JSON body `{error: {code, message, requestId}}` plus an HTTP status. Some
routes add fields next to `error`; those are noted below. See [Errors](/errors) for the shape and
[Idempotency](/concepts/idempotency) and [Fallback](/concepts/fallback) for background on a few of
these.

### validation_error

`400`. The request body or query failed schema validation. Retryable: no. Fix: read the message,
which names the field; check it against the endpoint's schema with `inspect` or
`GET /v1/operations/{id}`. See [`inspect`](/mcp-tools/inspect).

### unauthorized

`401`. The bearer token is missing or invalid. Retryable: no. Fix: sign in again
(`looot login`, or set `LOOOT_TOKEN`). See [Sign in](/get-started/sign-in).

### token_revoked

`401`. This token was revoked, or its login approval was denied. Retryable: no. Fix: run
`looot login` again, or an MCP client should reopen its sign-in flow. See [Sign in](/get-started/sign-in).

### forbidden

`403`. The token lacks the scope this route requires. Retryable: no. Fix: `looot whoami` (or the
MCP client's own sign-in) lists its scopes; an owner or admin can issue a token, or approve a
sign-in, with the scope you need. See [Access](/concepts/access).

### insufficient_balance

`402`. `POST /v1/runs` when the reservation would exceed the workspace's available balance. Body
adds `balanceMicros`, `estimatedCostMicros`, `topUpUrl`, and `topUp: {minimumUsd, suggestedUsd,
checkoutUrl, dashboardUrl, message}`. This is a pre-admission refusal: no hold is placed and
nothing is charged. Retryable: no. Fix: show `topUp.checkoutUrl` (Stripe's hosted page) or
`topUp.dashboardUrl` to whoever holds the account, then retry with a new `idempotencyKey` once
paid. See [Before your first run](/money#before-your-first-run) and [Top up](/money/top-up).

### not_found

`404`. Unknown `runId`, `connectionId`, or catalog item. This is not what you get for an unknown
`endpointId` on `POST /v1/runs`, though: that answers `201` with `status: "failed"` and a
populated `error` instead. See [Errors: failed runs](/errors#failed-runs). Retryable: no. See [`runs_list`](/mcp-tools/runs-list).

### route_not_found

`404`. There's no route at this method and path at all (a typo in the path, or a route this
gateway doesn't serve). Retryable: no. Fix: check the path against the
[API reference](/reference).

### method_not_allowed

`405`. The route exists, but not for this HTTP method. Retryable: no. Fix: check the method
against the [API reference](/reference).

### idempotency_conflict

`409`. The `idempotencyKey` was already used with a different `endpointId` or input than the
first call that used it. Retryable: no. Fix: send the exact same input to replay the original
run, or use a new key for a new one. See [Idempotency](/concepts/idempotency).

### rows_mode_unsupported

`422`. The request needs a fleet capability this gateway's storage mode doesn't provide right now
(for example `output.mode: "canonical"`, which production doesn't serve). Body adds `reason`.
Retryable: no. Fix: drop the unsupported option, or use `output.mode: "raw"`. See [`inspect`](/mcp-tools/inspect).

### too_many_inflight_runs

`429`. `POST /v1/runs` when this workspace already has `limit` runs in flight (8 by default). Body
adds `inflight`, `limit`, `retryAfterSeconds`, and a `retry-after` header. Retryable: yes. Fix:
wait for some runs to finish, then retry with the same idempotency key. See [Idempotency](/concepts/idempotency).

### rate_limit_exceeded

`429`. A token-bucket limiter rejected the request (60 burst, 10 per second per tenant by
default). Headers: `x-ratelimit-limit`, `x-ratelimit-remaining`, `retry-after`. Retryable: yes.
Fix: wait the `retry-after` time, then retry.

### runs_cursor_invalid

`400`. The `cursor` sent to `GET /v1/runs` (or `runs_list`) is invalid or stale. Retryable: no.
Fix: start paging again from `runs_list`/`GET /v1/runs` with no cursor. See [`runs_list`](/mcp-tools/runs-list).

### unknown_category

`404`. `GET /v1/catalog/overview?category=...` (or MCP `catalog_overview`, or
`looot catalog overview --category`) named a category id that doesn't exist. Body adds `field` and
`categories`, the valid ids. Retryable: no. See [`catalog_overview`](/mcp-tools/catalog-overview).

### unknown_platform

`404`. `GET /v1/catalog/overview?platform=...` named a platform id that doesn't exist, or one
outside the given `category`. Body adds `field`. Retryable: no. See [`catalog_overview`](/mcp-tools/catalog-overview).

### internal_error

`500`. An unhandled error on looot's side. Retryable: usually yes. Fix: retry once; if it
persists, report it with the `requestId`. See [Support](/support).

### catalog_loading

`503`. The catalog hasn't finished loading in this gateway process yet. Retry with the
`retry-after` header. Retryable: yes.

### storage_busy

`503`. The storage backend is briefly overloaded. Body adds `retryAfterSeconds`, and a
`retry-after` header is sent too. Retryable: yes. Fix: wait a few seconds and retry with the same
idempotency key. See [Idempotency](/concepts/idempotency).

### storage_uncertain

`503`. Service storage couldn't confirm the last write. Retryable: yes. Fix: wait briefly, then
retry with the same idempotency key; this never means the write actually failed, only that this
node can't yet confirm it landed. See [Idempotency](/concepts/idempotency).

### billing_unavailable

`503`. A billing route was called, but billing isn't enabled on this gateway. Retryable: no. See [Support](/support).

### runtime_bridge_timeout

`503`. The storage bridge didn't respond in time. Body adds `retryAfterSeconds`, and a
`retry-after` header is sent too. Retryable: yes.

## Settlement outcomes, not HTTP errors

Once a run is admitted (past every refusal above), its money outcome is never a blanket "the HTTP
call failed": a definitive failure settles at $0 or the provider's evidenced cost with the hold
released, and an uncertain outcome parks at `status: "reconciliation_pending"` with the hold kept
until it is reviewed. Check `status` and `error` on the run, not the HTTP status, to see which
applies; see [Errors: failed runs](/errors#failed-runs) and [Run errors](/errors/run-errors).

<Related />
