API Tutorials

Type-Safe CaptchaAI Client with TypeScript Generics

TypeScript generics let you build a CAPTCHA client where the compiler enforces correct parameter types for each CAPTCHA method. Pass wrong parameters for a Turnstile solve? The compiler catches it before runtime.


Type definitions

// types.ts

export interface RecaptchaV2Params {
  method: "userrecaptcha";
  googlekey: string;
  pageurl: string;
  invisible?: boolean;
  "data-s"?: string;
  proxy?: string;
  proxytype?: "HTTP" | "HTTPS" | "SOCKS4" | "SOCKS5";
}

export interface RecaptchaV3Params {
  method: "userrecaptcha";
  version: "v3";
  googlekey: string;
  pageurl: string;
  action?: string;
  min_score?: number;
}

export interface TurnstileParams {
  method: "turnstile";
  sitekey: string;
  pageurl: string;
  action?: string;
  data?: string;
}

export interface ImageParams {
  method: "base64";
  body: string;
  phrase?: boolean;
  regsense?: boolean;
  numeric?: 0 | 1 | 2;
  min_len?: number;
  max_len?: number;
}

export type CaptchaParams =
  | RecaptchaV2Params
  | RecaptchaV3Params
  | TurnstileParams
  | ImageParams;

export interface SolveResult {
  taskId: string;
  token: string;
  solveTime: number;
}

export interface APIResponse {
  status: number;
  request: string;
}

export class CaptchaAIError extends Error {
  constructor(public code: string) {
    super(`CaptchaAI error: ${code}`);
    this.name = "CaptchaAIError";
  }
}

export class TimeoutError extends CaptchaAIError {
  constructor(taskId: string) {
    super(`TIMEOUT: task ${taskId}`);
    this.name = "TimeoutError";
  }
}

Generic client

// client.ts

import {
  CaptchaParams,
  SolveResult,
  APIResponse,
  CaptchaAIError,
  TimeoutError,
} from "./types";

const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

interface ClientOptions {
  apiKey: string;
  timeout?: number;
  pollInterval?: number;
}

export class CaptchaAI {
  private apiKey: string;
  private timeout: number;
  private pollInterval: number;

  constructor(options: ClientOptions) {
    this.apiKey = options.apiKey;
    this.timeout = options.timeout ?? 120_000;
    this.pollInterval = options.pollInterval ?? 5_000;
  }

  async solve<T extends CaptchaParams>(params: T): Promise<SolveResult> {
    const start = Date.now();

    // Build form body
    const body = new URLSearchParams();
    body.set("key", this.apiKey);
    body.set("json", "1");

    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined) {
        body.set(key, String(value));
      }
    }

    // Submit
    const submitResp = await fetch(SUBMIT_URL, {
      method: "POST",
      body,
    });
    const submitData: APIResponse = await submitResp.json();

    if (submitData.status !== 1) {
      throw new CaptchaAIError(submitData.request);
    }

    const taskId = submitData.request;

    // Poll
    const deadline = start + this.timeout;
    while (Date.now() < deadline) {
      await this.delay(this.pollInterval);

      const url = new URL(RESULT_URL);
      url.searchParams.set("key", this.apiKey);
      url.searchParams.set("action", "get");
      url.searchParams.set("id", taskId);
      url.searchParams.set("json", "1");

      const pollResp = await fetch(url.toString());
      const pollData: APIResponse = await pollResp.json();

      if (pollData.status === 1) {
        return {
          taskId,
          token: pollData.request,
          solveTime: (Date.now() - start) / 1000,
        };
      }

      if (pollData.request !== "CAPCHA_NOT_READY") {
        throw new CaptchaAIError(pollData.request);
      }
    }

    throw new TimeoutError(taskId);
  }

  async getBalance(): Promise<number> {
    const url = new URL(RESULT_URL);
    url.searchParams.set("key", this.apiKey);
    url.searchParams.set("action", "getbalance");
    url.searchParams.set("json", "1");

    const resp = await fetch(url.toString());
    const data: APIResponse = await resp.json();

    if (data.status !== 1) {
      throw new CaptchaAIError(data.request);
    }

    return parseFloat(data.request);
  }

  async reportBad(taskId: string): Promise<void> {
    const url = new URL(RESULT_URL);
    url.searchParams.set("key", this.apiKey);
    url.searchParams.set("action", "reportbad");
    url.searchParams.set("id", taskId);
    url.searchParams.set("json", "1");
    await fetch(url.toString());
  }

  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

Type-safe usage

The generic solve<T> method enforces parameter shapes at compile time:

import { CaptchaAI } from "./client";
import type {
  RecaptchaV2Params,
  RecaptchaV3Params,
  TurnstileParams,
  ImageParams,
} from "./types";

const solver = new CaptchaAI({ apiKey: "YOUR_API_KEY" });

// reCAPTCHA v2 — compiler requires googlekey and pageurl
const v2Result = await solver.solve<RecaptchaV2Params>({
  method: "userrecaptcha",
  googlekey: "6Le-SITEKEY",
  pageurl: "https://example.com",
});
console.log(`v2 token: ${v2Result.token.substring(0, 40)}...`);

// reCAPTCHA v3 — compiler requires version, action fields
const v3Result = await solver.solve<RecaptchaV3Params>({
  method: "userrecaptcha",
  version: "v3",
  googlekey: "6Le-V3KEY",
  pageurl: "https://example.com",
  action: "login",
  min_score: 0.7,
});
console.log(`v3 score token: ${v3Result.token.substring(0, 40)}...`);

// Turnstile — compiler requires sitekey (not googlekey)
const turnstileResult = await solver.solve<TurnstileParams>({
  method: "turnstile",
  sitekey: "0x4AAAAAAAB...",
  pageurl: "https://example.com",
});
console.log(`Turnstile token: ${turnstileResult.token.substring(0, 40)}...`);

// Image — compiler requires body (base64)
const imageResult = await solver.solve<ImageParams>({
  method: "base64",
  body: "iVBORw0KGgoAAAANSUhEUg...",
});
console.log(`Image text: ${imageResult.token}`);

Compile-time error examples

// ERROR: Property 'sitekey' does not exist on type 'RecaptchaV2Params'
await solver.solve<RecaptchaV2Params>({
  method: "userrecaptcha",
  sitekey: "wrong-field",  // should be 'googlekey'
  pageurl: "https://example.com",
});

// ERROR: Property 'version' is missing in type
await solver.solve<RecaptchaV3Params>({
  method: "userrecaptcha",
  googlekey: "6Le-KEY",
  pageurl: "https://example.com",
  // missing: version: "v3"
});

Error handling with type narrowing

import { CaptchaAIError, TimeoutError } from "./types";

try {
  const result = await solver.solve<RecaptchaV2Params>({
    method: "userrecaptcha",
    googlekey: "6Le-SITEKEY",
    pageurl: "https://example.com",
  });
  console.log(`Solved: ${result.token.substring(0, 50)}...`);
} catch (error) {
  if (error instanceof TimeoutError) {
    console.error("Solve timed out — increase timeout or check parameters");
  } else if (error instanceof CaptchaAIError) {
    switch (error.code) {
      case "ERROR_ZERO_BALANCE":
        console.error("Add funds to your account");
        break;
      case "ERROR_WRONG_USER_KEY":
        console.error("Check your API key");
        break;
      default:
        console.error(`API error: ${error.code}`);
    }
  }
}

Troubleshooting

Problem Cause Fix
Type errors on solve() call Wrong parameter interface Match the generic type to the method field
fetch is not defined Node.js < 18 Use Node.js 18+ or install node-fetch
CaptchaAIError: ERROR_WRONG_USER_KEY Invalid API key Verify key in CaptchaAI dashboard

FAQ

Why use generics instead of separate methods?

Generics give you one solve() method that handles all types while the compiler enforces correct parameters. You get the simplicity of a single method with the safety of per-type validation.

Can I extend this with new CAPTCHA types?

Yes. Define a new params interface, add it to the CaptchaParams union, and the solve() method works with it automatically.


Build type-safe CAPTCHA solving into your TypeScript projects

Get your API key at captchaai.com.


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.