All SnapAPI errors follow a consistent format:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": {}
}
}| Status | Code | Description | What to do |
|---|---|---|---|
400 |
INVALID_REQUEST |
Missing or invalid request parameters | Check required fields |
400 |
INVALID_URL |
URL is malformed or missing scheme | Ensure URL starts with http:// or https:// |
400 |
INVALID_FORMAT |
Unsupported output format | Use png, jpeg, webp, avif, or pdf |
401 |
INVALID_API_KEY |
API key is missing, invalid, or revoked | Check your key in the Dashboard |
402 |
QUOTA_EXCEEDED |
Monthly screenshot quota exceeded | Upgrade plan or wait for monthly reset |
404 |
NOT_FOUND |
Endpoint or resource not found | Check the endpoint URL |
422 |
CONTENT_CHECK_FAILED |
failIfContentMissing or failIfContentContains triggered |
Expected content not found (or unwanted content found) |
429 |
RATE_LIMITED |
Too many requests per second | Implement exponential backoff |
500 |
SCREENSHOT_FAILED |
Internal error during capture | Retry — if persistent, check URL accessibility |
500 |
RENDER_FAILED |
HTML/Markdown rendering failed | Check your HTML/Markdown syntax |
504 |
TIMEOUT |
Page load exceeded timeout | Increase timeout, or check if the URL is reachable |
Missing required parameter or invalid value.
{
"error": {
"code": "INVALID_REQUEST",
"message": "Request validation failed",
"details": {
"url": "URL is required when html and markdown are not provided"
}
}
}Fix: Ensure url, html, or markdown is included.
{
"error": {
"code": "INVALID_URL",
"message": "The provided URL is not valid: 'example.com'",
"details": {
"url": "example.com"
}
}
}Fix: Add https:// prefix — use https://example.com not example.com.
{
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid or has been revoked."
}
}Fix:
- Check that you're passing the key correctly:
X-Api-Key: YOUR_API_KEY - Verify the key in your Dashboard
- If recently regenerated, update all references
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "You have exceeded your monthly quota of 200 screenshots.",
"details": {
"used": 200,
"limit": 200,
"resetAt": "2026-04-01T00:00:00Z"
}
}
}Fix: Upgrade your plan or wait for the monthly reset.
{
"error": {
"code": "CONTENT_CHECK_FAILED",
"message": "Content check failed: 'Please log in' was found on the page.",
"details": {
"trigger": "failIfContentContains",
"match": "Please log in"
}
}
}Fix: Review your cookies/auth setup, or adjust failIfContentContains/failIfContentMissing.
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded. Maximum 5 requests per second on your plan.",
"details": {
"limit": 5,
"retryAfter": 1
}
}
}The response includes a Retry-After header (seconds to wait before retrying).
Fix: Implement exponential backoff — see Retry Logic.
{
"error": {
"code": "TIMEOUT",
"message": "Page load timed out after 30000ms.",
"details": {
"timeout": 30000,
"url": "https://slow-site.example.com"
}
}
}Fix:
- Increase
timeout(up to 60000ms) - Use
waitUntil: "domcontentloaded"instead ofnetworkidlefor faster capture - Check if the URL is publicly accessible
For 429 RATE_LIMITED and 5xx errors, implement exponential backoff:
async function screenshotWithRetry(client, options, maxRetries = 3) {
let delay = 1000; // Start with 1 second
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await client.screenshot(options);
} catch (error) {
if (attempt === maxRetries) throw error;
// Only retry on rate limit or server errors
if (error.statusCode === 429 || error.statusCode >= 500) {
const retryAfter = error.details?.retryAfter ?? delay / 1000;
const waitMs = retryAfter * 1000;
console.log(`Attempt ${attempt + 1} failed. Retrying in ${waitMs}ms...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
delay *= 2; // Exponential backoff
} else {
throw error; // Don't retry 4xx errors (except 429)
}
}
}
}import time
def screenshot_with_retry(client, options, max_retries=3):
delay = 1.0
for attempt in range(max_retries + 1):
try:
return client.screenshot(**options)
except Exception as e:
if attempt == max_retries:
raise
status = getattr(e, 'status_code', None)
if status in (429,) or (status and status >= 500):
wait = getattr(e, 'retry_after', delay)
print(f"Attempt {attempt + 1} failed. Retrying in {wait}s...")
time.sleep(wait)
delay *= 2
else:
raise| Status | Should Retry? | Strategy |
|---|---|---|
400 |
❌ No | Fix the request parameters |
401 |
❌ No | Fix the API key |
402 |
❌ No | Upgrade plan |
422 |
❌ No | Fix content check conditions |
429 |
✅ Yes | Wait for Retry-After header |
500 |
✅ Yes | Exponential backoff (usually transient) |
504 |
Increase timeout, then retry once |
- Check
/v1/usage— confirm quota isn't the issue - Test the URL in a browser — confirm it's publicly accessible
- Try
waitUntil: "load"— simpler wait condition catches fewer timeouts - Add
"delay": 2000"— give JS-heavy pages time to render - Check
failIfContentContainspatterns — overly broad strings trigger false positives