✨ NoCaptcha v2 is here — fresh look, API v2 & agentic solving.Read the changelog →

Error Handling

Understand error codes and build resilient captcha-solving integrations.

A robust integration handles failures gracefully: it retries transient errors with backoff and fails fast on permanent ones. This page lists every error code the API can return and how to react to each.

Error response shape

When a request fails, the API returns a JSON body with a numeric code, a human-readable msg, and—for rate or capacity errors—a retryAfterSec hint telling you how long to wait before retrying.

{
  "code": 16,
  "msg": "RATE_LIMITED",
  "retryAfterSec": 5
}

Error codes

CodeNameMeaningWhat to do
1KEY_DOES_NOT_EXISTThe API key is missing, malformed, or invalid.Fix the key. Do not retry.
2NO_SLOT_AVAILABLENo worker slot is currently free for your account.Retry after a short backoff.
3ZERO_BALANCEAccount balance is exhausted.Top up credits. Do not retry.
10ERROR_BAD_PARAMETERSThe task payload is invalid or incomplete.Fix the request. Do not retry.
12ERROR_CAPTCHA_UNSOLVABLEThe captcha could not be solved.Retry, or resubmit with cleaner input.
14PLAN_EXPIREDYour subscription plan has expired.Renew the plan. Do not retry.
15PLAN_INACTIVEYour plan is not active.Activate the plan. Do not retry.
16RATE_LIMITEDToo many requests in a short window.Back off, respect retryAfterSec.
17DAILY_LIMIT_EXCEEDEDDaily request quota reached.Retry later (often next day).
18QUOTA_LIMIT_EXCEEDEDPlan quota reached.Retry later or upgrade the plan.
21SERVICE_UNAVAILABLEThe service is temporarily unavailable.Back off, respect retryAfterSec.

Retry strategy

Group errors into three buckets: retryable, permanent, and input-related. Only retry the first bucket, and always honor retryAfterSec when it is present.

  • Retryable with backoff: 2, 16, 17, 18, 21. These are transient (rate, quota, or capacity). Wait retryAfterSec if provided, otherwise use exponential backoff with jitter.
  • Permanent — fix your account or key: 1, 3, 14, 15. Retrying will not help; surface the error to the operator.
  • Bad request: 10. Fix the task payload before resending.
  • Unsolvable: 12. Retry a few times, or resubmit with a clearer image or corrected parameters.

Never retry permanent errors in a tight loop. Doing so wastes requests, can trigger RATE_LIMITED (code 16), and delays surfacing the real problem (an expired plan or empty balance).

Async flow with backoff (Python)

The example below submits a task with POST /createTask, then polls POST /getTaskResult until the status is ready. It treats 16, 17, 18, 21, and 2 as retryable and respects retryAfterSec.

import time
import random
import requests

BASE_URL = "https://api.nocaptchaai.com"
API_KEY = "YOUR_API_KEY"

RETRYABLE = {2, 16, 17, 18, 21}


def post(path, payload):
    resp = requests.post(f"{BASE_URL}{path}", json=payload, timeout=30)
    data = resp.json()
    code = data.get("code")
    if code in RETRYABLE:
        wait = data.get("retryAfterSec")
        raise RetryableError(wait)
    if code:  # non-zero code that is not retryable
        raise RuntimeError(f"{code}: {data.get('msg')}")
    return data


class RetryableError(Exception):
    def __init__(self, retry_after_sec=None):
        self.retry_after_sec = retry_after_sec


def with_backoff(fn, max_attempts=6):
    for attempt in range(max_attempts):
        try:
            return fn()
        except RetryableError as err:
            if attempt == max_attempts - 1:
                raise
            wait = err.retry_after_sec
            if wait is None:
                wait = min(2 ** attempt, 30) + random.random()
            time.sleep(wait)
    raise RuntimeError("exceeded max retry attempts")


def solve(task):
    created = with_backoff(lambda: post("/createTask", {
        "clientKey": API_KEY,
        "task": task,
    }))
    task_id = created["taskId"]

    while True:
        result = with_backoff(lambda: post("/getTaskResult", {
            "clientKey": API_KEY,
            "taskId": task_id,
        }))
        status = result.get("status")
        if status == "ready":
            return result["solution"]
        time.sleep(2)  # still processing; poll again


if __name__ == "__main__":
    solution = solve({"type": "ImageToTextTask", "body": "BASE64_IMAGE"})
    print(solution)

Cap your total polling time and number of attempts so a stuck task cannot block your worker indefinitely. A few seconds between polls keeps you well under the rate limit.

On this page

Content-Length: 0