Official PHP client for the CaptchaAI captcha-solving service.
Call one method per captcha type — the SDK handles HTTP, polling, retries, and error mapping. Your code never sees raw API responses or polling loops.
Repository: github.com/CaptchaAI/captchaai-PHP
- Requirements
- Installation
- Quick start
- Configuration
- Solve result
- Normal captcha
- Grid captcha
- BLS captcha
- reCAPTCHA v2
- reCAPTCHA v3
- Cloudflare Turnstile
- Cloudflare Challenge
- GeeTest
- CaptchaFox
- Friendly Captcha
- Lemin
- Per-call options
- Proxies
- Account utilities
- Manual two-step API
- Error handling
- Thread limits and retries
- Architecture
- Development
- License
- PHP 8.2+
- guzzlehttp/guzzle ^7.9 (installed automatically via Composer)
composer require captchaai/captchaai-php<?php
require 'vendor/autoload.php';
use CaptchaAI\CaptchaAI;
$solver = new CaptchaAI(apiKey: 'YOUR_32_CHAR_KEY');
$result = $solver->recaptchaV2(
sitekey: '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
url: 'https://google.com/recaptcha/api2/demo',
);
echo $result->solution; // g-recaptcha-response token
echo (string) $result; // __toString() returns the token directly
$solver->close();This SDK is synchronous only — designed for standard PHP runtimes (CLI, FPM, Apache). There is no async client; PHP's request/response model maps naturally to the sync orchestrator.
All user-facing settings are provided at client creation:
$solver = new CaptchaAI(
apiKey: 'YOUR_32_CHAR_KEY',
proxy: 'user:pass@host:port', // optional — applied to every solve
proxytype: 'HTTP', // required when proxy is set
autoRetry: true,
baseUrl: 'https://ocr.captchaai.com',
threadBusyTimeout: 120.0,
maxRetries: null, // optional — overrides autoRetry when set
);| Option | Default | Description |
|---|---|---|
apiKey |
(required) | 32-character API key, validated at startup |
proxy |
null |
Proxy string applied to every solve unless overridden per call |
proxytype |
null |
HTTP / HTTPS / SOCKS4 / SOCKS5 — required when proxy is set |
autoRetry |
true |
true = SDK retries transient errors with backoff + jitter; false = raise immediately |
baseUrl |
https://ocr.captchaai.com |
API host for in.php / res.php |
threadBusyTimeout |
120.0 |
Seconds to keep retrying while all account threads are busy |
maxRetries |
null |
When set: 0 disables retries; N > 0 enables up to N submit attempts (supersedes autoRetry) |
transport |
null |
Optional TransportInterface for testing; defaults to Guzzle-backed SyncTransport |
The constructor validates the key and fetches the account thread cap via a startup threadsInfo call. A bad key raises InvalidKeyException before any solve is attempted.
Call $solver->close() when finished to release transport resources.
Every solve method returns a SolveResult:
$result->taskId; // string — task ID from submit (always present)
$result->solution; // string | array — token, cell indices, or structured answer
$result->userAgent; // string | null — browser UA (Enterprise / Cloudflare / CaptchaFox types)
$result->raw; // array | null — full parsed API response
$result->rawText; // string | null — unparsed solution string before JSON parsing
(string) $result; // __toString() — token string, or JSON for array solutionsStructured types return solution as an array:
| Type | solution shape |
|---|---|
grid, bls |
List of cell indices, e.g. [0, 3, 7] |
geetest |
['challenge' => ..., 'validate' => ..., 'seccode' => ...] |
lemin |
['answer' => ..., 'challenge_uuid' => ...] |
The image parameter on image-based methods accepts a file path, URL, data-URI, or raw base64 string.
Solve a text/image captcha; solution is the recognised string.
$result = $solver->normal(image: 'captcha.png');
echo $result->solution; // e.g. "XY7K2"Solve a tile-selection captcha; gridSize must be "3x3" or "4x4", and solution is a list of cell indices.
$result = $solver->grid(
image: 'grid.png',
instructions: 'select all traffic lights',
gridSize: '3x3',
);
print_r($result->solution); // e.g. [1, 4, 7]Solve a multi-image captcha — exactly 9 images are required; solution is a list of cell indices.
$images = array_map(fn (int $i) => "img{$i}.png", range(0, 8));
$result = $solver->bls(images: $images, instructions: '664');
print_r($result->solution);Solve reCAPTCHA v2 and receive a g-recaptcha-response token in solution.
$result = $solver->recaptchaV2(
sitekey: '6Lc...',
url: 'https://example.com',
);
echo (string) $result;Invisible — pass invisible: true:
$result = $solver->recaptchaV2(
sitekey: '6Lc...',
url: 'https://example.com',
invisible: true,
);Enterprise — pass enterprise: true; solution and userAgent are returned:
$result = $solver->recaptchaV2(
sitekey: '6Lc...',
url: 'https://example.com',
enterprise: true,
action: 'login', // optional for v2
);
echo $result->solution, $result->userAgent;Solve reCAPTCHA v3; action is required for the standard v3 registry entry.
$result = $solver->recaptchaV3(
sitekey: '6Lc...',
url: 'https://example.com',
action: 'login',
);
echo (string) $result;Enterprise — pass enterprise: true; returns token and userAgent:
$result = $solver->recaptchaV3(
sitekey: '6Lc...',
url: 'https://example.com',
action: 'login',
enterprise: true,
minScore: 0.7, // optional
);
echo $result->solution, $result->userAgent;Solve a Cloudflare Turnstile challenge; solution is the response token.
$result = $solver->turnstile(
sitekey: '0x4A...',
url: 'https://example.com',
);
echo (string) $result;Solve a Cloudflare interstitial page; a proxy is required (client-level or per-call). Returns solution (e.g. clearance cookie value) and userAgent.
$cfSolver = new CaptchaAI(
apiKey: 'YOUR_32_CHAR_KEY',
proxy: 'user:pass@host:port',
proxytype: 'HTTP',
);
$result = $cfSolver->cloudflareChallenge(url: 'https://example.com');
echo $result->solution, $result->userAgent;Solve GeeTest v3; solution is a structured array with challenge, validate, and seccode keys.
$result = $solver->geetest(
gt: 'abc123',
challenge: 'xyz456',
url: 'https://example.com',
);
echo $result->solution['challenge'];
echo $result->solution['validate'];
echo $result->solution['seccode'];Solve a CaptchaFox slider challenge; a proxy is required. Returns token and userAgent.
$foxSolver = new CaptchaAI(
apiKey: 'YOUR_32_CHAR_KEY',
proxy: 'user:pass@host:port',
proxytype: 'HTTP',
);
$result = $foxSolver->captchafox(
sitekey: 'sk_xxxx...',
url: 'https://example.com',
);
echo $result->solution, $result->userAgent;Solve a Friendly Captcha proof-of-work challenge; solution is the verification token.
$result = $solver->friendlyCaptcha(
sitekey: 'FCMDESUD3M34857N',
url: 'https://example.com',
version: 'v2', // optional
);
echo $result->solution;Solve a Lemin puzzle challenge; solution is a structured array with answer and challenge_uuid keys.
$result = $solver->lemin(
captchaId: 'CROPPED_d3d4d56_73ca4008925b4f83a8bed59c2dd0df6d',
divId: 'lemin-cropped-captcha',
url: 'https://example.com',
apiServer: 'https://api.leminnow.com', // optional
);
echo $result->solution['answer'];
echo $result->solution['challenge_uuid'];Every solve method accepts optional trailing proxy and proxytype arguments to override the client-level proxy for a single call:
$result = $solver->recaptchaV2(
sitekey: '6Lc...',
url: 'https://example.com',
proxy: 'user:pass@other-host:port',
proxytype: 'SOCKS5',
);These methods accept optional cookies and userAgent parameters: recaptchaV2, recaptchaV3, turnstile, cloudflareChallenge, geetest, and captchafox.
$result = $solver->recaptchaV3(
sitekey: '6Lc...',
url: 'https://example.com',
action: 'login',
cookies: 'session=abc123; csrf=xyz',
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
);Fine-tune text/image captcha solving (values are forwarded to the API as-is):
$result = $solver->normal(
image: 'captcha.png',
numeric: 1,
minLen: 4,
maxLen: 6,
phrase: 0,
caseSensitive: 1,
lang: 'en',
instructions: 'Type the characters you see',
);Set a client-level proxy at construction to apply it to every solve:
$solver = new CaptchaAI(
apiKey: 'YOUR_32_CHAR_KEY',
proxy: 'user:pass@host:port',
proxytype: 'HTTP',
);Or override per call — see Per-call proxy.
Proxy-required types: cloudflareChallenge and captchafox raise ValidationException if no proxy is provided (client-level or per-call).
Common proxytype values: HTTP, HTTPS, SOCKS4, SOCKS5 (passed through to the API; not validated locally by the SDK).
$info = $solver->threadsInfo();
// ['threads' => 10, 'working_threads' => 3]Returns the account's total thread cap and how many are currently in use. The same call runs automatically at startup to validate the key and set the internal thread cap.
Submit a task and fetch its result separately — useful when you want to do other work while the captcha is being solved:
$taskId = $solver->send('recaptcha_v2', [
'googlekey' => '6Lc...',
'pageurl' => 'https://example.com',
]);
// ... do other work ...
$result = $solver->getResult('recaptcha_v2', $taskId);
echo $result->solution;Use registry parameter names (e.g. googlekey, pageurl, sitekey) in the $params array passed to send().
All SDK exceptions extend CaptchaAIException:
use CaptchaAI\CaptchaAI;
use CaptchaAI\Exception\ApiException;
use CaptchaAI\Exception\CaptchaAIException;
use CaptchaAI\Exception\InvalidKeyException;
use CaptchaAI\Exception\NetworkException;
use CaptchaAI\Exception\NoThreadsException;
use CaptchaAI\Exception\ProxyException;
use CaptchaAI\Exception\ThreadLimitException;
use CaptchaAI\Exception\TimeoutException;
use CaptchaAI\Exception\UnsolvableException;
use CaptchaAI\Exception\ValidationException;
$solver = new CaptchaAI(apiKey: 'YOUR_32_CHAR_KEY');
try {
$result = $solver->recaptchaV2(sitekey: '6Lc...', url: 'https://example.com');
} catch (InvalidKeyException) {
// Wrong or missing API key (constructor or API response)
} catch (ValidationException) {
// Missing/invalid input, unknown captcha type, proxy required but missing, image validation
} catch (UnsolvableException) {
// Captcha could not be solved (ERROR_CAPTCHA_UNSOLVABLE)
} catch (ThreadLimitException) {
// All account threads busy, or local inflight cap reached (see Thread limits and retries)
} catch (NoThreadsException) {
// Account expired or has no active plan
} catch (ProxyException) {
// Bad or unreachable proxy
} catch (NetworkException) {
// Network timeout, DNS failure, or connection error reaching the API
} catch (TimeoutException) {
// Poll deadline exceeded before a result was ready
} catch (ApiException) {
// Unmapped API error, malformed response, IP banned, transient server error, etc.
} catch (CaptchaAIException) {
// Catch-all for any SDK error
}| Exception | Typical cause |
|---|---|
InvalidKeyException |
Empty/non-32-char key at construction; ERROR_WRONG_USER_KEY / ERROR_KEY_DOES_NOT_EXIST from API |
ValidationException |
Missing params, invalid grid_size, proxy without proxytype, proxy-required type without proxy, bad image input, unknown captcha type in send()/getResult() |
UnsolvableException |
ERROR_CAPTCHA_UNSOLVABLE during poll |
ThreadLimitException |
All threads busy; local inflight cap reached when retries disabled; busy-wait timeout exhausted |
NoThreadsException |
ERROR_ZERO_BALANCE with available threads but no active plan |
ProxyException |
ERROR_BAD_PROXY or ERROR_PROXY_CONNECTION_FAILED |
NetworkException |
HTTP transport failure (timeout, DNS, connection refused) |
TimeoutException |
Poll loop exceeded internal deadline (CAPCHA_NOT_READY too long) |
ApiException |
Unexpected API codes, malformed JSON, missing response fields, IP_BANNED, server errors |
- At construction, the SDK calls
threadsInfoand stores the account'sthreadsvalue as an internal cap. - When
autoRetryisfalse(ormaxRetries: 0):- Before submit, the SDK checks whether all account threads are busy →
ThreadLimitException. - A local inflight counter prevents more concurrent solves than the thread cap within one client instance →
ThreadLimitException.
- Before submit, the SDK checks whether all account threads are busy →
- When
autoRetryistrue(default):- On
ERROR_ZERO_BALANCE, the SDK distinguishes all threads busy (ThreadLimitExceptionafter retries/timeouts) from dead account (NoThreadsException).
- On
| Setting | Behaviour |
|---|---|
autoRetry: true (default) |
SDK retries transient submit errors and thread-busy conditions with exponential backoff + jitter |
autoRetry: false |
SDK raises immediately on transient errors — you control the retry loop |
maxRetries: N (N > 0) |
Same as autoRetry: true, capped at N submit attempts |
maxRetries: 0 |
Same as autoRetry: false — raises immediately |
maxRetries supersedes autoRetry when both are provided.
Fail-fast errors (InvalidKeyException, ValidationException, UnsolvableException, NoThreadsException) always raise immediately regardless of retry settings.
The SDK is four layers with strict boundaries:
CaptchaAI — one method per type; zero solve logic
│
▼
Orchestrator (Core/Orchestrator/) — validate → submit → poll → extract
│ PHP generators; never touches the network
▼
Helpers (Helper/) — validation, image resolver, params, extractor, retry
│
▼
Transport (Transport/) — HTTP pipe; Guzzle-backed SyncTransport
The orchestrator yields ApiCall and Sleep effect objects. The sync client drives those effects with Guzzle and usleep(). All business logic — validation, error mapping, retries, polling — lives in the shared engine.
Every captcha type is one row in Core/Registry.php. The engine reads the row and never branches on the type name.
Run the test suite (99 tests, mock transport — no real API key required):
composer install
vendor/bin/phpunit --configuration tests/phpunit.xmlMIT — Copyright (c) 2026 Dev@Captchaai. See LICENSE.