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. |
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.