Migrate a createTask client
More
Clients written for CapSolver’s or Anti-Captcha’s JSON API call createTask, then getTaskResult
until the task is ready. ZeroCaptcha answers the same calls, with the same shapes, for Cloudflare
Turnstile and Cloudflare challenge pages. The change is the host and the key, plus a look at the
field names your client sends.
ZeroCaptcha is not affiliated with CapSolver or Anti-Captcha. This guide names them only to show what to change.
Before you start
Section titled “Before you start”- Sign up, create a key and add funds. Keep the key in your environment, as
ZEROCAPTCHA_KEY, and the API’s address asZEROCAPTCHA_API. - Check that your code keeps task IDs as text: ZeroCaptcha’s are UUIDs.
- Check the task types you send. ZeroCaptcha takes
TurnstileTaskProxyless,TurnstileTask,CloudflareChallengeTask, and the same names with CapSolver’sAntiprefix (AntiTurnstileTaskProxyLess,AntiTurnstileTaskandAntiCloudflareTask), in any case. Any other type isERROR_TASK_NOT_SUPPORTED.
Change the host and the key
Section titled “Change the host and the key”The task stays as your client sends it, with the widget’s action and cData in metadata: many
sites check both when they verify the token, so keep sending them. See
action and cData. An Idempotency-Key header, new to most of these
clients, makes a retried create return the same task.
# Before: https://api.capsolver.com/createTask or https://api.anti-captcha.com/createTask# After. 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.curl "$ZEROCAPTCHA_API/createTask" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"task\": {\"type\": \"AntiTurnstileTaskProxyLess\", \"websiteURL\": \"https://shop.example.com/login\", \"websiteKey\": \"0x4AAAAAAAB1cD2eF3gH4iJ5\", \"metadata\": {\"action\": \"login\", \"cdata\": \"session-7f3a9c2e\"}}}"// Before// const BASE = "https://api.capsolver.com";// const clientKey = process.env.CAPSOLVER_KEY;
// Afterconst BASE = process.env.ZEROCAPTCHA_API;const clientKey = process.env.ZEROCAPTCHA_KEY;
const created = await fetch(`${BASE}/createTask`, { method: "POST", // One Idempotency-Key per task: a retried create with it returns the same task. headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ clientKey, task: { type: "AntiTurnstileTaskProxyLess", websiteURL: "https://shop.example.com/login", websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", // 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" }, }, }),}).then((response) => response.json());console.log(created.errorId === 0 ? created.taskId : created.errorCode);import osimport uuid
import requests
# Before# BASE = "https://api.anti-captcha.com"# CLIENT_KEY = os.environ["ANTICAPTCHA_KEY"]
# AfterBASE = os.environ["ZEROCAPTCHA_API"]CLIENT_KEY = os.environ["ZEROCAPTCHA_KEY"]
created = requests.post( f"{BASE}/createTask", json={ "clientKey": CLIENT_KEY, "task": { "type": "TurnstileTaskProxyless", "websiteURL": "https://shop.example.com/login", "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", # 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"}, }, }, # One Idempotency-Key per task: a retried create with it returns the same task. headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=15,).json()print(created["taskId"] if created["errorId"] == 0 else created["errorCode"])// Before:// base, clientKey := "https://api.capsolver.com", os.Getenv("CAPSOLVER_KEY")
// After:base, clientKey := os.Getenv("ZEROCAPTCHA_API"), os.Getenv("ZEROCAPTCHA_KEY")
body, _ := json.Marshal(map[string]any{ "clientKey": clientKey, "task": map[string]any{ "type": "AntiTurnstileTaskProxyLess", "websiteURL": "https://shop.example.com/login", "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", // 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"}, },})req, _ := http.NewRequest(http.MethodPost, base+"/createTask", bytes.NewReader(body))req.Header.Set("Content-Type", "application/json")// One Idempotency-Key per task: a retried create with it returns the same task.req.Header.Set("Idempotency-Key", rand.Text())resp, err := http.DefaultClient.Do(req)<?php// Before// $base = 'https://api.anti-captcha.com';// $clientKey = getenv('ANTICAPTCHA_KEY');
// After$base = getenv('ZEROCAPTCHA_API');$clientKey = getenv('ZEROCAPTCHA_KEY');
$created = json_decode(file_get_contents("$base/createTask", false, stream_context_create(['http' => [ 'method' => 'POST', // One Idempotency-Key per task: a retried create with it returns the same task. 'header' => "Content-Type: application/json\r\nIdempotency-Key: " . bin2hex(random_bytes(16)), 'content' => json_encode([ 'clientKey' => $clientKey, 'task' => [ 'type' => 'TurnstileTaskProxyless', 'websiteURL' => 'https://shop.example.com/login', 'websiteKey' => '0x4AAAAAAAB1cD2eF3gH4iJ5', // 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'], ], ]),]])), true);echo $created['errorId'] === 0 ? $created['taskId'] : $created['errorCode'], PHP_EOL;getTaskResult and getBalance move the same way: the same bodies, on ZeroCaptcha’s host. If you
use a client library, point it at the API’s address if it lets you set the host; if you are not sure
it does, call the API over HTTP as above.
Check the field names
Section titled “Check the field names”ZeroCaptcha reads the spellings these clients send, and ignores fields it does not use, so a field under a name it does not know is silently left out. Check yours against these:
| What | Names ZeroCaptcha reads |
|---|---|
| The page | websiteURL, websiteUrl |
| The site key | websiteKey |
| The action | action, pageAction, metadata.action |
| The cData | cdata, cData, data, turnstileCData, metadata.cdata (or metadata.cData) |
| A proxy | proxy as a URL, or proxyType, proxyAddress, proxyPort, proxyLogin, proxyPassword |
| A callback | callbackUrl, beside task |
Each of these is read as the clients send it, Anti-Captcha’s cData included. A cData under any
other name is left out, and the site may then refuse the token.
CapMonster Cloud’s clients send pageAction and data, both read. Its cloudflareTaskType may be
token or left out; its cf_clearance mode is refused with ERROR_TASK_NOT_SUPPORTED, as the
clearance would be tied to a user agent the client did not choose. Use CloudflareChallengeTask
with your proxy instead: its reply gives the clearance and the user agent to send it with.
Replies
Section titled “Replies”- A created task:
{"errorId": 0, "taskId": "…"}. - While it runs:
{"errorId": 0, "status": "processing"}. - Solved:
status: "ready",solution.token, andcost,createTime,endTime,solveCountandexpiresAt. A challenge page’s solution also hasuserAgentandcookies.cf_clearance, as CapSolver’sAntiCloudflareTaskanswers. - Failed:
errorId: 1witherrorCode, such asERROR_CAPTCHA_UNSOLVABLE, and nothing charged. - Balance:
{"errorId": 0, "balance": 12.3456}, in US dollars. - Reports:
reportIncorrect,reportCorrect, Anti-Captcha’sreportIncorrectRecaptchaandreportCorrectRecaptcha, and CapSolver’sfeedbackTaskanswer{"errorId": 0, "status": "success"}. Each is recorded for our staff and refunds nothing, as every charge is final.
Every reply is HTTP 200; a failure is errorId: 1. See createTask format for
every field and the errors reference for every code.
What to check after you switch
Section titled “What to check after you switch”- Proxies:
httpandhttpsonly; SOCKS is not supported yet. A challenge page always needs your proxy: there is no proxyless challenge task. - Errors:
ERROR_RATE_LIMITasks you to slow your polling;ERROR_NO_SLOT_AVAILABLEmeans your account’s share of the queue is full for a moment. Both come with a wait: see Errors and retries. - Money: prepaid US dollars, charged only when a task is solved. See pricing.
- Idempotency: send an
Idempotency-Keyheader with eachcreateTask, so a retry after a lost reply returns the first task instead of creating a second.
Roll it out one service at a time, with a key of its own and a daily spend cap, and watch its tasks in the dashboard.