Get started/Errors

    Get started

    Errors

    The scanr.ai API uses standard HTTP status codes. A 2xx is success; anything else carries a message saying what went wrong.

    Error bodies

    Read the status code first; it tells you what kind of failure you have. Every error body is JSON, with the message in error.message:

    json
    {  "error": {    "statusCode": 429,    "message": "Rate limit exceeded for minutely requests. Limit: 30 requests per minute. Resets at: 2026-09-29T20:14:00.000Z"  }}

    message is written for a person and says what failed: the field that didn't validate, the plan a feature needs, the window that ran out. Don't parse it; branch on the status code. A 5xx message is for a support request, not for your code to act on.

    A scan the scanner refuses (422 from POST /api/v2/scan) adds the scanner's code and params, and causes when several refusals applied at once:

    json
    {  "error": {    "statusCode": 422,    "message": "Custom indicator \"My RSI\" has been switched off.",    "code": "scan.custom_indicator.disabled",    "params": { "indicator_id": "42", "indicator_name": "My RSI", "version": 3 }  }}

    New scan refusal codes appear without notice, so treat an unknown code as "show message", never as a failure to parse.

    One scan 422 is not about the payload alone: request.query_timeout means the scan ran past the scanner's time limit. It depends on load as well as on what the scan asks for, so narrow the scan (fewer tickers, filters or lookback), or retry it later with backoff.

    Concept research refusals add a stable code and retryable, because one status can mean different things there (a 503 is either "not available yet" or "try again later"). Branch on code for those endpoints; see Research a Concept.

    The AI credit budget's 429 adds machine-readable fields:

    json
    {  "error": {    "code": "api_ai_quota_exceeded",    "message": "Daily AI credit budget of 45,000 reached for the Starter API tier. Upgrade your API subscription to continue using AI features.",    "budget": 45000,    "used": 45210.4,    "usedPct": 100,    "resetAt": "2026-09-30T00:00:00.000Z",    "tier": "Starter"  }}

    Reading the message takes one line. The fallback covers a response that never reached the API, such as a proxy's error page:

    python
    import requests
    def scanr_error(response: requests.Response) -> str:    """The message from a failed scanr.ai response."""    try:        return response.json()["error"]["message"]    except (ValueError, KeyError, TypeError):        return response.text

    Status codes

    StatusMeaningWhat to do
    400 Bad RequestThe request body or query failed validation, or a scan payload is not valid (an unknown filter, an impossible value). The message names the field.Fix the request. Don't retry it unchanged.
    401 UnauthorizedNo token, a malformed token, or a token that is expired or revoked.Check the header and the token. See Authentication.
    403 ForbiddenThe token is valid but not allowed here: the account's API plan is inactive, the endpoint needs a higher plan, the token lacks a scope (write), or personal data access for API tokens is switched off.Read the message. Renew or upgrade the plan, add the scope, or re-enable data access.
    404 Not FoundThe resource doesn't exist: an unknown saved scan, a ticker with no data for that endpoint, or a path the API doesn't have.Check the identifier and the path.
    413 Payload Too LargeThe request body is over the endpoint's size limit (1 MiB for scans).Send a smaller payload.
    422 Unprocessable EntityA scan payload is valid but the scanner won't run it: a custom indicator you can't use or that was switched off, a pattern model that needs retraining, too many models or scripts in one scan, a filter value the scanner can't take. code names the refusal. With code request.query_timeout, the scan ran past the scanner's time limit instead.Change the payload. Don't retry it unchanged, except a request.query_timeout, which can succeed later.
    429 Too Many RequestsA per-minute, per-hour or per-day request limit, or the daily AI credit budget, is used up. The message says which.Wait the seconds in the Retry-After header (or until resetAt, for the AI credit budget) before retrying. See Plans & Rate Limits.
    500 Internal Server ErrorSomething failed on our side.Retry with backoff. If it persists, contact us with the time and endpoint.
    502 / 503 / 504An upstream data service was unavailable or slow, or (503) your token's plan couldn't be checked.Retry with backoff.

    Retrying

    Retry only what can succeed on a second attempt:

    • Retry 429 after the seconds in its Retry-After header, and 500, 502, 503 and 504 with exponential backoff (for example 1 s, 2 s, 4 s, then give up).
    • Don't retry 400, 401, 403, 404, 413 or 422. The same request will fail the same way. The exception is a scan 422 with code request.query_timeout: narrowing the scan is the reliable fix, but the same scan can also succeed later, so it may be retried with backoff.
    • A retry before the reset time is refused again. It doesn't extend the window, but it wastes a call, so wait for the reset instead of looping.
    python
    import timeimport requests
    RETRYABLE = {429, 500, 502, 503, 504}
    def request_with_retry(method, url, *, attempts=4, **kwargs):    for attempt in range(attempts):        response = requests.request(method, url, timeout=60, **kwargs)        if response.status_code not in RETRYABLE or attempt == attempts - 1:            return response        # A 429 says how long to wait; 5xx are usually brief.        if response.status_code == 429:            time.sleep(int(response.headers.get("Retry-After", 60)))        else:            time.sleep(2**attempt)    return response

    MCP errors

    The MCP server reports errors differently. Authentication failures are an HTTP error before any tool runs, with the reason in error_description: 401 for a missing, malformed, revoked or expired token, 403 for a missing read scope or an inactive API plan, 503 when the plan couldn't be checked. Everything else (a rate limit, the AI budget, personal data access being off, an invalid scan payload, a scan the scanner refused) comes back as a normal tool result with isError: true and the message as text, so the assistant can read it and adjust. A refused scan's text ends with Refusal code: <code>, the same code the REST 422 carries.

    json
    {  "error": "invalid_token",  "error_description": "Token has expired"}
    json
    {  "isError": true,  "content": [    {      "type": "text",      "text": "Rate limit exceeded for minutely requests. Limit: 30 requests per minute. Resets at: 2026-09-29T20:14:00.000Z"    }  ]}