Setup
Before you start
- Python 3.8 or newer
- The
requestspackage - A bypass.so API key (create one) with a positive balance
Install
python -m pip install requestsStore your key
The examples read your key from the BYPASS_API_KEY environment variable, so it never lands in source control.
export BYPASS_API_KEY="your-api-key"$env:BYPASS_API_KEY = "your-api-key"Check that the key works: curl -H "Authorization: Bearer $BYPASS_API_KEY" https://bypass.so/balance returns your balance.
Step 1
Add the helper
Save this file next to your code. It wraps the whole task lifecycle in one function, solve(task_type, params), so the rest of your project never deals with polling.
- createTask: sends your key, the task type and its fields.
- Fast path: if the answer is already ready, it returns at once.
- Polling: otherwise it checks
/getTaskResult/{taskId}every two seconds until the result is ready or your timeout runs out. - Errors: any
errorId: 1becomes aBypassErrorcarrying theerrorCode, so you can branch on it. - taskId: added to the returned solution, so you can report a wrong answer later.
import os
import time
import requests
API_URL = "https://bypass.so"
CLIENT_KEY = os.environ.get("BYPASS_API_KEY", "")
class BypassError(Exception):
def __init__(self, code, description="", task_id=None):
super().__init__(f"{code}: {description}")
self.code = code
self.description = description
self.task_id = task_id
def _read(response):
try:
data = response.json()
except ValueError:
raise BypassError("HTTP_ERROR", f"HTTP {response.status_code} without a JSON body")
if data.get("errorId"):
raise BypassError(
data.get("errorCode", "ERROR_UNKNOWN"),
data.get("errorDescription", ""),
data.get("taskId"),
)
return data
def solve(task_type, params, timeout=120, poll_every=2.0):
if not CLIENT_KEY:
raise BypassError("ERROR_KEY_INVALID", "set the BYPASS_API_KEY environment variable")
body = {"clientKey": CLIENT_KEY, "type": task_type, **params}
data = _read(requests.post(f"{API_URL}/createTask", json=body, timeout=30))
task_id = data["taskId"]
deadline = time.monotonic() + timeout
while data.get("status") != "ready":
if time.monotonic() > deadline:
raise TimeoutError(f"task {task_id} was not ready after {timeout}s")
time.sleep(poll_every)
data = _read(requests.get(
f"{API_URL}/getTaskResult/{task_id}",
params={"clientKey": CLIENT_KEY},
timeout=30,
))
solution = dict(data.get("solution") or {})
solution["taskId"] = task_id
return solution
def token_of(solution):
return solution.get("gRecaptchaResponse") or solution.get("token") or solution.get("text")
def balance():
headers = {"Authorization": f"Bearer {CLIENT_KEY}"}
return _read(requests.get(f"{API_URL}/balance", headers=headers, timeout=30))
def report_bad(task_id):
headers = {"Authorization": f"Bearer {CLIENT_KEY}"}
return _read(requests.post(
f"{API_URL}/reportBad",
params={"task_id": task_id},
headers=headers,
timeout=30,
))Step 2
Solve any supported type
Every task type uses the same call. Only the type name and its fields change. Pick a tab to see a complete, runnable file. Tabs marked Soon are ready for launch day, but those types are not live yet. Run it with python solve_image.py (rename to match the tab).
from bypass_client import solve
solution = solve("MathCaptchaTask", {"captchaText": "12 + 30 = ?"})
print(solution["text"])import base64
from bypass_client import solve
with open("captcha.png", "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
solution = solve("ImageToTextTask", {"imageBase64": image_b64})
print(solution["text"])import base64
from bypass_client import solve
with open("captcha.mp3", "rb") as f:
audio_b64 = base64.b64encode(f.read()).decode()
solution = solve("AudioCaptchaTask", {"audioBase64": audio_b64})
print(solution["text"])import requests
from bypass_client import solve, token_of
PAGE_URL = "https://example.com/contact"
solution = solve("RecaptchaV2Task", {
"websiteURL": PAGE_URL,
"websiteKey": "SITE_KEY_FROM_THE_PAGE",
})
reply = requests.post(PAGE_URL, data={
"email": "[email protected]",
"message": "Hello",
"g-recaptcha-response": token_of(solution),
}, timeout=30)
print(reply.status_code)Coming soon
RecaptchaV3Task is not live yet. Until launch, createTask returns ERROR_TASK_NOT_SUPPORTED and nothing is charged. Get notified when it ships.
import requests
from bypass_client import solve, token_of
PAGE_URL = "https://example.com/checkout"
solution = solve("RecaptchaV3Task", {
"websiteURL": PAGE_URL,
"websiteKey": "SITE_KEY_FROM_THE_PAGE",
"action": "submit",
})
reply = requests.post(PAGE_URL, data={
"order": "1042",
"g-recaptcha-response": token_of(solution),
}, timeout=30)
print(reply.status_code)Coming soon
HCaptchaTask is not live yet. Until launch, createTask returns ERROR_TASK_NOT_SUPPORTED and nothing is charged. Get notified when it ships.
import requests
from bypass_client import solve, token_of
PAGE_URL = "https://example.com/signup"
solution = solve("HCaptchaTask", {
"websiteURL": PAGE_URL,
"websiteKey": "SITE_KEY_FROM_THE_PAGE",
})
token = token_of(solution)
reply = requests.post(PAGE_URL, data={
"email": "[email protected]",
"h-captcha-response": token,
"g-recaptcha-response": token,
}, timeout=30)
print(reply.status_code)import requests
from bypass_client import solve, token_of
PAGE_URL = "https://example.com/login"
solution = solve("TurnstileTask", {
"websiteURL": PAGE_URL,
"websiteKey": "SITE_KEY_FROM_THE_PAGE",
})
reply = requests.post(PAGE_URL, data={
"email": "[email protected]",
"cf-turnstile-response": token_of(solution),
}, timeout=30)
print(reply.status_code)Token types and your form
For reCAPTCHA, Turnstile and (once live) hCaptcha the token goes into the same form field the widget would fill: g-recaptcha-response, h-captcha-response or cf-turnstile-response. Driving a real page? See the Selenium, Playwright or Puppeteer guides.
Step 3
Handle errors and retries
Some failures are worth a second try and some are not. A missing balance or a wrong key will fail again, so stop and fix it. An unsolvable task or a network hiccup is worth one or two more attempts with a short pause.
import time
import requests
from bypass_client import BypassError, solve
RETRYABLE = {"ERROR_CAPTCHA_UNSOLVABLE", "ERROR_INTERNAL", "HTTP_ERROR"}
def solve_with_retry(task_type, params, attempts=3):
for attempt in range(1, attempts + 1):
try:
return solve(task_type, params)
except BypassError as err:
if err.code not in RETRYABLE or attempt == attempts:
raise
print(f"attempt {attempt}: {err.code}, retrying")
except (TimeoutError, requests.RequestException) as err:
if attempt == attempts:
raise
print(f"attempt {attempt}: {err!r}, retrying")
time.sleep(2 * attempt)
if __name__ == "__main__":
print(solve_with_retry("MathCaptchaTask", {"captchaText": "12 + 30 = ?"}))Failed tasks cost nothing
The task price is held when you create a task and released if it fails. You are only charged when the result is ready.
Step 4
Check your balance and flag wrong answers
GET /balance and POST /reportBad use your key as a Bearer token. Report a task only when the target site actually rejected its answer. The refund is applied once per task, and the task is closed afterwards.
from bypass_client import balance, report_bad
def flag_wrong_answer(solution):
result = report_bad(solution["taskId"])
print("refunded units:", result["refunded"], "balance now:", result["balanceUsd"])
if __name__ == "__main__":
info = balance()
print(f"balance: ${info['balanceUsd']:.2f}")Scale
Solve many tasks in parallel
Each task is independent, so throughput grows with concurrency. A thread pool is the simplest way to run several blocking solve calls at once. Start with a limit of 8 and adjust while you watch error rates and spend.
import base64
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from bypass_client import BypassError, solve
def read_one(path):
image_b64 = base64.b64encode(path.read_bytes()).decode()
return path.name, solve("ImageToTextTask", {"imageBase64": image_b64})["text"]
files = sorted(Path("captchas").glob("*.png"))
with ThreadPoolExecutor(8) as pool:
jobs = [pool.submit(read_one, p) for p in files]
for done in as_completed(jobs):
try:
name, text = done.result()
print(f"{name}: {text}")
except BypassError as err:
print("failed:", err)Reference
Task type reference
What each type expects and returns. Prices are per 1,000 solved tasks; larger top-ups add bonus credit (see pricing).
| Task type | You send | You get | Price / 1k |
|---|
Errors
Troubleshooting
| Error | Source | What it means | How to fix it |
|---|---|---|---|
ERROR_KEY_INVALID | HTTP 403 | The key is missing, mistyped or revoked. | Check that BYPASS_API_KEY is set in the same shell that runs your code, with no quotes or spaces around it. |
ERROR_INSUFFICIENT_CREDITS | HTTP 402 | Your balance cannot cover the task price. | Top up from the dashboard. Nothing was charged for this request. |
ERROR_TASK_NOT_SUPPORTED | HTTP 200 | The type string is unknown, or the type is still coming soon (reCAPTCHA v3, hCaptcha). | Copy the exact name, for example TurnstileTask. Names are case-sensitive. Nothing is charged. |
ERROR_BAD_REQUEST | HTTP 400 | The body is not valid JSON, or a field has the wrong type. | Send Content-Type: application/json and string values for clientKey, type and the task fields. |
ERROR_CAPTCHA_UNSOLVABLE | HTTP 200 | This task could not be completed. | You are not charged. Check the image, sitekey and page URL, then submit a new task. |
ERROR_NOT_FOUND | HTTP 404 | The taskId is unknown or belongs to another key. | Poll with the taskId returned by your own createTask call, using the same key. |
ERROR_INTERNAL | HTTP 500 | Something went wrong on our side. | Retry after a short pause. The helper’s retry wrapper does this for you. |
Helper timeout | your code | No result inside your timeout. | Token types usually need several seconds. Keep at least 60–120 s and retry with a new task. |
HTTP_ERROR | your code | A proxy or network layer answered without JSON. | Retry with backoff. If it persists, check that your code calls https://bypass.so. |
Every error body has the same shape: {"errorId":1,"errorCode":"…","errorDescription":"…"}. Full list in the API reference.
Production
Best practices
Keep the key out of code
Read it from an environment variable or your secret store. Never commit it or ship it to a web page.
Solve right before submit
Tokens are short-lived. Fill the form first, then request the token and submit at once.
Cap your concurrency
Start with 5–10 parallel tasks and raise it while you watch error rates and spend.
Log every taskId
It is the only thing you need to debug a result or report a wrong answer.
Match the page exactly
Send the URL where the widget renders and the sitekey from that page, not from a different form.
Report wrong answers
If a site rejects a solved result, call reportBad with its taskId. The price returns to your balance once.
FAQ
Questions
Do I need an SDK?
No. The API is two HTTPS endpoints that take and return JSON. The helper on this page is about sixty lines that you own, can read in a minute and can change freely.
Why does createTask sometimes return the answer straight away?
Math, image and audio tasks often finish while the createTask request is still open. The response then already has "status": "ready" and a solution, so the helper returns without polling. Token types always come back as processing first.
How often should I poll getTaskResult?
Every two seconds is a good default. Polling faster does not make a task finish sooner. Keep the overall timeout at 60 to 120 seconds for token types.
Am I charged for failed tasks?
No. You pay only when a task comes back ready. If a site rejects a solved answer, call reportBad with its taskId and the price returns to your balance once. A reported task is closed, so submit a new one to try again.
Can I use an async client instead?
Yes. The flow is the same two calls. In Python, swap requests for an async HTTP client and time.sleep for asyncio.sleep. In Node.js, any client works as long as you parse the JSON body on error status codes too.
Policy
Responsible use
Use it where you are allowed to
Automate pages you own, test environments you run, or sites whose terms permit automated access. Do not use bypass.so for fraud, account takeover, spam, scalping, evading bans or collecting personal data without a lawful basis. Keep request rates reasonable and stop when a site asks you to. Our acceptable use policy applies to every key, and keys used for abuse are closed.
Keep going