Working with Cloudflare Turnstile in automated tests is one of those tasks that looks easy until you try it. The widget is invisible most of the time, but your script still needs a valid cf-turnstile-response value before the form will go through, and there is no way to "click" it with Selenium.

The practical solution is to hand the challenge to a solving API, get back a token, and submit it as part of your request. In this post I will show the smallest working version of that flow in Python.

This guide is for testing your own applications and QA runs against staging, or for authorised public data collection. Use it accordingly.

How Turnstile fits into a form

When a page loads a Turnstile widget, Cloudflare injects a hidden input named cf-turnstile-response. Real visitors get a token written into that field automatically, and your form handler verifies the token server-side with Cloudflare's siteverify endpoint.

So the goal for automation is simple: obtain a valid token, then include it in the POST payload.

What you need

  • Python 3.8+
  • pip install requests
  • A Kaptra account with some credit (sign up at kaptra.ai)

Kaptra is a captcha-solving API that bills per successful task only, so a failed or timed-out solve costs nothing. Its request format follows a common industry structure, so if you have used another solver API before, this will feel familiar.

Step 1: find the sitekey

The widget's sitekey lives in the page HTML as the data-sitekey attribute:

<div class="cf-turnstile" data-sitekey="0x4AAAAAAAxxxxxxxxxxxx"></div> 

Copy that value. If the page builds the widget dynamically, search the loaded JavaScript for turnstile.render and take the key passed there.

Steps 2 and 3: create the task, then poll for the result

The API flow uses two endpoints: POST /createTask starts the solve, POST /getTaskResult polls until the token is ready. Poll every two seconds or so; most Turnstile tasks finish in under three.

import time import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.kaptra.ai" def solve_turnstile(website_url: str, website_key: str) -> dict: task = requests.post( f"{BASE_URL}/createTask", json={ "clientKey": API_KEY, "task": { "type": "AntiTurnstileTask", "websiteURL": website_url, "websiteKey": website_key, }, }, timeout=30, ).json() if task.get("errorId") != 0: raise RuntimeError(f"createTask failed: {task.get('errorCode')}") task_id = task["taskId"] for _ in range(60): time.sleep(2) result = requests.post( f"{BASE_URL}/getTaskResult", json={"clientKey": API_KEY, "taskId": task_id}, timeout=30, ).json() if result.get("status") == "ready": return result["solution"] if result.get("status") == "failure": raise RuntimeError(f"task failed: {result.get('errorCode')}") raise TimeoutError("solver did not return in time") if __name__ == "__main__": solution = solve_turnstile( website_url="https://example.com/signup", website_key="0x4AAAAAAA...", ) print(solution["token"]) print(solution["userAgent"]) 

Two details worth knowing: websiteURL must be the exact URL where the widget actually runs, and the same API key works across all task types, so you only manage one credential.

Step 4: submit the token

The solution comes back with a token and the userAgent that Cloudflare associated with it. When you submit the form, send the token as cf-turnstile-response and reuse that same user agent:

session = requests.Session() session.headers.update({"User-Agent": solution["userAgent"]}) resp = session.post( "https://example.com/signup", data={ "email": "test@example.com", "password": "hunter2", "cf-turnstile-response": solution["token"], }, timeout=30, ) print(resp.status_code) 

In a browser context such as Playwright, write the token into the hidden input instead, for example with page.evaluate setting the value of [name="cf-turnstile-response"].

What it costs

Turnstile tasks on Kaptra are $1.20 per 1,000 solves. Failed and timed-out tasks are not billed, there is no monthly fee, and unused credit never expires.

Wrapping up

That is the whole loop: read the sitekey, create a task, poll, then submit the token with a matching user agent. The full parameter reference lives on the AntiTurnstileTask docs page, and the same pattern covers other challenge types such as Cloudflare Challenge, AWS WAF, and DataDome.

As always, use this for testing your own systems and for data you are authorised to collect.