Setup
Before you start
- Python 3.8 or newer
- Selenium 4.6 or newer, which fetches a matching driver for your installed Chrome or Firefox
- A bypass.so API key (create one) with a positive balance
Install
python -m pip install selenium 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.
Overview
How it fits together
Your Selenium session stays in charge of the page. bypass.so only sees the sitekey and the page URL you send, and returns a token. The whole round trip is four steps:
- 01
Read the sitekey
Run a small probe in the page to find the widget type and its public sitekey.
execute_script - 02
Create a task
Send the type, sitekey and page URL to the API.
POST /createTask - 03
Wait for the token
Poll every two seconds until the result is ready.
GET /getTaskResult - 04
Inject and submit
Write the token into the response field, fire the callback, submit.
injectToken
Project layout used below:
contact_selenium.py
bypass_client.py
page_helpers.pyStep 1
Add the API helper
This is the same helper as in the Python guide: one solve function that creates a task, polls for the result, raises a clear error and enforces a timeout.
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
Read the sitekey from the page
reCAPTCHA, hCaptcha and Turnstile all publish a sitekey in the page: usually a data-sitekey attribute on the widget element, otherwise a parameter in the widget’s iframe URL. The probe below checks both, tells you which widget it found and picks up the data-callback name if the page sets one. The second function writes a token into the right field later.
FIELDS = {
"RecaptchaV2Task": "g-recaptcha-response",
"RecaptchaV3Task": "g-recaptcha-response",
"HCaptchaTask": "h-captcha-response",
"TurnstileTask": "cf-turnstile-response",
}
PROBE_JS = r"""
function () {
const kinds = [
["TurnstileTask", ".cf-turnstile", "iframe[src*='challenges.cloudflare.com']"],
["HCaptchaTask", ".h-captcha", "iframe[src*='hcaptcha.com']"],
["RecaptchaV2Task", ".g-recaptcha", "iframe[src*='/recaptcha/']"],
];
for (const [type, box, frame] of kinds) {
const host = document.querySelector(box + "[data-sitekey]");
if (host) return { type, sitekey: host.dataset.sitekey, callback: host.dataset.callback || null };
const iframe = document.querySelector(frame);
if (iframe) {
const url = new URL(iframe.src, location.href);
const hash = new URLSearchParams(url.hash.slice(1));
const sitekey = url.searchParams.get("k") || url.searchParams.get("sitekey")
|| hash.get("sitekey") || (url.pathname.match(/0x4[\w-]+/) || [])[0];
if (sitekey) return { type, sitekey, callback: null };
}
}
const any = document.querySelector("[data-sitekey]");
if (!any) return null;
const key = any.dataset.sitekey;
const type = key.startsWith("0x4") ? "TurnstileTask"
: /^[0-9a-f-]{36}$/i.test(key) ? "HCaptchaTask" : "RecaptchaV2Task";
return { type, sitekey: key, callback: any.dataset.callback || null };
}
"""
INJECT_JS = r"""
function ({ field, token, callback }) {
let targets = [...document.querySelectorAll('[name="' + field + '"]')];
if (field === "h-captcha-response") {
targets.push(...document.querySelectorAll('[name="g-recaptcha-response"]'));
}
if (!targets.length) {
const input = document.createElement("input");
input.type = "hidden";
input.name = field;
(document.querySelector("form") || document.body).appendChild(input);
targets = [input];
}
for (const el of targets) {
el.value = token;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
const fn = callback && callback.split(".").reduce((o, k) => (o ? o[k] : undefined), window);
if (typeof fn === "function") fn(token);
return targets.length;
}
"""Wait for the probe to return something, because many widgets load after the rest of the page:
widget = WebDriverWait(driver, 20).until(
lambda d: d.execute_script("return (" + PROBE_JS + ")();")
)
print(widget)Step 3
Request a token
Send the widget type, its sitekey and the exact URL of the page the widget is on. Fill in the rest of the form before this step, because tokens are short-lived.
solution = solve(widget["type"], {
"websiteURL": driver.current_url,
"websiteKey": widget["sitekey"],
})
token = token_of(solution)What comes back
For token types the solution holds the token (in token or gRecaptchaResponse) and usually tokenField, the name of the field it belongs in. The helper’s token_of reads either key.
Step 4
Inject the token and submit
Write the token into the hidden response field, call the page’s callback if it has one, then submit the form as a user would. If the field does not exist yet, injectToken creates it inside the form.
field = solution.get("tokenField") or FIELDS[widget["type"]]
driver.execute_script(
"return (" + INJECT_JS + ")(arguments[0]);",
{"field": field, "token": token, "callback": widget["callback"]},
)
driver.find_element(By.CSS_SELECTOR, 'form [type="submit"]').click()Field reference
| Widget | Where the sitekey lives | Where the answer goes |
|---|---|---|
reCAPTCHA v2 RecaptchaV2Task | .g-recaptcha[data-sitekey] or the k parameter of its iframe | textarea[name="g-recaptcha-response"] |
reCAPTCHA v3 Coming soon RecaptchaV3Task | render= parameter of the api.js script tag | The field or request body the page sends, usually g-recaptcha-response |
hCaptcha Coming soon HCaptchaTask | .h-captcha[data-sitekey] or sitekey in the iframe URL | textarea[name="h-captcha-response"] plus g-recaptcha-response if present |
Turnstile TurnstileTask | .cf-turnstile[data-sitekey] (keys start with 0x4) | input[name="cf-turnstile-response"] |
Image ImageToTextTask | Screenshot of the captcha <img> as base64 | Type solution.text into the answer input |
Full code
Complete example
Everything above in one file. It opens a contact form, fills it, finds the captcha, gets a token, injects it and submits. Replace the URL and field names with your own page.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from bypass_client import solve, token_of
from page_helpers import FIELDS, INJECT_JS, PROBE_JS
PAGE_URL = "https://example.com/contact"
PROBE = "return (" + PROBE_JS + ")();"
INJECT = "return (" + INJECT_JS + ")(arguments[0]);"
def main():
driver = webdriver.Chrome()
try:
driver.get(PAGE_URL)
wait = WebDriverWait(driver, 20)
widget = wait.until(lambda d: d.execute_script(PROBE))
print("found", widget["type"], widget["sitekey"])
driver.find_element(By.NAME, "name").send_keys("Ada")
driver.find_element(By.NAME, "email").send_keys("[email protected]")
driver.find_element(By.NAME, "message").send_keys("Hello from my test run")
solution = solve(widget["type"], {
"websiteURL": driver.current_url,
"websiteKey": widget["sitekey"],
})
field = solution.get("tokenField") or FIELDS[widget["type"]]
driver.execute_script(INJECT, {
"field": field,
"token": token_of(solution),
"callback": widget["callback"],
})
driver.find_element(By.CSS_SELECTOR, 'form [type="submit"]').click()
wait.until(EC.url_changes(PAGE_URL))
print("submitted, now at", driver.current_url)
finally:
driver.quit()
if __name__ == "__main__":
main()python contact_selenium.pyfound TurnstileTask 0x4AAAAAAA-SITE-KEY
submitted, now at https://example.com/contact/thanksWhat it costs
Recognition
Image captchas: screenshot to text
For classic text-in-an-image captchas there is no token. Take a screenshot of just the image element, send it as imageBase64 and type the returned text into the answer box. A screenshot works even when the image URL changes on every request.
from selenium.webdriver.common.by import By
from bypass_client import solve
image = driver.find_element(By.CSS_SELECTOR, "img.captcha")
solution = solve("ImageToTextTask", {"imageBase64": image.screenshot_as_base64})
box = driver.find_element(By.NAME, "captcha")
box.clear()
box.send_keys(solution["text"])
driver.find_element(By.CSS_SELECTOR, 'form [type="submit"]').click()Math and audio
For a written sum, read the text and send it as captchaText with MathCaptchaTask. For audio, download the clip in your session and send it base64-encoded as audioBase64 with AudioCaptchaTask.
Coming soon
reCAPTCHA v3 pages
Coming soon
RecaptchaV3Task is not live yet: createTask currently returns ERROR_TASK_NOT_SUPPORTED and nothing is charged. The steps below are ready for launch day. Get notified.
reCAPTCHA v3 has no visible widget. The sitekey is the render parameter of the script tag, and the page asks for a token with an action name when the user submits. Find that name in the page source (look for grecaptcha.execute) and send the same value.
site_key = driver.execute_script("""
const s = document.querySelector("script[src*='recaptcha'][src*='render=']");
return s ? new URL(s.src).searchParams.get("render") : null;
""")
solution = solve("RecaptchaV3Task", {
"websiteURL": driver.current_url,
"websiteKey": site_key,
"action": "submit",
})Where the token goes
Version 3 pages often send the token from JavaScript rather than a form field. Watch the network request the page makes on submit and put the token in the same field or parameter.
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. |
No widget found | your code | The probe returned nothing within 20 seconds. | The widget may render late or only after a click. Wait for the form, scroll it into view, or pass the sitekey yourself. |
Site rejects the token | your code | The token expired, or it was requested for a different URL or sitekey. | Request the token right before you submit, and send the exact page URL as websiteURL. |
Form still asks for the captcha | your code | The page waits for its widget callback before enabling submit. | Pass the data-callback name to injectToken (the probe does this when the attribute exists). |
Helper timeout | your code | No result inside your timeout. | Raise the timeout for token types, then retry with a new task. |
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 to click the captcha widget?
No. The probe only reads attributes from the page. You request a token through the API and place it in the hidden field the widget would have filled. The checkbox does not need to look ticked, because the site checks the token, not the widget’s appearance.
The submit button stays disabled. What now?
Some pages enable the button from the widget’s JavaScript callback. When the widget element has a data-callback attribute, the probe returns it and injectToken calls that function with the token. If the callback is registered another way, find its name in the page source and pass it yourself.
Does this work for invisible reCAPTCHA v2?
Yes. Use RecaptchaV2Task. The sitekey is on the element with data-sitekey (often the submit button) or in the k parameter of the widget iframe. Invisible widgets almost always rely on a callback, so pass it along.
How long does a token stay valid?
Not long. reCAPTCHA tokens expire after about two minutes, and Turnstile results include tokenLifetime when it is known. Fill the form first, then request the token and submit straight away.
Can I run many sessions in parallel?
Yes. Every call to solve is independent. Run one per session and cap how many run at once so a slow site does not pile up open tasks.
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