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" }
  • ok is the only field guaranteed on every response. Always branch on it first.
  • error is meant for humans — log it, surface it in dashboards. The wording is not stable across releases.
  • code is 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

HTTPcodeWhat it meansBilled?
200Success. ok: true, data populated.yes (per the credit schedule)
400bad_requestMalformed JSON, missing required field, or input failed schema validation.no
401unauthorizedMissing, malformed, or revoked API key.no
402credits_exhaustedWorkspace has zero credits remaining. Top up or wait for plan renewal. Never returned for internal-plan workspaces.no
403forbidden_urlTarget URL is blocked by SSRF rules (private IPs, link-local, localhost, non-HTTP schemes).no
404not_foundThe origin returned 404 for the requested URL. The request itself succeeded — the page just isn't there.no
408timeoutThe 25-second hard deadline tripped before useful work completed.no
429rate_limitedYou exceeded the per-workspace request rate. Back off and retry.no
502upstream_errorThe origin returned a 5xx, or the headless-browser worker failed. Gyrence didn't get the content you asked for.no
502provider_quota_exceededA 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
503unavailableGyrence 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. A 404 from the origin surfaces as 404 not_found. A 403 from the origin (bot block) is escalated to the browser tier first; if the block persists, you get 502 upstream_error, not 403.
  • /gyre. Per-page failures are not top-level errors — they land in the response's errors[] array while the overall call returns 200. A 200 gyre with zero successful pages still bills 1 credit (the floor).
  • /search. If fetch: true is set, individual result fetches that fail land in each result's fetchError field. The search call itself returns 200 as long as Brave returned results.
  • /map. A site with no discoverable sitemap returns 200 with an empty urls[], not a 404. 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: true with errors inside data) means some sub-units failed but the overall call returned something. Gyre errors[], search per-result fetchError, 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 code values are frozen for /api/v1. New codes may be added; existing codes will not be renamed or repurposed.
  • error message 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.

Status page

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.