# Cloudflare Turnstile action and cData

> When a Cloudflare Turnstile task needs the widget's action and cData, where to find them, what happens without them, and how to send them in every format.

Source: https://zerocaptcha.io/docs/action-and-cdata

A Cloudflare Turnstile widget can carry two values besides its site key: an **action**, a short
label such as `login`, and **cData**, a value such as a session ID. When a widget sets them,
Cloudflare returns both to the site when it verifies the token, and many sites refuse a token
whose action or cData is not the one they expect. So when the widget sets them, send both with
the task, exactly as the widget sets them. When it sets neither, leave them out.

## When they're required

ZeroCaptcha cannot tell whether a site checks them, so it never requires them: a task without
them is solved and charged like any other. The site decides.

- **The widget sets an action or cData:** send it. Cloudflare's own advice to sites is to
  "validate the action and hostname when specified", and its example refuses a token whose action
  does not match. Treat both values as required for that page.
- **The widget sets neither:** leave both out. An action the widget does not have is as wrong as
  a missing one.
- **The cData changes on every visit,** as a session ID does: read it from the page you will
  submit, just before you create the task, and solve once per visit.

Cloudflare allows an action of up to 32 characters and cData of up to 255, each letters, digits,
`_` and `-` ([widget configurations](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/)).
ZeroCaptcha checks the same limits when you create the task.

## Where to find them

Open the page with the widget, then its source or the developer tools' **Elements** panel:

- **In the HTML:** the element with the class `cf-turnstile` carries them as `data-action` and
  `data-cdata`, beside `data-sitekey`:

  ```html
  <div class="cf-turnstile" data-sitekey="0x4AAAAAAAB1cD2eF3gH4iJ5"
       data-action="login" data-cdata="session-7f3a9c2e"></div>
  ```

- **In a script:** the page passes them to `turnstile.render()` as the `action` and `cData`
  options:

  ```js
  turnstile.render("#captcha", {
    sitekey: "0x4AAAAAAAB1cD2eF3gH4iJ5",
    action: "login",
    cData: "session-7f3a9c2e",
  });
  ```

Search the page's scripts for `turnstile.render` when the HTML has no `data-sitekey`. In a browser
you drive, read them from the live page: the [browser automation](https://zerocaptcha.io/docs/browser-automation)
samples do, for both kinds of widget. The
[Cloudflare Turnstile sitekey finder](https://zerocaptcha.io/tools/cloudflare-turnstile-sitekey-finder) reads all three
values from HTML you paste.

## What happens without them

The task still succeeds: the token is real, and it is charged. What changes is the site's answer
when it checks the token with Cloudflare's siteverify. The reply names the action and cData the
token was solved with
([server-side validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)):

```json
{
  "success": true,
  "challenge_ts": "2026-10-01T09:30:00.000Z",
  "hostname": "shop.example.com",
  "error-codes": [],
  "action": "login",
  "cdata": "session-7f3a9c2e"
}
```

A site that compares them with what its widget set refuses a token solved without them, or with
other values, usually with the same error it shows for a failed check. Nothing in the token or
the task tells you that this is why. If a site keeps refusing tokens that arrive in time, compare
the action and cData you send with the ones in the live page first.

A value outside Cloudflare's limits is refused when you create the task, before anything is held:
[`validation_failed`](https://zerocaptcha.io/docs/reference/errors#validation_failed) on REST, `ERROR_INVALID_TASK_DATA`
in the createTask format and `ERROR_BAD_PARAMETERS` in the 2Captcha format, each naming the field.

## Send them

Each format has its own names for the two fields:

| Format | Action | cData |
| --- | --- | --- |
| REST, `POST /v1/tasks` | `action` | `cdata` |
| createTask format | `metadata.action` | `metadata.cdata` |
| 2Captcha format, `in.php` | `action` | `data` |
| JavaScript and Python clients | `action` | `cdata` |
| Go client | `Action` | `CData` |

**REST**

```sh
# websiteURL is the page with the widget; websiteKey its data-sitekey. 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. To solve through 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". The Idempotency-Key is
# your ID for this task: sending the create again with it returns the same task.
reply=$(curl -sS --fail-with-body "$ZEROCAPTCHA_API/v1/tasks" \
  -H "Authorization: Bearer $ZEROCAPTCHA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: login-2026-10-01-0001" \
  -d '{
    "type": "TurnstileTaskProxyless",
    "websiteURL": "https://shop.example.com/login",
    "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",
    "action": "login",
    "cdata": "session-7f3a9c2e"
  }') || { echo "refused: $reply" >&2; exit 1; } # a problem document; its code says why
task_id=$(jq -r .id <<<"$reply")

# Read the task every 2 seconds until it ends.
while :; do
  task=$(curl -sS --fail-with-body "$ZEROCAPTCHA_API/v1/tasks/$task_id" \
    -H "Authorization: Bearer $ZEROCAPTCHA_KEY") || { echo "$task" >&2; exit 1; }
  case $(jq -r .status <<<"$task") in
    succeeded) jq -r .solution.token <<<"$task"; break ;;
    failed | expired) jq -r '"\(.errorCode): \(.errorDescription)"' <<<"$task" >&2; exit 1 ;;
  esac
  sleep 2
done
```

**createTask format**

```sh
# The createTask format nests the widget's action and cData in the task's metadata: copy them from
# its data-action and data-cdata, or the action and cData options of turnstile.render(), and 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. The Idempotency-Key is your
# ID for this task: sending the create again with it returns the same task.
reply=$(curl -sS "$ZEROCAPTCHA_API/createTask" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: login-2026-10-01-0001" \
  -d '{
    "clientKey": "'"$ZEROCAPTCHA_KEY"'",
    "task": {
      "type": "TurnstileTaskProxyless",
      "websiteURL": "https://shop.example.com/login",
      "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",
      "metadata": {"action": "login", "cdata": "session-7f3a9c2e"}
    }
  }')
if [ "$(jq -r .errorId <<<"$reply")" != 0 ]; then echo "refused: $reply" >&2; exit 1; fi
task_id=$(jq -r .taskId <<<"$reply")

# Ask getTaskResult every 2 seconds until the task is ready, or failed (errorId 1).
while :; do
  sleep 2
  result=$(curl -sS "$ZEROCAPTCHA_API/getTaskResult" -H "Content-Type: application/json" \
    -d '{"clientKey": "'"$ZEROCAPTCHA_KEY"'", "taskId": "'"$task_id"'"}')
  if [ "$(jq -r .errorId <<<"$result")" != 0 ]; then echo "$result" >&2; exit 1; fi
  if [ "$(jq -r .status <<<"$result")" = ready ]; then jq -r .solution.token <<<"$result"; break; fi
done
```

**2Captcha format**

```sh
# 2Captcha's in.php takes the widget's action as action and its cData as data: copy them from its
# data-action and data-cdata, or the action and cData options of turnstile.render(), and leave out
# any the widget does not set. Add proxy=user:pass@proxy.example.net:8080 with proxytype=HTTP to
# solve through your own proxy, and pingback=https://hooks.example.com/zerocaptcha to be called
# when it ends. The Idempotency-Key is your ID for this task, as on REST.
reply=$(curl -sS "$ZEROCAPTCHA_API/in.php" \
  -H "Idempotency-Key: login-2026-10-01-0001" \
  --data-urlencode "key=$ZEROCAPTCHA_KEY" \
  --data-urlencode "method=turnstile" \
  --data-urlencode "pageurl=https://shop.example.com/login" \
  --data-urlencode "sitekey=0x4AAAAAAAB1cD2eF3gH4iJ5" \
  --data-urlencode "action=login" \
  --data-urlencode "data=session-7f3a9c2e" \
  --data-urlencode "json=1")
if [ "$(jq -r .status <<<"$reply")" != 1 ]; then echo "refused: $reply" >&2; exit 1; fi
id=$(jq -r .request <<<"$reply")

# res.php answers CAPCHA_NOT_READY until the task ends: ask every 2 seconds.
while :; do
  sleep 2
  result=$(curl -sS "$ZEROCAPTCHA_API/res.php?key=$ZEROCAPTCHA_KEY&action=get&id=$id&json=1")
  if [ "$(jq -r .status <<<"$result")" = 1 ]; then jq -r .request <<<"$result"; break; fi
  if [ "$(jq -r .request <<<"$result")" != CAPCHA_NOT_READY ]; then echo "$result" >&2; exit 1; fi
done
```

**Clients**

```ts
// JavaScript: the client sends an Idempotency-Key with the create, and throws a TaskFailedError
// when the task fails or expires (which costs nothing).
const token = await client.solve({
  websiteURL: "https://shop.example.com/login", // the page with the widget
  websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", // its data-sitekey
  action: "login", // its data-action, or turnstile.render()'s action option
  cdata: "session-7f3a9c2e", // its data-cdata, or turnstile.render()'s cData option
  // proxy: "http://user:pass@proxy.example.net:8080", // to solve through your own proxy
  // callbackUrl: "https://hooks.example.com/zerocaptcha", // to be called when it ends
});
```

```python
# Python: the same names in snake case; TaskFailedError when the task fails or expires.
token = client.solve(
    website_url="https://shop.example.com/login",  # the page with the widget
    website_key="0x4AAAAAAAB1cD2eF3gH4iJ5",  # its data-sitekey
    action="login",  # its data-action, or turnstile.render()'s action option
    cdata="session-7f3a9c2e",  # its data-cdata, or turnstile.render()'s cData option
    # proxy="http://user:pass@proxy.example.net:8080",  # to solve through your own proxy
    # callback_url="https://hooks.example.com/zerocaptcha",  # to be called when it ends
)
```

```go
// Go: Action and CData; a *zerocaptcha.TaskFailedError when the task fails or expires.
token, err := client.Solve(ctx, zerocaptcha.NewTask{
	WebsiteURL: "https://shop.example.com/login", // the page with the widget
	WebsiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5",       // its data-sitekey
	Action:     "login",                          // its data-action, or turnstile.render()'s action option
	CData:      "session-7f3a9c2e",               // its data-cdata, or turnstile.render()'s cData option
	// Proxy:       "http://user:pass@proxy.example.net:8080", // to solve through your own proxy
	// CallbackURL: "https://hooks.example.com/zerocaptcha",   // to be called when it ends
})
if err != nil {
	log.Fatal(err)
}
fmt.Println(token)
```

The clients are not in their registries yet: see [SDKs](https://zerocaptcha.io/docs/sdks).

> **Note**
>
> The createTask format also reads the spellings clients written for other services send: the
> action as `action` or CapMonster Cloud's `pageAction` at the top level of the task, and cData as
> `cdata`, Anti-Captcha's `cData`, `data` or `turnstileCData` there, or as `metadata.cData`.
> `metadata.action` and `metadata.cdata` are what most createTask clients send. See
> [the createTask format](https://zerocaptcha.io/docs/createtask#cloudflare-turnstile-task).

## Try it

The [Cloudflare Turnstile action and cData demo](https://zerocaptcha.io/captcha-test/cloudflare-turnstile-action-cdata)
carries a widget that sets both. Solve it with and without them, and its check shows the action
and cData siteverify returns for each token. The
[Cloudflare Turnstile action and cData guide](https://zerocaptcha.io/guides/cloudflare-turnstile-action-and-cdata) goes
further into what sites use them for.
