Use Guzzle with the CAPTCHA task API
Use Guzzle 7 to create a task, then poll its ID until the task is ready or failed. Keep the requestId if anything goes wrong: a connection timeout doesn’t prove that task creation failed.
This is a REST integration, not an official PHP SDK.
On this page
Prepare the reCAPTCHA request
- Install Guzzle 7 with Composer. The installation command appears at the top of the example.
- Set SOLVETHREE_API_KEY to your workspace API key and SOLVETHREE_PROXY to the proxy URL. Keep the key on the server.
- Create an active profile for your target site in the console before requesting a solve. See site profile setup.
- Replace accounts.example.com and login with the target hostname and action. The site field takes a hostname, not a full URL.
The example requests reCAPTCHA Enterprise v3, the default recaptchaType. Don’t add type or url fields. It supplies one proxy; if you use proxyCandidates instead, omit proxy entirely.
Create once, then poll within a fixed budget
<?php
// composer require guzzlehttp/guzzle:^7
require __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://api.solvethree.com/v1/',
'headers' => ['Authorization' => 'Bearer '.getenv('SOLVETHREE_API_KEY'), 'Accept' => 'application/json'],
'allow_redirects' => false,
'http_errors' => false,
]);
$input = ['site' => 'accounts.example.com', 'action' => 'login',
'proxy' => getenv('SOLVETHREE_PROXY'), 'requestId' => bin2hex(random_bytes(16))];
$deadline = microtime(true) + 180;
$call = function (string $method, string $path, array $options = []) use ($client, $deadline): array {
$remaining = $deadline - microtime(true);
if ($remaining <= 0) throw new RuntimeException('Local deadline reached');
$response = $client->request($method, $path, $options + ['timeout' => min(25, $remaining), 'connect_timeout' => min(10, $remaining)]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
throw new RuntimeException('API HTTP '.$response->getStatusCode());
}
return json_decode((string)$response->getBody(), true, 512, JSON_THROW_ON_ERROR);
};
$id = null;
try {
$result = $call('POST', 'tasks?waitMs=0', ['json' => $input]);
$id = $result['id'];
while (in_array($result['status'], ['pending', 'solving'], true)) {
$result = $call('GET', 'tasks/'.rawurlencode($id).'?waitMs=15000');
if (in_array($result['status'], ['pending', 'solving'], true)) usleep(1000000);
}
if ($result['status'] !== 'ready') {
throw new RuntimeException('Task failed: '.($result['errorCode'] ?? 'unknown'));
}
echo 'Ready task: '.$id.PHP_EOL;
// Submit $result['solution']['token'] with userAgent/secChUa and the same proxy.
} catch (Throwable $error) {
fwrite(STDERR, 'Recovery requestId='.$input['requestId'].' taskId='.($id ?? 'unknown').PHP_EOL);
throw $error;
}The client authenticates with a Bearer key. Its base_uri ends in /v1/, and the request paths are relative, so they retain that API prefix. Guzzle’s json option sends the task input as JSON.
The helper makes one creation POST. It then polls the returned task ID while status is pending or solving. Creation and polling share a 180-second budget; each HTTP call is capped at the remaining time, with maximum request and connection timeouts of 25 and 10 seconds respectively. A new poll doesn’t restart the overall budget.
Redirects are disabled. Setting http_errors to false lets the helper check non-2xx responses explicitly before decoding a success payload; it doesn’t suppress connection errors or timeouts.
- A non-2xx HTTP response throws an API HTTP error. Diagnose the response status and body rather than treating it as a failed solve.
- A terminal failed task stops polling. The example throws its errorCode; inspect the task’s error field as well for the diagnostic message. Failed solves are refunded.
- Only ready contains a usable solution. The printed “Ready task” line reports task readiness, not acceptance by the target website.
Recover a timed-out request without paying for another task
Keep the generated requestId and exact task input before sending the POST. The catch block prints requestId and, if creation returned one, taskId. That output helps recovery, but it isn’t a substitute for saving the request before sending it.
- 1.
If you have a task ID, retrieve that task
Use GET /v1/tasks/:id to check its status. Reaching the helper’s local deadline doesn’t establish that the remote task failed.
- 2.
If creation timed out without returning an ID, recover with requestId
Submit the same task request with the saved requestId. Matching inputs return the existing task; different inputs with that ID conflict.
- 3.
Don’t rerun the script as a recovery shortcut
Each run generates a fresh requestId. Don’t add an automatic POST retry that generates another ID, and don’t blindly retry invalid input unchanged.
Submit the token through the same proxy
The example stops before contacting the target. Submit solution.token—not the task ID—and use the returned solution.userAgent and solution.secChUa as the target request’s User-Agent and Sec-CH-UA headers.
The proxy inside the task’s JSON tells solvethree which proxy to use for the solve. It doesn’t configure Guzzle’s connection to the API or your later request to the target. Configure the target request separately to use that same proxy.
Use a separate client for the target request so the API client’s default Authorization header isn’t sent to the website. Keep the token, returned browser identity, proxy, and intended target together. A ready token can still be rejected if the subsequent request doesn’t match.
Sources and API reference
Run your first task.
Start with $5 in workspace credit.