Migrate a 2Captcha client
More
Code that solves Cloudflare Turnstile through 2Captcha moves to ZeroCaptcha without a rewrite:
ZeroCaptcha serves both of the formats 2Captcha documents for Turnstile, in.php and res.php, and
the JSON createTask format. In most cases the change is the host and the key.
ZeroCaptcha is not affiliated with 2Captcha. This guide names it 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 how your code keeps task IDs.
in.phpanswers numbers, as 2Captcha does, such as10000004821. The createTask format answers UUIDs, such as0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b, so code that uses it must keep the ID as text. - Check what your code asks for: ZeroCaptcha serves
method=turnstileonly, and challenge pages through the createTask format or REST.
Your 2Captcha balance does not move: ZeroCaptcha is prepaid separately, in US dollars.
in.php and res.php
Section titled “in.php and res.php”The host and the key change; the parameters and replies stay. Keep sending the widget’s action
and cData as action and data, as 2Captcha documents them: many sites check both when they
verify the token. See action and cData.
# action and data are 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.# Beforecurl "https://2captcha.com/in.php?key=$TWOCAPTCHA_KEY&method=turnstile&sitekey=0x4AAAAAAAB1cD2eF3gH4iJ5&pageurl=https://shop.example.com/login&action=login&data=session-7f3a9c2e&json=1"
# After, with an Idempotency-Key so a retried submit returns the same task:curl -H "Idempotency-Key: $(uuidgen)" \ "$ZEROCAPTCHA_API/in.php?key=$ZEROCAPTCHA_KEY&method=turnstile&sitekey=0x4AAAAAAAB1cD2eF3gH4iJ5&pageurl=https://shop.example.com/login&action=login&data=session-7f3a9c2e&json=1"// Before// const BASE = "https://2captcha.com";// const KEY = process.env.TWOCAPTCHA_KEY;
// Afterconst BASE = process.env.ZEROCAPTCHA_API;const KEY = process.env.ZEROCAPTCHA_KEY;
const params = new URLSearchParams({ key: KEY, method: "turnstile", sitekey: "0x4AAAAAAAB1cD2eF3gH4iJ5", pageurl: "https://shop.example.com/login", // 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. action: "login", data: "session-7f3a9c2e", json: "1",});// One Idempotency-Key per task: a retried submit with it returns the same task.const headers = { "Idempotency-Key": crypto.randomUUID() };const submitted = await fetch(`${BASE}/in.php?${params}`, { headers }).then((response) => response.json());if (submitted.status !== 1) throw new Error(`${submitted.request}: ${submitted.error_text}`);console.log(submitted.request); // the task ID, as textimport osimport uuid
import requests
# Before# BASE = "https://2captcha.com"# KEY = os.environ["TWOCAPTCHA_KEY"]
# AfterBASE = os.environ["ZEROCAPTCHA_API"]KEY = os.environ["ZEROCAPTCHA_KEY"]
submitted = requests.get( f"{BASE}/in.php", params={ "key": KEY, "method": "turnstile", "sitekey": "0x4AAAAAAAB1cD2eF3gH4iJ5", "pageurl": "https://shop.example.com/login", # 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. "action": "login", "data": "session-7f3a9c2e", "json": 1, }, # One Idempotency-Key per task: a retried submit with it returns the same task. headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=15,).json()if submitted["status"] != 1: raise RuntimeError(f"{submitted['request']}: {submitted.get('error_text')}")print(submitted["request"]) # the task ID, as text// Before:// base, key := "https://2captcha.com", os.Getenv("TWOCAPTCHA_KEY")
// After:base, key := os.Getenv("ZEROCAPTCHA_API"), os.Getenv("ZEROCAPTCHA_KEY")
query := url.Values{ "key": {key}, "method": {"turnstile"}, "sitekey": {"0x4AAAAAAAB1cD2eF3gH4iJ5"}, "pageurl": {"https://shop.example.com/login"}, // 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. "action": {"login"}, "data": {"session-7f3a9c2e"}, "json": {"1"},}req, _ := http.NewRequest(http.MethodGet, base+"/in.php?"+query.Encode(), nil)// One Idempotency-Key per task: a retried submit with it returns the same task.req.Header.Set("Idempotency-Key", rand.Text())resp, err := http.DefaultClient.Do(req)<?php// Before// $base = 'https://2captcha.com';// $key = getenv('TWOCAPTCHA_KEY');
// After$base = getenv('ZEROCAPTCHA_API');$key = getenv('ZEROCAPTCHA_KEY');
$query = http_build_query([ 'key' => $key, 'method' => 'turnstile', 'sitekey' => '0x4AAAAAAAB1cD2eF3gH4iJ5', 'pageurl' => 'https://shop.example.com/login', // 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. 'action' => 'login', 'data' => 'session-7f3a9c2e', 'json' => 1,]);// One Idempotency-Key per task: a retried submit with it returns the same task.$context = stream_context_create(['http' => ['header' => 'Idempotency-Key: ' . bin2hex(random_bytes(16))]]);$submitted = json_decode(file_get_contents("$base/in.php?$query", false, $context), true);if ($submitted['status'] !== 1) { throw new RuntimeException("{$submitted['request']}: {$submitted['error_text']}");}echo $submitted['request'], PHP_EOL; // the task ID, as textPolling res.php?action=get&id=… is the same: CAPCHA_NOT_READY, then OK|<token> or an error
code. 2Captcha format lists every parameter and code.
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.
The createTask format
Section titled “The createTask format”2Captcha’s JSON API moves the same way: post the same body to /createTask and /getTaskResult on
ZeroCaptcha’s host, with your ZeroCaptcha key as clientKey. The samples send the widget’s action
and cData in the task’s metadata, as most createTask clients do; a 2Captcha client that sends them
as action and data works unchanged.
# Before: https://api.2captcha.com/createTask with your 2Captcha key as clientKey.# After, with the widget's data-action and data-cdata (or turnstile.render()'s action and cData) in# metadata, and an Idempotency-Key so a retried create returns the same task:curl "$ZEROCAPTCHA_API/createTask" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \ -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"task\": {\"type\": \"TurnstileTaskProxyless\", \"websiteURL\": \"https://shop.example.com/login\", \"websiteKey\": \"0x4AAAAAAAB1cD2eF3gH4iJ5\", \"metadata\": {\"action\": \"login\", \"cdata\": \"session-7f3a9c2e\"}}}"// Before: const BASE = "https://api.2captcha.com";const BASE = process.env.ZEROCAPTCHA_API;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: process.env.ZEROCAPTCHA_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" }, }, }),}).then((response) => response.json());if (created.errorId !== 0) throw new Error(`${created.errorCode}: ${created.errorDescription}`);console.log(created.taskId);import osimport uuid
import requests
# Before: BASE = "https://api.2captcha.com"BASE = os.environ["ZEROCAPTCHA_API"]created = requests.post( f"{BASE}/createTask", json={ "clientKey": os.environ["ZEROCAPTCHA_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()if created["errorId"] != 0: raise RuntimeError(f"{created['errorCode']}: {created['errorDescription']}")print(created["taskId"])// Before: base := "https://api.2captcha.com"base := os.Getenv("ZEROCAPTCHA_API")body, _ := json.Marshal(map[string]any{ "clientKey": os.Getenv("ZEROCAPTCHA_KEY"), "task": map[string]any{ "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": 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.2captcha.com';$base = getenv('ZEROCAPTCHA_API');$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' => getenv('ZEROCAPTCHA_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'], ], ]),]])), true);if ($created['errorId'] !== 0) { throw new RuntimeException("{$created['errorCode']}: {$created['errorDescription']}");}echo $created['taskId'], PHP_EOL;The widget’s cData goes in data, cdata, cData, turnstileCData, metadata.cdata or
metadata.cData, and its action in action, pageAction or metadata.action. A field under any other name is ignored, so check the names your code
sends. See createTask format.
What to check after you switch
Section titled “What to check after you switch”- Task IDs are numbers in
in.phpandres.php, and UUIDs, kept as text, in the createTask format. - Pingbacks need no registration: any public URL works. Each call is signed; see Polling and callbacks to check it.
- Reports:
reportbad,reportgoodand the createTask format’sreportIncorrectandreportCorrectare recorded for our staff and never refunded: every charge is final. - Not offered: CAPTCHA types other than Cloudflare Turnstile.
- Money: balances and prices are in US dollars;
getbalanceanswers the available balance, and a task is charged only when it is solved. See pricing. - Errors: most codes are 2Captcha’s own; a few are ZeroCaptcha’s, such as
ERROR_SPEND_CAP_REACHEDandERROR_IDEMPOTENCY_KEY_REUSED. The 2Captcha format lists them all.
Roll it out safely
Section titled “Roll it out safely”- Point one service, or a share of your traffic, at ZeroCaptcha with a key of its own and a daily spend cap.
- Watch its tasks in the dashboard’s task log and on the status page.
- Move the rest once it behaves, then retire the old keys.