Error codes
Every error the API returns, what it means, and what to do about it.
HTTP is always 200, including for errors. That is the anti-captcha contract and we keep it, so a client written for CapSolver does not need its error handling rewritten. Branch on errorId: 0 means success, 1 means read errorCode.
{
"errorId": 1,
"errorCode": "ERROR_KEY_DOES_NOT_EXIST",
"errorDescription": "No API key matches that clientKey."
}| Code | Meaning | What to do |
|---|---|---|
ERROR_KEY_DOES_NOT_EXIST | No API key matches the clientKey you sent. | Check for a copy-paste error or a stale environment variable. Keys are shown in full only once, at creation. |
ERROR_ZERO_BALANCE | Your wallet cannot cover the price of the task. | Top up. Tasks already in flight are unaffected and are still refunded if they fail. |
ERROR_TASK_TYPE_NOT_SUPPORTED | The task.type value is not one we handle. | Compare it against the task types reference. The value is case-insensitive but the spelling must match. |
ERROR_NO_SUCH_TASK | That taskId does not exist, or does not belong to your key. | Results are retained for 5 minutes after completion. Poll sooner, and never share a taskId across accounts. |
ERROR_RATE_LIMIT | Too many requests in the current window. | Back off and retry. The task itself is unaffected — keep the same taskId and keep polling. |
ERROR_KEY_DISABLED | The key exists but has been disabled. | Re-enable it in the dashboard, or generate a new one. Keys are disabled by you, or by us after an acceptable-use investigation. |
ERROR_UPSTREAM | The solver refused or crashed on this task. | You have been refunded. Retry once; if it repeats on the same target, send us the sitekey and page URL. |
ERROR_TIMEOUT | The task did not resolve within its deadline. | You have been refunded. Hardened targets sometimes need a proxy or an exact User-Agent — try the proxy variant of the task type. |
ERROR_PROXY_REQUIRED | A DataDome task arrived without a proxy, or asked for a ProxyLess variant. | Supply your proxy. DataDome binds the cookie to the exit IP, so a solve from any other address is worthless — and replaying it hard-bans that IP. There is no proxyless DataDome. |
ERROR_IP_BANNED | The captchaUrl you sent carries t=bv: DataDome has already banned that proxy IP and serves no CAPTCHA. | Nothing is served, so nothing is charged. Rotate to a fresh proxy IP and read a new captchaUrl before retrying. |
Solve outcome codes
The codes above come from the gateway — your key, your balance, your rate limit. When a solve itself fails, the solver's own code passes straight through. You are always refunded for these; the action column says whether retrying is worth it and, when the cause is your proxy, that it is yours to fix.
| Code | Meaning | What to do |
|---|---|---|
CAPTCHA_UNSOLVABLE | We ran the solve but could not produce a valid token or cookie for this target. | Refunded. Retry once; if it repeats on the same sitekey, send it to us — some targets are genuinely hard and worth tuning for. |
TASK_TIMEOUT | The solve did not finish within its deadline. | Refunded. Usually a slow or overloaded target; retry, and on a hardened site supply a residential proxy. |
CHALLENGE_NOT_PRESENT | For a Cloudflare cookie task, the page served directly — there was no challenge to clear on this IP. | Refunded, and nothing to do: the page is already reachable from that IP. If you expected a challenge, it fires from datacenter IPs, not clean residential ones. |
PROXY_BANNED | The target kept looping its Cloudflare challenge and never issued a token — this exit IP is refused by that site. | Refunded. Retry with a different, higher-trust proxy (residential or mobile). No amount of retrying helps the same refused IP. |
PROXY_CONNECTION_FAILED | We could not open a connection through the proxy you supplied. | Refunded. Check the proxy host, port and that it is up. This is your proxy, not the target. |
PROXY_AUTH_FAILED | The proxy rejected the credentials you supplied. | Refunded. Check proxyLogin and proxyPassword. |
PROXY_INCOMPATIBLE | The proxy could not carry the request — usually SOCKS-only, transparent, or a broken TLS certificate. | Refunded. Use an HTTP(S) proxy that supports CONNECT tunnelling. |
INVALID_SITEKEY | The websiteKey was rejected by the target — wrong, expired, or for a different domain. | Refunded. Copy the sitekey from the page source of the exact URL you are solving. |
INVALID_USERAGENT | The userAgent you sent is not a current browser User-Agent. | Refunded. Send a real, recent browser UA, or omit the field and we use our own. |
INVALID_TASK_DATA | A field the task type requires is missing or malformed (for example a websiteKey on a widget task). | Refunded. Check the create-task field table for the type you are sending. |
Handling them
Only two codes are worth retrying automatically. ERROR_RATE_LIMIT means back off and keep the same task. ERROR_UPSTREAM and ERROR_TIMEOUT mean the task is dead and refunded — retry by creating a new one, at most once, or you will spend your budget on a target that is not going to solve. Everything else needs a human.
const result = await post('/getTaskResult', { taskId });
if (result.errorId !== 0) {
switch (result.errorCode) {
case 'ERROR_RATE_LIMIT':
// Back off and retry the same taskId — the task is still alive.
await sleep(2000);
continue;
case 'ERROR_ZERO_BALANCE':
case 'ERROR_KEY_DISABLED':
// Nothing to retry. Alert an operator.
throw new Error(result.errorDescription);
default:
// Refunded already; create a fresh task if it is worth retrying.
throw new Error(`${result.errorCode}: ${result.errorDescription}`);
}
}An error response never leaves you charged. If you ever see a debit without a matching credit for a failed task, that is a bug — send us the task id and we will fix it and refund it.