The EZCaptchaSolver Python SDK is an open-source Python client maintained by EZXLabs for its CAPTCHA recognition task API. It provides typed requests and async and blocking clients for the supported task types below; the package also includes a TLS forwarding task that does not solve CAPTCHAs. For the wider SDK family, see the EZCaptchaSolver SDK product page; for HTTP request and response fields, see the EZCaptchaSolver API reference; for Python usage, see the examples in this repository.
Captcha task types come in a synchronous and an asynchronous form:
- Synchronous: the request blocks after the task is created and returns once the task is done.
- Asynchronous: creating the task returns a task ID, and the result is fetched later by polling that ID. This suits captcha types that take a while to solve.
A captcha type can support both forms at once, and almost every type supports the synchronous one. A few types are asynchronous only.
| Task type | Modes | Example | Description |
|---|---|---|---|
ReCaptchaV2TaskProxyless |
all | Example | reCAPTCHA v2 |
ReCaptchaV2TaskProxylessS9 |
all | Example | reCAPTCHA v2, returns a token scored ≥ 0.9 |
ReCaptchaV2STaskProxyless |
all | Example | reCAPTCHA v2 carrying the challenge-bound s parameter |
ReCaptchaV2EnterpriseTaskProxyless |
all | Example | reCAPTCHA v2 Enterprise |
ReCaptchaV2SEnterpriseTaskProxyless |
all | Example | reCAPTCHA v2 Enterprise, carrying the s parameter |
ReCaptchaV2Classification |
sync | Example | reCAPTCHA v2 image recognition |
| Task type | Modes | Example | Description |
|---|---|---|---|
ReCaptchaV3TaskProxyless |
all | Example | reCAPTCHA v3 |
ReCaptchaV3TaskProxylessS9 |
all | Example | reCAPTCHA v3, returns a token scored ≥ 0.9 |
ReCaptchaV3EnterpriseTaskProxyless |
all | Example | reCAPTCHA v3 Enterprise |
ReCaptchaV3EnterpriseTaskProxylessS9 |
all | Example | reCAPTCHA v3 Enterprise, returns a token scored ≥ 0.9 |
| Task type | Modes | Example | Description |
|---|---|---|---|
FuncaptchaTaskProxyless |
async | Example | FunCaptcha / Arkose Labs |
FunCaptchaClassification |
sync | Example | FunCaptcha image recognition |
| Task type | Modes | Example | Description |
|---|---|---|---|
HCaptcha |
async | Example | hCaptcha |
HCaptchaClassification |
sync | Example | hCaptcha image recognition, single or multiple images |
| Task type | Modes | Example | Description |
|---|---|---|---|
CloudFlare5STask |
async | Example | CF five-second interstitial, requires a proxy |
CloudFlareTurnstileTask |
async | Example | Turnstile, returns a token |
| Task type | Modes | Example | Description |
|---|---|---|---|
AkamaiWEBTaskProxyless |
sync | Example | Akamai Web |
AkamaiSBSDTaskProxyless |
sync | Example | Akamai SBSD |
Akamai Web is a multi-round flow: feed the
encodedataof one round back as theencode_dataof the next. The two spellings genuinely differ on the wire; the SDK keeps the service's definitions as they are rather than "fixing" them.
| Task type | Modes | Example | Description |
|---|---|---|---|
DataDomeTaskProxyless |
sync | Example | The challenge after an interception, in two steps selected by step |
DataDomeTagsTaskProxyless |
sync | Example | Reports a fingerprint on the normal browsing path |
| Task type | Modes | Example | Description |
|---|---|---|---|
PerimeterX |
async | Example | PerimeterX clearance cookies |
IncapsulaTaskProxyless |
sync | Example | Incapsula Reese84 payload |
TlsTask |
sync | Example | HTTP request forwarded over TLS, returns the upstream response |
pip install ezcapsolver-pyRequires Python 3.12+. The only runtime dependency is httpx; both clients are built on it, so their behaviour cannot drift apart.
The distribution is named
ezcapsolver-py, the import isezcapsolver.
The client reads EZCAPTCHA_API_KEY from the environment when no key is passed explicitly.
from ezcapsolver import EzCapSolverClient
with EzCapSolverClient() as client:
solved = client.solve_recaptcha_v2_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
)
print("task_id =", solved.task_id)
print("token =", solved.solution.token)The async client has exactly the same method names; add await:
from ezcapsolver import AsyncEzCapSolverClient
async with AsyncEzCapSolverClient() as client:
solved = await client.solve_recaptcha_v2_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
)
print("token =", solved.solution.token)One section per task type below. The snippets assume a client is already in scope, and leave out the with block to keep the call itself in focus.
Every task type has two methods, taking the same arguments and returning the same type; only the endpoint differs:
# Create, then poll
solved = client.solve_recaptcha_v2_task_proxyless(url, site_key)
assert solved.task_id is not None
# The synchronous endpoint, answering on the creating request
solved = client.sync_solve_recaptcha_v2_task_proxyless(url, site_key)
assert solved.task_id is None # the synchronous endpoint assigns no task IDThe same holds for the task-object form: solve() always polls, sync_solve() always uses the synchronous endpoint.
from ezcapsolver import ReCaptchaV2Task
task = ReCaptchaV2Task(website_url=url, website_key=site_key)
solved = client.solve(task)
solved = client.sync_solve(task)Each task class carries a mode recording the execution mode the service documents for it. This is informational — nothing in the SDK reads it to pick a path. It matters because the service may reject a type on the endpoint it does not serve, with ERROR_TASK_TYPE_NOT_ALLOWED on the synchronous side. Such a rejection is refused before the task is billed, so it costs a round trip rather than a task.
from ezcapsolver import ReCaptchaV2ClassificationTask, TaskMode
ReCaptchaV2ClassificationTask.mode is TaskMode.SYNC # TrueTask types that accept a proxy take it in one of two shapes, decided by the task type:
| Format | Shape | Used by |
|---|---|---|
NORMAL |
protocol://username:password@host:port |
every type except FunCaptcha |
FUN |
protocol://host:port:username:password |
FunCaptchaTask only |
protocol is one of http, https or socks5. Both credentials are required -- the service
rejects an unauthenticated proxy -- and the host may not be a private address (127.0.*,
192.168.*, 172.16.*, 10.0.*). Where the field is optional, leaving it empty is fine; it is
only a non-empty malformed value that is rejected.
The first five types share ReCaptchaV2Task and ReCaptchaSolution; only the method name differs.
solved = client.solve_recaptcha_v2_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
)
print("task_id =", solved.task_id)
print("token =", solved.solution.token)Identical parameters to plain v2; runs on the high-score queue and returns a token scored ≥ 0.9.
solved = client.solve_recaptcha_v2_task_proxyless_s9(
"https://example.com",
"6Lc_your_site_key",
)
print("token =", solved.solution.token)Carries the challenge-bound s parameter. It is not mandatory; without it the behaviour matches plain v2.
solved = client.solve_recaptcha_v2_s_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
s="value-from-the-page",
)
print("token =", solved.solution.token)Enterprise. If the site uses enterprise parameters beyond data-s, pass them as keyword arguments and they are forwarded as-is.
solved = client.solve_recaptcha_v2_enterprise_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
)
print("token =", solved.solution.token)Enterprise, carrying the s parameter.
solved = client.solve_recaptcha_v2_s_enterprise_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
s="value-from-the-page",
)
print("token =", solved.solution.token)reCAPTCHA v2 classification API reference
Recognises the image grid directly and returns tile indices rather than a token.
solution = client.sync_solve_recaptcha_v2_classification(
image_base64,
"/m/0k4j",
size=4, # 1 = 1x1, 3 = 3x3, 4 = 4x4
).solution
if solution.is_multi:
print("tiles to click", solution.objects)
elif solution.is_single:
print("contains the object:", solution.has_object)
else:
print("result type:", solution.type, "pass-through:", solution.extra)On ReClassificationSolution the JSON field hasObject maps to has_object. An unknown type value is kept verbatim and extra fields land in extra. Missing fields fall back to an empty type, False and an empty list; is_multi and is_single only look at type.
The four types share ReCaptchaV3Task and ReCaptchaSolution. page_action has to match the action the page passes to grecaptcha.execute, otherwise the site-side check fails.
⚠️ is_invisibledefaults toFalseonReCaptchaV2Taskand toTrueonReCaptchaV3Task, matching the service. The two models do not share a default.
solved = client.solve_recaptcha_v3_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
page_action="login",
)
print("task_id =", solved.task_id)
print("token =", solved.solution.token)solved = client.solve_recaptcha_v3_task_proxyless_s9(
"https://example.com",
"6Lc_your_site_key",
page_action="login",
)
print("token =", solved.solution.token)solved = client.solve_recaptcha_v3_enterprise_task_proxyless(
"https://example.com",
"6Lc_your_site_key",
page_action="login",
)
print("token =", solved.solution.token)solved = client.solve_recaptcha_v3_enterprise_task_proxyless_s9(
"https://example.com",
"6Lc_your_site_key",
page_action="login",
)
print("token =", solved.solution.token)solved = client.solve_funcaptcha_task_proxyless(
"https://example.com",
"your-public-key",
)
print("token =", solved.solution.token)FunCaptcha is the one task type whose proxy uses the
FUNformat:protocol://host:port:username:password, with the credentials after the host rather than before it.
solution = client.sync_solve_funcaptcha_classification(
image_base64,
"Pick the animal facing left",
).solution
# The result shape of this type is unconfirmed; every field the worker returns lands in extra.
print(solution.extra)The hCaptcha token field is generated_pass_uuid, not token — that is the service's naming, and the SDK keeps it.
solved = client.solve_hcaptcha(
"https://example.com",
"your-site-key",
"en-US",
invisible=False,
)
print("token =", solved.solution.generated_pass_uuid)Use image for one image and images for several; every field is optional, because different recognition modules need different combinations of input.
solution = client.sync_solve_hcaptcha_classification(
images=images,
question="Please click each image containing a bicycle",
).solution
# The shape is unconfirmed here too; every field is in extra.
print(solution.extra)The five-second interstitial requires a proxy, and returns not a single token but the headers and clearance cookies to replay against the target site:
solution = client.solve_cloudflare_5s_task(
"https://example.com",
"http://user:pass@127.0.0.1:8080",
).solution
for name, value in solution.cookies.items():
print(f"{name}={value}")
print("TLS fingerprint =", solution.tls_version)Replaying those headers and cookies against the protected site is what actually clears the challenge — the result is a whole browser state, not a token.
Cloudflare Turnstile API reference
Turnstile's proxy is optional, and it returns a single token.
solved = client.solve_cloudflare_turnstile_task(
"https://example.com",
"0x4AAA_your_site_key",
)
print("token =", solved.solution.token)Akamai Web is a multi-round flow: feed the encodedata of one round back as the encode_data of the next. The two spellings genuinely differ on the wire; the SDK keeps the service's definitions as they are.
encode_data = ""
for index in range(1, 4):
solution = client.sync_solve_akamai_web_task_proxyless(
"https://example.com",
"https://example.com/v3.js",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"zh-CN",
index=index,
abck=abck,
bmsz=bmsz,
script_base64=script_base64,
encode_data=encode_data,
).solution
print(f"round {index} payload =", solution.payload)
encode_data = solution.encodedataA single-round task; all six fields are required.
solution = client.sync_solve_akamai_sbsd_task_proxyless(
"https://example.com",
"https://example.com/.well-known/sbsd",
"value-from-the-page",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"zh-CN",
script_base64,
).solution
print("payload =", solution.payload)The challenge after a DataDome interception runs in two steps that share one method and are selected by step; which fields of DataDomeSolution carry a value depends on the step:
from ezcapsolver import DataDomeStep
# Step one: get the challenge address from the intercepted page.
solution = client.sync_solve_data_dome_task_proxyless(
html_b64,
step=DataDomeStep.ONE,
).solution
print("challenge address =", solution.url)
# Step two uses DataDomeStep.TWO, and the result carries the validation body.Reports a fingerprint on the normal browsing path. Its field names are camelCase, unlike DataDomeTask above.
solution = client.sync_solve_data_dome_tags_task_proxyless(
"your-datadome-key",
"https://example.com",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
bpc=1,
).solution
print(solution.extra)solution = client.solve_perimeter_x("PX_your_app_id").solution
print("_px3 =", solution.px3)
print("_pxvid =", solution.pxvid)solution = client.sync_solve_incapsula_task_proxyless(
script,
"https://example.com/sensor.js",
"https://example.com",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
accept_language="zh-CN,zh;q=0.9",
).solution
print("reese84 =", solution.data)This type does not solve a captcha. It sends one HTTP request through the worker's TLS fingerprint and brings the upstream response back untouched:
from ezcapsolver import TlsHttpMethod
solution = client.sync_solve_tls_task(
"chrome",
"http://user:pass@127.0.0.1:8080",
"https://example.com/api",
method=TlsHttpMethod.GET,
).solution
print(f"HTTP {solution.status}, {len(solution.body)} bytes of body")The solve_* shortcuts take fields directly, so a call site that needs exactly one task type does not have to import its class. When the task object is built elsewhere, or has to be passed between functions, use solve(task):
from ezcapsolver import ReCaptchaV2Task
task = ReCaptchaV2Task(
website_url="https://example.com",
website_key="6Lc_your_site_key",
)
solved = client.solve(task)Both forms share the same validation, errors and return types — the shortcuts are thin wrappers, and their names line up with the other language SDKs. Variants within a family (high-score, enterprise) are subclasses overriding only task_type, so field definitions are never repeated.
Result models have named fields, so reading a result never means writing solution["gRecaptchaResponse"]. Each model also carries an extra map that catches fields the service adds later:
solved.solution.token # a modelled field
solved.solution.extra # fields this release does not model
solved.raw # the raw JSON the worker returnedA field the worker omits does not break decoding. At the transport level, "the response had no solution field" stays distinguishable from "solution was JSON null":
from ezcapsolver import MISSING
result = client.get_result(task_id)
if result.solution is MISSING:
... # the task has not finishedWhen decoding genuinely fails, SolutionDecodeError.raw carries the original value out — which is exactly what a diagnosis needs.
Every shortcut ends in **extra, and unmodelled keywords are flattened into the task JSON. Parameters the service adds later work without an SDK upgrade:
solved = client.solve_cloudflare_turnstile_task(url, key, proxy=proxy, someNewField=1)Task objects use an extra map for the same effect. The reserved type field, and every field the model defines, always stay under the SDK's control:
from ezcapsolver import HCaptchaTask
task = HCaptchaTask(
website_url="https://example.com",
website_key="site-key",
lang="en-US",
invisible=False,
# A parameter this release does not model, or one the service ships later.
# Naming a modelled field here instead would be dropped.
extra={"futureFlag": True},
)
⚠️ **extraalso means a misspelled keyword is forwarded to the worker rather than rejected.
Task results carry an extra map too, handing back fields present in the response but absent from the model:
solution = client.solve_cloudflare_5s_task(url, proxy).solution
if "aFieldAddedLater" in solution.extra:
print(solution.extra["aFieldAddedLater"])Task types are an open set. Pass the type name as a string and the parameters as any mapping that serialises to a JSON object, keyed by wire names:
solved = client.solve_raw(
"BrandNewTaskType",
{"websiteURL": "https://example.com", "anyFutureParam": 42},
)
print(solved.solution) # the raw JSON
# The synchronous-endpoint counterpart
solved = client.sync_solve_raw("BrandNewSyncType", {"input": "..."})Use this when the service ships a new captcha type the SDK has not caught up with yet.
solve() is the one-step form of "create, then poll". Take the wait apart when you need to own it — to persist the task ID and fetch the result after a process restart, for instance:
task_id = client.create_task(ReCaptchaV2Task(website_url=url, website_key=key))
...
result = client.get_result(task_id)
if result.is_ready:
print(result.solution)from ezcapsolver import EzCapSolverClient, PollingConfig
client = EzCapSolverClient(
"your-client-key",
timeout=30.0,
sync_timeout=240.0,
polling=PollingConfig(interval=3.0, max_attempts=50),
app_id=42,
proxy="http://127.0.0.1:8080",
user_agent="my-app/1.0",
)| Setting | Default | Scope |
|---|---|---|
timeout |
30 s | Request timeout for the asynchronous endpoint |
sync_timeout |
240 s | Request timeout for the synchronous endpoint, clearing the service's 180 s worker deadline |
polling |
3 s × 50 | Result queries, capped at 150 s per task |
proxy |
none | The SDK's own egress, unrelated to the proxy inside task parameters |
async_base_url |
https://api.ez-captcha.com |
Asynchronous tasks and balance queries |
sync_base_url |
https://sync.ez-captcha.com |
Synchronous tasks; the service splits the two deployments |
Pass a ClientConfig to build the configuration once and reuse it; pass http_client to reuse a connection pool you already have.
Passing an
httpxclient with its ownbase_urldoes not redirect the SDK — it always builds absolute URLs, andhttpxappliesbase_urlonly to relative ones. Setasync_base_url/sync_base_urlinstead.
solve() does both. Split them when you want to hold on to the id — which is what makes a PollingExhaustedError recoverable, since the task keeps running:
task_id = client.create_task(task)
try:
result = client.wait_for_result(task_id)
except PollingExhaustedError:
# Already billed; the service holds the result for five minutes after
# creation, so wait for it again rather than paying twice.
result = client.wait_for_result(task_id, polling=PollingConfig(interval=5, max_attempts=20))polling= is accepted by solve(), solve_raw() and wait_for_result() alike: task types differ widely in how long they take, so one client-wide budget does not fit all of them. create_sync_task() is the synchronous counterpart of create_task(), returning the undecoded TaskResult.
Every exception the SDK raises inherits from EzCaptchaError, so catching that one base class covers all of them.
| Exception | Raised when |
|---|---|
ApiError |
An EZCaptchaSolver service error, carrying the error details |
TransportError |
The network connection failed or timed out |
PollingExhaustedError |
The polling attempts ran out and the task is still unfinished |
WaitInterruptedError |
The task was created and billed, but the wait broke off; carries task_id so the result can still be fetched |
SolutionDecodeError |
The task result shape changed and the SDK has not caught up, so decoding failed. |
UnexpectedResponseError |
The response violates the API contract, including a ready result with no solution |
EzCaptchaError |
Base class, and raised directly when the configuration is invalid or the key is missing |
Whichever failure it is, one question answers whether a billed task is still recoverable:
from ezcapsolver import task_id_of
if task_id := task_id_of(exc):
# The task is on the service; its result is held for five minutes after
# creation. Waiting again is free — creating a second task is billed again.
solved = client.wait_for_result(task_id)None means nothing was billed, so there is nothing to recover.
from ezcapsolver import ApiError, EzCaptchaError, PollingExhaustedError, SolutionDecodeError
try:
solved = client.solve(task)
except ApiError as exc:
# The code, description and HTTP status are all there; a field validation
# failure also lists the per-field reasons in exc.errors.
if exc.is_authentication_error():
... # stop: the service counts these per key and bans after thirty in a minute
elif exc.is_terminal():
... # fix the request; resending it changes nothing
elif exc.is_rate_limited():
... # throttled: both codes clear on their own, so ask again later
print(exc.error_code, exc.error_description, exc.http_status)
except PollingExhaustedError as exc:
# The polling budget ran out. The task may still be running, and this call
# has already been billed — hand the id to wait_for_result() rather than
# paying twice. The service holds the result for five minutes.
print("unfinished:", exc.task_id)
except SolutionDecodeError as exc:
# The worker returned a shape this release does not model.
print("unexpected shape:", exc.raw)
except EzCaptchaError as exc:
print(exc)A throttled poll is the one thing wait_for_result() retries. ERROR_REQUEST_LIMIT and ERROR_REQUEST_BANNED refuse the query, not the task: the service turns the request away before it ever looks the task up, so the task is still queued and still billed. The loop spends the attempt and polls again rather than discarding a result that was about to arrive. Every other ApiError is the poll's answer and ends the wait. Note that is_rate_limited() is narrower than not is_terminal(), which is also true of every unrecognised code — including the worker codes that report a task that genuinely failed.
The SDK logs through the standard logging module under the ezcapsolver logger, with only a NullHandler attached — handlers and levels are entirely the host application's decision.
| Level | Events |
|---|---|
INFO |
Task created, task solved, balance queried |
DEBUG |
The request lifecycle and every polling attempt |
TRACE |
One line per request and per response, carrying the truncated body |
TRACE (ezcapsolver.TRACE, value 5) sits one step below DEBUG: a DataDome html_b64 or an Akamai script_base64 runs to megabytes, and mixing those into DEBUG would make the level unusable for watching the task lifecycle.
Bodies are rendered with clientKey and proxy replaced by [REDACTED] at any nesting depth; with TRACE off the body is never rendered at all, so the default level costs nothing.
Scope the level to this logger — a global DEBUG drowns the SDK's output in httpx's own:
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("ezcapsolver").setLevel(logging.DEBUG)A client's state is read-only after construction and the underlying httpx pool is shareable, so one instance serves a whole process. Creating a client per task only wastes connections.
async with AsyncEzCapSolverClient() as client:
results = await asyncio.gather(
*(client.solve_recaptcha_v2_task_proxyless(u, k) for u, k in sites),
return_exceptions=True,
)The blocking client is equally safe to share across threads.
examples/ holds one runnable file per task type, named after the wire task type, plus five about the SDK itself. The full list is in the example index.
export EZCAPTCHA_API_KEY=your-client-key
# These two use the vendors' own demo pages and run as they are.
python examples/recaptcha_v2/recaptcha_v2_task_proxyless.py
python examples/hcaptcha/hcaptcha.py
# The rest need a key and page data captured from the target site, for instance:
python examples/cloudflare/cloud_flare_turnstile_task.py
python examples/akamai/akamai_web_task_proxyless.py
# About the SDK itself rather than a task type
python examples/async_client_task.py # every client setting, one by one
python examples/blocking_client_task.py # the same, blocking
python examples/concurrency.py # one client, many coroutines
python examples/raw_usage.py # manual polling, unknown task types
python examples/logging_and_errors.py # error paths and redacted logsExamples that need a proxy read EZCAPTCHA_PROXY; none of them hard-code credentials.
Every run creates a real task and is billed, whether or not the worker succeeds.
Requires Python 3.12+ and uv.
uv sync --all-groups
uv run ruff format --check . && uv run ruff check .
uv run mypy ezcapsolver examples
uv run pytest -q
uvx typos # spell check
uv run --with pip-audit pip-audit # dependency auditOptional: install the git hook so every commit runs the same checks.
uv tool install pre-commit && pre-commit installConventions and the release process are in CONTRIBUTING.md.
Licensed under the Apache License 2.0.