Errors
One error envelope for every route and MCP tool, with stable codes, HTTP statuses and what to do about each.
Every error, from every route and MCP tool, has the same shape:
{
"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 |
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
| 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
- Retry with backoff:
429(afterretry_after_ms),503and502/504from a vendor. A routed/v1/runhas 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
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.
Next