DEV Community

DBC
DBC

Posted on

Selenium Captcha Solver: Wire Death By Captcha into Your Test Runs

Selenium turns a browser script into a test that acts like a
user: it clicks, types, waits, asserts. A captcha challenge is
where that illusion breaks — the run stalls on a widget that no
amount of click emulation reliably dismisses. This quickstart
wires Death By Captcha (DBC) into a Selenium script with one
repeatable pattern: the script reads the challenge parameters
from the page, requests a solution from the DBC API, injects the
token back into the page, and continues — the site verifies it
exactly as if the widget had been solved in a real browser.

Everything below runs against Google's own public reCAPTCHA demo,
so you can test the whole loop end to end before pointing it at
your own target. The calls are the ones from the official
samples — the Selenium integration page and the runnable
examples that ship in the Python and Node.js client
repositories — nothing here is invented. The shape is the same
one the Playwright integration uses: extract, request, inject,
submit, assert.

Why the token flow

reCAPTCHA v2 has two integration shapes: replaying the interactive
challenge inside your browser context, or the token flow used here.
The token flow solves the challenge on the provider's side and returns
a response token that is valid for one submission; your script injects
it and moves on. That is what makes it work headless and inside CI:
nothing in your driver has to mimic a human pointer, and the page
sees a normal solved widget.

The same shape repeats across the supported set — the payload fields
change, the loop does not.

What you need

  • A DBC account — solutions are paid from your account balance, so check the balance before decoding: https://deathbycaptcha.com/register
  • Python 3 or Node.js, plus Selenium and Firefox or Chrome (Selenium 4 resolves the driver on its own, so the browser is the only extra install)
  • Credentials in environment variables, never in the code:
export DBC_USERNAME=your_username
export DBC_PASSWORD=your_password
Enter fullscreen mode Exit fullscreen mode

Install

Python (the integration page's line):

pip install selenium deathbycaptcha-official
Enter fullscreen mode Exit fullscreen mode

Node.js:

npm install selenium-webdriver deathbycaptcha-lib
Enter fullscreen mode Exit fullscreen mode

The full flow in Python

Adapted from the official runnable in the Python client
repository (examples/deathbycaptcha-python-selenium):

import json
import os
import time

import deathbycaptcha
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

USERNAME = os.getenv("DBC_USERNAME")
PASSWORD = os.getenv("DBC_PASSWORD")
DEMO_URL = "https://www.google.com/recaptcha/api2/demo"


def solve_captcha(payload):
    client = deathbycaptcha.SocketClient(USERNAME, PASSWORD)
    # optional pre-flight, both official samples do it:
    # client.get_balance()
    result = client.decode(type=4, token_params=json.dumps(payload))
    return result.get("text") if result else None


with webdriver.Firefox() as driver:
    driver.set_page_load_timeout(60)
    driver.get(DEMO_URL)

    # 1. extract the challenge parameters from the page
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "recaptcha-demo")))
    sitekey = element.get_attribute("data-sitekey")

    # 2. request the solution token from DBC (type 4 = reCAPTCHA v2)
    token = solve_captcha({"googlekey": sitekey, "pageurl": DEMO_URL})
    if not token:
        raise SystemExit("no solution yet - retry with backoff")

    # 3. inject the token back into the page
    driver.execute_script(
        "document.getElementById('g-recaptcha-response')"
        ".value = arguments[0]", token)

    # 4. submit and check the success element
    driver.find_element(By.ID, "recaptcha-demo-submit").click()
    time.sleep(2)
    print(driver.find_element(By.CLASS_NAME, "recaptcha-success").text)
Enter fullscreen mode Exit fullscreen mode

Two details lifted straight from the official runnable: the
explicit WebDriverWait instead of blind sleeps for finding the
widget, and the page-load timeout so a stalled navigation fails
loudly instead of hanging the suite. The full script also switches
Firefox/Chrome and headless mode from environment variables.

The same flow in Node.js

Adapted from the official runnable in the Node.js client
repository (examples/selenium/recaptcha_v2_selenium.js):

const { Builder, until, By } = require('selenium-webdriver');
const { HttpClient } = require('deathbycaptcha-lib');

const USERNAME = process.env.DBC_USERNAME;
const PASSWORD = process.env.DBC_PASSWORD;
const DEMO_URL = 'https://www.google.com/recaptcha/api2/demo';

const client = new HttpClient(USERNAME, PASSWORD);

function decode(payload) {
  return new Promise((resolve, reject) =>
    client.decode({ extra: payload }, (solution) =>
      solution && solution.text
        ? resolve(solution.text)
        : reject(new Error('no solution yet - retry with backoff'))));
}

(async () => {
  const driver = await new Builder().forBrowser('firefox').build();

  try {
    await driver.get(DEMO_URL);

    // 1. extract the challenge parameters from the page
    const element = await driver.wait(
      until.elementLocated(By.id('recaptcha-demo')), 15000);
    const sitekey = await element.getDomAttribute('data-sitekey');

    // 2. request the solution token from DBC (type 4 = reCAPTCHA v2)
    const token = await decode({
      type: 4,
      token_params: JSON.stringify(
        { googlekey: sitekey, pageurl: DEMO_URL }),
    });

    // 3. inject the token back into the page
    await driver.executeScript(`
      const token = arguments[0];
      const textarea = document.getElementById('g-recaptcha-response');
      textarea.style.display = 'block';
      textarea.value = token;
      textarea.innerHTML = token;
      textarea.dispatchEvent(new Event('input', { bubbles: true }));
      textarea.dispatchEvent(new Event('change', { bubbles: true }));
      if (window.___grecaptcha_cfg && window.___grecaptcha_cfg.clients) {
        const clients = window.___grecaptcha_cfg.clients;
        for (const clientKey of Object.keys(clients)) {
          const item = clients[clientKey];
          for (const key of Object.keys(item)) {
            const entry = item[key];
            if (entry && typeof entry.callback === 'function') {
              entry.callback(token);
              return true;
            }
            if (entry && entry.W && typeof entry.W.callback === 'function') {
              entry.W.callback(token);
              return true;
            }
          }
        }
      }
      return true;
    `, token);

    // 4. submit and wait for the success element
    await driver.findElement(By.id('recaptcha-demo-submit')).click();
    await driver.wait(
      until.elementLocated(By.className('recaptcha-success')), 15000);
    console.log('success');
  } finally {
    await driver.quit();
  }
})();
Enter fullscreen mode Exit fullscreen mode

The page-side steps are identical to the Python version; the
injection block goes one step further and fires the widget's own
callback from the reCAPTCHA config tree — that is what wakes the
page up when setting the textarea alone is not enough. The full
runnable wraps the same logic in an exported function with the
browser and timeouts read from environment variables.

Beyond reCAPTCHA v2

The same loop covers the rest of the supported set. Turnstile is the
same call with a different type and payload name — type=12 with a
turnstile_params payload carrying sitekey and pageurl — and
reCAPTCHA v3, GeeTest, DataDome, plus image and text captchas follow
their own documented payloads: each CAPTCHA type is routed to a model
specialized for that type. The official Python client ships a runnable
sample per type under examples/.

Both clients default to the encrypted HTTPS transport and also ship a
socket transport for high-throughput production runs; the method names
stay the same, so moving between them later is a constructor change,
not a rewrite. The Python flow above already runs on the socket
client — same decode call the HTTPS transport serves.

On the library side, official clients cover Python, Node.js, C#, Java,
.NET, Go, C, C++ and Perl, and the browser extensions for Chrome/Brave
and Firefox cover the no-code path for manual sessions.

Fitting it into a bigger run

The official runnables are built for pipelines: browser choice,
headless mode and wait timeouts all come from environment
variables, and the Node.js sample extends its wait when it
detects CI so slow runners do not race the page. Keep three
habits from them:

  • One code path for local and CI — only flags change, never the flow. Inconsistent challenge handling across environments is how green suites start failing on runners.
  • Explicit waits around the widget and the success element; a sleep is only safe where the official sample uses it as a short demo pause, not as synchronization.
  • Fail loudly on timeouts (page-load timeout, element waits) instead of letting a stalled challenge hang the pipeline.

Notes that save runs

  • Credentials stay in environment variables in both official samples — the Node.js one refuses to start with the literal placeholder values still in place.
  • The balance call is a pre-flight, not part of the solve loop: check it once at pipeline start, not per captcha.
  • Sitekey extraction has a fallback in the official Python script (attribute read, then a script-side read) because widgets render differently across drivers — keep both.
  • Both samples end with the same honest branch: no solution -> retry with backoff. Wire that branch before production; a retry policy is what separates flaky solves causing intermittent test failures from a pipeline that recovers.
  • Treat a missing success element as a first-class outcome (the Python sample catches it and reports it) — silent passes are how broken flows ship.

Docs and sign-up

The same shape applies to the other runners: the integration
pages for Playwright and Puppeteer walk the same
request-and-inject loop, and the per-type examples document
every payload field the clients accept.

Top comments (0)