How to Solve reCAPTCHA v3 in PHP and Set an Action

solve recaptcha v3 in php - How to Solve reCAPTCHA v3 in PHP and Set an Action

reCAPTCHA v3 never renders a challenge. It scores the visit quietly and gives the page a token, which the site’s backend then verifies. From your code that means there is nothing to click, so the whole task is producing a token the site accepts. In PHP that is the same recaptcha method you would use for v2, with a version flag in the options array.

The thing that decides whether it works is the action.

Setup

# PHP 8.0+, with the curl and json extensions.
composer require capskip/capskip
use CapSkip\CapSkip;

$solver = new CapSkip([
    'host' => '127.0.0.1',
    'port' => 8080,
    'recaptchaTimeout' => 300,   // seconds, shared with Turnstile and GeeTest
]);

CapSkip runs on your own machine, so the desktop app has to be open first.

The basic call

$result = $solver->recaptcha(
    '6Lc...YOUR_SITEKEY',
    'https://example.com/checkout',
    [
        'version' => 'v3',
        'action' => 'submit',
    ]
);

echo $result['code'];   // the v3 token

Two things differ from v2. version must be v3, and action should match whatever the page passes to grecaptcha.execute. Omit it and it defaults to verify.

Why the action matters

Actions are labels a site attaches to each protected interaction so a login and a checkout can be scored separately. Most backends check that the action carried by the token matches the one they expected for that endpoint.

Send the wrong label and the token is genuine but tagged for a different interaction, which many verifiers reject outright. Read the real value from the page instead of guessing:

$html = file_get_contents('https://example.com/checkout');

// Sites normally call execute() with the action as a string literal.
preg_match('/execute\([^,]+,\s*\{\s*action:\s*[\'"]([^\'"]+)/', $html, $m);
$action = $m[1] ?? 'verify';

$result = $solver->recaptcha($sitekey, $pageUrl, [
    'version' => 'v3',
    'action' => $action,
]);

Common values are login, submit, homepage and checkout, but they are arbitrary strings picked by whoever built the site.

Enterprise v3

$result = $solver->recaptcha($sitekey, $pageUrl, [
    'version' => 'v3',
    'enterprise' => 1,
    'action' => 'submit',
]);

Enterprise is an orthogonal flag rather than a separate product, so it stacks on the v3 call. Tell them apart by the script the page loads: Enterprise pulls enterprise.js, standard pulls api.js. Guessing wrong fails the solve rather than returning a bad token, so it costs nothing to test.

Submitting the token

$ch = curl_init('https://example.com/checkout');

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'token' => $result['code'],
        'order_id' => '...',
    ]),
]);

$response = curl_exec($ch);
curl_close($ch);

Unlike v2 there is no standard form widget constraining the shape, so v3 integrations vary. Some use a hidden g-recaptcha-response input, others post JSON with a custom key. Check the page’s own JavaScript before assuming a field name.

Errors

use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\NetworkException;
use CapSkip\Exceptions\TimeoutException;

try {
    $result = $solver->recaptcha($sitekey, $pageUrl, ['version' => 'v3']);
} catch (NetworkException $e) {
    // CapSkip is not running on the configured port
} catch (ApiException $e) {
    // the sitekey or pageurl was rejected
} catch (TimeoutException $e) {
    // exceeded recaptchaTimeout
}

Note the CapSkip\Exceptions namespace. Copying imports from the Python or Node examples is a common source of fatal errors here.

Frequently asked questions

Can I check the score before submitting?

No. The score lives with Google and is only revealed to the site owner when their backend verifies the token. From the client side you receive a token and nothing else, so there is nothing to inspect or filter on beforehand.

Should I solve inside a web request?

Preferably not. PHP is synchronous and a solve can take several seconds, so doing it during a page render ties up a worker for the duration. Move it into a queued job or a CLI worker.

What if the page never sets an action?

Some pages call execute without one, in which case the default verify is correct. If your regex finds nothing, that is usually the reason rather than a parsing bug.

Summary

Set version to v3, match action to what the page executes, add enterprise when it loads enterprise.js, import exceptions from CapSkip\Exceptions, and submit the token quickly because it expires in about two minutes.

Other languages are covered on the reCAPTCHA v3 solver page, Enterprise details on the Enterprise solver page, and the wider PHP surface on the PHP CAPTCHA solver page. See a token generated live on our v3 demo. CapSkip is a captcha solver that runs on your own hardware.