Error Codes Reference
Complete reference for AlterLab API errors, status codes, and how to handle them gracefully.
Error Response Structure
error,message, and optional details fields. Check the HTTP status code first, then parse the response body for details.HTTP Status Codes
| Code | Status | Description | Retry? |
|---|---|---|---|
| 200 | OK | Request succeeded (sync response with content) | N/A |
| 202 | Accepted | Job queued for async processing (poll job_id) | N/A |
| 204 | No Content | Successful deletion, no response body | N/A |
| 400 | Bad Request | Invalid request parameters or malformed JSON | No |
| 401 | Unauthorized | Missing, invalid, or expired API key/token | No |
| 402 | Payment Required | Insufficient balance or paywall detected | No |
| 403 | Forbidden | Access denied by target site or blocked by anti-bot | Maybe |
| 404 | Not Found | Resource not found (job, webhook, etc.) | No |
| 409 | Conflict | Duplicate resource (webhook URL already exists) or organization billing misconfigured | No |
| 413 | Payload Too Large | Content exceeds size limit (10MB) | No |
| 415 | Unsupported Media | URL content type not supported for requested mode | No |
| 422 | Unprocessable | All scraping tiers failed, content extraction failed, or invalid parameter combination (e.g. render_js=true with max_tier below 4) | Maybe |
| 429 | Too Many Requests | Rate limit exceeded, or the same request failed the same way several times in a row (repeat-failure back-off, no credits charged). Check the Retry-After header | Yes (after delay) |
| 500 | Internal Error | Unexpected server error | Yes |
| 502 | Bad Gateway | Upstream error or worker queue issue | Yes |
| 503 | Service Unavailable | Service, API-key authentication, or organization billing temporarily unavailable, or target site down. Honor Retry-After | Yes |
| 504 | Gateway Timeout | Request timed out waiting for response | Yes |
Scraping Errors
These errors occur during the scraping process. Most result in automatic refunds.
BLOCKED_BY_ANTIBOTThe target site detected and blocked the scraping attempt. All tier escalations failed.
Solution: Try using max_tier: "4" to enable CAPTCHA solving, or contact support for sites with advanced protection.
CHALLENGE_DETECTEDA bot detection challenge (anti-bot wall, reCAPTCHA, hCaptcha) was encountered.
Solution: Enable JavaScript rendering with mode: "js" or increase max tier.
TIMEOUTThe scraping operation timed out before completing.
Solution: Increase timeout parameter or use async mode with polling.
CONTENT_TOO_LARGEThe page content exceeds the 10MB size limit.
Solution: Target a specific section of the page or use a different scraping strategy.
UNSUPPORTED_CONTENT_TYPEThe URL returns a content type not supported by the selected mode.
Solution: Use the appropriate mode - pdf for PDFs, ocr for images.
EXTRACTION_FAILEDContent was fetched but could not be parsed or extracted.
Solution: Check if the URL returns valid HTML/content. Try a different extraction mode.
REQUIRES_AUTHThe page requires authentication to access.
Solution: This URL is behind a login wall and cannot be scraped without credentials.
PAYWALL_DETECTEDThe content is behind a paywall and requires subscription to access.
Solution: Content requires paid subscription. Cannot be accessed via scraping.
NETWORK_ERRORNetwork connection failed - DNS error, connection refused, or SSL error.
Solution: Verify the URL is valid and the site is online. Retry after a brief delay.
render_js + max_tier below 4The request sets advanced.render_js: true together with a cost_controls.max_tier below "4". Browser rendering runs on tier 4, so the request is rejected at submission on /api/v1/scrape, /api/v1/batch items, and /api/v1/crawl, before any balance is reserved or charged. In production the response body is {"detail": "Invalid request parameters"}.
Solution: Set max_tier to "4", omit it, or use render_js: "auto", which lets AlterLab decide when browser rendering is needed. Do not retry the same request unchanged.
error.hint (forced-render hint)When a request forces browser rendering (advanced.render_js: true or cost_controls.force_tier: "4") and the job fails because the page returned a verification challenge or did not finish loading in time, the error object includes a hint. It appears on the synchronous 422 response, on GET /api/v1/jobs/{job_id}, and on failed crawl pages. Successful jobs and jobs that do not force rendering never include it. Existing fields and status codes are unchanged.
{
"detail": "anti_bot_blocked: ...",
"error": {
"error_code": "anti_bot_blocked",
"message": "...",
"protection_type": "cloudflare",
"hint": {
"code": "render_js_auto",
"reason": "challenge",
"message": "This request forced browser rendering and the page returned a verification screen instead of content. Retrying with render_js=\"auto\" lets the platform start at the lowest tier that returns the page, which succeeds more often for requests like this one."
}
}
}Fields: code is always render_js_auto; reason is challenge or timeout; message is human-readable guidance.
Solution: Retry with render_js: "auto" (and without force_tier) so AlterLab starts at the lowest tier that returns the page.
Authentication Errors
INVALID_API_KEYThe API key provided is invalid, malformed, or has been revoked.
Solution: Check that your API key starts with sk_live_ or sk_test_ and is active in your dashboard.
MISSING_API_KEYNo API key was provided in the request headers.
Solution: Include the X-API-Key header with your API key.
SESSION_EXPIREDThe session token (for dashboard APIs) has expired.
Solution: Log in again to get a fresh session token.
Session Errors (Authenticated Scraping)
Errors related to authenticated scraping (BYOS) session management and usage.
SESSION_NOT_FOUNDThe provided session_id does not exist or belongs to another user.
Solution: Verify the UUID. Use GET /sessions to list your valid sessions.
SESSION_EXPIREDThe session cookies have expired or the session was marked as expired after repeated validation failures.
Solution: Log in to the target site again and call POST /sessions/:id/refresh with fresh cookies.
SESSION_DOMAIN_MISMATCHThe scrape URL domain does not match the session's stored domain. For example, using an amazon.com session to scrape walmart.com.
Solution: Create a separate session for each target domain.
SESSION_INACTIVEThe session has been deleted or disabled.
Solution: Create a new session with fresh cookies.
SESSION_LIMIT_REACHEDMaximum number of sessions per account (50) has been reached.
Solution: Delete unused or expired sessions. Use GET /sessions?status=expired to find candidates for cleanup.
SESSION_COOKIES_CONFLICTBoth session_id and cookies were provided in the scrape request. These parameters are mutually exclusive.
Solution: Use either session_id (for stored sessions) or cookies (for inline one-off use), not both.
Billing Errors
Organization API keys
401 Invalid API key. Any organization-key request can return the two organization billing errors below.INSUFFICIENT_CREDITSYour account has insufficient balance to complete this request.
Solution: Check your balance with GET /api/v1/usage and upgrade your plan or add funds.
EXCEEDS_COST_LIMITThe estimated cost exceeds your max_credits limit.
Solution: Increase max_credits or use a simpler scraping mode.
SUBSCRIPTION_INACTIVEYour subscription has been cancelled or payment failed.
Solution: Check your billing status in the dashboard and update payment method if needed.
org_billing_misconfiguredThe request uses an organization API key (member or service account), but the organization has no active owner to bill. No personal wallet is charged instead. Not retryable.
Solution: An organization administrator must assign an active owner. The error is returned in detail.error with a readable detail.message.
org_billing_unavailableThe organization billing account could not be resolved for this request. The request is rejected rather than billed to the wrong account.
Solution: Retry after the number of seconds in the Retry-After header.
Rate Limit Errors
RATE_LIMIT_EXCEEDEDYou have exceeded your plan's rate limit (requests per minute).
Solution: Wait the number of seconds in the Retry-After header (also detail.retry_after_seconds in the body), then retry.
Rate Limit Headers:
Retry-After: Whole seconds to wait before retryingRateLimit-Limit: The request-rate limit that was exceeded (not sent for spend, concurrency, crawl-slot or dispatch limits)RateLimit-Remaining: Requests remaining in the window (0 on a 429)RateLimit-Reset: Seconds until a retry can succeed (equals Retry-After)X-RateLimit-Scope: Which limit was hit (for example account, org, ip)
REPEAT_FAILURE_BACKOFFAfter 3 consecutive identical failures of the same URL with the same request options, POST /api/v1/scrape pauses that exact request and returns repeat_failure_backoff instead of running it again. No job is created and no credits are charged (X-AlterLab-Credits: 0). Retry-After is short (about a minute) when the page returned a verification screen and longer (up to 15 minutes) for repeated errors from the site. Any successful result resets the count.
{
"error": "repeat_failure_backoff",
"message": "This exact request returned a verification screen 3 times in a row, so it was paused for 60s and no credits were charged. ...",
"detail": {
"scope": "url",
"limit": null,
"account_tier": null,
"retry_after_seconds": 60,
"consecutive_failures": 3,
"outcome_class": "challenge",
"failure_code": "anti_bot_blocked",
"hint": {
"code": "repeat_failure_backoff",
"message": "This exact request returned a verification screen 3 times in a row, so it was paused for 60s and no credits were charged. Retry after the Retry-After delay, or retry now with different request options (for example render_js=\"auto\") or with force_refresh=true."
}
},
"docs_url": "https://alterlab.io/docs/reference/error-codes#repeat-failure-backoff"
}Fields: outcome_class is challenge, timeout, origin or other; hint.code is always repeat_failure_backoff.
Solution: Wait for Retry-After, or retry right away with different request options (for example render_js: "auto") or with force_refresh: true, which always runs the request.
Per-account limits refill continuously. After an idle period a full minute's allowance can be sent back to back; sustained traffic is held to the per-minute rate. See Rate Limits & Quotas for every limit.
| Plan | Rate Limit | Burst After Idle |
|---|---|---|
| Free | 30 req/min | 30 requests |
| Basic | 60 req/min | 60 requests |
| Growth | 100 req/min | 100 requests |
| Scale | 300 req/min | 300 requests |
| High Volume | 500 req/min | 500 requests |
| Enterprise | up to 10,000 req/min | 10,000 requests |
Refund Policy
Costs are automatically refunded for most error conditions where the scraping failed through no fault of the user.
Costs Refunded
- Site blocked access (403, anti-bot)
- Request timeout
- Network/connection errors
- Content extraction failed
- Unsupported content type
- Page requires authentication
- Paywall detected
- Server errors (5xx)
Costs NOT Refunded
- Successful scrape (any tier)
- Cache hit (free)
- Invalid API key
- Rate limit exceeded
- Invalid request parameters
- Cancelled by user
Refund Timing
billing.refundedfield in error responses to confirm.Error Response Format
{
"error": "INSUFFICIENT_CREDITS",
"message": "Your account has insufficient balance for this request",
"request_id": "req_550e8400-e29b-41d4-a716-446655440000",
"details": {
"required_credits": 5,
"available_credits": 2,
"estimated_tier": "2"
}
}| Field | Type | Description |
|---|---|---|
| error | string | Machine-readable error code |
| message | string | Human-readable error description |
| request_id | string? | Unique request ID for support inquiries |
| details | object? | Additional context about the error |
| hint | object? | Present on job errors when a forced-render request fails on a challenge or timeout. See forced-render hint. |
Handling Errors
import time
import requests
from requests.exceptions import RequestException
def scrape_with_error_handling(url: str, api_key: str, max_retries: int = 3):
"""Scrape with proper error handling and retries."""
for attempt in range(max_retries):
try:
response = requests.post(
"https://api.alterlab.io/api/v1/scrape",
headers={"X-API-Key": api_key},
json={"url": url},
timeout=60
)
# Success
if response.status_code in (200, 202):
return response.json()
# Parse error response
error = response.json()
error_code = error.get("error", "UNKNOWN")
# Don't retry client errors
if response.status_code in (400, 401, 402, 404, 409, 415):
raise Exception(f"Client error: {error_code} - {error.get('message')}")
# Rate limited - wait and retry
if response.status_code == 429:
wait_time = int(response.headers.get("Retry-After", 1))
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
continue
# Server error - retry with backoff
if response.status_code >= 500:
wait_time = 2 ** attempt
print(f"Server error {response.status_code}. Retrying in {wait_time}s...")
time.sleep(wait_time)
continue
# 422 - Invalid parameters (e.g. render_js with max_tier < 4) or
# all tiers failed: change the request before retrying
if response.status_code == 422:
raise Exception(f"Scraping failed: {error.get('message')}")
except RequestException as e:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
raise
raise Exception(f"Max retries ({max_retries}) exceeded")