Errors
All errors are returned as JSON with a stable error code and a human-readable message.
{
"error": "target_dns_error",
"message": "Could not resolve target hostname. Check the URL spelling."
}
Error codes
| HTTP |
Code |
When |
| 401 |
missing_api_key |
No Bearer / X-API-Key / access_key sent |
| 401 |
invalid_api_key |
Key not found, revoked, or wrong format |
| 401 |
invalid_access_key |
Signed URL: access_key not found |
| 403 |
invalid_signature |
Signed URL: HMAC mismatch |
| 405 |
method_not_allowed |
Wrong HTTP method (use POST for /screenshot, /animate, /bulk) |
| 422 |
invalid_url |
url is missing, malformed, or not http(s) |
| 422 |
invalid_format |
format not in allowed list |
| 422 |
invalid_scenario |
scenario not in default, scroll |
| 422 |
invalid_webhook_url |
webhook_url malformed |
| 422 |
webhook_secret_too_long |
webhook_secret > 256 chars |
| 422 |
urls_required |
bulk: urls field missing or empty |
| 422 |
too_many_urls |
bulk: > 500 URLs |
| 422 |
invalid_batch_id |
GET /batch/{id}: malformed id |
| 422 |
invalid_id |
GET /screenshot/{id}: not a number |
| 422 |
target_dns_error |
Hostname doesn't resolve (typo) |
| 422 |
target_unsafe_port |
Browser refused port (1, 7, 9, 25, …) |
| 422 |
selector_not_found |
CSS selector didn't match anything |
| 422 |
unsupported_format |
Internal: format not handled |
Quota / rate limit (HTTP 402, 429)
| HTTP |
Code |
When |
| 402 |
quota_exceeded |
Monthly quota reached |
| 429 |
rate_limited |
RPM limit hit |
| 429 |
demo_limit_reached |
/v1/demo: 3/IP/24h hit |
Upstream errors (HTTP 5xx) — target site issue
| HTTP |
Code |
When |
| 502 |
target_connection_refused |
Target server refused connection |
| 502 |
target_connection_reset |
Connection reset mid-load |
| 502 |
target_redirect_loop |
Too many redirects on target |
| 502 |
target_ssl_error |
Target's TLS cert is invalid/expired |
| 502 |
target_blocked |
Target blocks our crawler (anti-bot, CSP) |
| 504 |
target_connection_timeout |
Target didn't respond in time |
| 504 |
target_load_timeout |
Page load timeout (try wait_until=domcontentloaded) |
Internal errors (HTTP 5xx) — our side
| HTTP |
Code |
When |
| 500 |
video_recording_failed |
Internal video pipeline failed |
| 500 |
transcode_failed |
ffmpeg failed |
| 500 |
render_error |
Catch-all internal error |
| 503 |
service_unavailable |
We're temporarily at capacity |
Async / job state
| HTTP |
Code |
When |
| 202 |
(no error, status: "processing") |
Render still in progress, poll status_url |
| 410 |
result_expired |
File deleted (24h retention exceeded). Re-render to regenerate |
| 404 |
not_found |
Batch/screenshot ID doesn't exist or isn't yours |
Retry policy (recommended)
| Code |
Retry? |
How |
| 401, 403, 422 |
No |
Fix input, then retry |
| 402 |
No |
Upgrade plan or wait for cycle reset |
| 429 |
Yes |
Wait Retry-After seconds |
| 502, 503, 504 |
Yes |
Exponential backoff (1s, 2s, 4s, 8s, give up after 4 tries) |
| 500 |
Yes once |
Single retry, then give up — log for support |
Diagnosing target errors
When a target site fails, the error code tells you why:
| Code |
Likely cause |
Fix |
target_dns_error |
Typo in URL |
Re-check spelling |
target_blocked |
Cloudflare / WAF / anti-bot |
Try stealth: true or contact target site |
target_load_timeout |
Slow site, JS-heavy |
Use wait_until=domcontentloaded instead of networkidle |
target_ssl_error |
Bad cert on target |
Out of our control, contact target |
target_redirect_loop |
Misconfigured target |
Check the URL with curl -L |
Webhook / async errors
When a render fails in async mode, the webhook payload still arrives but with status: "failed":
{
"event": "screenshot.completed",
"screenshot": {
"status": "failed",
"error_code": "target_dns_error",
"error_message": "Could not resolve target hostname. Check the URL spelling.",
"completed_at": "..."
}
}