Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,23 +330,29 @@ that the target page expects in `cf-turnstile-response`.
| `action` | no | Value of the widget's `data-action` attribute, if set. |
| `data` | no | Custom payload from the widget's `data-cdata` attribute, if set. |
| `pagedata` | no | Value of the `chlPageData` parameter, needed for some Cloudflare challenge pages beyond the basic widget. |
| `userAgent` | no | User-Agent to solve with -- the returned token is tied to it, submit with the same one. |
| `userAgent` | no | Current browser User-Agent. Pass it for Cloudflare Challenge pages; it is not needed for a standalone widget. |

**Response:** `token` -- submit as `cf-turnstile-response`. For Cloudflare
Challenge pages, the solution also includes `userAgent`; use that User-Agent
when submitting the token. Pass `action`, `data`, and `pagedata` when the page
provides them. The API field is spelled `pagedata`, all lowercase.
Challenge pages, pass `action`, `data`, `pagedata`, and the current browser
`userAgent` in the task. The solution also contains `userAgent`; switch the
browser or HTTP client to this returned value before invoking the callback with
the token. The API field is spelled `pagedata`, all lowercase.

```python
from captcha_solver_api import CaptchaClient
from captcha_solver_api.tasks import TurnstileTaskProxyless
client = CaptchaClient("your_api_key")
task = TurnstileTaskProxyless(
websiteURL="https://example.com/login",
websiteKey="YOUR_WEBSITE_KEY"
websiteKey="YOUR_WEBSITE_KEY",
action="managed",
data="INTERCEPTED_CDATA",
pagedata="INTERCEPTED_CHL_PAGE_DATA",
userAgent="CURRENT_BROWSER_USER_AGENT",
)
result = client.solve(task)
print(result["token"])
print(result.get("userAgent"))
```

With proxy, use `TurnstileTask` (same proxy fields as reCAPTCHA v2).
Expand Down Expand Up @@ -712,6 +718,7 @@ Full API reference: https://captcha-solver.com/en/docs/captcha-types

## Useful Links

- [Cloudflare Turnstile Puppeteer Demo](https://github.com/captcha-solver-api/cloudflare-turnstile-puppeteer-demo) — a working browser automation example for Cloudflare Turnstile.
- [How to Automate Tencent CAPTCHA](https://captcha-solver.com/en/blog/how-to-automate-tencent-captcha) — a step-by-step guide with Python and JavaScript SDK examples.

## License
Expand Down
13 changes: 10 additions & 3 deletions captcha_solver_api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,11 +303,14 @@ class TurnstileTaskProxyless(BaseTask):
challenge pages beyond the basic widget. Named lowercase (not `pageData`)
to match the API field name exactly -- Cloudflare-specific fields are
the one place this API doesn't camelCase.
userAgent: User-Agent to solve with. The returned token is tied to it --
submit it with the same User-Agent.
userAgent: User-Agent of the browser that loaded the challenge. Pass it
together with `action`, `data`, and `pagedata` for Cloudflare
Challenge pages. It is not needed for a standalone widget.

Returns (`solution` from `solve()`):
`token` -- the value to submit as `cf-turnstile-response`.
`userAgent` -- for Cloudflare Challenge pages, switch the browser or HTTP
client to this returned User-Agent before invoking the callback.
"""

type = "TurnstileTaskProxyless"
Expand Down Expand Up @@ -345,10 +348,14 @@ class TurnstileTask(BaseTask, ProxyMixin):
pagedata: Value of the `chlPageData` parameter, needed for some Cloudflare
challenge pages beyond the basic widget. Named lowercase (not `pageData`)
to match the API field name exactly.
userAgent: User-Agent to solve with. The returned token is tied to it.
userAgent: User-Agent of the browser that loaded the challenge. Pass it
for Cloudflare Challenge pages; it is not needed for a standalone
widget.

Returns (`solution` from `solve()`):
`token` -- the value to submit as `cf-turnstile-response`.
`userAgent` -- for Cloudflare Challenge pages, switch the browser or HTTP
client to this returned User-Agent before invoking the callback.
"""

type = "TurnstileTask"
Expand Down
25 changes: 23 additions & 2 deletions tests/sync/test_turnstile.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@


class TestTurnstile:
def test_proxyless_includes_challenge_user_agent(self):
task = TurnstileTaskProxyless(
websiteURL="https://example.com",
websiteKey="test_key",
action="managed",
data="cdata",
pagedata="page-data",
userAgent="Mozilla/5.0",
)

assert task.to_dict()["userAgent"] == "Mozilla/5.0"

def test_proxyless_to_dict(self):
task = TurnstileTaskProxyless(
websiteURL="https://example.com",
Expand All @@ -25,12 +37,14 @@ def test_task_with_proxy_to_dict(self):
proxyType="http",
proxyAddress="1.2.3.4",
proxyPort=8080,
userAgent="Mozilla/5.0",
)
result = task.to_dict()

assert result["type"] == "TurnstileTask"
assert result["proxyType"] == "http"
assert result["proxyAddress"] == "1.2.3.4"
assert result["userAgent"] == "Mozilla/5.0"

def test_solve(self):
client = CaptchaClient("test_key", polling_interval=0.1)
Expand All @@ -39,8 +53,15 @@ def test_solve(self):
with patch.object(client, "_request") as mock_request:
mock_request.side_effect = [
{"errorId": 0, "taskId": 103},
{"errorId": 0, "status": "ready", "solution": {"token": "turnstile_token"}},
{
"errorId": 0,
"status": "ready",
"solution": {
"token": "turnstile_token",
"userAgent": "returned-agent",
},
},
]
result = client.solve(task)

assert result == {"token": "turnstile_token"}
assert result == {"token": "turnstile_token", "userAgent": "returned-agent"}
Loading