Tutorials

TypeScript + CaptchaAI: Type-Safe CAPTCHA Solving

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.

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.