Cloudflare Turnstile action and cData
More
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
Section titled “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).
ZeroCaptcha checks the same limits when you create the task.
Where to find them
Section titled “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-turnstilecarries them asdata-actionanddata-cdata, besidedata-sitekey:<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 theactionandcDataoptions: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
samples do, for both kinds of widget. The
Cloudflare Turnstile sitekey finder reads all three
values from HTML you paste.
What happens without them
Section titled “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):
{ "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 on REST, ERROR_INVALID_TASK_DATA
in the createTask format and ERROR_BAD_PARAMETERS in the 2Captcha format, each naming the field.
Send them
Section titled “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 |
# 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 whytask_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 2done# 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; fitask_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; fidone# 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; fiid=$(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; fidone// 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: 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: 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.
Try it
Section titled “Try it”The Cloudflare Turnstile action and cData demo 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 goes further into what sites use them for.