Errors and limits
How the API reports failures, what each status code and error code means, and the rate limits, concurrency limits and monthly quotas that apply to every organization. Read this before putting an integration into production.
Error format
Every error is JSON with the same fields:
{
"success": false,
"code": "CONCURRENCY_LIMIT",
"error": "too many concurrent requests for this organization (plan limit 5); retry shortly",
"retryable": true,
"docs_url": "https://xdive.dev/docs/errors#error-codes"
}| Field | Meaning |
|---|---|
code | A stable identifier. Specific failures have their own code (see Error codes); every other error gets the code for its status, such as INVALID_REQUEST, UNAUTHORIZED, QUOTA_EXCEEDED or NOT_FOUND. |
error | A message written for people. It may change; do not parse it. |
retryable | true when sending the same request again later can succeed: rate limits, overload, timeouts and failures of a site or service the request depends on. false means the request, the credentials or the account must change first. |
docs_url | This page. |
Branch on the HTTP status and on code, and retry only when retryable is true, after Retry-After seconds when the header is present.
Every response carries an X-Request-ID header (req_…). Include it when you report a problem.
A few endpoints add detail to the error body:
POST /orgs/{id}/batch/scrapewith malformed URLs returns400with aninvalid_urlsarray, unless you passignore_invalid_urls.POST /orgs/{id}/extractreturns502with asourcesarray giving each URL's error when no source could be read.- The OAuth endpoints use the OAuth shape,
{"error": "invalid_grant", "error_description": "…"}, and the MCP endpoint returns JSON-RPC errors.
Crawls and batch scrapes can succeed while some of their pages fail. Each failed page in the results has an error message and, for failures you may want to branch on, an error_code such as SITE_RESTRICTED or PROXY_NOT_ENABLED.
HTTP status codes
| Status | Meaning in this API |
|---|---|
200 | Success. |
201 | Created (an API key, a browser session). |
202 | Accepted: a crawl or batch scrape was queued. Poll its id for the result. |
204 | Success with no body (revocations and deletions). |
304 | The crawl has not changed since the ETag you sent in If-None-Match. |
400 | The request is malformed or a parameter is invalid. The message says which. |
401 | No credentials, or the credentials are invalid, expired, used from an address outside the key's allowlist, or belong to another organization. |
402 | The organization's monthly quota for this kind of work is used up. See Quotas. |
403 | Authenticated but not allowed: the key lacks a scope, the user is not a member of the organization, the organization is suspended, the account is pending or rejected, or the target page is refused (robots.txt, SITE_RESTRICTED, PROXY_NOT_ENABLED). |
404 | The resource does not exist, or the work found nothing (NO_SOURCES, SCRAPE_LOCKDOWN_CACHE_MISS). |
405 | GET or DELETE on the MCP endpoint, which accepts POST only. |
409 | Conflict with the current state: cancelling a finished crawl, exporting an unfinished one, no free browser session. |
410 | The browser session has expired. |
415 | POST /orgs/{id}/parse was not sent as multipart/form-data. |
422 | The content could not be processed, for example a document that cannot be parsed. |
429 | Too many requests. Wait for Retry-After seconds. See Rate limits and concurrency. |
500 | An unexpected server error. |
501 | The feature is not configured on this server: a language model, browser automation or billing. |
502 | Something the request depends on failed: the target site, a search engine or the language model. |
503 | The server is at capacity (SERVER_BUSY) or a dependency is unavailable, such as the job queue or an unconfigured search engine. |
504 | The work did not finish in time (RESEARCH_TIMEOUT). |
Error codes
| Code | Status | Returned by | Meaning |
|---|---|---|---|
CONCURRENCY_LIMIT | 429 | Synchronous endpoints | The organization already has its plan's maximum number of synchronous requests running. Sent with Retry-After: 1. |
SERVER_BUSY | 503 | Synchronous endpoints | This API server is at capacity and its short wait queue is full. Sent with Retry-After: 2. |
PROXY_NOT_ENABLED | 403 | Scrape; error_code on crawl pages | The site blocks data-centre networks, and the residential proxy that can reach it is not enabled for your organization, or your organization has used its monthly proxy allowance. The platform administrator enables the proxy and sets the allowance per organization. |
SITE_RESTRICTED | 403 | Scrape; error_code on crawl pages | Your organization may use the residential proxy, but the proxy provider refused this destination. Providers typically refuse government and banking sites until the platform's proxy account is identity-verified. Retrying will not help; the platform administrator can see more in the audit log. |
SCRAPE_LOCKDOWN_CACHE_MISS | 404 | Scrape with lockdown | Lockdown allows cache reads only, and no cached copy of the page exists. |
NO_SOURCES | 404 | Research | No readable sources were found for the question. |
RESEARCH_TIMEOUT | 504 | Research | The run did not finish in time (45 seconds for quick, 150 for deep). Try depth: "quick" or a narrower question. |
ACCOUNT_PENDING_APPROVAL | 403 | Sign-in, sessions, OAuth, MCP | The account is waiting for an administrator's approval. |
ACCOUNT_REJECTED | 403 | Sign-in, sessions, OAuth, MCP | The account's access request was not approved. |
EMAIL_NOT_VERIFIED | 403 | Sign-in | An administrator address listed in SUPER_ADMIN_EMAILS has not been verified yet; a new verification link is emailed. |
SIGNED_WEBHOOK_REQUIRED | 400 | Crawl, batch scrape, schedules | A webhook_url was given without a webhook_secret while the server requires signed webhooks (the default). |
WEBHOOK_TARGET_DENIED | 400 | Crawl, batch scrape, schedules | The webhook URL is not an absolute public http(s) URL, for example it points at a private address. |
CORS_ORIGIN_DENIED | 403 | Any endpoint called from a browser | The page's origin is not on the server's allowlist. |
RATE_LIMITED | 429 | Any endpoint | The per-client or per-organization request rate was exceeded. Wait for Retry-After seconds. |
QUOTA_EXCEEDED | 402 | Work-creating endpoints | The organization's monthly quota, or the API key's own monthly budget, is used up. The message says which. |
INVALID_IDEMPOTENCY_KEY | 400 | Work-creating POST endpoints | The Idempotency-Key header is longer than 255 characters or has characters outside printable ASCII. |
IDEMPOTENCY_KEY_REUSED | 422 | Work-creating POST endpoints | The key was already used with a different request (another path or body). Use a new key for a new request. |
IDEMPOTENCY_KEY_IN_PROGRESS | 409 | Work-creating POST endpoints | The first request with this key is still running. Retry after Retry-After seconds to receive its response. |
Rate limits and concurrency
Four limits protect the service. Each answers with 429 or 503 and a Retry-After header in seconds.
| Limit | Applies to | Over the limit |
|---|---|---|
| Per client | Every request: by default 20 per second per API key, or per IP address for requests without a key. | 429 RATE_LIMITED, Retry-After: 1 |
| Requests per minute | Work-creating requests, counted per organization. | 429 RATE_LIMITED, with Retry-After until the minute resets |
| Concurrent requests | Synchronous requests running at once, per organization. | 429 CONCURRENCY_LIMIT, Retry-After: 1 |
| Server capacity | Synchronous requests running on one API server. | 503 SERVER_BUSY, Retry-After: 2 |
Requests per minute counts the calls that create work: scrape, crawl, batch scrape, map, search, research, extract, llms.txt, parse, connectors, and creating or driving browser sessions. Status polling, cancellation and other reads are never counted, so polling a running crawl cannot use up the budget. The window is a fixed minute. These responses carry the budget in the standard RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers (seconds until the window resets), and under the same names with an X- prefix for older clients. Pace requests from RateLimit-Remaining rather than waiting for a 429.
Concurrent requests and server capacity apply to the endpoints that fetch while you wait: scrape, map, search, research, extract, llms.txt, parse and connectors. Crawls and batch scrapes are queued instead and never rejected for concurrency: while other organizations' jobs are waiting, an organization gets its plan's fair share of crawl workers, and with nothing else queued it can use more.
Each API server runs up to 128 synchronous requests at once and lets 1,024 more wait up to 5 seconds before answering SERVER_BUSY. These are defaults that self-hosted servers can change.
| Plan | Requests per minute | Concurrent synchronous requests | Concurrent crawls (fair share) |
|---|---|---|---|
free | 60 | 5 | 2 |
pro | 600 | 50 | 10 |
enterprise | 6,000 | 500 | 50 |
Retrying safely with Idempotency-Key
A timeout or a dropped connection does not tell you whether the request ran. Retrying a crawl or a research call blindly can start it twice and use the quota twice. Send an Idempotency-Key header on work-creating POST requests to make retries safe:
curl -X POST "$XDIVE_URL/api/orgs/$ORG_ID/crawl" \
-H "Authorization: Bearer $XDIVE_API_KEY" \
-H "Idempotency-Key: 6f1c2d4e-8a7b-4c3d-9e0f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "max_depth": 1}'- Use a new random value, such as a UUID, for each new request, and the same value for every retry of it.
- For 24 hours, a retry with the same key and the same body returns the first response, with the header
Idempotent-Replayed: true, without running the request or counting it again. - A retry while the first request is still running gets
409IDEMPOTENCY_KEY_IN_PROGRESSwithRetry-After. - The same key with a different path or body gets
422IDEMPOTENCY_KEY_REUSED. - Rate-limit (
429) and server (5xx) responses are not stored, so a retry after them runs the request. Responses larger than 4 MB are delivered but not stored. - Keys belong to the organization. Two organizations can use the same value without effect on each other.
Quotas
Each plan has monthly quotas, counted per calendar month in UTC (see Usage and billing for what counts):
| Plan | Pages per month | Searches per month | Browser seconds per month | Browser sessions at once | Schedules |
|---|---|---|---|---|---|
free | 1,000 | 100 | 600 | 1 | 3 |
pro | 50,000 | 10,000 | 36,000 | 3 | 50 |
enterprise | Unlimited | Unlimited | Unlimited | 10 | Unlimited |
When a quota is used up, requests that would use more of it return 402:
{ "error": "monthly page quota for the free plan reached (1000 pages); upgrade to continue" }Quotas are checked before work starts, so a crawl that starts under the limit runs to completion even if it ends above it. Creating a schedule over the plan's cap also returns 402; starting a browser session when the plan's sessions are all in use returns 409.
Retries
429and503: wait forRetry-Afterseconds, then retry.402: retrying will not help until the next month or a plan upgrade.- Other
4xx: fix the request; the same request fails again. 502and504: a dependency failed or was slow. A retry may succeed, but a successful retry is metered like any other request.
How the SDKs retry
Both SDKs retry 429 and 503 automatically. Each retry waits the response's Retry-After (1 second when it is missing), up to max_retries (Python) or maxRetries (JavaScript) times, default 3. After that they raise RateLimited / RateLimitedError, which carries the wait the server asked for and the error code.
| Situation | Python | JavaScript |
|---|---|---|
Still throttled after retries (429, 503) | RateLimited (retry_after) | RateLimitedError (retryAfterSeconds) |
Quota used up (402) | QuotaExceeded | QuotaExceededError |
| Any other error status | XDiveError | XDiveError |
| Crawl not finished within the wait timeout | CrawlTimeout | CrawlTimeoutError |
All of them extend XDiveError, which exposes status, code and the message.
from xdive import XDive, XDiveError, QuotaExceeded, RateLimited
xdive = XDive(max_retries=5)
try:
page = xdive.scrape("https://example.com")
except RateLimited as error:
print(f"still throttled ({error.code}); retry in {error.retry_after}s")
except QuotaExceeded as error:
print("quota used up:", error.message)
except XDiveError as error:
print(error.status, error.code, error.message)import { XDive, XDiveError, QuotaExceededError, RateLimitedError } from "xdive";
const xdive = new XDive({ maxRetries: 5 });
try {
const page = await xdive.scrape("https://example.com");
} catch (error) {
if (error instanceof RateLimitedError) {
console.log(`still throttled (${error.code}); retry in ${error.retryAfterSeconds}s`);
} else if (error instanceof QuotaExceededError) {
console.log("quota used up:", error.message);
} else if (error instanceof XDiveError) {
console.log(error.status, error.code, error.message);
} else {
throw error;
}
}The SDKs retry every 429 and 503, including a 503 from a server whose search engine is not configured, so such a call fails only after the retries.