Appearance
Errors & Retries
Runo always returns JSON. The shape is consistent, the codes are machine-readable, and the retryable flag tells you whether to try again.
Error Envelope
json
{
"status": "error",
"error": {
"code": "FETCH_BLOCKED",
"message": "Both plain fetch and stealth headless were blocked.",
"retryable": true
}
}Top-level status: "error" means the whole call failed. Inside /batch and /crawl responses, individual URL entries can carry the same envelope while the overall response is still 200 OK.
Codes
Schema & Request Shape
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
SCHEMA_INVALID | 400 | no | Schema is malformed (missing field, unknown type, etc.) |
SCHEMA_REQUIRED | 422 | no | Dynamic key called without a schema |
SCHEMA_NOT_ALLOWED | 400 | no | Static key included an inline schema |
TYPE_COERCION_FAILED | 200 | no | A specific field couldn't be coerced to its declared type |
Quotas & Access
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
RATE_LIMITED | 429 | yes (after Retry-After) | Per-minute limit hit |
QUOTA_EXCEEDED | 429 | no this month | Monthly quota exhausted |
TIER_REQUIRED | 402 | no | URL requires T4 (CAPTCHA) or T5 (residential proxy) - Pro/Scale only |
KEY_LIMIT_REACHED | 429 | no | Active-key cap hit on your plan |
ACCOUNT_SUSPENDED | 403 | no | Account is suspended |
Fetch & Rendering
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
FETCH_BLOCKED | 200 | yes | Anti-bot defeated both fetch and headless |
URL_UNREACHABLE | 200 | yes | DNS or network failure |
TIMEOUT | 200 | yes | Page exceeded options.timeout_ms |
Crawl
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
CRAWL_LIMIT_REACHED | 200 | n/a | max_pages hit |
LLM
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
LLM_ERROR | 200 | yes | Model returned unusable response |
LLM_UNAVAILABLE | 200 | yes (back off) | Model 503 / overloaded after 4 internal attempts |
LLM_RATE_LIMITED | 200 | yes (back off) | Model 429 after internal key rotation |
LLM_TIMEOUT | 200 | yes | DeadlineExceeded / 504 after retry |
LLM_TRUNCATED | 200 | yes | JSON still truncated after output-budget bumps |
LLM_BLOCKED | 200 | no | Safety/policy block |
LLM_EMPTY | 200 | yes | Empty candidates from the model |
LLM_BAD_REQUEST | 200 | no | Invalid argument / prompt too long |
Jobs
| Code | HTTP | Retryable | Meaning |
|---|---|---|---|
JOB_CANCELLED | n/a | n/a | Per-URL marker in batch/crawl results for units that didn't run |
JOB_NOT_FOUND | 404 | no | DELETE on an unknown or completed job |
JOB_FORBIDDEN | 403 | no | DELETE on someone else's job |
Recommended Retry Pattern
For top-level errors flagged retryable: true:
- Honor
Retry-Afteron429. Otherwise back off exponentially: 1 s, 2 s, 4 s, 8 s. - Cap at 4 attempts.
- Treat
LLM_BLOCKED,LLM_BAD_REQUEST,SCHEMA_*,TIER_REQUIRED,QUOTA_EXCEEDED,ACCOUNT_SUSPENDEDas terminal. Retrying will not help.
py
import time, requests
def call_with_retry(url, body, headers, max_attempts=4):
delay = 1.0
for attempt in range(max_attempts):
r = requests.post(url, json=body, headers=headers, timeout=60)
data = r.json()
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", delay)))
delay *= 2
continue
if data.get("status") == "error" and data["error"].get("retryable"):
time.sleep(delay)
delay *= 2
continue
return data
return data # last attempt's payloadWarnings (Non-Fatal)
When the call succeeds but something looked off, the response may include a warnings array:
json
{
"status": "success",
"data": { "price": 19.99 },
"warnings": ["coerced 'price' from '$19.99 USD' to 19.99"]
}warnings is omitted when empty.