# Python SDK

> The official ZeroCaptcha client for Python, standard library only: solve Cloudflare Turnstile and WAF challenge pages, handle errors and check callbacks.

Source: https://zerocaptcha.io/docs/sdks/python

The official ZeroCaptcha client for Python: create a Cloudflare Turnstile task or a Cloudflare
challenge page's task, wait for its result, read your balance, and check a task callback's signature. It uses the standard library
only, and runs on Python 3.9 and later.

Every task is real and paid from your prepaid balance, and only a task that succeeds is charged.

## Install

```sh
pip install zerocaptcha
```

The package is not on PyPI yet. Until it is, call the API with `requests`, as the
[quickstart](https://zerocaptcha.io/docs/quickstart)'s Python program does: it makes the same calls.

## Use

Give the client your API key (`zc_live_…`, from the dashboard's API keys page) and the API's
address. Keep both in your environment rather than in your code.

```python
import os

from zerocaptcha import TaskFailedError, ZeroCaptcha

client = ZeroCaptcha(api_key=os.environ["ZEROCAPTCHA_KEY"], base_url=os.environ["ZEROCAPTCHA_API"])

# Create a task and wait for its token: one call.
try:
    token = client.solve(
        website_url="https://shop.example.com/login",  # the page with the widget
        website_key="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",  # to solve through your own proxy
        # callback_url="https://hooks.example.com/zerocaptcha",  # to be called when it ends
    )
    print(token)
except TaskFailedError as failed:
    print(failed.code)  # such as ERROR_CAPTCHA_UNSOLVABLE; nothing was charged

# Or step by step.
task = client.create_task(
    website_url="https://shop.example.com/login",
    website_key="0x4AAAAAAAB1cD2eF3gH4iJ5",
    action="login",  # the widget's data-action, if it sets one
    cdata="session-7f3a9c2e",  # the widget's data-cdata, if it sets one
    # Your ID for this task, sent as the Idempotency-Key; one is made for you when you give none.
    idempotency_key="login-2026-10-01-0001",
)
done = client.wait_for_result(task["id"], timeout=120)
print(done["solution"]["token"], done["cost"])

# Your balance, in US dollars.
print(client.get_balance()["available"])
```

| Method | What it does |
| --- | --- |
| `ZeroCaptcha(api_key, base_url)` | A client. |
| `create_task(website_url, website_key, type=, action=, cdata=, proxy=, callback_url=, idempotency_key=)` | Creates a task. |
| `get_task(task_id)` | Reads a task; its token is in `["solution"]` while it is available. |
| `wait_for_result(task_id, timeout=180, interval=2)` | Polls until the task ends. |
| `solve(website_url, website_key, **task)` | `create_task` then `wait_for_result`: the token. |
| `create_challenge_task(website_url, proxy, callback_url=, idempotency_key=)` | Creates a Cloudflare challenge page's task, through your proxy. |
| `solve_challenge(website_url, proxy)` | `create_challenge_task` then `wait_for_result`: `{"cf_clearance", "user_agent", "token_expires_at"}`. |
| `get_balance()` | `{"available", "held", "currency"}`, as decimal strings. |
| `verify_signature(secret, header, raw_body, tolerance=300)` | Whether a callback is genuine. |

- Tasks come back as dictionaries, as the API writes them (`id`, `status`, `cost`, `solution`…).
- `proxy="http://user:pass@proxy.example.net:8080"` solves a task through your proxy.
- `create_task` sends an `Idempotency-Key` with every call, one of its own unless you give
  `idempotency_key`, so retrying it never makes a second task.
- A request the API asks you to slow down (429) or cannot serve for a moment (502, 503, 504) is
  tried again after the wait it asks for, three times in all, as is one that got no answer or an
  answer cut short, with the same `Idempotency-Key`. Any other refusal raises
  `ZeroCaptchaError` with the API's `code`, such as `insufficient_funds`, and its `request_id`.
- `wait_for_result` never runs past `timeout`: each read gets only the time left, and a retry that
  would wait longer than that is not made. It raises `TaskFailedError` when the task fails or
  expires; a wait that runs out raises `WaitTimeoutError`, with the task as last read (`task`,
  `None` if no read finished in time), and you can wait again.

## Cloudflare challenge pages

A [challenge page](https://zerocaptcha.io/docs/challenges) is passed through your proxy, and gives the `cf_clearance`
cookie with the user agent it is bound to. Send both, through the same proxy:

```python
clearance = client.solve_challenge(
    "https://shop.example.com/",
    os.environ["PROXY_URL"],  # such as http://user:pass@proxy.example.net:8080
)
print(clearance["cf_clearance"], clearance["user_agent"])
```

## Callbacks

A task created with `callback_url` is POSTed to it once it ends, with the task as JSON. Check each
call's signature against the raw body, before you parse it. With Flask:

```python
import json
import os

from flask import Flask, abort, request
from zerocaptcha import verify_signature

app = Flask(__name__)

@app.post("/zerocaptcha/callback")
def callback():
    if not verify_signature(
        os.environ["ZEROCAPTCHA_CALLBACK_SECRET"],
        request.headers.get("ZeroCaptcha-Signature"),
        request.get_data(),  # the raw bytes, before parsing
    ):
        abort(401)
    task = json.loads(request.get_data())
    print(task["id"], task["status"])
    return "", 204
```

A call older than five minutes does not verify, so a recorded call cannot be replayed. See
[Polling and callbacks](https://zerocaptcha.io/docs/callbacks).
