Browser automation
More
When your automation drives a real browser, the flow is the same in every tool:
- Open the page and read the widget’s site key (and its action and cData, if set) from it.
- Solve it through ZeroCaptcha from your script, as in Solving Cloudflare Turnstile.
- Put the token into the page: the
cf-turnstile-responsefield, and the widget’s callback if the page uses one. - Submit as a person would, within 300 seconds of the token being issued.
The key stays in your script’s environment. Never put it into the page: the page, and anything running in it, could read it.
Read the widget and solve it
Section titled “Read the widget and solve it”Send the widget’s action and cData with its site key whenever it sets them: many sites check both when they verify the token, and refuse one solved without them. See action and cData.
A widget written in the HTML carries all three as data-sitekey, data-action and data-cdata.
A widget rendered by script gets them as the sitekey, action and cData options of
turnstile.render(), which leave no attribute behind, so note each call’s options before the
page’s own scripts run (Playwright’s addInitScript, Puppeteer’s evaluateOnNewDocument, or
Chrome’s Page.addScriptToEvaluateOnNewDocument from Selenium or chromedp):
// Runs before the page's scripts: keeps the options of every turnstile.render() call.() => { let api; Object.defineProperty(window, "turnstile", { configurable: true, get: () => api, set(value) { const render = value.render; value.render = (container, options = {}) => { window.__turnstileRenders = [...(window.__turnstileRenders ?? []), options]; return render.call(value, container, options); }; api = value; }, });};This runs in the page once it has loaded, and returns the widget’s values from either place:
() => { const widget = document.querySelector("[data-sitekey]"); const rendered = window.__turnstileRenders?.[0] ?? {}; return { websiteKey: widget?.dataset.sitekey ?? rendered.sitekey, action: widget?.dataset.action ?? rendered.action ?? null, cdata: widget?.dataset.cdata ?? rendered.cData ?? null, callback: widget?.dataset.callback ?? null, };};Then solve with the values it found and the page’s URL. With the SDKs, that is one call; with plain HTTP, create the task and poll it as in Solving Cloudflare Turnstile:
# websiteKey, action and cdata as the page function found them; leave out action or cdata when it# found none. For your own proxy, make the type TurnstileTask and add "proxy"; to be called when# the task ends, add "callbackUrl". The reply is the task, or a problem document saying why not.curl -sS --fail-with-body "$ZEROCAPTCHA_API/v1/tasks" \ -H "Authorization: Bearer $ZEROCAPTCHA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"type": "TurnstileTaskProxyless", "websiteURL": "https://example.com/login", "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", "action": "login", "cdata": "session-7f3a9c2e"}'import { ZeroCaptcha } from "@zerocaptcha/sdk";
const zerocaptcha = new ZeroCaptcha({ apiKey: process.env.ZEROCAPTCHA_KEY, baseUrl: process.env.ZEROCAPTCHA_API,});// widget: what the page function returned. The client sends an Idempotency-Key, and throws a// TaskFailedError, with its code, when the task fails or expires (which costs nothing).const token = await zerocaptcha.solve({ websiteURL: "https://example.com/login", websiteKey: widget.websiteKey, action: widget.action ?? undefined, // left out when the widget sets none cdata: widget.cdata ?? undefined, // proxy: "http://user:pass@proxy.example.net:8080", // to solve through your own proxy});import os
from zerocaptcha import ZeroCaptcha
zerocaptcha = ZeroCaptcha(api_key=os.environ["ZEROCAPTCHA_KEY"], base_url=os.environ["ZEROCAPTCHA_API"])# widget: what the page function returned. The client sends an Idempotency-Key, and raises# TaskFailedError, with its code, when the task fails or expires (which costs nothing).token = zerocaptcha.solve( website_url="https://example.com/login", website_key=widget["websiteKey"], action=widget["action"], # None, and left out, when the widget sets none cdata=widget["cdata"], # proxy="http://user:pass@proxy.example.net:8080", # to solve through your own proxy)client, err := zerocaptcha.NewClient(os.Getenv("ZEROCAPTCHA_KEY"), os.Getenv("ZEROCAPTCHA_API"))if err != nil { log.Fatal(err)}// widget: what the page function returned; an empty Action or CData is left out. The client// sends an Idempotency-Key, and returns a *zerocaptcha.TaskFailedError when the task fails.token, err := client.Solve(ctx, zerocaptcha.NewTask{ WebsiteURL: "https://example.com/login", WebsiteKey: widget.WebsiteKey, Action: widget.Action, CData: widget.CData, // Proxy: "http://user:pass@proxy.example.net:8080", // to solve through your own proxy})Put the token into the page
Section titled “Put the token into the page”This also runs in the page, with the token as its argument. It fills every cf-turnstile-response
field and calls the widget’s callback, if the page named one:
(token) => { for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = token; const name = document.querySelector("[data-callback]")?.dataset.callback; if (name && typeof window[name] === "function") window[name](token);};Playwright
Section titled “Playwright”import { chromium } from "playwright";import { ZeroCaptcha } from "@zerocaptcha/sdk";
const zerocaptcha = new ZeroCaptcha({ apiKey: process.env.ZEROCAPTCHA_KEY, baseUrl: process.env.ZEROCAPTCHA_API });const browser = await chromium.launch();const page = await browser.newPage();await page.goto("https://example.com/login");
const widget = page.locator("[data-sitekey]").first();await widget.waitFor({ state: "attached" });const token = await zerocaptcha.solve({ websiteURL: page.url(), websiteKey: await widget.getAttribute("data-sitekey"), action: (await widget.getAttribute("data-action")) ?? undefined, cdata: (await widget.getAttribute("data-cdata")) ?? undefined,});
await page.evaluate((value) => { for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = value; const name = document.querySelector("[data-callback]")?.dataset.callback; if (name && typeof window[name] === "function") window[name](value);}, token);await page.fill("#email", "you@example.com");await page.click("button[type=submit]");await browser.close();import os
from playwright.sync_api import sync_playwrightfrom zerocaptcha import ZeroCaptcha
INJECT = """(value) => { for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = value; const name = document.querySelector("[data-callback]")?.dataset.callback; if (name && typeof window[name] === "function") window[name](value);}"""
zerocaptcha = ZeroCaptcha(api_key=os.environ["ZEROCAPTCHA_KEY"], base_url=os.environ["ZEROCAPTCHA_API"])with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("https://example.com/login") widget = page.locator("[data-sitekey]").first widget.wait_for(state="attached") token = zerocaptcha.solve( website_url=page.url, website_key=widget.get_attribute("data-sitekey"), action=widget.get_attribute("data-action"), cdata=widget.get_attribute("data-cdata"), ) page.evaluate(INJECT, token) page.fill("#email", "you@example.com") page.click("button[type=submit]") browser.close()Puppeteer
Section titled “Puppeteer”import puppeteer from "puppeteer";import { ZeroCaptcha } from "@zerocaptcha/sdk";
const zerocaptcha = new ZeroCaptcha({ apiKey: process.env.ZEROCAPTCHA_KEY, baseUrl: process.env.ZEROCAPTCHA_API });const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto("https://example.com/login", { waitUntil: "domcontentloaded" });
await page.waitForSelector("[data-sitekey]");const widget = await page.$eval("[data-sitekey]", (element) => ({ ...element.dataset }));const token = await zerocaptcha.solve({ websiteURL: page.url(), websiteKey: widget.sitekey, action: widget.action, cdata: widget.cdata,});
await page.evaluate((value) => { for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = value; const name = document.querySelector("[data-callback]")?.dataset.callback; if (name && typeof window[name] === "function") window[name](value);}, token);await page.type("#email", "you@example.com");await page.click("button[type=submit]");await browser.close();Selenium
Section titled “Selenium”import os
from selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support import expected_conditions as ecfrom selenium.webdriver.support.ui import WebDriverWaitfrom zerocaptcha import ZeroCaptcha
INJECT = """const token = arguments[0];for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = token;const name = document.querySelector("[data-callback]")?.dataset.callback;if (name && typeof window[name] === "function") window[name](token);"""
zerocaptcha = ZeroCaptcha(api_key=os.environ["ZEROCAPTCHA_KEY"], base_url=os.environ["ZEROCAPTCHA_API"])driver = webdriver.Chrome()try: driver.get("https://example.com/login") widget = WebDriverWait(driver, 15).until( ec.presence_of_element_located((By.CSS_SELECTOR, "[data-sitekey]")) ) token = zerocaptcha.solve( website_url=driver.current_url, website_key=widget.get_attribute("data-sitekey"), action=widget.get_attribute("data-action"), cdata=widget.get_attribute("data-cdata"), ) driver.execute_script(INJECT, token) driver.find_element(By.ID, "email").send_keys("you@example.com") driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()finally: driver.quit()import { Builder, By, until } from "selenium-webdriver";import { ZeroCaptcha } from "@zerocaptcha/sdk";
const zerocaptcha = new ZeroCaptcha({ apiKey: process.env.ZEROCAPTCHA_KEY, baseUrl: process.env.ZEROCAPTCHA_API });const driver = await new Builder().forBrowser("chrome").build();try { await driver.get("https://example.com/login"); const widget = await driver.wait(until.elementLocated(By.css("[data-sitekey]")), 15_000); const token = await zerocaptcha.solve({ websiteURL: await driver.getCurrentUrl(), websiteKey: await widget.getAttribute("data-sitekey"), action: (await widget.getAttribute("data-action")) || undefined, cdata: (await widget.getAttribute("data-cdata")) || undefined, }); await driver.executeScript( `const token = arguments[0]; for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = token; const name = document.querySelector("[data-callback]")?.dataset.callback; if (name && typeof window[name] === "function") window[name](token);`, token, ); await driver.findElement(By.id("email")).sendKeys("you@example.com"); await driver.findElement(By.css("button[type=submit]")).click();} finally { await driver.quit();}chromedp (Go)
Section titled “chromedp (Go)”package main
import ( "context" "encoding/json" "log" "os"
"github.com/chromedp/chromedp" zerocaptcha "github.com/zerocaptcha/zerocaptcha-go")
const inject = `(token) => { for (const input of document.querySelectorAll('[name="cf-turnstile-response"]')) input.value = token; const name = document.querySelector("[data-callback]")?.dataset.callback; if (name && typeof window[name] === "function") window[name](token);}`
func main() { client, err := zerocaptcha.NewClient(os.Getenv("ZEROCAPTCHA_KEY"), os.Getenv("ZEROCAPTCHA_API")) if err != nil { log.Fatal(err) } ctx, cancel := chromedp.NewContext(context.Background()) defer cancel()
// The widget's site key, action and cData; an attribute the widget does not set stays empty, // and an empty Action or CData is left out of the task. var siteKey, action, cdata, pageURL string var found bool err = chromedp.Run(ctx, chromedp.Navigate("https://example.com/login"), chromedp.WaitReady("[data-sitekey]"), chromedp.AttributeValue("[data-sitekey]", "data-sitekey", &siteKey, &found), chromedp.AttributeValue("[data-sitekey]", "data-action", &action, &found), chromedp.AttributeValue("[data-sitekey]", "data-cdata", &cdata, &found), chromedp.Location(&pageURL), ) if err != nil { log.Fatal(err) } token, err := client.Solve(ctx, zerocaptcha.NewTask{ WebsiteURL: pageURL, WebsiteKey: siteKey, Action: action, // many sites check both when they verify the token CData: cdata, }) if err != nil { log.Fatal(err) } err = chromedp.Run(ctx, chromedp.Evaluate("("+inject+")("+quote(token)+")", nil), chromedp.SendKeys("#email", "you@example.com"), chromedp.Click("button[type=submit]"), ) if err != nil { log.Fatal(err) }}
// quote makes a JavaScript string literal of s.func quote(s string) string { b, _ := json.Marshal(s) return string(b)}Things that trip automation up
Section titled “Things that trip automation up”- Solve late, submit at once. A token lasts 300 seconds and works once. Fill the rest of the form first, then solve, inject and submit.
- The widget may overwrite the field. If the page’s own widget finishes after you inject, it replaces your token with its own. Inject right before submitting, and submit in the same step.
- Use the page’s real URL as
websiteURL, after any redirect, and the sameactionandcdatathe widget has. - Challenge pages are different. A “Just a moment…” page before the site loads is a
challenge page: solve it with a challenge task through the proxy your browser
uses, and set its
cf_clearancecookie and user agent in the browser before you load the page.