Errors
Gyrence uses a single, predictable error envelope across every /api/v1/* endpoint. One shape of error handling covers the whole API surface.
The envelope
Every response — success or failure — is JSON with an ok boolean.
Success:
{ "ok": true, "data": { /* endpoint-specific payload */ } }Failure:
{ "ok": false, "error": "human-readable message", "code": "machine_code" }okis the only field guaranteed on every response. Always branch on it first.erroris meant for humans — log it, surface it in dashboards. The wording is not stable across releases.codeis meant for machines — branch on it. Codes are stable within/api/v1; renames require/api/v2.
const res = await fetch(url, opts);
const json = await res.json();
if (!json.ok) {
// Branch on json.code, not on res.status — they match, but code is the source of truth.
throw new Error(`${json.code}: ${json.error}`);
}
return json.data;Code reference
| HTTP | code | What it means | Billed? |
|---|---|---|---|
200 | — | Success. ok: true, data populated. | yes (per the credit schedule) |
400 | bad_request | Malformed JSON, missing required field, or input failed schema validation. | no |
401 | unauthorized | Missing, malformed, or revoked API key. | no |
402 | credits_exhausted | Workspace has zero credits remaining. Top up or wait for plan renewal. Never returned for internal-plan workspaces. | no |
403 | forbidden_url | Target URL is blocked by SSRF rules (private IPs, link-local, localhost, non-HTTP schemes). | no |
404 | not_found | The origin returned 404 for the requested URL. The request itself succeeded — the page just isn't there. | no |
408 | timeout | The 25-second hard deadline tripped before useful work completed. | no |
429 | rate_limited | You exceeded the per-workspace request rate. Back off and retry. | no |
502 | upstream_error | The origin returned a 5xx, or the headless-browser worker failed. Gyrence didn't get the content you asked for. | no |
502 | provider_quota_exceeded | A Gyrence-side provider subscription (e.g. Brave Search) returned a billing/quota signal (HTTP 402). Platform-side condition — not caller fault, not a per-workspace credit issue. Do not retry; surface to status. details.provider names the upstream. | no |
503 | unavailable | Gyrence itself is degraded — the platform is having a bad minute. Retry with backoff. | no |
The rule is consistent: if the response doesn't include the data you asked for, you weren't billed. The one exception is /gyre, which has a 1-credit floor so even a zero-page success records as a metered call.
Branching by category
In practice, error handling collapses to four cases:
const { ok, data, error, code } = await res.json();
if (ok) return data;
switch (code) {
case "unauthorized":
case "forbidden_url":
case "bad_request":
// Caller error — do not retry. Fix the code or the input.
throw new PermanentError(code, error);
case "credits_exhausted":
// Operational — alert a human, stop sending traffic on this workspace.
throw new BillingError(error);
case "rate_limited":
case "timeout":
case "upstream_error":
case "unavailable":
// Transient — retry with exponential backoff (see /docs/rate-limits).
throw new RetryableError(code, error);
case "provider_quota_exceeded":
// Platform-side: a Gyrence upstream provider is out of quota.
// Not your fault, not your credits. Don't retry — alert and wait.
throw new PermanentError(code, error);
case "not_found":
// Logical — the URL doesn't exist. Often you want to record and move on.
return null;
default:
throw new Error(`unknown code: ${code}`);
}Per-primitive notes
Most error codes are universal. A few have endpoint-specific nuances:
/fetch,/extract. A404from the origin surfaces as404 not_found. A403from the origin (bot block) is escalated to the browser tier first; if the block persists, you get502 upstream_error, not403./gyre. Per-page failures are not top-level errors — they land in the response'serrors[]array while the overall call returns200. A200gyre with zero successful pages still bills 1 credit (the floor)./search. Iffetch: trueis set, individual result fetches that fail land in each result'sfetchErrorfield. The search call itself returns200as long as Brave returned results./map. A site with no discoverable sitemap returns200with an emptyurls[], not a404. Use the result, don't catch.
Partial success vs. hard failure
The pattern across the API is:
- Hard failure (
ok: false) means the request as a whole produced nothing usable. - Partial success (
ok: truewith errors insidedata) means some sub-units failed but the overall call returned something. Gyreerrors[], search per-resultfetchError, and extract field-level confidence are all in this category.
Both are normal. Code defensively on the inside-data errors, but don't conflate them with envelope-level failures.
Stability guarantees
- HTTP status codes and
codevalues are frozen for/api/v1. New codes may be added; existing codes will not be renamed or repurposed. errormessage wording is not stable — it's tuned for human readability and may change without notice. Never branch on the string.- New endpoints may add endpoint-specific codes; the universal codes in the table above will continue to mean the same thing everywhere.
MCP error shape
Calls to the /api/mcp endpoint use the same code set, but wrapped in JSON-RPC instead of the HTTP envelope. Transport-level failures (bad auth, wrong method) come back as { jsonrpc, error: { code, message }, id } with codes -32001 (unauthorized) or -32000 (method not allowed). Primitive failures (credits_exhausted, upstream_error, etc.) surface inside the tools/call result with isError: true and a text content payload of the form "<code>: <message>" — same code, different envelope. See MCP for examples.
Persistent 503 unavailable or 502 upstream_error across many requests usually means a platform-side incident. Check status before adding retry logic for what's actually an outage.
