An Enterprise-protected page never shows a checkbox: it scores the visit silently and hides the token inside the request the page itself builds. This post assembles the complete Playwright flow for reCAPTCHA v3 Enterprise — read the page's parameters, create the solve task, put gRecaptchaResponse where the page sends it, submit, and confirm the site actually accepted the result.
What you need from the page
Four values describe an Enterprise integration, and all of them come from the page you are automating:
| Field | Where it lives |
|---|---|
websiteURL |
The exact URL of the page that calls grecaptcha.enterprise.execute, including its path and query string |
websiteKey |
The render= parameter of the page's enterprise.js script URL, or a data-sitekey attribute |
pageAction |
The action argument of the page's grecaptcha.enterprise.execute(key, { action }) call |
enterprisePayload |
Extra values the integration passes at render or execute time, such as an s value, captured shortly before the solve |
The script URL is also the fastest identification point: an Enterprise page loads recaptcha/enterprise.js and calls grecaptcha.enterprise.execute, while a standard v3 page loads recaptcha/api.js and calls grecaptcha.execute. Copy pageAction exactly as the page sends it — a token generated for another action does not reproduce the site's check.
minScore is optional. It asks the solve to target a threshold you choose, from 0 to 1. It does not override the protected site's own policy: the site still evaluates whatever it receives.
The example below reads websiteURL and websiteKey from the live page and takes the values the page hides as constants at the top of the script, so nothing has to be edited to move between environments.
The Playwright flow
// recaptcha-v3-enterprise-playwright.js - solve reCAPTCHA v3 Enterprise on the live page and
// submit the form with the token.
// Install: npm install playwright && npx playwright install chromium
// Run: node recaptcha-v3-enterprise-playwright.js
const { chromium } = require('playwright');
const { setTimeout: sleep } = require('node:timers/promises');
// Your SurSolver API key.
const CLIENT_KEY = 'YOUR_API_KEY';
// The page that renders the widget, with its path and query string.
const TARGET_URL = 'https://example.com/page';
// Optional, read from the page when empty: the render= value of the api.js or enterprise.js
// script URL.
const SITE_KEY = '';
// The action in the page's grecaptcha.execute(key, { action }) call.
const PAGE_ACTION = 'YOUR_PAGE_ACTION';
// The lowest score you need, from 0 to 1.
const MIN_SCORE = undefined;
// The object passed to grecaptcha.enterprise.render, such as { "s": "..." }, copied just before
// the solve.
const ENTERPRISE_PAYLOAD = undefined;
// The control that submits the form.
const SUBMIT_SELECTOR = '.PASTE_SUBMIT_SELECTOR';
// A selector that matches only after the site accepted the result.
const SUCCESS_SELECTOR = '.PASTE_ACCEPTED_SELECTOR';
const API = 'https://api.sursolver.com';
const POLL_ATTEMPTS = 60;
// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data;
}
async function solve(task) {
const created = await call('/createTask', { clientKey: CLIENT_KEY, task });
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const result = await call('/getTaskResult', {
clientKey: CLIENT_KEY,
taskId: created.taskId,
});
if (result.status === 'ready') return result.solution;
await sleep(3000);
}
throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}
// Optional task fields are sent only when they have a value.
function withoutEmpty(fields) {
return Object.fromEntries(
Object.entries(fields).filter(([, value]) => value !== undefined && value !== ''),
);
}
// A value the page did not show must be pasted into its constant at the top.
function need(name, value) {
if (value === undefined || value === null || value === '') {
throw new Error(`Could not read ${name} from the page; fill in the ${name} constant.`);
}
return value;
}
// Opens the page once and reads what the solve needs from it.
async function readPage(browser) {
const page = await browser.newPage();
try {
await page.goto(TARGET_URL, {
waitUntil: 'domcontentloaded',
timeout: 45000,
});
// The widget is often added after the page loads; give it a moment to appear.
await page
.waitForFunction(
() =>
!!(() => {
const src = [...document.scripts]
.map((script) => script.src)
.find((url) => /recaptcha\/(api|enterprise)\.js\?.*render=/.test(url));
const key = src ? new URL(src).searchParams.get('render') : null;
const fallback = document.querySelector('[data-sitekey]')?.dataset.sitekey;
return key && key !== 'explicit' ? key : fallback;
})(),
undefined,
{ timeout: 10000 },
)
.catch(() => undefined);
const dom = await page.evaluate(() => ({
websiteKey: (() => {
const src = [...document.scripts]
.map((script) => script.src)
.find((url) => /recaptcha\/(api|enterprise)\.js\?.*render=/.test(url));
const key = src ? new URL(src).searchParams.get('render') : null;
const fallback = document.querySelector('[data-sitekey]')?.dataset.sitekey;
return key && key !== 'explicit' ? key : fallback;
})(),
}));
return {
pageUrl: page.url(),
...dom,
};
} finally {
await page.close();
}
}
async function main() {
const browser = await chromium.launch();
try {
const found = await readPage(browser);
const solution = await solve({
type: 'ReCaptchaV3EnterpriseTokenProxyLess',
websiteURL: found.pageUrl,
websiteKey: need('websiteKey', SITE_KEY || found.websiteKey),
pageAction: PAGE_ACTION,
...withoutEmpty({
minScore: MIN_SCORE,
enterprisePayload: ENTERPRISE_PAYLOAD,
}),
});
// Submit from a fresh session that presents the solver's user agent.
const context = await browser.newContext(
solution.userAgent ? { userAgent: solution.userAgent } : {},
);
const page = await context.newPage();
await page.goto(found.pageUrl);
// reCAPTCHA defines execute() a moment after the page loads.
await page.waitForFunction(
() => typeof window.grecaptcha?.enterprise?.execute === 'function',
undefined,
{ timeout: 15000 },
);
const applied = await page.evaluate(
({ token }) => {
const api = window.grecaptcha?.enterprise;
if (!api || typeof api.execute !== 'function') return false;
// The page now receives the solved token wherever it calls execute().
api.execute = () => Promise.resolve(token);
for (const field of document.querySelectorAll('[name="g-recaptcha-response"]')) {
field.value = token;
}
return true;
},
{ token: solution.gRecaptchaResponse },
);
if (!applied) {
throw new Error('The page has no reCAPTCHA API; check how it requests the token.');
}
await page.click(SUBMIT_SELECTOR);
await page.locator(SUCCESS_SELECTOR).first().waitFor({ timeout: 15000 });
console.log('The site accepted the token.');
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
The script does six things, in order:
- Opens the page once and reads what the solve needs: the URL after redirects, and the site key from the Enterprise script's
render=parameter (with thedata-sitekeyattribute as a fallback). The read proceeds from the DOM as soon as it is interactive and then waits up to ten seconds for a widget the page adds late. - Creates a
ReCaptchaV3EnterpriseTokenProxyLesstask withpageActionand, when set,minScoreandenterprisePayload. UseReCaptchaV3EnterpriseTokeninstead when the solve must run through your own proxy; the public catalog (GET /v1/task-types) lists both names and their fields. - Polls
getTaskResultevery three seconds until the status isready. API errors arrive as HTTP 200 with a non-zeroerrorId, so the script fails loudly onERROR_TASK_TIMEOUT,ERROR_FAILEDand every other code instead of pretending to succeed. - Submits from a fresh context whose user agent is the
userAgentthe solve reports.userAgentis an output, never an input: leave it out of the task, and let the solve tell you which browser it used. - Applies the result inside the page:
grecaptcha.enterprise.executeis replaced with a call that resolves to the solved token, and everyg-recaptcha-responsefield receives the same value. From here the page assembles its protected request exactly as it always does. - Clicks the page's submit control and waits for
SUCCESS_SELECTOR— a selector that matches only after the site accepted the flow. The console line appears only once that wait succeeds.
Point SUBMIT_SELECTOR at the page's real submit control and choose SUCCESS_SELECTOR for something only a successful submission produces: a dashboard heading, a "check your inbox" note, a URL change.
What success means here
Two different checks happen on this flow, and they are not interchangeable:
- The task reaching
readyproves the solver produced a token for yourwebsiteURL, key and action. It says nothing about the site's decision. SUCCESS_SELECTORappearing proves the protected step went through — the page submitted its request and the backend answered with post-submit state.
The site's server verifies the token in the context of its own action, hostname and score policy. That verification happens on the site side of the protected request. If you operate the site yourself, confirm the token inside the backend handler where the protected request lands, and treat the post-submit page state in the browser as the end-to-end confirmation.
Node.js
When the protected action is a plain form post, the browser is optional: node recaptcha-v3-enterprise-node.js (Node 18 or newer, no packages) solves ReCaptchaV3EnterpriseTokenProxyLess from your pageAction, minScore and enterprisePayload constants, then posts the token in g-recaptcha-response with the form's other fields and the solution's userAgent.
// recaptcha-v3-enterprise-node.js - solve reCAPTCHA v3 Enterprise and post the token with the
// form.
// Install: Node.js 18 or newer (no packages).
// Run: node recaptcha-v3-enterprise-node.js
const { setTimeout: sleep } = require('node:timers/promises');
// Your SurSolver API key.
const CLIENT_KEY = 'YOUR_API_KEY';
// The page that renders the widget, with its path and query string.
const TARGET_URL = 'https://example.com/page';
// The render= value of the api.js or enterprise.js script URL.
const SITE_KEY = 'YOUR_SITE_KEY';
// The action in the page's grecaptcha.execute(key, { action }) call.
const PAGE_ACTION = 'YOUR_PAGE_ACTION';
// The lowest score you need, from 0 to 1.
const MIN_SCORE = undefined;
// The object passed to grecaptcha.enterprise.render, such as { "s": "..." }, copied just before
// the solve.
const ENTERPRISE_PAYLOAD = undefined;
// Where the page posts the form (the form action, or the request in DevTools, Network).
const FORM_URL = 'https://example.com/submit';
// The form's other fields as a JSON object, such as {"username":"demo"}.
const FORM_FIELDS = undefined;
// Text that appears only in a response the site accepted.
const SUCCESS_TEXT = 'PASTE_ACCEPTED_MARKER';
const API = 'https://api.sursolver.com';
const POLL_ATTEMPTS = 60;
// API errors are HTTP 200 with errorId 1; anything else is a transport failure.
async function call(path, body) {
const res = await fetch(`${API}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data;
}
async function solve(task) {
const created = await call('/createTask', { clientKey: CLIENT_KEY, task });
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
const result = await call('/getTaskResult', {
clientKey: CLIENT_KEY,
taskId: created.taskId,
});
if (result.status === 'ready') return result.solution;
await sleep(3000);
}
throw new Error(`task ${created.taskId} was not ready in ${POLL_ATTEMPTS} polls`);
}
// Optional task fields are sent only when they have a value.
function withoutEmpty(fields) {
return Object.fromEntries(
Object.entries(fields).filter(([, value]) => value !== undefined && value !== ''),
);
}
async function main() {
const solution = await solve({
type: 'ReCaptchaV3EnterpriseTokenProxyLess',
websiteURL: TARGET_URL,
websiteKey: SITE_KEY,
pageAction: PAGE_ACTION,
...withoutEmpty({
minScore: MIN_SCORE,
enterprisePayload: ENTERPRISE_PAYLOAD,
}),
});
// Send the token the way the page does; some sites name it differently or send JSON.
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
// The solver reports a user agent only for some solves; present it when it does.
if (solution.userAgent) headers['User-Agent'] = solution.userAgent;
const body = new URLSearchParams({
...(FORM_FIELDS ?? {}),
['g-recaptcha-response']: solution.gRecaptchaResponse,
});
const response = await fetch(FORM_URL, { method: 'POST', headers, body });
const text = await response.text();
if (!response.ok || !text.includes(SUCCESS_TEXT)) {
throw new Error(`The site did not accept the result (HTTP ${response.status}).`);
}
console.log('The site accepted the result.');
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Python
pip install requests and python recaptcha-v3-enterprise-python.py run the same task and the same form post, judged by SUCCESS_TEXT in the response body; the server-side verification above is unchanged.
# recaptcha-v3-enterprise-python.py - solve reCAPTCHA v3 Enterprise and post the token with the
# form.
# Install: pip install requests
# Run: python recaptcha-v3-enterprise-python.py
import time
import requests
# Your SurSolver API key.
CLIENT_KEY = "YOUR_API_KEY"
# The page that renders the widget, with its path and query string.
TARGET_URL = "https://example.com/page"
# The render= value of the api.js or enterprise.js script URL.
SITE_KEY = "YOUR_SITE_KEY"
# The action in the page's grecaptcha.execute(key, { action }) call.
PAGE_ACTION = "YOUR_PAGE_ACTION"
# The lowest score you need, from 0 to 1.
MIN_SCORE = None
# The object passed to grecaptcha.enterprise.render, such as { "s": "..." }, copied just before
# the solve.
ENTERPRISE_PAYLOAD = None
# Where the page posts the form (the form action, or the request in DevTools, Network).
FORM_URL = "https://example.com/submit"
# The form's other fields as a JSON object, such as {"username": "demo"}.
FORM_FIELDS = None
# Text that appears only in a response the site accepted.
SUCCESS_TEXT = "PASTE_ACCEPTED_MARKER"
API = "https://api.sursolver.com"
POLL_ATTEMPTS = 60
def call(path, body):
# API errors are HTTP 200 with errorId 1; anything else is a transport failure.
response = requests.post(f"{API}{path}", json=body, timeout=30)
response.raise_for_status()
data = response.json()
if data["errorId"] != 0:
raise RuntimeError(f'{data["errorCode"]}: {data["errorDescription"]}')
return data
def solve(task):
created = call("/createTask", {"clientKey": CLIENT_KEY, "task": task})
for _ in range(POLL_ATTEMPTS):
result = call("/getTaskResult", {"clientKey": CLIENT_KEY, "taskId": created["taskId"]})
if result["status"] == "ready":
return result["solution"]
time.sleep(3)
raise TimeoutError(f'task {created["taskId"]} was not ready in {POLL_ATTEMPTS} polls')
def without_empty(fields):
# Optional task fields are sent only when they have a value.
return {name: value for name, value in fields.items() if value not in (None, "")}
def main():
solution = solve({
"type": "ReCaptchaV3EnterpriseTokenProxyLess",
"websiteURL": TARGET_URL,
"websiteKey": SITE_KEY,
"pageAction": PAGE_ACTION,
**without_empty({
"minScore": MIN_SCORE,
"enterprisePayload": ENTERPRISE_PAYLOAD,
}),
})
# The solver reports a user agent only for some solves; present it when it does.
headers = {}
if solution.get("userAgent"):
headers["User-Agent"] = solution["userAgent"]
# Send the token the way the page does; some sites name it differently or send JSON.
response = requests.post(
FORM_URL,
data={**(FORM_FIELDS or {}), "g-recaptcha-response": solution["gRecaptchaResponse"]},
headers=headers,
timeout=30,
)
if not response.ok or SUCCESS_TEXT not in response.text:
raise SystemExit(f"The site did not accept the result (HTTP {response.status_code}).")
print("The site accepted the result.")
if __name__ == "__main__":
main()
When the flow fails
| Symptom | Likely cause | What to check |
|---|---|---|
Could not read websiteKey from the page; set SITE_KEY. |
The key is neither in the script URL nor the DOM (render=explicit, widget inside a frame) |
Copy the render= value of the enterprise.js request from the Network panel into SITE_KEY |
The page has no reCAPTCHA API; check how it requests the token. |
grecaptcha.enterprise never became callable (script blocked, widget in another frame) |
Check the script request in the Network panel; a widget inside a cross-origin frame must be driven by that frame's own flow |
createTask answers ERROR_INVALID_REQUEST naming a field |
pageAction missing, or enterprisePayload is not a JSON object |
Send the exact action string; pass the payload as JSON, for example {"s": "..."} |
The task was ready but the site refuses the submission |
Action, hostname or payload mismatch; the site's own score policy | Re-capture pageAction and enterprisePayload immediately before the solve and compare them with what the page sends |
ERROR_TASK_TIMEOUT or ERROR_FAILED |
The solve itself failed; both are final | Submit a new task — Errors lists every code and what it means for billing |
Polling, charging and result retention follow the standard two-call lifecycle: a task is charged when it is accepted, and a failed task returns its charge once (Task lifecycle).
For the score-and-action model behind this task type, see the reCAPTCHA v3 score and action guide; for the many ways a token can reach a page, see how to submit a captcha token. The reCAPTCHA v3 Enterprise solver guide and the Enterprise API reference document the full request and solution shape.