View .md

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:

json
{
  "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"
}
FieldMeaning
codeA 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.
errorA message written for people. It may change; do not parse it.
retryabletrue 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_urlThis 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/scrape with malformed URLs returns 400 with an invalid_urls array, unless you pass ignore_invalid_urls.
  • POST /orgs/{id}/extract returns 502 with a sources array 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

StatusMeaning in this API
200Success.
201Created (an API key, a browser session).
202Accepted: a crawl or batch scrape was queued. Poll its id for the result.
204Success with no body (revocations and deletions).
304The crawl has not changed since the ETag you sent in If-None-Match.
400The request is malformed or a parameter is invalid. The message says which.
401No credentials, or the credentials are invalid, expired, used from an address outside the key's allowlist, or belong to another organization.
402The organization's monthly quota for this kind of work is used up. See Quotas.
403Authenticated 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).
404The resource does not exist, or the work found nothing (NO_SOURCES, SCRAPE_LOCKDOWN_CACHE_MISS).
405GET or DELETE on the MCP endpoint, which accepts POST only.
409Conflict with the current state: cancelling a finished crawl, exporting an unfinished one, no free browser session.
410The browser session has expired.
415POST /orgs/{id}/parse was not sent as multipart/form-data.
422The content could not be processed, for example a document that cannot be parsed.
429Too many requests. Wait for Retry-After seconds. See Rate limits and concurrency.
500An unexpected server error.
501The feature is not configured on this server: a language model, browser automation or billing.
502Something the request depends on failed: the target site, a search engine or the language model.
503The server is at capacity (SERVER_BUSY) or a dependency is unavailable, such as the job queue or an unconfigured search engine.
504The work did not finish in time (RESEARCH_TIMEOUT).

Error codes

CodeStatusReturned byMeaning
CONCURRENCY_LIMIT429Synchronous endpointsThe organization already has its plan's maximum number of synchronous requests running. Sent with Retry-After: 1.
SERVER_BUSY503Synchronous endpointsThis API server is at capacity and its short wait queue is full. Sent with Retry-After: 2.
PROXY_NOT_ENABLED403Scrape; error_code on crawl pagesThe 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_RESTRICTED403Scrape; error_code on crawl pagesYour 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_MISS404Scrape with lockdownLockdown allows cache reads only, and no cached copy of the page exists.
NO_SOURCES404ResearchNo readable sources were found for the question.
RESEARCH_TIMEOUT504ResearchThe run did not finish in time (45 seconds for quick, 150 for deep). Try depth: "quick" or a narrower question.
ACCOUNT_PENDING_APPROVAL403Sign-in, sessions, OAuth, MCPThe account is waiting for an administrator's approval.
ACCOUNT_REJECTED403Sign-in, sessions, OAuth, MCPThe account's access request was not approved.
EMAIL_NOT_VERIFIED403Sign-inAn administrator address listed in SUPER_ADMIN_EMAILS has not been verified yet; a new verification link is emailed.
SIGNED_WEBHOOK_REQUIRED400Crawl, batch scrape, schedulesA webhook_url was given without a webhook_secret while the server requires signed webhooks (the default).
WEBHOOK_TARGET_DENIED400Crawl, batch scrape, schedulesThe webhook URL is not an absolute public http(s) URL, for example it points at a private address.
CORS_ORIGIN_DENIED403Any endpoint called from a browserThe page's origin is not on the server's allowlist.
RATE_LIMITED429Any endpointThe per-client or per-organization request rate was exceeded. Wait for Retry-After seconds.
QUOTA_EXCEEDED402Work-creating endpointsThe organization's monthly quota, or the API key's own monthly budget, is used up. The message says which.
INVALID_IDEMPOTENCY_KEY400Work-creating POST endpointsThe Idempotency-Key header is longer than 255 characters or has characters outside printable ASCII.
IDEMPOTENCY_KEY_REUSED422Work-creating POST endpointsThe key was already used with a different request (another path or body). Use a new key for a new request.
IDEMPOTENCY_KEY_IN_PROGRESS409Work-creating POST endpointsThe 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.

LimitApplies toOver the limit
Per clientEvery 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 minuteWork-creating requests, counted per organization.429 RATE_LIMITED, with Retry-After until the minute resets
Concurrent requestsSynchronous requests running at once, per organization.429 CONCURRENCY_LIMIT, Retry-After: 1
Server capacitySynchronous 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.

PlanRequests per minuteConcurrent synchronous requestsConcurrent crawls (fair share)
free6052
pro6005010
enterprise6,00050050

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:

bash
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 409 IDEMPOTENCY_KEY_IN_PROGRESS with Retry-After.
  • The same key with a different path or body gets 422 IDEMPOTENCY_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):

PlanPages per monthSearches per monthBrowser seconds per monthBrowser sessions at onceSchedules
free1,00010060013
pro50,00010,00036,000350
enterpriseUnlimitedUnlimitedUnlimited10Unlimited

When a quota is used up, requests that would use more of it return 402:

json
{ "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

  • 429 and 503: wait for Retry-After seconds, 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.
  • 502 and 504: 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.

SituationPythonJavaScript
Still throttled after retries (429, 503)RateLimited (retry_after)RateLimitedError (retryAfterSeconds)
Quota used up (402)QuotaExceededQuotaExceededError
Any other error statusXDiveErrorXDiveError
Crawl not finished within the wait timeoutCrawlTimeoutCrawlTimeoutError

All of them extend XDiveError, which exposes status, code and the message.

python
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)

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.