Skip to content
ZeroCaptcha

Errors and retries

Most errors say something is wrong with the request, the key or the balance, and sending the same request again fails the same way. A few say only “not now”. Retry those, and nothing else, and your integration neither gives up too early nor hammers the API.

Format A failure is
REST A 4xx or 5xx status with an RFC 9457 problem document (application/problem+json): branch on its code
createTask HTTP 200 with errorId: 1, errorCode and errorDescription
2Captcha HTTP 200 with the code in place of the result, such as ERROR_ZERO_BALANCE

A request can also fail before it reaches the API’s own format, at a proxy, on a body that is too large, or on a server timeout. That comes back as a 4xx or 5xx status, so check the HTTP status as well as the body in every format. Every reply carries X-Request-Id; quote it when you write to support. The errors reference explains every code.

What to do about each error code
PolicyWhat your code doesCodes
retryRetry the same request with exponential backoff (a create with the same Idempotency-Key); honour Retry-After when present.internal_error, service_unavailable, request_timeout, payments_unavailable, ERROR_SERVICE_UNAVAILABLE
waitWait for Retry-After (or 2 to 5 seconds when absent), then send the same request again.idempotency_key_in_use, queue_full, rate_limited, ERROR_NO_SLOT_AVAILABLE, ERROR_RATE_LIMIT, ERROR_IDEMPOTENCY_KEY_IN_USE, MAX_USER_TURN, ERROR: 1005
fixDo not retry as is: fix the request, key, balance or setting the message names, then try again.bad_request, not_found, method_not_allowed, payload_too_large, unauthorized, invalid_credentials, key_revoked, key_limit_reached, key_state_conflict, csrf_rejected, insufficient_scope, ip_not_allowed, insufficient_funds, validation_failed, idempotency_key_reused, reauthentication_required, link_invalid, link_expired, link_used, weak_password, state_conflict, role_required, spend_cap_reached, email_taken, email_unverified, ERROR_TASK_ABSENT, ERROR_TASK_NOT_SUPPORTED, ERROR_INVALID_TASK_DATA, ERROR_KEY_DOES_NOT_EXIST, ERROR_KEY_REVOKED, ERROR_IP_NOT_ALLOWED, ERROR_ACCESS_DENIED, ERROR_ZERO_BALANCE, ERROR_IDEMPOTENCY_KEY_REUSED, ERROR_NO_SUCH_CAPCHA_ID, ERROR_REPORT_NOT_RECORDED, ERROR_DUPLICATE_REPORT, ERROR_INVALID_REQUEST, ERROR_SPEND_CAP_REACHED, ERROR_PROXY_NOT_ALLOWED, ERROR_WRONG_USER_KEY, ERROR_PAGEURL, ERROR_BAD_PARAMETERS, ERROR_PROXY_FORMAT, ERROR_EMPTY_ACTION, ERROR_WRONG_ID_FORMAT, ERROR_WRONG_CAPTCHA_ID, ERROR_BAD_PROXY
new-taskThe task is over and nothing more will come of it: create a new task if you still need a token.ERROR_TOKEN_EXPIRED, ERROR_CAPTCHA_UNSOLVABLE, ERROR_TASK_TIMEOUT
stopStop: do not send it again. Tell the user; a person must act (support, or not using this site).account_suspended, domain_blocked, ERROR_ACCOUNT_SUSPENDED, ERROR_DOMAIN_BLOCKED, ERROR_ACCOUNT_DELETED

In short: retry 429, 5xx, a lost connection or a timeout, and 409 idempotency_key_in_use. Everything else needs a change first, or a new task.

A task that failed or expired is final: reading it again will not change it. Create a new task if you still need a token; the failed one cost nothing.

  1. When the reply has Retry-After, wait that many seconds. rate_limited names the time until your budget has room; queue_full and idempotency_key_in_use say 2 seconds.
  2. Otherwise, back off: wait 1 second, then 2, 4, 8, and 16 seconds at most, each multiplied by a random factor between 0.5 and 1, so many clients that failed together do not retry together.
  3. Give up at a deadline you choose for the whole operation, such as 3 minutes for one solve, and give each request its own timeout, such as 15 seconds.
Terminal window
# Retries a request on 429, 5xx or no answer, waiting as Retry-After says, else 1, 2, 4, 8, 16 s.
attempt=0
while :; do
status=$(curl -s -o reply.json -D headers.txt -w '%{http_code}' --max-time 15 \
"$ZEROCAPTCHA_API/v1/tasks/$TASK_ID" -H "Authorization: Bearer $ZEROCAPTCHA_KEY") || status=000
case $status in 2??) break ;; 429 | 5?? | 000) ;; *) cat reply.json; exit 1 ;; esac
wait=$(tr -d '\r' < headers.txt | awk 'tolower($1) == "retry-after:" { print $2 }')
sleep "${wait:-$(( attempt < 4 ? 1 << attempt : 16 ))}"
attempt=$(( attempt + 1 ))
done
cat reply.json

Retrying a create is safe only if the retry cannot make a second task. Send an Idempotency-Key header with every create: POST /v1/tasks, POST /createTask or in.php.

  • The value is yours: 1 to 255 visible ASCII characters, such as a UUID. Make a new one for each task you mean to create, before the first attempt, and send the same one on every retry of that create.
  • For 24 hours from the first request, the same key with the same request returns the first reply, with the same task, instead of creating another. REST marks such a reply with Idempotent-Replayed: true.
  • The same key with a different request is refused with idempotency_key_reused (HTTP 422), or ERROR_IDEMPOTENCY_KEY_REUSED. Reuse a key only to retry exactly the same task.
  • While the first request is still being served, a retry gets idempotency_key_in_use (HTTP 409, Retry-After: 2), or ERROR_IDEMPOTENCY_KEY_IN_USE. Wait and send it again: you then get the first request’s reply.
  • To find the task a lost reply created, list your tasks with the key as a filter: GET /v1/tasks?idempotencyKey=…. The list holds that task, or nothing if the first request never arrived.

Reads (GET /v1/tasks/{id}, getTaskResult, getBalance) change nothing, so they need no key and can always be retried.

A refused request is never charged, whatever its code. A task that fails or expires is released in full. Only a task that succeeds is charged, even if you read it after its token expired (ERROR_TOKEN_EXPIRED): use tokens as soon as they are ready.