Один и тот же кусок cURL-кода — отправить задачу в CaptchaAI, подождать результат, разобрать JSON — рано или поздно появляется в нескольких PHP-проектах: в парсере, в тестовом стенде, в скрипте для конкретного клиента. Composer-пакет решает эту проблему один раз: вызов $client->solveRecaptchaV2($sitekey, $url) заменяет ручной HTTP-запрос и разбор ответа, а обновлять логику работы с API нужно в одном месте, а не в каждом репозитории отдельно.
Ниже — рабочая структура пакета для API CaptchaAI: класс клиента поверх Guzzle, отдельная иерархия исключений для разных типов сбоев и типизированные методы для reCAPTCHA v2/v3, Cloudflare Turnstile, GeeTest v3 и графических CAPTCHA. Весь код — по шагам, от composer.json до готового примера использования.
Пакет закрывает типовой набор задач одним клиентом:
- решение reCAPTCHA v2 и reCAPTCHA v3;
- прохождение проверки Cloudflare Turnstile;
- решение GeeTest v3;
- распознавание графических и текстовых CAPTCHA по base64;
- проверку баланса и отправку жалобы на неверное решение через API.
Структура пакета
Пакет собран по стандартному для Packagist макету: PSR-4-совместимый src/, отдельная папка под исключения и composer.json в корне.
captchaai-php/
├── src/
│ ├── CaptchaAI.php # Main client class
│ ├── Exception/
│ │ ├── CaptchaAIException.php
│ │ ├── SubmitException.php
│ │ ├── SolveException.php
│ │ └── TimeoutException.php
│ └── Enum/
│ └── Method.php
├── composer.json
└── README.md
Composer: настройка зависимостей
Файл composer.json объявляет пространство имён CaptchaAI\ через PSR-4-автозагрузку и требует PHP 8.1+ и guzzlehttp/guzzle версии ^7.0 — этого достаточно, чтобы пакет подключался в любой проект через composer require.
{
"name": "your-vendor/captchaai",
"description": "PHP client library for CaptchaAI API",
"type": "library",
"license": "MIT",
"require": {
"php": ">=8.1",
"guzzlehttp/guzzle": "^7.0"
},
"autoload": {
"psr-4": {
"CaptchaAI\\": "src/"
}
}
}
Иерархия исключений
Одна общая ошибка на все случаи неудобна: код вызова не может отличить временный сбой от фатального. Поэтому в пакете четыре класса: базовый CaptchaAIException с методом isFatal(), который сверяет код ошибки со списком фатальных (ERROR_WRONG_USER_KEY, ERROR_ZERO_BALANCE и другие), и три наследника под конкретные стадии — отправку задачи, решение и таймаут ожидания.
<?php
// src/Exception/CaptchaAIException.php
namespace CaptchaAI\Exception;
class CaptchaAIException extends \RuntimeException
{
private ?string $errorCode;
private const FATAL_CODES = [
'ERROR_WRONG_USER_KEY',
'ERROR_KEY_DOES_NOT_EXIST',
'ERROR_ZERO_BALANCE',
'ERROR_IP_NOT_ALLOWED',
];
public function __construct(string $message, ?string $errorCode = null)
{
parent::__construct($message);
$this->errorCode = $errorCode;
}
public function getErrorCode(): ?string
{
return $this->errorCode;
}
public function isFatal(): bool
{
return in_array($this->errorCode, self::FATAL_CODES, true);
}
}
<?php
// src/Exception/SubmitException.php
namespace CaptchaAI\Exception;
class SubmitException extends CaptchaAIException
{
public function __construct(string $code)
{
parent::__construct("Task submission failed: {$code}", $code);
}
}
<?php
// src/Exception/SolveException.php
namespace CaptchaAI\Exception;
class SolveException extends CaptchaAIException
{
public function __construct(string $code)
{
parent::__construct("Task solving failed: {$code}", $code);
}
}
<?php
// src/Exception/TimeoutException.php
namespace CaptchaAI\Exception;
class TimeoutException extends CaptchaAIException
{
private string $taskId;
public function __construct(string $taskId, int $timeoutSeconds)
{
parent::__construct("Task {$taskId} timed out after {$timeoutSeconds}s");
$this->taskId = $taskId;
}
public function getTaskId(): string
{
return $this->taskId;
}
}
Разница между SubmitException и SolveException важна на практике: первая обычно указывает на проблему со стороны запроса (неверный ключ, исчерпан баланс), вторая — что CaptchaAI не смог решить конкретную задачу.
Клиент CaptchaAI: submit, poll и типизированные методы
Основной класс собирает три внутренних метода в единый публичный API: submit() отправляет задачу на in.php и возвращает ID задачи, poll() опрашивает res.php с интервалом $pollInterval до получения ответа или истечения $timeout, а solve() просто связывает эти два шага. Поверх них — типизированные публичные методы: solveRecaptchaV2(), solveRecaptchaV3(), solveTurnstile(), solveImage() и solveGeeTestV3(), плюс служебные getBalance() и reportBad().
<?php
// src/CaptchaAI.php
namespace CaptchaAI;
use GuzzleHttp\Client as HttpClient;
use CaptchaAI\Exception\SubmitException;
use CaptchaAI\Exception\SolveException;
use CaptchaAI\Exception\TimeoutException;
class CaptchaAI
{
private const SUBMIT_URL = 'https://ocr.captchaai.com/in.php';
private const RESULT_URL = 'https://ocr.captchaai.com/res.php';
private string $apiKey;
private HttpClient $http;
private int $pollInterval;
private int $timeout;
public function __construct(
string $apiKey,
int $pollInterval = 5,
int $timeout = 180,
?HttpClient $httpClient = null
) {
$this->apiKey = $apiKey;
$this->pollInterval = $pollInterval;
$this->timeout = $timeout;
$this->http = $httpClient ?? new HttpClient(['timeout' => 30]);
}
// --- Core methods ---
private function submit(array $params): string
{
$params['key'] = $this->apiKey;
$params['json'] = 1;
$response = $this->http->post(self::SUBMIT_URL, [
'form_params' => $params,
]);
$result = json_decode($response->getBody()->getContents(), true);
if (($result['status'] ?? 0) !== 1) {
throw new SubmitException($result['request'] ?? 'unknown');
}
return $result['request']; // task ID
}
private function poll(string $taskId): string
{
$startTime = time();
while (time() - $startTime < $this->timeout) {
sleep($this->pollInterval);
$response = $this->http->get(self::RESULT_URL, [
'query' => [
'key' => $this->apiKey,
'action' => 'get',
'id' => $taskId,
'json' => 1,
],
]);
$result = json_decode($response->getBody()->getContents(), true);
if (($result['request'] ?? '') === 'CAPCHA_NOT_READY') {
continue;
}
if (($result['status'] ?? 0) === 1) {
return $result['request'];
}
throw new SolveException($result['request'] ?? 'unknown');
}
throw new TimeoutException($taskId, $this->timeout);
}
private function solve(array $params): string
{
$taskId = $this->submit($params);
return $this->poll($taskId);
}
// --- Solver methods ---
/**
* Solve reCAPTCHA v2
*/
public function solveRecaptchaV2(
string $sitekey,
string $pageurl,
bool $invisible = false,
?string $cookies = null
): string {
$params = [
'method' => 'userrecaptcha',
'googlekey' => $sitekey,
'pageurl' => $pageurl,
];
if ($invisible) $params['invisible'] = 1;
if ($cookies) $params['cookies'] = $cookies;
return $this->solve($params);
}
/**
* Solve reCAPTCHA v3
*/
public function solveRecaptchaV3(
string $sitekey,
string $pageurl,
string $action = 'verify',
): string {
return $this->solve([
'method' => 'userrecaptcha',
'version' => 'v3',
'googlekey' => $sitekey,
'pageurl' => $pageurl,
'action' => $action,
]);
}
/**
* Solve Cloudflare Turnstile
*/
public function solveTurnstile(
string $sitekey,
string $pageurl,
?string $action = null,
?string $cdata = null
): string {
$params = [
'method' => 'turnstile',
'sitekey' => $sitekey,
'pageurl' => $pageurl,
];
if ($action) $params['action'] = $action;
if ($cdata) $params['data'] = $cdata;
return $this->solve($params);
}
/**
* Solve image/text CAPTCHA from base64
*/
public function solveImage(
string $base64Image,
bool $caseSensitive = false,
?int $minLength = null,
?int $maxLength = null
): string {
$params = [
'method' => 'base64',
'body' => $base64Image,
];
if ($caseSensitive) $params['regsense'] = 1;
if ($minLength !== null) $params['min_len'] = $minLength;
if ($maxLength !== null) $params['max_len'] = $maxLength;
return $this->solve($params);
}
/**
* Solve GeeTest v3
*/
public function solveGeeTestV3(
string $gt,
string $challenge,
string $pageurl
): string {
return $this->solve([
'method' => 'geetest',
'gt' => $gt,
'challenge' => $challenge,
'pageurl' => $pageurl,
]);
}
// --- Utility methods ---
/**
* Get current account balance
*/
public function getBalance(): float
{
$response = $this->http->get(self::RESULT_URL, [
'query' => [
'key' => $this->apiKey,
'action' => 'getbalance',
'json' => 1,
],
]);
$result = json_decode($response->getBody()->getContents(), true);
return (float) ($result['request'] ?? 0);
}
/**
* Report a bad solution
*/
public function reportBad(string $taskId): bool
{
$response = $this->http->get(self::RESULT_URL, [
'query' => [
'key' => $this->apiKey,
'action' => 'reportbad',
'id' => $taskId,
'json' => 1,
],
]);
$result = json_decode($response->getBody()->getContents(), true);
return ($result['status'] ?? 0) === 1;
}
}
Значения по умолчанию — опрос раз в 5 секунд, таймаут 180 секунд — подходят для reCAPTCHA v2 и Turnstile. Для более медленных типов таймаут стоит увеличить при создании клиента, а не сокращать интервал опроса: частые запросы к res.php решение не ускоряют.
Как использовать пакет на практике
После composer require your-vendor/captchaai клиент создаётся одной строкой и сразу готов к вызовам. Ниже — пример с проверкой баланса, решением reCAPTCHA v2 с разбором обеих веток ошибок, Turnstile и графической CAPTCHA.
<?php
require_once 'vendor/autoload.php';
use CaptchaAI\CaptchaAI;
use CaptchaAI\Exception\SubmitException;
use CaptchaAI\Exception\TimeoutException;
$client = new CaptchaAI(
apiKey: 'YOUR_API_KEY',
pollInterval: 5,
timeout: 120
);
// Check balance
$balance = $client->getBalance();
echo "Balance: \${$balance}\n";
// Solve reCAPTCHA v2
try {
$token = $client->solveRecaptchaV2(
sitekey: '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
pageurl: 'https://staging.example.com/qa-login'
);
echo "Token: " . substr($token, 0, 40) . "...\n";
} catch (TimeoutException $e) {
echo "Timed out: {$e->getMessage()}\n";
} catch (SubmitException $e) {
if ($e->isFatal()) {
echo "Fatal: {$e->getErrorCode()}\n";
exit(1);
}
echo "Retryable: {$e->getErrorCode()}\n";
}
// Solve Turnstile
$turnstileToken = $client->solveTurnstile(
sitekey: '0x4AAAAAAADnPIDROrmt1Wwj',
pageurl: 'https://example.com/checkout'
);
// Solve image CAPTCHA
$imageBase64 = base64_encode(file_get_contents('captcha.png'));
$text = $client->solveImage($imageBase64, caseSensitive: true);
echo "Text: {$text}\n";
Для агентств и фрилансеров из России, Беларуси и Казахстана, которые ведут параллельно несколько клиентских интеграций, такой пакет особенно удобен: один и тот же клиент подключается во все проекты, а расход потоков планируется по тарифу CaptchaAI — например, STANDARD ($30/мес, 15 потоков) обычно хватает на несколько одновременных задач без ожидания в очереди, и стоимость остаётся предсказуемой в USD независимо от курса.
Типичные ошибки и их решение
Большинство проблем при первом подключении пакета сводится к пяти сценариям:
| Проблема | Причина | Решение |
|---|---|---|
SubmitException: ERROR_WRONG_USER_KEY |
Неверный API-ключ | Проверьте ключ в панели управления CaptchaAI |
TimeoutException возникает часто |
Таймаут выставлен слишком коротким | Увеличьте $timeout до 180 секунд и более |
Class not found |
Автозагрузчик не настроен | Выполните composer dump-autoload |
| Ошибка соединения Guzzle | Проблема сети или блокировка файрволом | Убедитесь, что сервер может достучаться до ocr.captchaai.com |
json_decode возвращает null |
Некорректное тело ответа | Проверьте адрес API и залогируйте сырой ответ для отладки |
Частые вопросы
Обязательно ли использовать именно Guzzle, или подойдёт любой PSR-18-клиент?
Guzzle выбран за PSR-7-совместимые сообщения, пул соединений и поддержку middleware, но конструктор CaptchaAI принимает любой клиент, совместимый с PSR-18 — тип HttpClient в примере можно заменить без изменений в коде решателей.
Как понять, что ошибка фатальная, а не временная?
Метод isFatal() в CaptchaAIException сверяет код ошибки со списком FATAL_CODES — ERROR_WRONG_USER_KEY, ERROR_KEY_DOES_NOT_EXIST, ERROR_ZERO_BALANCE, ERROR_IP_NOT_ALLOWED. Если он вернул true, повторять запрос бессмысленно — нужно чинить конфигурацию, а не делать повторную попытку.
Что делать, если CAPCHA_NOT_READY держится дольше обычного?
poll() продолжает опрос, пока не истечёт $timeout, поэтому по умолчанию скрипт ждёт до 180 секунд, прежде чем выбросить TimeoutException. Если reCAPTCHA v2 или Turnstile регулярно упираются в этот таймаут, стоит увеличить его при создании клиента — сокращение pollInterval решение не ускорит.
Сколько потоков нужно, чтобы отправлять задачи параллельно?
Пакет сам по себе не ограничивает параллелизм — сколько задач слать одновременно, определяет тарифный план CaptchaAI. Например, ADVANCE ($90/мес, 50 потоков) позволяет держать открытыми до 50 задач одновременно; запросы сверх лимита встают в очередь на стороне API.
Стоит ли публиковать пакет в Packagist, если он нужен только внутри команды?
Не обязательно: для внутреннего использования достаточно указать repositories в composer.json со ссылкой на приватный Git-репозиторий. В Packagist имеет смысл выкладывать пакет только для публичного распространения — с версионированием и README.
Что дальше
Когда пакет подключён и первый токен получен, стоит закрепить основы работы с API CaptchaAI: