# Browser automation

> Read a page's Cloudflare Turnstile site key in Playwright, Puppeteer, Selenium or chromedp, solve it with ZeroCaptcha, and put the token in the page.

Source: https://zerocaptcha.io/docs/browser-automation

When your automation drives a real browser, the flow is the same in every tool:

1. **Open the page** and read the widget's site key (and its action and cData, if set) from it.
2. **Solve it** through ZeroCaptcha from your script, as in [Solving Cloudflare Turnstile](https://zerocaptcha.io/docs/cloudflare-turnstile).
3. **Put the token into the page**: the `cf-turnstile-response` field, and the widget's callback if
   the page uses one.
4. **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

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](https://zerocaptcha.io/docs/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):

```js
// 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:

```js
() => {
  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](https://zerocaptcha.io/docs/sdks), that is one
call; with plain HTTP, create the task and poll it as in [Solving Cloudflare Turnstile](https://zerocaptcha.io/docs/cloudflare-turnstile#solve-it):

**curl**

```sh
# 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"}'
```

**Node**

```js
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
});
```

**Python**

```python
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
)
```

**Go**

```go
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

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:

```js
(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

**Node**

```js
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();
```

**Python**

```python
import os

from playwright.sync_api import sync_playwright
from 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

**Node**

```js
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

**Python**

```python
import os

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 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()
```

**Node**

```js
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)

**Go**

```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

- **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 same `action` and
  `cdata` the widget has.
- **Challenge pages are different.** A "Just a moment…" page before the site loads is a
  [challenge page](https://zerocaptcha.io/docs/challenges): solve it with a challenge task through the proxy your browser
  uses, and set its `cf_clearance` cookie and user agent in the browser before you load the page.
