TypeScript catches bugs at compile time. A fully typed CaptchaAI client prevents wrong parameters, invalid method names, and missing fields before your code runs.
Type definitions
// captcha-types.ts
export type CaptchaMethod =
| "userrecaptcha"
| "turnstile"
| "geetest"
| "geetest_v4"
| "bls"
| "base64"
| "post"
| "cloudflare_challenge";
export interface RecaptchaV2Params {
method: "userrecaptcha";
googlekey: string;
pageurl: string;
invisible?: "1";
"data-s"?: string;
}
export interface RecaptchaV3Params {
method: "userrecaptcha";
googlekey: string;
pageurl: string;
version: "v3";
action?: string;
min_score?: string;
}
export interface TurnstileParams {
method: "turnstile";
sitekey: string;
pageurl: string;
action?: string;
data?: string;
}
export interface GeeTestV3Params {
method: "geetest";
gt: string;
challenge: string;
pageurl: string;
api_server?: string;
}
export interface GeeTestV4Params {
method: "geetest_v4";
captcha_id: string;
pageurl: string;
}
export interface ImageCaptchaParams {
method: "base64";
body: string;
numeric?: "0" | "1" | "2" | "3" | "4";
min_len?: string;
max_len?: string;
language?: "0" | "1" | "2";
}
export interface BLSParams {
method: "bls";
sitekey: string;
pageurl: string;
}
export type CaptchaParams =
| RecaptchaV2Params
| RecaptchaV3Params
| TurnstileParams
| GeeTestV3Params
| GeeTestV4Params
| ImageCaptchaParams
| BLSParams;
export interface SubmitResponse {
status: 0 | 1;
request: string;
}
export interface PollResponse {
status: 0 | 1;
request: string;
}
export interface GeeTestResult {
geetest_challenge: string;
geetest_validate: string;
geetest_seccode: string;
}
export type SolveResult<T extends CaptchaParams> = T extends GeeTestV3Params
? GeeTestResult
: string;
export type CaptchaErrorCode =
| "ERROR_WRONG_USER_KEY"
| "ERROR_KEY_DOES_NOT_EXIST"
| "ERROR_ZERO_BALANCE"
| "ERROR_NO_SLOT_AVAILABLE"
| "ERROR_CAPTCHA_UNSOLVABLE"
| "ERROR_BAD_PARAMETERS"
| "ERROR_WRONG_CAPTCHA_ID"
| "CAPCHA_NOT_READY";
Typed error classes
// errors.ts
export class CaptchaError extends Error {
constructor(
public readonly code: string,
message?: string
) {
super(message || code);
this.name = "CaptchaError";
}
}
export class RetriableError extends CaptchaError {
constructor(code: string) {
super(code, `Retriable: ${code}`);
this.name = "RetriableError";
}
}
export class FatalError extends CaptchaError {
constructor(code: string) {
super(code, `Fatal: ${code}`);
this.name = "FatalError";
}
}
export class TimeoutError extends CaptchaError {
constructor(public readonly elapsed: number) {
super("TIMEOUT", `Solve timed out after ${elapsed}ms`);
this.name = "TimeoutError";
}
}
const FATAL_ERRORS = new Set([
"ERROR_WRONG_USER_KEY",
"ERROR_KEY_DOES_NOT_EXIST",
"ERROR_ZERO_BALANCE",
"ERROR_CAPTCHA_UNSOLVABLE",
"ERROR_BAD_PARAMETERS",
]);
export function classifyError(code: string): never {
if (FATAL_ERRORS.has(code)) {
throw new FatalError(code);
}
throw new RetriableError(code);
}
Type-safe solver class
// solver.ts
import type {
CaptchaParams,
SubmitResponse,
PollResponse,
GeeTestV3Params,
GeeTestResult,
SolveResult,
} from "./captcha-types";
import { CaptchaError, FatalError, TimeoutError, classifyError } from "./errors";
interface SolverOptions {
apiKey: string;
pollInterval?: number;
maxPollTime?: number;
maxRetries?: number;
}
export class CaptchaAISolver {
private readonly apiKey: string;
private readonly pollInterval: number;
private readonly maxPollTime: number;
private readonly maxRetries: number;
constructor(options: SolverOptions) {
this.apiKey = options.apiKey;
this.pollInterval = options.pollInterval ?? 5000;
this.maxPollTime = options.maxPollTime ?? 150000;
this.maxRetries = options.maxRetries ?? 3;
}
async solve<T extends CaptchaParams>(params: T): Promise<SolveResult<T>> {
const taskId = await this.submit(params);
return await this.poll<T>(taskId);
}
private async submit(params: CaptchaParams): Promise<string> {
const { method, ...rest } = params;
const body = new URLSearchParams({
key: this.apiKey,
method,
json: "1",
...Object.fromEntries(
Object.entries(rest).filter(([, v]) => v !== undefined)
),
});
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
const resp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body,
});
const data: SubmitResponse = await resp.json();
if (data.status === 1) {
return data.request;
}
if (data.request === "ERROR_NO_SLOT_AVAILABLE" && attempt < this.maxRetries) {
await this.sleep(3000 * (attempt + 1));
continue;
}
classifyError(data.request);
}
throw new CaptchaError("MAX_RETRIES", "Max submit retries exceeded");
}
private async poll<T extends CaptchaParams>(taskId: string): Promise<SolveResult<T>> {
const start = Date.now();
const params = new URLSearchParams({
key: this.apiKey,
action: "get",
id: taskId,
json: "1",
});
while (Date.now() - start < this.maxPollTime) {
await this.sleep(this.pollInterval);
const resp = await fetch(`https://ocr.captchaai.com/res.php?${params}`);
const data: PollResponse = await resp.json();
if (data.status === 1) {
// GeeTest returns JSON, others return a string token
try {
const parsed = JSON.parse(data.request);
if (parsed.geetest_challenge) {
return parsed as SolveResult<T>;
}
} catch {}
return data.request as SolveResult<T>;
}
if (data.request === "CAPCHA_NOT_READY") {
continue;
}
classifyError(data.request);
}
throw new TimeoutError(Date.now() - start);
}
async getBalance(): Promise<number> {
const resp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: this.apiKey,
action: "getbalance",
json: "1",
})}`
);
const data = await resp.json();
return parseFloat(data.request);
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
Usage examples
reCAPTCHA v2
const solver = new CaptchaAISolver({ apiKey: "YOUR_API_KEY" });
// TypeScript enforces correct parameters
const token = await solver.solve({
method: "userrecaptcha",
googlekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl: "https://example.com/login",
});
// token is typed as string
console.log(`Token: ${token.substring(0, 50)}...`);
Cloudflare Turnstile
const token = await solver.solve({
method: "turnstile",
sitekey: "0x4AAAAAAABS7vwvV6VFfMcD",
pageurl: "https://example.com/login",
action: "login",
});
GeeTest v3
const result = await solver.solve({
method: "geetest",
gt: "81dc9bdb52d04dc20036dbd8313ed055",
challenge: "d93591bdf7860e1e4ee2fca799911587",
pageurl: "https://example.com",
});
// result is typed as GeeTestResult
console.log(result.geetest_validate);
Image CAPTCHA
const answer = await solver.solve({
method: "base64",
body: imageBase64String,
numeric: "1", // Numbers only
min_len: "4",
max_len: "6",
});
Compile-time safety examples
// These will cause TypeScript errors:
// Missing required field
await solver.solve({
method: "userrecaptcha",
pageurl: "https://example.com",
// Error: Property 'googlekey' is missing
});
// Wrong parameter for method
await solver.solve({
method: "turnstile",
googlekey: "key", // Error: 'googlekey' does not exist on TurnstileParams
pageurl: "https://example.com",
});
// Invalid method name
await solver.solve({
method: "invalid_method", // Error: not assignable to CaptchaMethod
pageurl: "https://example.com",
});
Generic batch solver
interface BatchTask<T extends CaptchaParams> {
id: string;
params: T;
}
interface BatchResult<T extends CaptchaParams> {
id: string;
status: "solved" | "error";
result?: SolveResult<T>;
error?: string;
}
async function solveBatch<T extends CaptchaParams>(
solver: CaptchaAISolver,
tasks: BatchTask<T>[],
maxConcurrent = 5
): Promise<BatchResult<T>[]> {
const results: BatchResult<T>[] = [];
const queue = [...tasks];
const worker = async () => {
while (queue.length > 0) {
const task = queue.shift();
if (!task) break;
try {
const result = await solver.solve(task.params);
results.push({ id: task.id, status: "solved", result });
} catch (error) {
results.push({
id: task.id,
status: "error",
error: error instanceof Error ? error.message : String(error),
});
}
}
};
const workers = Array.from(
{ length: Math.min(maxConcurrent, tasks.length) },
() => worker()
);
await Promise.all(workers);
return results;
}
// Usage — fully typed
const results = await solveBatch(solver, [
{
id: "task_1",
params: {
method: "userrecaptcha",
googlekey: "KEY_1",
pageurl: "https://example.com/1",
},
},
{
id: "task_2",
params: {
method: "turnstile",
sitekey: "0xKEY_2",
pageurl: "https://example.com/2",
},
},
]);
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Type error on solve() |
Wrong params for method | Match params to the method's interface |
SolveResult is string | GeeTestResult |
Generic not narrowed | Pass explicit type param or use discriminated union |
fetch not found |
Missing types | Add "lib": ["ES2022", "DOM"] to tsconfig.json |
| JSON parse error | Response not JSON | Check resp.ok before parsing |
| Runtime error despite types | API behavior changed | Types don't cover runtime — use try/catch |
Frequently asked questions
Is there an official TypeScript SDK for CaptchaAI?
Not currently. The typed client in this guide provides full type safety using the REST API directly.
Can I use this with Deno or Bun?
Yes. The code uses standard fetch and TypeScript — it works in Deno, Bun, and Node 18+.
How do I handle union types in solve results?
Use the discriminated union pattern: check params.method to narrow the result type. For GeeTest, the result is GeeTestResult; for everything else, it's string.
Summary
TypeScript + CaptchaAI provides compile-time safety for CAPTCHA solving: the typed CaptchaAISolver class prevents wrong parameters, invalid methods, and missing fields. Use generics for batch processing and discriminated unions for type-safe results.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.