createTask format
More
ZeroCaptcha speaks the createTask format that CapSolver, Anti-Captcha and 2Captcha’s JSON API
use, so a client written for one of them solves Turnstile and Cloudflare challenge pages here after
two changes: its base URL, to our API’s address, and its key, to your ZeroCaptcha key. A task made
this way is priced, held and charged exactly as one made with REST.
ZeroCaptcha is not affiliated with CapSolver, Anti-Captcha or 2Captcha. Their names appear here only to say which format this is.
The calls
Section titled “The calls”| Call | Body | Reply |
|---|---|---|
POST /createTask |
{"clientKey": "…", "task": {…}}, and optionally callbackUrl |
{"errorId": 0, "taskId": "…"} |
POST /getTaskResult |
{"clientKey": "…", "taskId": "…"} |
processing, then ready with the solution |
POST /getBalance |
{"clientKey": "…"} |
{"errorId": 0, "balance": 12.3456} |
POST /reportIncorrect, POST /reportCorrect |
{"clientKey": "…", "taskId": "…"} |
{"errorId": 0, "status": "success"} |
POST /reportIncorrectRecaptcha, POST /reportCorrectRecaptcha |
The same, as Anti-Captcha’s clients send it | The same |
POST /feedbackTask |
{"clientKey": "…", "taskId": "…", "result": {"invalid": true}}, as CapSolver’s clients send it |
The same |
- Every reply is HTTP 200. Success is
errorId: 0; a failure iserrorId: 1witherrorCodeanderrorDescription. Only a failure outside the format, such as a body over 64 KiB, a server timeout or the service shedding load, answers with an HTTP error and a problem document, so check the status too. - The body is read leniently, as these clients send it: JSON whatever the
Content-Type(some sendtext/plain), a number wherever text is expected,nullfor an absent field, and fields it does not use are ignored. Idempotency-Keyworks oncreateTaskas on REST: send the same header again within 24 hours and you get the first task back instead of a second one.clientKeyis your API key,zc_live_…, 41 characters.
Cloudflare Turnstile task
Section titled “Cloudflare Turnstile task”| Field | Required | Other spellings | What it is |
|---|---|---|---|
type |
Yes | TurnstileTaskProxyless, or TurnstileTask through your proxy; CapSolver’s AntiTurnstileTaskProxyLess too, and AntiTurnstileTask for the proxy variant. Case does not matter. |
|
websiteURL |
Yes | websiteUrl |
The page with the widget. |
websiteKey |
Yes | The widget’s site key. | |
action |
When the widget sets one | pageAction (CapMonster Cloud’s), metadata.action |
The widget’s action: its data-action, or the action option of turnstile.render(). |
cdata |
When the widget sets one | cData, data, turnstileCData, metadata.cdata, metadata.cData |
The widget’s cData: its data-cdata, or the cData option of turnstile.render(). |
proxy |
With TurnstileTask |
the proxyAddress fields |
Your proxy as a URL: http://user:pass@proxy.example.net:8080. |
proxyType, proxyAddress, proxyPort, proxyLogin, proxyPassword |
Instead of proxy |
Your proxy in parts: proxyType http (the default) or https, a public host, a port (a number or a string of digits), and an optional login and password of at most 255 bytes each. |
|
cloudflareTaskType |
No | CapMonster Cloud’s mode: token, or absent, for the widget’s token. Its cf_clearance and wait_room modes are refused with ERROR_TASK_NOT_SUPPORTED, before anything is made or charged: for a challenge page’s clearance, use the challenge page task. |
Most createTask clients send the action and cData nested in the task’s metadata, as
"metadata": {"action": "login", "cdata": "session-7f3a9c2e"}, and the samples below do too.
Many sites check both when they verify the token, so send them whenever the widget sets them:
see Cloudflare Turnstile action and cData.
Where a field has more than one spelling, the first one sent wins, in the order listed. The limits are REST’s: see Solving Cloudflare Turnstile. SOCKS proxies are not supported yet.
Challenge page task
Section titled “Challenge page task”| Field | Required | What it is |
|---|---|---|
type |
Yes | CloudflareChallengeTask, or CapSolver’s AntiCloudflareTask. |
websiteURL or websiteUrl |
Yes | The page behind the challenge. |
proxy, or the proxyAddress fields |
Yes | Your proxy: the clearance works only from its address. |
A websiteKey, action or cdata is ignored, as are fields other providers take for a challenge
page, such as userAgent and html. A proxyless challenge task, such as
CloudflareChallengeTaskProxyless or AntiCloudflareTaskProxyLess, is refused with
ERROR_TASK_NOT_SUPPORTED. See Cloudflare WAF and 5-second challenges.
Replies of getTaskResult
Section titled “Replies of getTaskResult”While the task runs:
{ "errorId": 0, "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "status": "processing" }Solved, a Turnstile task:
{ "errorId": 0, "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "status": "ready", "solution": { "token": "0.Zm9vYmFy…", "type": "turnstile" }, "cost": "0.000800", "createTime": 1790776925, "endTime": 1790776934, "solveCount": 1, "expiresAt": "2026-09-30T14:07:14Z"}Solved, a challenge page: solution has "type": "cloudflare", userAgent, and
"cookies": {"cf_clearance": "…"} beside token, as CapSolver’s AntiCloudflareTask answers.
Failed, or not solved in time (nothing is charged):
{ "errorId": 1, "errorCode": "ERROR_CAPTCHA_UNSOLVABLE", "errorDescription": "Every attempt to solve the challenge failed. Nothing was charged.", "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "status": "failed", "cost": "0.000000"}Solved, but read after its token expired: errorId: 1, ERROR_TOKEN_EXPIRED, "status": "ready"
and the cost it was charged. Use tokens as soon as they are ready.
| Field | Meaning |
|---|---|
status |
processing until the task ends, then ready; failed on an error reply for a task that did not succeed |
solution.token |
The Turnstile token, or the cf_clearance cookie’s value |
expiresAt |
When the token stops being accepted, in UTC |
cost |
What the task cost, in US dollars with six decimals |
createTime, endTime |
When it was created and solved, in Unix seconds |
solveCount |
The solve attempts it took |
Samples
Section titled “Samples”# metadata holds the widget's data-action and data-cdata, or the action and cData options of# turnstile.render(): leave out any the widget does not set. For your own proxy, make the type# TurnstileTask and add "proxy": "http://user:pass@proxy.example.net:8080" to the task; to be# called when it ends, add "callbackUrl": "https://hooks.example.com/zerocaptcha" beside it.reply=$(curl -sS "$ZEROCAPTCHA_API/createTask" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"task\": {\"type\": \"TurnstileTaskProxyless\", \"websiteURL\": \"https://example.com/login\", \"websiteKey\": \"0x4AAAAAAAB1cD2eF3gH4iJ5\", \"metadata\": {\"action\": \"login\", \"cdata\": \"session-7f3a9c2e\"}}}")# errorId 1 is a refusal; its errorCode and errorDescription say why.if [ "$(jq -r .errorId <<<"$reply")" != 0 ]; then echo "$reply" >&2; exit 1; fiTASK_ID=$(jq -r .taskId <<<"$reply")
# Every 2 seconds, until "status" is ready, or errorId 1 says the task failed:curl "$ZEROCAPTCHA_API/getTaskResult" -H "Content-Type: application/json" \ -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"taskId\": \"$TASK_ID\"}"const api = process.env.ZEROCAPTCHA_API;const clientKey = process.env.ZEROCAPTCHA_KEY;const post = async (path, body, headers = {}) => { const response = await fetch(`${api}${path}`, { method: "POST", headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify({ clientKey, ...body }), }); if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`); const reply = await response.json(); if (reply.errorId !== 0) throw new Error(`${reply.errorCode}: ${reply.errorDescription}`); return reply;};
const { taskId } = await post( "/createTask", { task: { type: "TurnstileTaskProxyless", // or "TurnstileTask", with proxy below websiteURL: "https://example.com/login", // the page with the widget websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", // its data-sitekey // The widget's data-action and data-cdata, or the action and cData options of // turnstile.render(). Leave out any the widget does not set. metadata: { action: "login", cdata: "session-7f3a9c2e" }, // proxy: "http://user:pass@proxy.example.net:8080", // TurnstileTask only }, // callbackUrl: "https://hooks.example.com/zerocaptcha", // to be called when it ends }, { "Idempotency-Key": crypto.randomUUID() },);let result;do { await new Promise((resolve) => setTimeout(resolve, 2000)); result = await post("/getTaskResult", { taskId });} while (result.status === "processing");console.log(result.solution.token);import osimport timeimport uuid
import requests
API = os.environ["ZEROCAPTCHA_API"]KEY = os.environ["ZEROCAPTCHA_KEY"]
def post(path, body, headers=None): response = requests.post(f"{API}{path}", json={"clientKey": KEY, **body}, headers=headers, timeout=15) response.raise_for_status() reply = response.json() if reply["errorId"] != 0: raise RuntimeError(f"{reply['errorCode']}: {reply['errorDescription']}") return reply
task_id = post( "/createTask", { "task": { "type": "TurnstileTaskProxyless", # or "TurnstileTask", with proxy below "websiteURL": "https://example.com/login", # the page with the widget "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", # its data-sitekey # The widget's data-action and data-cdata, or the action and cData options of # turnstile.render(). Leave out any the widget does not set. "metadata": {"action": "login", "cdata": "session-7f3a9c2e"}, # "proxy": "http://user:pass@proxy.example.net:8080", # TurnstileTask only }, # "callbackUrl": "https://hooks.example.com/zerocaptcha", # to be called when it ends }, headers={"Idempotency-Key": str(uuid.uuid4())},)["taskId"]while True: time.sleep(2) result = post("/getTaskResult", {"taskId": task_id}) if result["status"] != "processing": breakprint(result["solution"]["token"])package main
import ( "bytes" "crypto/rand" "encoding/json" "fmt" "net/http" "os" "time")
type reply struct { ErrorID int `json:"errorId"` ErrorCode string `json:"errorCode"` ErrorDescription string `json:"errorDescription"` TaskID string `json:"taskId"` Status string `json:"status"` Solution struct { Token string `json:"token"` } `json:"solution"`}
func post(path string, body map[string]any, idempotencyKey string) (reply, error) { var r reply body["clientKey"] = os.Getenv("ZEROCAPTCHA_KEY") payload, _ := json.Marshal(body) req, err := http.NewRequest(http.MethodPost, os.Getenv("ZEROCAPTCHA_API")+path, bytes.NewReader(payload)) if err != nil { return r, err } req.Header.Set("Content-Type", "application/json") if idempotencyKey != "" { req.Header.Set("Idempotency-Key", idempotencyKey) } resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req) if err != nil { return r, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return r, fmt.Errorf("%s: HTTP %d", path, resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { return r, err } if r.ErrorID != 0 { return r, fmt.Errorf("%s: %s", r.ErrorCode, r.ErrorDescription) } return r, nil}
func main() { created, err := post("/createTask", map[string]any{ "task": map[string]any{ "type": "TurnstileTaskProxyless", // or "TurnstileTask", with proxy below "websiteURL": "https://example.com/login", // the page with the widget "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", // its data-sitekey // The widget's data-action and data-cdata, or the action and cData options of // turnstile.render(). Leave out any the widget does not set. "metadata": map[string]string{"action": "login", "cdata": "session-7f3a9c2e"}, // "proxy": "http://user:pass@proxy.example.net:8080", // TurnstileTask only }, // "callbackUrl": "https://hooks.example.com/zerocaptcha", // to be called when it ends }, rand.Text()) result := created for err == nil && (result.Status == "" || result.Status == "processing") { time.Sleep(2 * time.Second) result, err = post("/getTaskResult", map[string]any{"taskId": created.TaskID}, "") } if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println(result.Solution.Token)}<?phpfunction zerocaptcha(string $path, array $body, array $headers = []): array{ $body['clientKey'] = getenv('ZEROCAPTCHA_KEY'); $curl = curl_init(getenv('ZEROCAPTCHA_API') . $path); curl_setopt_array($curl, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json'], $headers), CURLOPT_POSTFIELDS => json_encode($body), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, ]); $raw = curl_exec($curl); $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); if ($raw === false || $status !== 200) { throw new RuntimeException("$path: HTTP $status"); } $reply = json_decode($raw, true); if ($reply['errorId'] !== 0) { throw new RuntimeException("{$reply['errorCode']}: {$reply['errorDescription']}"); } return $reply;}
$created = zerocaptcha('/createTask', [ 'task' => [ 'type' => 'TurnstileTaskProxyless', // or 'TurnstileTask', with 'proxy' below 'websiteURL' => 'https://example.com/login', // the page with the widget 'websiteKey' => '0x4AAAAAAAB1cD2eF3gH4iJ5', // its data-sitekey // The widget's data-action and data-cdata, or the action and cData options of // turnstile.render(). Leave out any the widget does not set. 'metadata' => ['action' => 'login', 'cdata' => 'session-7f3a9c2e'], // 'proxy' => 'http://user:pass@proxy.example.net:8080', // TurnstileTask only ], // 'callbackUrl' => 'https://hooks.example.com/zerocaptcha', // to be called when it ends], ['Idempotency-Key: ' . bin2hex(random_bytes(16))]);do { sleep(2); $result = zerocaptcha('/getTaskResult', ['taskId' => $created['taskId']]);} while ($result['status'] === 'processing');echo $result['solution']['token'], PHP_EOL;Reports
Section titled “Reports”A report says whether the site took a solved task’s token: reportIncorrect (or Anti-Captcha’s
reportIncorrectRecaptcha, or CapSolver’s feedbackTask with "invalid": true) that it refused
it, and reportCorrect (or reportCorrectRecaptcha, or "invalid": false) that it took it. Each is
recorded against the task, where our staff read it to find sites and settings that fail.
Nothing is refunded: a task is charged only when it is solved, and every charge is final. A task
takes one report: a second is ERROR_DUPLICATE_REPORT, and a report of a task that was not solved is
ERROR_REPORT_NOT_RECORDED. A key needs tasks:write to report.
Error codes
Section titled “Error codes”Every errorCode of this format, with whether a retry helps and what it costs, is in the
errors reference, and a task’s own failures under
task outcomes. The ones clients meet most:
| Code | What to do |
|---|---|
ERROR_KEY_DOES_NOT_EXIST, ERROR_KEY_REVOKED |
Use a working key from the dashboard. |
ERROR_ZERO_BALANCE |
Add funds, then create the task again. |
ERROR_TASK_NOT_SUPPORTED, ERROR_INVALID_TASK_DATA |
Fix the task as errorDescription says. |
ERROR_NO_SLOT_AVAILABLE, ERROR_RATE_LIMIT |
Wait a few seconds (or Retry-After), then retry. |
ERROR_CAPTCHA_UNSOLVABLE, ERROR_TASK_TIMEOUT |
The task failed and cost nothing: create a new one. |
ERROR_TOKEN_EXPIRED |
The task was solved and charged; create a new one and use its token at once. |
ERROR_NO_SUCH_CAPCHA_ID |
Poll with the taskId createTask gave, with a key of the same account. |
What differs from other providers
Section titled “What differs from other providers”- Task IDs are ZeroCaptcha’s own, UUIDs such as
0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b, not numbers. A client that keeps the ID as text passes it back unchanged. - Only Turnstile and Cloudflare challenge pages are offered; other task types are refused with
ERROR_TASK_NOT_SUPPORTED. callbackUrlbesidetasknames a callback, signed so you can check it.- There is no free trial or test key: every task is real, and charged only when it is solved.