# Errors (/docs/errors)



Every error, from every route and MCP tool, has the same shape:

```json
{
  "error": {
    "code": "vendor_key_required",
    "message": "Add your Hunter key in Settings → Vendor keys. GridRouter runs Hunter on your own key.",
    "request_id": "req_…",
    "call_id": "call_…",
    "details": { "provider": "hunter" }
  }
}
```

| Field            | Meaning                                                                                                                             |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `code`           | Stable and machine-readable. Branch on this, never on `message`                                                                     |
| `message`        | Human-readable; may change                                                                                                          |
| `request_id`     | Quote it to support                                                                                                                 |
| `call_id`        | Present when a call was created; it is in your [logs](/docs/concepts/logs-and-drains)                                               |
| `retry_after_ms` | When to retry; `429` responses also send `Retry-After`                                                                              |
| `details`        | Code-specific context: `issues` with field paths on validation errors, `layer` on rate limits, the partial `run` on waterfall stops |

A vendor that answered but found nothing is **not** an error: the call succeeds with
`hit: false`.

## Codes [#codes]

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_meta` | `meta` must be up to 16 string values (500 chars) under `[A-Za-z0-9_.-]{1,40}` keys. |
| 400 | `invalid_request` | The body, query or a header failed validation. |
| 401 | `unauthorized` | No API key, or the key is unknown, revoked or expired. |
| 402 | `insufficient_balance` | The wallet cannot hold the call's maximum cost. |
| 402 | `max_cost_exceeded` | The quoted price is above `max_cost_micro` (or `X-Grid-Max-Cost`). |
| 403 | `account_frozen` | The organization is on a dispute or fraud hold. |
| 403 | `budget_blocked` | A key, app or workspace budget is spent for the period. |
| 403 | `plan_limit_reached` | A plan limit (keys, workspaces, drains, retention) is reached. |
| 403 | `scope_denied` | The key lacks the scope this route needs, or the IP is not on its allowlist. |
| 404 | `not_found` | No such endpoint, capability or resource in this workspace. |
| 409 | `cancelled` | The run was cancelled by the caller. |
| 409 | `conflict` | The request conflicts with the resource's current state. |
| 422 | `idempotency_mismatch` | An `Idempotency-Key` was reused with a different body. |
| 422 | `strict_filters` | Strict provider filters (`only`, `ignore`, no fallbacks, max price) left no endpoint. |
| 422 | `validation_failed` | A definition or input failed validation; `details.issues` has field paths. |
| 429 | `rate_limited` | A rate limit was hit; wait `retry_after_ms` or the `Retry-After` seconds. |
| 500 | `internal_error` | Something failed on our side; the `request_id` identifies it. |
| 502 | `response_buffer_limit` | The vendor's response was larger than the gateway buffers. |
| 502 | `upstream_error` | The vendor returned an error; `details.upstream_status` has its status. |
| 503 | `grid_saturated` | The gateway is shedding load; retry after the hint. |
| 503 | `provider_capacity_unavailable` | The vendor's concurrency or rate budget is exhausted right now. |
| 504 | `deadline_exceeded` | A waterfall hit its total deadline; the partial result is in `details`. |
| 504 | `upstream_timeout` | The vendor did not answer within the timeout. |

## Retrying [#retrying]

* **Retry with backoff:** `429` (after `retry_after_ms`), `503` and `502`/`504` from a vendor. A
  routed `/v1/run` has already tried the other vendors before returning one of these.
* **Fix the request:** `400`, `404`, `409`, `422`.
* **Fix the account or key:** `401`, `402`, `403`. Retrying cannot help.

Failed calls are never charged.

## Idempotency [#idempotency]

Send `Idempotency-Key` on any `POST` that runs something: 8 to 128 characters of letters, digits
and `_ . : -`.

* The same key with the same body returns the stored first response, free.
* The same key with a different body is `422 idempotency_mismatch`.
* The same key while the first request is still running is `409`.
* Streaming responses and 5xx errors are not stored, so the key is released and you can retry.

