How to Solve reCAPTCHA v2 in PHP, Including Invisible

reCAPTCHA v2 has three variants, and in PHP they are one method with an options array. Checkbox is the bare call, Invisible and Enterprise are flags, and both can be set together. PHP is also the one CapSkip SDK with no async story at all, which simplifies the code but changes how you should structure bulk work.
Setup
# PHP 8.0 or newer. Needs the curl and json extensions, # both of which ship with almost every PHP install. composer require capskip/capskip
CapSkip solves on your own machine, so the desktop app has to be running. Point the client at the port from its settings:
use CapSkip\CapSkip;
$solver = new CapSkip([
'apiKey' => 'capskip', // any string when key validation is off
'host' => '127.0.0.1',
'port' => 8080,
'recaptchaTimeout' => 300, // seconds
]);In production, read those from the environment:
use CapSkip\CapSkip;
$solver = new CapSkip([
'apiKey' => getenv('CAPSKIP_API_KEY') ?: 'capskip',
'host' => getenv('CAPSKIP_HOST') ?: '127.0.0.1',
'port' => (int) (getenv('CAPSKIP_PORT') ?: 8080),
]);The three variants
| Variant | Option to add |
|---|---|
| Checkbox | none |
| Invisible | ['invisible' => 1] |
| Enterprise | ['enterprise' => 1] |
| Invisible Enterprise | both keys |
// Checkbox: sitekey and page URL only.
$result = $solver->recaptcha(
'6Lc...YOUR_SITEKEY',
'https://example.com/login'
);
echo $result['code']; // g-recaptcha-response token
// Invisible.
$result = $solver->recaptcha($sitekey, $pageUrl, ['invisible' => 1]);
// Enterprise, and both together.
$result = $solver->recaptcha($sitekey, $pageUrl, ['enterprise' => 1]);
$result = $solver->recaptcha($sitekey, $pageUrl, [
'enterprise' => 1,
'invisible' => 1,
]);The return value is an associative array, so $result['code'] is the token. $result['captchaId'] is there too if you want to log which solve produced it.
Getting the inputs right
The sitekey is the data-sitekey attribute on the widget container, or the first argument to grecaptcha.render when Invisible mode means there is no container to look at. It always starts with 6L and is public.
The URL must be the page the widget renders on. Passing your form handler or a post-login redirect is the usual cause of a token that solves fine and then fails verification.
Submitting the token
$ch = curl_init('https://example.com/login');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'g-recaptcha-response' => $result['code'],
'username' => '...',
'password' => '...',
]),
]);
$response = curl_exec($ch);
curl_close($ch);Tokens are single use and last about two minutes, so solve as late in the flow as possible. If the site hands the token to a JavaScript callback instead of a form field, the solve is identical but submission differs. Our reCAPTCHA v2 callback solver page covers that shape.
Errors, and the namespace people miss
PHP puts its exceptions under their own namespace, which trips up anyone copying imports from the Python or Node examples:
use CapSkip\CapSkip;
use CapSkip\Exceptions\ValidationException;
use CapSkip\Exceptions\NetworkException;
use CapSkip\Exceptions\ApiException;
use CapSkip\Exceptions\TimeoutException;
try {
$result = $solver->recaptcha($sitekey, $pageUrl);
} catch (ValidationException $e) {
// missing or malformed arguments
} 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
}All four extend CapSkip\Exceptions\CapSkipError, so a single catch on the base class handles everything if you would rather deal with failures in one place.
Solving several
PHP executes synchronously, so each solve blocks until it finishes:
use CapSkip\CapSkip;
$solver = new CapSkip();
foreach ($targets as $target) {
$results[] = $solver->recaptcha($target['sitekey'], $target['url']);
}The package exports AsyncCapSkip, but in PHP it is only an alias so that code ported from the other SDKs keeps running. It does not add concurrency. For genuine parallelism, run multiple worker processes or use a queue rather than expecting the SDK to overlap solves.
Using a proxy
$result = $solver->recaptcha($sitekey, $pageUrl, [
'proxy' => ['type' => 'HTTPS', 'uri' => 'user:[email protected]:3128'],
]);Proxies are supported for reCAPTCHA, Turnstile and GeeTest, but not for image CAPTCHAs, which are solved from the image bytes and never reach the target site.
Frequently asked questions
Do I need to poll for the result?
No. The SDK polls internally and returns the finished token, so CAPCHA_NOT_READY never reaches your code. It begins checking after 250ms and backs off from there.
Will a solve block my web request?
Yes, and that matters in PHP. A reCAPTCHA solve can take several seconds, so doing it inside a page render ties up a worker. Push solving into a queued job or a CLI worker rather than blocking a request thread.
What are the requirements?
PHP 8.0 or newer with the curl and json extensions, both bundled with most installs. There are no other runtime dependencies, so it drops into an existing project without pulling a tree of packages.
Summary
One method, three variants, selected with invisible and enterprise in the options array. Read $result['code'], submit it as g-recaptcha-response, import exceptions from CapSkip\Exceptions, and keep solves off your request threads.
The wider PHP surface is on the PHP CAPTCHA solver page, other languages on the reCAPTCHA v2 solver page, and there is a live v2 demo to test against. CapSkip is a local captcha solver, so nothing is billed per solve.
