# Solving Cloudflare Turnstile

> Every field of a Cloudflare Turnstile task, how to find the site key, action and cData on a page, and how to use the token in a form or a callback.

Source: https://zerocaptcha.io/docs/cloudflare-turnstile

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

| 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](https://zerocaptcha.io/docs/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](https://zerocaptcha.io/docs/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`](https://zerocaptcha.io/docs/reference/errors#validation_failed) (HTTP 422), whose `detail` names
the field; nothing is held. The same fields have other spellings in the
[createTask format](https://zerocaptcha.io/docs/createtask#cloudflare-turnstile-task) and the [2Captcha format](https://zerocaptcha.io/docs/2captcha).

### 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](https://zerocaptcha.io/pricing).

## 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`:

  ```html
  <div class="cf-turnstile" data-sitekey="0x4AAAAAAAB1cD2eF3gH4iJ5"
       data-action="login" data-cdata="sess_91f2c0" data-callback="onTurnstile"></div>
  ```

  `data-sitekey` is the `websiteKey`; `data-action` and `data-cdata`, when present, are `action`
  and `cdata`.

- **In a script,** as a call to `turnstile.render`:

  ```js
  turnstile.render("#captcha", {
    sitekey: "0x4AAAAAAAB1cD2eF3gH4iJ5",
    action: "login",
    cData: "sess_91f2c0",
    callback: (token) => submitLogin(token),
  });
  ```

  Search the page's scripts for `turnstile.render` or `sitekey`. `sitekey`, `action` and `cData`
  are 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](https://zerocaptcha.io/docs/action-and-cdata)
says when they are required, where to find them and what happens without them.

> **Note**
>
> A page that shows "Just a moment…" before any content is not a Turnstile widget but a Cloudflare
> challenge page. Solve it with a [challenge task](https://zerocaptcha.io/docs/challenges) instead.

## Solve it

Create the task with the widget's site key, action and cData, then read it every 2 seconds until
it ends:

**curl**

```sh
# 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"
```

**Node**

```js
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);
```

**Python**

```python
import os
import time
import 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"])
```

**Go**

```go
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](https://zerocaptcha.io/docs/quickstart)'s samples, the [SDKs](https://zerocaptcha.io/docs/sdks) and the
[AI brief's reference clients](https://zerocaptcha.io/docs/ai) do all of it. See
[Errors and retries](https://zerocaptcha.io/docs/errors-and-retries).

## Use the token

A token works **once**, for **300 seconds** from `tokenIssuedAt`. Submit it straight away.

### 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**

```sh
curl https://example.com/login \
  --data-urlencode "email=you@example.com" \
  --data-urlencode "password=$PASSWORD" \
  --data-urlencode "cf-turnstile-response=$TOKEN"
```

**Node**

```js
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);
```

**Python**

```python
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)
```

**Go**

```go
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

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](https://zerocaptcha.io/docs/browser-automation):

```js
// 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

Every kind of Cloudflare Turnstile widget has a live demo page to point a task at: the
[managed widget](https://zerocaptcha.io/captcha-test/cloudflare-turnstile-managed), the
[invisible widget](https://zerocaptcha.io/captcha-test/cloudflare-turnstile-invisible), the
[widget with action and cData](https://zerocaptcha.io/captcha-test/cloudflare-turnstile-action-cdata) and more, on the
[Cloudflare Turnstile demo and CAPTCHA test pages](https://zerocaptcha.io/captcha-test). Each shows its sitekey and the
exact request, and checks the token you bring with Cloudflare's siteverify; the
[Cloudflare Turnstile token checker](https://zerocaptcha.io/captcha-test/cloudflare-turnstile-token-checker) checks one on
its own.

## When it fails

A task that is not solved after every attempt fails with
[`ERROR_CAPTCHA_UNSOLVABLE`](https://zerocaptcha.io/docs/reference/errors#ERROR_CAPTCHA_UNSOLVABLE), and one not solved
by its deadline expires with [`ERROR_TASK_TIMEOUT`](https://zerocaptcha.io/docs/reference/errors#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`](https://zerocaptcha.io/docs/reference/errors#domain_blocked) and costs nothing.
