SolveHub

Python CAPTCHA Solver API Integration

Use SolveHub's Python CAPTCHA solver API in authorized tests. Run a dependency-free client to create a task, poll for the result, and resume safely after a timeout.

For authorized QA automation and browser integration. Use only on websites and workflows you have permission to test.

Use Python 3.10+ to create and poll SolveHub CAPTCHA tasks in a website or workflow you are authorized to test. The downloadable client uses only the Python standard library. It handles request timeouts, bounded retries, idempotent task creation and resumable polling.

This is a server-side integration, not a browser extension or a method for testing third-party sites without permission. Check that your task type is currently available before creating a paid task.

Before You Start

You need an active SolveHub API key with the tasks permission, sufficient balance, and the parameters for your authorized test page. Register and verify your email, then activate self-service if available or bind a key issued by SolveHub. Keep the API key and any proxy credentials out of your source code.

Check current task availability and pricing before running the example: live prices.

Create and Poll a Task with Python

Download python-client.py. Create a task.json file with the challenge type and fields for your own test page. For a Turnstile example:

{"type":"turnstile","task":{"websiteURL":"https://example.com/login","websiteKey":"REPLACE_WITH_SITE_KEY"}}

Replace the URL and sitekey with values from a page you are authorized to test. The placeholders above cannot create a usable task.

Run:

curl -fsSLO https://www.tiam.codes/downloads/python-client.py
export SOLVEHUB_API_KEY='sh_REPLACE_ME'
export SOLVEHUB_IDEMPOTENCY_KEY='persisted-authorized-test-0001'
export SOLVEHUB_TASK_FILE='task.json'
python3 python-client.py

The same flow in one copyable file, standard library only. It creates a task, polls until a terminal state, and resumes from SOLVEHUB_TASK_ID when you already have one:

import json, os, sys, time, urllib.request

BASE = os.environ.get("SOLVEHUB_URL", "https://www.tiam.codes")
KEY = os.environ["SOLVEHUB_API_KEY"]
DEADLINE = time.time() + 240


def call(method, path, payload=None, idem=None):
    body = json.dumps(payload).encode() if payload is not None else None
    request = urllib.request.Request(BASE + path, data=body, method=method)
    request.add_header("Authorization", "Bearer " + KEY)
    if body:
        request.add_header("Content-Type", "application/json")
    if idem:
        request.add_header("Idempotency-Key", idem)
    with urllib.request.urlopen(request, timeout=20) as response:
        return json.load(response)


def wait(task_id):
    while time.time() < DEADLINE:
        task = call("GET", "/v1/tasks/" + task_id)
        if task["status"] == "completed":
            return task["solution"]
        if task["status"] in {"failed", "cancelled"}:
            raise SystemExit("task %s ended as %s; check failureReason" % (task_id, task["status"]))
        time.sleep(3)
    raise SystemExit("client deadline reached; keep taskId and resume later")


if os.environ.get("SOLVEHUB_TASK_ID"):
    task_id = os.environ["SOLVEHUB_TASK_ID"]
else:
    with open(os.environ["SOLVEHUB_TASK_FILE"]) as handle:
        task_id = call("POST", "/v1/tasks", json.load(handle), os.environ["SOLVEHUB_IDEMPOTENCY_KEY"])["taskId"]

print("taskId=" + task_id, file=sys.stderr)
print(json.dumps(wait(task_id)))

The script prints only the solution to stdout and the task ID to stderr. A completed Turnstile task therefore prints:

{"value": "0.EXAMPLE_TOKEN"}

If the client deadline is reached the script exits but the server task keeps running; re-run it with SOLVEHUB_TASK_ID set to continue polling the same task instead of paying for a new one.

The client sends POST /v1/tasks, saves the returned taskId to stderr as soon as it is known, then polls GET /v1/tasks/{taskId}. It prints only the completed solution to stdout. Store taskId in your job record, and use a new idempotency key for each new logical task.

Resume After a Timeout

A client deadline does not necessarily mean the server task failed. If you saved taskId, resume polling that task instead of creating another one:

export SOLVEHUB_TASK_ID='SAVED_TASK_ID'
python3 python-client.py

If no taskId was received, retry only with the original idempotency key and the exact same request body within the documented replay window. Do not generate a fresh key just because the connection timed out. See retrying without duplicates.

Understand Failures Before Retrying

The example client uses a 20-second timeout per request, a 240-second overall budget and bounded retries for retryable responses. These are client settings, not a guarantee of task completion time. It stops on failed or cancelled tasks and continues checking an uncertain task. It also rejects HTTP redirects so the key is never forwarded to another origin.

For HTTP 402, check balance and pricing. For HTTP 429, respect Retry-After. For other errors, inspect the API error and X-Request-ID before changing the request. Never log your API key or proxy password.

Read error handling and safe polling before adapting this example for production.

Client version and failure handling

The downloadable client is version 1.0.0 and uses only the Python standard library (3.10+). Its behaviour is fixed and documented, so a failing run can be diagnosed without reading the source:

  • 20-second timeout per request, 240-second overall budget, at most 3 attempts per request.
  • Honours Retry-After in seconds or as an HTTP date on 429, 500, 502, 503 and 504.
  • Rejects HTTP redirects, so the API key is never forwarded to another origin.
  • Writes taskId to stderr as soon as it is known; stdout receives the solution only, so one completed task prints exactly one JSON value.
Response Meaning What to do
400 INVALID_TASK A task field is missing or invalid. Fix the fields, then send the corrected request as a new task with a new key. If the original outcome is uncertain, verify that task first instead of resending.
401 UNAUTHORIZED The key is invalid or revoked. Replace the key. Retrying the same request cannot succeed.
402 INSUFFICIENT_BALANCE Balance is too low; no task was created. Top up, then retry with the same idempotency key.
403 with a key error code Scope, expiry or IP allowlist rejects this key. Check the key scopes, expiry and allowlist in the console.
429 Rate limited. Wait for Retry-After; the client already backs off.
500, 502, 503, 504 Transient upstream failure. Let the bounded retries run, then retry with the same key and payload.
TASK_FAILED, TASK_TIMEOUT The task ended without a usable result. Read failureReason; do not automatically repost the same task.
TASK_UNCERTAIN The outcome is unknown. Keep polling the same taskId; do not create a replacement.

Exit codes are part of the contract: 0 means a solution was printed, 1 means an error message on stderr. A fixed payload is a different request, so it needs a new key — the same key must always carry the same body, as described in retrying without duplicates.

Next Steps

Read the quick start for the full task flow, the Turnstile guide for its required fields, and the idempotency and error-handling guides before adapting this example for production. The complete request and response contract is in the API reference.

Developer downloads

python-client.py v1.0.0 — standard library only; behaviour and error codes are documented on this page.

Related documentation

Start with cURL Create account

Last updated: