SurSolver

BlogHow to Identify a CAPTCHA and Find Its Parameters

How to Identify a CAPTCHA and Find Its Parameters

· Updated · 9 min read

A solve request is only as good as the parameters in it. Send the wrong task type, a site key from another widget or a stale one-time value, and the token comes back unusable. This guide shows how to name the CAPTCHA on a page from its markers, read its parameters from the source, and capture the values that are generated on every load. You will end with a checklist of what each SurSolver task type needs.

Identify the type before anything else

Several CAPTCHAs look alike on screen and take different task types: standard and Enterprise reCAPTCHA, invisible v2 and v3, a Turnstile widget and a Cloudflare challenge page, GeeTest v3 and v4. A request for the wrong one is rejected or produces a token the site refuses. Spend the first minute on identification and the rest gets easier.

Open DevTools (F12), reload the page with the Network tab open, then search the Elements tab and the page source for the markers below. The overview of CAPTCHA types explains what each one does after it passes.

Markers by type

You see or find It is Solver guide
g-recaptcha element, api.js, requests to /recaptcha/api2/anchor reCAPTCHA v2 reCAPTCHA v2
enterprise.js, requests to /recaptcha/enterprise/anchor reCAPTCHA Enterprise (v2 or v3) reCAPTCHA v2, reCAPTCHA v3
api.js?render=KEY, grecaptcha.execute(KEY, {action: …}), no widget reCAPTCHA v3 reCAPTCHA v3
cf-turnstile element, script from challenges.cloudflare.com/turnstile/v0/api.js, key starting 0x Cloudflare Turnstile Turnstile
First request returns 403 with a "Just a moment…" page, __cf_chl_f_tk= in the form action Cloudflare challenge page Cloudflare Challenge
initGeetest( or gt.js / initGeetest4(, gt4.js or gcaptcha4.geetest.com/load?captcha_id= GeeTest v3 / v4 GeeTest
widget.module.min.js or widget.min.js / site.min.js, a puzzle?sitekey= request FriendlyCaptcha v1 / v2 FriendlyCaptcha
<altcha-widget> with a challenge or challengeurl attribute Altcha Altcha
procaptcha element, js.prosopo.io/js/procaptcha.bundle.js Prosopo Prosopo
cdn.captchafox.com/api.js, key starting sk_ CaptchaFox CaptchaFox
service.mtcaptcha.com/mtcv1/client/mtcaptcha.min.js, mtcaptchaConfig with a MTPublic- key MTCaptcha MTCaptcha
_abck and bm_sz cookies, a POST of sensor_data, HTTP 428 Akamai Bot Manager Akamai
403 with a var dd object, geo.captcha-delivery.com, a datadome cookie DataDome DataDome
_px3 cookie, _pxAppId in the source, px-cdn.net requests, "Press & Hold" PerimeterX PerimeterX

Three of these need a second look:

  • Standard or Enterprise. The script file decides it. api.js is standard, enterprise.js is Enterprise, and each has its own task type. A token from the wrong one is rejected.
  • Invisible v2 or v3. Both show no widget. In the Network tab api2/reload?k= appears in both, while api2/userverify?k= appears only for invisible v2. If the widget iframe URL contains size=invisible, set isInvisible for v2.
  • Widget or challenge page. A Turnstile widget sits in a page that loads normally. A challenge page replaces the page you asked for, and it needs a proxy and different inputs.

The anchor request in the Network panel: the k parameter of its payload holds the site key

The research behind this table has no documented page markers for the Vercel Security Checkpoint, so identify that one by what it is: a full-page challenge shown before the site. See the Vercel guide.

Find the site key

Most widget types need websiteURL (the full URL of the page that shows the widget, including its path) and websiteKey.

  1. In the Elements panel press Ctrl+F and search for data-sitekey. The value on the widget element (g-recaptcha, cf-turnstile, procaptcha) is the key. CaptchaFox pages may use data-key instead.
  2. If the widget is rendered from JavaScript there is no attribute. Look in the Network tab: for reCAPTCHA open the anchor request and read its k parameter; for MTCaptcha filter for getchallenge.json, where sk is the key.
  3. For v3, search all files (Ctrl+Shift+F in the Sources panel) for grecaptcha.execute; the first argument is the key.

The g-recaptcha element selected in the Elements panel, with its data-sitekey attribute

This snippet, pasted into the Console, lists every data-sitekey element, the k value of reCAPTCHA iframes and the MTCaptcha key if the page defines one:

solve.js
const widgets = [...document.querySelectorAll('[data-sitekey], [data-key]')].map((element) => ({
  tag: element.tagName,
  className: element.className,
  sitekey: element.dataset.sitekey ?? element.dataset.key,
}));
const recaptchaFrames = [...document.querySelectorAll('iframe[src*="recaptcha"]')].map((frame) =>
  new URL(frame.src).searchParams.get('k'),
);
console.table(widgets);
console.log('reCAPTCHA iframe k parameters:', recaptchaFrames);
console.log('MTCaptcha sitekey:', window.mtcaptchaConfig?.sitekey);

Some types have no site key. Cloudflare Challenge, Akamai and Vercel take the URL; Altcha takes the challenge itself; GeeTest takes captchaId (v4) or a gt and challenge pair (v3).

Capture values that change on every load

Some parameters are generated per render and expire quickly. Copy them from a page you loaded just before creating the task, never from an old session.

  • reCAPTCHA Enterprise s. Search the inline scripts for grecaptcha.enterprise.render. The s value in its argument goes in enterprisePayload, and it is short-lived.
  • reCAPTCHA v3 action. The action in grecaptcha.execute(key, {action: 'login'}) goes in pageAction. It must match exactly.
  • Turnstile action, cData, chlPageData. They are data-action and data-cdata attributes or arguments of turnstile.render. Copy them exactly, and only when the widget declares them. Guessing is worse than omitting.
  • GeeTest v3 challenge. The page fetches it from the site's own init endpoint on every load. Request it yourself immediately before each task. The gt key stays the same.
  • Altcha challenge. The GET response from the challenge endpoint is the JSON to send as challengeJSON, or send the URL as challengeURL. Each challenge is single-use and has an expiry.

For values that only exist inside a script call, set a breakpoint: find the call with global search, click its line number in the Sources panel, reload, and read the arguments in the Scope pane when execution stops. A script that does this for you is more repeatable. The following Playwright script wraps turnstile.render before the page's own scripts run and prints each call's sitekey, action, cData and chlPageData. It also keeps the page's callback in a global so it can be invoked later; the submit guide shows how.

solve.js
import { chromium } from 'playwright';

const TARGET_URL = process.argv[2];
const CALLS_GLOBAL = '__turnstileCalls';
const CALLBACK_GLOBAL = '__turnstileCallback';
const SETTLE_MS = 5000;

function hookTurnstile({ callsGlobal, callbackGlobal }) {
  let current;
  window[callsGlobal] = [];
  Object.defineProperty(window, 'turnstile', {
    configurable: true,
    get: () => current,
    set: (value) => {
      current = value;
      if (!value || typeof value.render !== 'function') return;
      const render = value.render.bind(value);
      value.render = (container, params) => {
        window[callsGlobal].push({
          sitekey: params?.sitekey,
          action: params?.action,
          cData: params?.cData,
          chlPageData: params?.chlPageData,
        });
        if (typeof params?.callback === 'function') window[callbackGlobal] = params.callback;
        return render(container, params);
      };
    },
  });
}

const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
await context.addInitScript(hookTurnstile, {
  callsGlobal: CALLS_GLOBAL,
  callbackGlobal: CALLBACK_GLOBAL,
});
const page = await context.newPage();
await page.goto(TARGET_URL);
await page.waitForTimeout(SETTLE_MS);
console.log(await page.evaluate((name) => window[name], CALLS_GLOBAL));
await browser.close();

This only sees widgets the page renders with turnstile.render. A widget declared in HTML has its values in the data-* attributes instead. The same pattern works for grecaptcha.execute to log the key and action of a v3 page. For the full Playwright flow see the Playwright recipe.

Common mistakes

  • The wrong URL. websiteURL is the page that renders the widget, not the site root. For a challenge page it is the route that returned the 403.
  • The iframe URL. For PerimeterX, send the parent page, never an iframe.hsprotect.net or px-cdn.net frame URL.
  • A stale value. If a solve worked once and then fails with the same parameters, re-extract the dynamic ones.
  • A key from another page. Keys belong to one widget configuration. A key harvested from site A will not solve site B, and some pages serve a different key per session or page load.
  • A development key. Providers publish test keys that always pass. They prove your wiring and nothing else.

Checklist by task type

Names are the real task fields. Anything marked "when present" is optional and should be sent only if the page has it. The full list for each type is in the public catalog, GET /v1/task-types.

Type Send Where it comes from
reCAPTCHA v2 websiteURL, websiteKey; isInvisible, enterprisePayload, recaptchaDataSValue, apiDomain when present data-sitekey, anchor k, size=invisible, grecaptcha.enterprise.render
reCAPTCHA v3 websiteURL, websiteKey; pageAction, minScore, enterprisePayload when present api.js?render=KEY, grecaptcha.execute
Turnstile websiteURL, websiteKey; action, cData, chlPageData when declared data-sitekey, turnstile.render arguments
Cloudflare Challenge websiteURL, a proxy; userAgent, html when you have them the URL that returns the 403
Vercel websiteURL, a proxy the checkpoint URL
FriendlyCaptcha websiteURL, websiteKey; version, moduleScript, nomoduleScript when present data-sitekey, widget script URLs, x-frc-client header
Altcha websiteURL and challengeURL or challengeJSON challenge attribute, the challenge response
Prosopo websiteURL, websiteKey data-sitekey
CaptchaFox websiteURL, websiteKey, a proxy sk_ key in the source or Network
GeeTest websiteURL and either captchaId (v4) or gt plus a fresh challenge (v3) initGeetest4 call, init endpoint
MTCaptcha websiteURL, websiteKey; pageAction, isInvisible when present sk and act in getchallenge.json
Akamai websiteURL, a proxy the protected page
DataDome websiteURL, captchaUrl, a proxy; datadomeCookie when present the url in the block response or iframe src
PerimeterX websiteURL, pxAppId, a proxy; uuid, vid, blockData, blockMode, initCookies when present _pxAppId, the _pxvid cookie, a captured block page

Which types need a proxy is on the Proxies guide.

Next steps

Once you know the type and have its parameters, send them to createTask and poll getTaskResult every 3 seconds; the Quickstart has the exact calls. Then deliver the result with the submit guide.

More articles