Errors
The Ray9 error envelope, the full `code` reference, and per-status retry guidance.
Every 4xx and 5xx response from Ray9 ships in the same envelope. Build your error handling against the code field — it's the stable contract. Treat message as a human-readable string we may rephrase; never branch on it.
Envelope
{
"requestId": "req_4f3a2c1b9e8d",
"error": {
"code": "rate_limited",
"message": "Too many requests. Try again in a moment.",
"details": {
"retryAfterMs": 1200
}
}
}requestId— Fastify-issued per-request id. Mirrors thex-request-idresponse header so you can correlate a body to logs without parsing headers.error.code— stable, snake_case, machine-readable. The list below is the complete set.error.message— short human-readable string. May change for clarity; do not parse.error.details— optional, code-specific. Open object — read only the keys you recognise; we may add more.
Headers that travel with errors
| Header | When |
|---|---|
x-request-id | Every response, success or error. Always echo this in support tickets. |
Retry-After | On 429 and 503. Integer seconds. Honour it before retrying. |
Code reference
Every code Ray9 emits, with the HTTP status, the trigger, and what your client should do.
| HTTP | code | When | What to do |
|---|---|---|---|
| 400 | bad_request | Caller body fails JSON-schema validation (missing field, wrong type, out-of-range value). | Fix the request and resend. |
| 400 | query_rejected | The search engine refused the query (e.g. malformed for the target engine, location/language combination unsupported). | Adjust the query, country, or language. Don't retry blindly. |
| 401 | unauthenticated | No key, malformed Authorization header, unknown / expired / revoked key, or session signed-out. | Re-authenticate. Never retry blindly. See Authentication. |
| 402 | out_of_credits | Org credit balance can't cover the request cost. | Surface to the user; the details payload carries { action: "topup", topup_url }. See Billing. |
| 402 | plan_required | The route isn't included in the org's current plan (Free covers SERP only; PayG unlocks the rest). | Surface to the user; the details payload carries { required_plan, current_plan, action: "topup", topup_url } to drive the upgrade flow. |
| 404 | not_found | Route doesn't exist. | Check the URL and method. Don't auto-create. |
| 404 | no_results | The query ran successfully but returned zero results. | Treat as a valid empty result, not an error condition. Adjust the query if you expected hits. |
| 429 | rate_limited | Per-org inbound rate limiter tripped (60/min, shared across all keys + sessions). | Honour Retry-After and details.retryAfterMs. Back off with jitter. See Rate limits. |
| 501 | not_implemented | Feature not wired in this version (e.g. engine: bing in v1). | Don't retry. Use a supported value. |
| 502 | service_unavailable | A dependency we call to fulfil the request failed after retries. | Retry with exponential backoff and jitter. Alert if it persists. |
| 503 | service_busy | We're throttling outbound traffic to protect the service; your request couldn't be queued within budget. | Retry per Retry-After. |
| 504 | service_timeout | The request exceeded our end-to-end time budget. | Retry; if it recurs, the query is likely too heavy — narrow it (depth, location). |
Per-status guidance
400 — caller-side fix. Re-read the schema for the endpoint and resend.
401 / 403 — re-authenticate. Never retry the same request — you'll just 401 again. Check that the Authorization: Bearer rk_… header is present and unmangled.
402 — payment / plan. Surface to the user; programmatic retries don't help. Useful details keys: required_plan, current_plan, action, topup_url.
404 — the resource doesn't exist (not_found) or the search returned no hits (no_results). Treat no_results as a valid empty result; don't paper over not_found with auto-create logic.
429 — back off. Honour Retry-After (seconds) or details.retryAfterMs (milliseconds, takes precedence). Use exponential backoff with jitter for repeated breaches.
5xx — transient by definition. Retry with exponential backoff and jitter. Alert if a single endpoint stays in 5xx for more than a few minutes — check the status page; we'll usually have an incident posted by then.
Credits and errors
You're not billed for failed calls. Both 4xx and 5xx are auto-refunded — the credit debit fires on entry to the handler and is reversed on any non-2xx response. Cached 200s and live 200s are the only billing events. See Billing for the full meter rules.
Idempotent retries
The Ray9 API is read-only, so retries are at-most-once safe by construction.
Logging the right things
When something goes wrong, log:
- The full
requestId(or thex-request-idheader). - The
code, the HTTP status, and the request URL + method. - Whatever in
detailsis structured (e.g.retryAfterMs).
Don't log the message as your primary error key — branch on code. Don't log the request body if it might contain user content; log a hash or the request URL plus parameter shape.