Solving Cloudflare Turnstile
More
Cloudflare Turnstile is the widget that shows “Verify you are human”, or runs unseen, and puts a token in the page’s form. A Turnstile task gets you that token for a page you name, so your code can submit the form as a browser would. Send tasks only for sites you are allowed to automate.
The task’s fields
Section titled “The task’s fields”| Field | Required | What it is |
|---|---|---|
type |
Yes | TurnstileTaskProxyless, or TurnstileTask to solve through your proxy. CapSolver’s AntiTurnstileTaskProxyLess works too, and AntiTurnstileTask for the proxy variant, and case does not matter. |
websiteURL |
Yes | The full address of the page with the widget, such as https://example.com/login: http or https, at most 2,048 characters, without a username or password, on the scheme’s default port, on a public domain name (not an IP address, localhost or a .local or .internal name). |
websiteKey |
Yes | The widget’s site key: 1 to 100 letters, digits, _ and -, such as 0x4AAAAAAAB1cD2eF3gH4iJ5. |
action |
When the widget sets one | The widget’s action: up to 32 letters, digits, _ and -. See action and cData. |
cdata |
When the widget sets one | The widget’s cData: up to 255 letters, digits, _ and -. |
proxy |
With TurnstileTask |
Your proxy as a URL with its port: http://user:pass@proxy.example.net:8080. http or https; SOCKS is not supported yet. Its host must be public, its port not one another protocol reserves (such as 25), and its login and password at most 255 bytes each. TurnstileTaskProxyless takes none. |
callbackUrl |
No | Where to POST the result when the task ends. See Polling and callbacks. |
Spaces around a value are trimmed, and an empty optional field counts as absent. A field outside
these, or a value out of bounds, is refused with
validation_failed (HTTP 422), whose detail names
the field; nothing is held. The same fields have other spellings in the
createTask format and the 2Captcha format.
Proxyless or your proxy
Section titled “Proxyless or your proxy”A proxyless task is solved from our network. Choose TurnstileTask when the site should see the
solve come from your own address, such as when it checks that the token’s solver and the form’s
sender match. Your proxy’s password is never logged, and is deleted when the task finishes. Prices
differ by type: see pricing.
Find the site key, action and cData
Section titled “Find the site key, action and cData”Open the page in a browser, then its source or the developer tools’ Elements panel, and look for the widget. It is written one of two ways:
-
In the HTML, as an element with the class
cf-turnstile:<div class="cf-turnstile" data-sitekey="0x4AAAAAAAB1cD2eF3gH4iJ5"data-action="login" data-cdata="sess_91f2c0" data-callback="onTurnstile"></div>data-sitekeyis thewebsiteKey;data-actionanddata-cdata, when present, areactionandcdata. -
In a script, as a call to
turnstile.render:turnstile.render("#captcha", {sitekey: "0x4AAAAAAAB1cD2eF3gH4iJ5",action: "login",cData: "sess_91f2c0",callback: (token) => submitLogin(token),});Search the page’s scripts for
turnstile.renderorsitekey.sitekey,actionandcDataare the three values.
A live site key usually starts with 0x4 (Cloudflare’s testing site keys start with 1x, 2x or
3x). Send action and cdata exactly as the page sets them, or leave them
out when it sets none: the site sees them in its verification, and may refuse a token whose action
or cData does not match. If the page makes the cData new on each visit, read it from the page you
will submit, just before creating the task. Cloudflare Turnstile action and cData
says when they are required, where to find them and what happens without them.
Solve it
Section titled “Solve it”Create the task with the widget’s site key, action and cData, then read it every 2 seconds until it ends:
# Create the task; the reply is the task, with its id, or a problem document whose code says why# not. action and cdata 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. For your own proxy, make# the type TurnstileTask and add "proxy": "http://user:pass@proxy.example.net:8080"; to be called# when the task ends, add "callbackUrl": "https://hooks.example.com/zerocaptcha".reply=$(curl -sS --fail-with-body "$ZEROCAPTCHA_API/v1/tasks" \ -H "Authorization: Bearer $ZEROCAPTCHA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"type": "TurnstileTaskProxyless", "websiteURL": "https://example.com/login", "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", "action": "login", "cdata": "session-7f3a9c2e"}') || { echo "refused: $reply" >&2; exit 1; }TASK_ID=$(jq -r .id <<<"$reply")
# Then, every 2 seconds, until "status" is succeeded (with solution.token), failed or expired:curl "$ZEROCAPTCHA_API/v1/tasks/$TASK_ID" -H "Authorization: Bearer $ZEROCAPTCHA_KEY"const api = process.env.ZEROCAPTCHA_API;const headers = { Authorization: `Bearer ${process.env.ZEROCAPTCHA_KEY}` };
const created = await fetch(`${api}/v1/tasks`, { method: "POST", headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ 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. 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 }),});if (!created.ok) throw new Error(`createTask: HTTP ${created.status}: ${await created.text()}`);let task = await created.json();
while (task.status === "queued" || task.status === "running") { await new Promise((resolve) => setTimeout(resolve, 2000)); const read = await fetch(`${api}/v1/tasks/${task.id}`, { headers }); if (!read.ok) throw new Error(`getTask: HTTP ${read.status}: ${await read.text()}`); task = await read.json();}if (task.status !== "succeeded" || !task.solution) throw new Error(`${task.errorCode}: ${task.errorDescription}`);console.log(task.solution.token);import osimport timeimport uuid
import requests
api = os.environ["ZEROCAPTCHA_API"]headers = {"Authorization": f"Bearer {os.environ['ZEROCAPTCHA_KEY']}"}
created = requests.post( f"{api}/v1/tasks", headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, json={ "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. "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 }, timeout=15,)created.raise_for_status()task = created.json()
while task["status"] in ("queued", "running"): time.sleep(2) read = requests.get(f"{api}/v1/tasks/{task['id']}", headers=headers, timeout=15) read.raise_for_status() task = read.json()
if task["status"] != "succeeded" or not task.get("solution"): raise SystemExit(f"{task['errorCode']}: {task['errorDescription']}")print(task["solution"]["token"])package main
import ( "bytes" "crypto/rand" "encoding/json" "fmt" "io" "net/http" "os" "time")
type task struct { ID string `json:"id"` Status string `json:"status"` ErrorCode *string `json:"errorCode"` Solution *struct { Token string `json:"token"` } `json:"solution"`}
func call(method, path string, body any, idempotencyKey string) (task, error) { var t task var content io.Reader if body != nil { payload, err := json.Marshal(body) if err != nil { return t, err } content = bytes.NewReader(payload) } req, err := http.NewRequest(method, os.Getenv("ZEROCAPTCHA_API")+path, content) if err != nil { return t, err } req.Header.Set("Authorization", "Bearer "+os.Getenv("ZEROCAPTCHA_KEY")) if body != nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", idempotencyKey) } resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req) if err != nil { return t, err } defer resp.Body.Close() if resp.StatusCode >= 300 { return t, fmt.Errorf("%s %s: HTTP %d", method, path, resp.StatusCode) } return t, json.NewDecoder(resp.Body).Decode(&t)}
func main() { key := make([]byte, 16) _, _ = rand.Read(key) t, err := call(http.MethodPost, "/v1/tasks", map[string]string{ "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. "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 }, fmt.Sprintf("%x", key)) for err == nil && (t.Status == "queued" || t.Status == "running") { time.Sleep(2 * time.Second) t, err = call(http.MethodGet, "/v1/tasks/"+t.ID, nil, "") } if err != nil || t.Solution == nil { code := "" if t.ErrorCode != nil { code = *t.ErrorCode } fmt.Fprintln(os.Stderr, "no token:", err, t.Status, code) os.Exit(1) } fmt.Println(t.Solution.Token)}These are short on purpose. Production code also retries a 429 or 5xx after Retry-After, sends
the same Idempotency-Key when it retries a create, and stops waiting at a deadline: the
quickstart’s samples, the SDKs and the
AI brief’s reference clients do all of it. See
Errors and retries.
Use the token
Section titled “Use the token”A token works once, for 300 seconds from tokenIssuedAt. Submit it straight away.
In the form
Section titled “In the form”The widget puts its token in a hidden field named cf-turnstile-response in the form around it.
Send the token in that field with the rest of the form, as the browser would:
curl https://example.com/login \ --data-urlencode "email=you@example.com" \ --data-urlencode "password=$PASSWORD" \ --data-urlencode "cf-turnstile-response=$TOKEN"const form = new URLSearchParams({ email: "you@example.com", password: process.env.PASSWORD, "cf-turnstile-response": token,});const response = await fetch("https://example.com/login", { method: "POST", body: form });console.log(response.status);response = requests.post( "https://example.com/login", data={ "email": "you@example.com", "password": os.environ["PASSWORD"], "cf-turnstile-response": token, }, timeout=15,)print(response.status_code)form := url.Values{ "email": {"you@example.com"}, "password": {os.Getenv("PASSWORD")}, "cf-turnstile-response": {token},}resp, err := http.PostForm("https://example.com/login", form)Some sites name the field differently with the widget’s data-response-field-name, or send the
token in a JSON body or a header from their own script. Look at the request the page makes when
you submit it by hand (the developer tools’ Network panel) and send the token the same way.
Through the widget’s callback
Section titled “Through the widget’s callback”When the page reacts to the token in JavaScript, through the widget’s data-callback attribute or
the callback option of turnstile.render, put the token where the widget would, then call that
function with it. In a browser you control, as in browser automation:
// Run in the page: fill the hidden field, then hand the token to the page's own callback.(token) => { for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = token; const widget = document.querySelector(".cf-turnstile[data-callback]"); const callback = widget && window[widget.dataset.callback]; if (typeof callback === "function") callback(token);};If the callback was passed to turnstile.render as an inline function, find what it does in the
page’s script, such as submitLogin(token), and call that.
Test against a live widget
Section titled “Test against a live widget”Every kind of Cloudflare Turnstile widget has a live demo page to point a task at: the managed widget, the invisible widget, the widget with action and cData and more, on the Cloudflare Turnstile demo and CAPTCHA test pages. Each shows its sitekey and the exact request, and checks the token you bring with Cloudflare’s siteverify; the Cloudflare Turnstile token checker checks one on its own.
When it fails
Section titled “When it fails”A task that is not solved after every attempt fails with
ERROR_CAPTCHA_UNSOLVABLE, and one not solved
by its deadline expires with ERROR_TASK_TIMEOUT.
Neither is charged. If one keeps failing, check the websiteURL (the page with the widget, not
the form’s target), the websiteKey, and, with TurnstileTask, that your proxy works. A site on
our blocklist is refused with
domain_blocked and costs nothing.