Captcha solver in PHP with cURL: working example (2026)
PHP needs only the cURL extension, which ships with most installs, to solve a captcha. You send one JSON POST to the Scarnox API, read solution.token from the response, and submit that token to the site's form. This guide walks through the full PHP flow for PopularCaptcha, which Scarnox solves today on a proxyless basis.
Start with free trial credit, no card needed and try the script on a page you control before using it on anything else.
Requirements
- PHP 8.0 or newer, with the
curlandjsonextensions enabled. Check withphp -m. - A Scarnox API key.
- The sitekey of the page's PopularCaptcha widget. For help finding it, see how to find a captcha sitekey.
The solver function
This function posts to the API, checks errorId, and returns the token. It throws an exception on any failure, so the caller can decide whether to retry:
<?php
declare(strict_types=1);
const SCARNOX_API_KEY = 'scarnox_YOURAPIKEY';
const SCARNOX_API_URL = 'https://api.scarnox.com/api/tasks/create';
function solveCaptcha(string $sitekey, string $pageUrl, bool $invisible = false): string
{
$body = [
'captcha_type' => 'PopularCaptchaTaskProxyless',
'site_key' => $sitekey,
'site_url' => $pageUrl,
];
if ($invisible) {
$body['is_invisible'] = true;
}
$ch = curl_init(SCARNOX_API_URL);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . SCARNOX_API_KEY,
'Content-Type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 185, // the call returns when the token is ready
]);
$raw = curl_exec($ch);
if ($raw === false) {
$error = curl_error($ch);
throw new RuntimeException('cURL error: ' . $error);
}
$data = json_decode((string) $raw, true);
if (!is_array($data)) {
throw new RuntimeException('Response was not JSON');
}
if (($data['errorId'] ?? 1) !== 0) {
throw new RuntimeException(
($data['errorCode'] ?? 'ERROR') . ': ' . ($data['errorDescription'] ?? 'unknown')
);
}
return (string) $data['solution']['token'];
}
The Authorization header uses the Bearer prefix. The timeout is 185 seconds because the API blocks until the token is ready, with no polling step.
Retry transient failures
A failed solve is not charged, so a few retries are cheap. Put the loop around the call and wait a little longer after each failure:
function solveWithRetry(string $sitekey, string $pageUrl, int $attempts = 3): string
{
$lastError = null;
for ($i = 1; $i <= $attempts; $i++) {
try {
return solveCaptcha($sitekey, $pageUrl);
} catch (RuntimeException $e) {
$lastError = $e;
fwrite(STDERR, "Attempt $i failed: " . $e->getMessage() . PHP_EOL);
sleep($i);
}
}
throw $lastError;
}
For a complete error policy, including which failures to stop on, see captcha solver error handling.
Submit the token with a cookie jar
Many sites tie the captcha to the session that loaded the form. A cookie file lets cURL send the same cookies on the GET and the POST. The function below submits the token to the page's form:
function submitToken(string $pageUrl, string $token): string
{
$cookieFile = sys_get_temp_dir() . '/scarnox-cookies.txt';
// GET first, so the site sets the session cookie.
$ch = curl_init($pageUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_COOKIEFILE => $cookieFile,
CURLOPT_COOKIEJAR => $cookieFile,
CURLOPT_TIMEOUT => 60,
]);
curl_exec($ch);
// Then POST the form with the token in both captcha fields.
$fields = http_build_query([
'email' => 'you@example.com',
'h-captcha-response' => $token,
'g-recaptcha-response' => $token,
]);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $fields,
CURLOPT_TIMEOUT => 60,
]);
$body = curl_exec($ch);
if ($body === false) {
throw new RuntimeException('Form submit failed: ' . curl_error($ch));
}
return (string) $body;
}
Two things matter. The GET runs before the solve so the cookie is in place. And the token goes out right after the solve, because it expires in about two minutes. Put the solve call directly before this function, not at the start of a long batch.
The cookie file is shared by every call in the same process, so give each account or session its own file if you run several sign-ups in parallel. Otherwise one session can overwrite another's cookies, and the second POST will fail for reasons that look like a captcha problem but are not.
Putting it together
$pageUrl = 'https://example.com/signup';
$token = solveWithRetry('YOUR-SITEKEY-UUID', $pageUrl);
$html = submitToken($pageUrl, $token);
echo strlen($html) . " bytes returned\n";
Run it from the command line:
php signup.php
Invisible widgets
If the widget is invisible, call solveCaptcha($sitekey, $pageUrl, true). The is_invisible field is then sent with the request. Some pages also need a JavaScript callback called with the token, which a server-side script cannot trigger. In that case use a browser tool instead. The invisible captcha guide covers the cases.
Timing and expiry
The token is valid for about two minutes. Keep the solve and the submit close together. Captcha token expiry explains how to tell an expired token from a rejected one.
Scraping responsibly
If your PHP script scrapes public pages, check robots.txt and the site's terms before you start. Add a delay between requests, and do not store personal data you do not need. Solving a captcha does not change what you are permitted to collect. For the legal side, see is web scraping legal.
Pricing
You pay per successful solve. Failed solves are not charged. See current pricing for the current rates, and start with free trial credit, no card needed.