Selenium guide

Handle captchas in Selenium flows

Read the sitekey with a few lines of JavaScript, get a token from bypass.so and drop it into the form your WebDriver session is filling. Works today for reCAPTCHA v2, Turnstile and image captchas, and the same code is ready for hCaptcha.

  • Python 3.8+
  • Selenium 4.6+
  • Chrome or Firefox
  • No plugin
example run
  1. 0.0sopen example.com/contactloaded
  2. 0.4sprobe · TurnstileTask 0x4AAA…found
  3. 0.6sfill name, email, messagedone
  4. 0.7sPOST /createTasktaskId
  5. 2.7sGET /getTaskResult/7f3c…processing
  6. 6.8sGET /getTaskResult/7f3c…ready
  7. 6.9sinject cf-turnstile-response1 field
  8. 7.4ssubmit form200
Example run of the flow described on this page

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

Shellterminal
python -m pip install selenium requests

Store your key

The examples read your key from the BYPASS_API_KEY environment variable, so it never lands in source control.

ShellmacOS / Linux
export BYPASS_API_KEY="your-api-key"
PowerShellWindows PowerShell
$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:

  1. 01

    Read the sitekey

    Run a small probe in the page to find the widget type and its public sitekey.

    execute_script
  2. 02

    Create a task

    Send the type, sitekey and page URL to the API.

    POST /createTask
  3. 03

    Wait for the token

    Poll every two seconds until the result is ready.

    GET /getTaskResult
  4. 04

    Inject and submit

    Write the token into the response field, fire the callback, submit.

    injectToken

Project layout used below:

Treefiles
contact_selenium.py
bypass_client.py
page_helpers.py

Step 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.

Pythonbypass_client.py
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.

Pythonpage_helpers.py
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:

Pythonstep 2
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.

Pythonstep 3
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.

Pythonstep 4
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

WidgetWhere the sitekey livesWhere the answer goes
reCAPTCHA v2RecaptchaV2Task
.g-recaptcha[data-sitekey] or the k parameter of its iframe
textarea[name="g-recaptcha-response"]
reCAPTCHA v3 Coming soonRecaptchaV3Task
render= parameter of the api.js script tag
The field or request body the page sends, usually g-recaptcha-response
hCaptcha Coming soonHCaptchaTask
.h-captcha[data-sitekey] or sitekey in the iframe URL
textarea[name="h-captcha-response"] plus g-recaptcha-response if present
TurnstileTurnstileTask
.cf-turnstile[data-sitekey] (keys start with 0x4)
input[name="cf-turnstile-response"]
ImageImageToTextTask
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.

Pythoncontact_selenium.py
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()
Shellrun it
python contact_selenium.py
Outputexpected output
found TurnstileTask 0x4AAAAAAA-SITE-KEY
submitted, now at https://example.com/contact/thanks

What 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.

Pythonimage_captcha.py
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.

Pythonrecaptcha v3
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

ErrorSourceWhat it meansHow 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

Next steps