API Tutorials

Building a Node.js SDK for CaptchaAI API

Raw fetch calls work, but repeating the same submit-poll-parse pattern across every project wastes time. An SDK wraps the CaptchaAI API into clean, typed methods — client.solveRecaptchaV2() instead of manually constructing URL parameters and polling loops every time.

SDK Architecture

captchaai-sdk/
├── src/
│   ├── client.js          # Main client class
│   ├── errors.js          # Custom error classes
│   └── constants.js       # API URLs and defaults
├── package.json
└── README.md

Error Classes

// src/errors.js

class CaptchaAIError extends Error {
  constructor(message, code = null) {
    super(message);
    this.name = "CaptchaAIError";
    this.code = code;
  }
}

class SubmitError extends CaptchaAIError {
  constructor(code) {
    super(`Task submission failed: ${code}`, code);
    this.name = "SubmitError";
  }
}

class SolveError extends CaptchaAIError {
  constructor(code) {
    super(`Task solving failed: ${code}`, code);
    this.name = "SolveError";
  }
}

class TimeoutError extends CaptchaAIError {
  constructor(taskId, timeoutMs) {
    super(`Task ${taskId} timed out after ${timeoutMs}ms`);
    this.name = "TimeoutError";
    this.taskId = taskId;
  }
}

class BalanceError extends CaptchaAIError {
  constructor(balance) {
    super(`Insufficient balance: $${balance}`);
    this.name = "BalanceError";
    this.balance = balance;
  }
}

module.exports = { CaptchaAIError, SubmitError, SolveError, TimeoutError, BalanceError };

Constants

// src/constants.js

module.exports = {
  SUBMIT_URL: "https://ocr.captchaai.com/in.php",
  RESULT_URL: "https://ocr.captchaai.com/res.php",
  DEFAULT_POLL_INTERVAL: 5000,
  DEFAULT_TIMEOUT: 180000,
  RETRYABLE_ERRORS: new Set([
    "ERROR_NO_SLOT_AVAILABLE",
    "ERROR_TOO_MUCH_REQUESTS",
  ]),
  FATAL_ERRORS: new Set([
    "ERROR_WRONG_USER_KEY",
    "ERROR_KEY_DOES_NOT_EXIST",
    "ERROR_ZERO_BALANCE",
    "ERROR_IP_NOT_ALLOWED",
  ]),
};

Main Client

// src/client.js

const { SUBMIT_URL, RESULT_URL, DEFAULT_POLL_INTERVAL, DEFAULT_TIMEOUT, FATAL_ERRORS } = require("./constants");
const { SubmitError, SolveError, TimeoutError, BalanceError } = require("./errors");

class CaptchaAI {
  /**

   * @param {string} apiKey - Your CaptchaAI API key
   * @param {object} [options]
   * @param {number} [options.pollInterval=5000] - Milliseconds between poll requests
   * @param {number} [options.timeout=180000] - Max wait time for a solution
   */
  constructor(apiKey, options = {}) {
    if (!apiKey) throw new Error("API key is required");

    this.apiKey = apiKey;
    this.pollInterval = options.pollInterval || DEFAULT_POLL_INTERVAL;
    this.timeout = options.timeout || DEFAULT_TIMEOUT;
  }

  // --- Core methods ---

  async _submit(params) {
    const body = new URLSearchParams({
      key: this.apiKey,
      json: "1",
      ...params,
    });

    const response = await fetch(SUBMIT_URL, { method: "POST", body });
    const result = await response.json();

    if (result.status !== 1) {
      throw new SubmitError(result.request);
    }

    return result.request; // task ID
  }

  async _poll(taskId) {
    const start = Date.now();

    while (Date.now() - start < this.timeout) {
      await this._sleep(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 response = await fetch(url);
      const result = await response.json();

      if (result.request === "CAPCHA_NOT_READY") {
        continue;
      }

      if (result.status === 1) {
        return result.request; // solution token
      }

      throw new SolveError(result.request);
    }

    throw new TimeoutError(taskId, this.timeout);
  }

  async _solve(params) {
    const taskId = await this._submit(params);
    return this._poll(taskId);
  }

  _sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  // --- Solver methods ---

  /**

   * Solve reCAPTCHA v2
   * @param {string} sitekey - The site's reCAPTCHA sitekey
   * @param {string} pageurl - The URL where the CAPTCHA appears
   * @param {object} [options]
   * @param {string} [options.cookies] - Cookies in "key=value; key2=value2" format
   * @param {string} [options.userAgent] - Custom user agent
   * @param {boolean} [options.invisible] - Set true for invisible reCAPTCHA
   * @returns {Promise<string>} The solved token
   */
  async solveRecaptchaV2(sitekey, pageurl, options = {}) {
    const params = {
      method: "userrecaptcha",
      googlekey: sitekey,
      pageurl,
    };
    if (options.cookies) params.cookies = options.cookies;
    if (options.userAgent) params.userAgent = options.userAgent;
    if (options.invisible) params.invisible = "1";

    return this._solve(params);
  }

  /**

   * Solve reCAPTCHA v3
   * @param {string} sitekey
   * @param {string} pageurl
   * @param {object} [options]
   * @param {string} [options.action] - The reCAPTCHA action value
   * @param {number} [options.minScore] - Minimum acceptable score (0.1–0.9)
   * @returns {Promise<string>}
   */
  async solveRecaptchaV3(sitekey, pageurl, options = {}) {
    const params = {
      method: "userrecaptcha",
      version: "v3",
      googlekey: sitekey,
      pageurl,
    };
    if (options.action) params.action = options.action;
    if (options.minScore) params.min_score = String(options.minScore);

    return this._solve(params);
  }

  /**

   * Solve Cloudflare Turnstile
   * @param {string} sitekey
   * @param {string} pageurl
   * @param {object} [options]
   * @param {string} [options.action] - Turnstile action parameter
   * @param {string} [options.cdata] - Turnstile cdata parameter
   * @returns {Promise<string>}
   */
  async solveTurnstile(sitekey, pageurl, options = {}) {
    const params = {
      method: "turnstile",
      sitekey,
      pageurl,
    };
    if (options.action) params.action = options.action;
    if (options.cdata) params.data = options.cdata;

    return this._solve(params);
  }

  /**

   * Solve hCaptcha
   * @param {string} sitekey
   * @param {string} pageurl
   * @returns {Promise<string>}
   */
  async solveHCaptcha(sitekey, pageurl) {
    return this._solve({
      method: "hcaptcha",
      sitekey,
      pageurl,
    });
  }

  /**

   * Solve image/text CAPTCHA from base64
   * @param {string} base64Image - Base64-encoded image (no data: prefix)
   * @param {object} [options]
   * @param {boolean} [options.caseSensitive] - Case-sensitive matching
   * @param {number} [options.minLength] - Minimum answer length
   * @param {number} [options.maxLength] - Maximum answer length
   * @returns {Promise<string>}
   */
  async solveImage(base64Image, options = {}) {
    const params = {
      method: "base64",
      body: base64Image,
    };
    if (options.caseSensitive) params.regsense = "1";
    if (options.minLength) params.min_len = String(options.minLength);
    if (options.maxLength) params.max_len = String(options.maxLength);

    return this._solve(params);
  }

  /**

   * Solve GeeTest v3
   * @param {string} gt - gt parameter
   * @param {string} challenge - challenge parameter
   * @param {string} pageurl
   * @returns {Promise<string>}
   */
  async solveGeeTestV3(gt, challenge, pageurl) {
    return this._solve({
      method: "geetest",
      gt,
      challenge,
      pageurl,
    });
  }

  // --- Utility methods ---

  /**

   * Get current account balance
   * @returns {Promise<number>}
   */
  async getBalance() {
    const url = new URL(RESULT_URL);
    url.searchParams.set("key", this.apiKey);
    url.searchParams.set("action", "getbalance");
    url.searchParams.set("json", "1");

    const response = await fetch(url);
    const result = await response.json();

    if (result.status === 0 && FATAL_ERRORS.has(result.request)) {
      throw new SubmitError(result.request);
    }

    return parseFloat(result.request);
  }

  /**

   * Report a bad solution
   * @param {string} taskId - The task ID to report
   * @returns {Promise<boolean>}
   */
  async reportBad(taskId) {
    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");

    const response = await fetch(url);
    const result = await response.json();
    return result.status === 1;
  }
}

module.exports = { CaptchaAI };

Package Configuration

{
  "name": "captchaai-sdk",
  "version": "1.0.0",
  "description": "Node.js SDK for CaptchaAI API",
  "main": "src/client.js",
  "exports": {
    ".": "./src/client.js",
    "./errors": "./src/errors.js"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "keywords": ["captcha", "captchaai", "recaptcha", "turnstile", "hcaptcha"],
  "license": "MIT"
}

Usage Examples

const { CaptchaAI } = require("captchaai-sdk");
const { TimeoutError, SubmitError } = require("captchaai-sdk/errors");

const client = new CaptchaAI("YOUR_API_KEY", {
  pollInterval: 5000,
  timeout: 120000,
});

// Check balance
const balance = await client.getBalance();
console.log(`Balance: $${balance.toFixed(2)}`);

// Solve reCAPTCHA v2
try {
  const token = await client.solveRecaptchaV2(
    "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    "https://example.com/login",
    { cookies: "session=abc123" }
  );
  console.log(`Token: ${token.substring(0, 40)}...`);
} catch (err) {
  if (err instanceof TimeoutError) {
    console.error("Solve timed out");
  } else if (err instanceof SubmitError) {
    console.error(`API error: ${err.code}`);
  }
}

// Solve Turnstile
const turnstileToken = await client.solveTurnstile(
  "0x4AAAAAAADnPIDROrmt1Wwj",
  "https://example.com/checkout"
);

// Solve image CAPTCHA
const fs = require("fs");
const imageBase64 = fs.readFileSync("captcha.png", "base64");
const text = await client.solveImage(imageBase64, { caseSensitive: true });
console.log(`Text: ${text}`);

Troubleshooting

Issue Cause Fix
SubmitError: ERROR_WRONG_USER_KEY Invalid API key Verify key from CaptchaAI dashboard
TimeoutError on every solve Timeout too short; network issues Increase timeout option; check connectivity
TypeError: fetch is not a function Node.js < 18 Upgrade Node.js to 18+ or install node-fetch
Wrong token format returned Used wrong method for CAPTCHA type Match method to provider: userrecaptcha for reCAPTCHA, turnstile for Cloudflare
SubmitError: ERROR_ZERO_BALANCE No funds Top up balance at CaptchaAI dashboard

FAQ

Should I publish this as an npm package?

For internal use, keep it as a local dependency. For distribution, add JSDoc types, tests, a README with examples, and publish to npm with npm publish. Consider TypeScript for the published version.

How do I add retry logic to the SDK?

Wrap _submit with a retry loop for ERROR_NO_SLOT_AVAILABLE. The SDK already handles polling retries; submission retries need explicit backoff (e.g., 5s, 10s, 15s delays).

Can I use this with TypeScript?

This JavaScript SDK works with TypeScript via JSDoc annotations. For a fully typed experience, see Type-Safe CaptchaAI Client with TypeScript Generics.

Next Steps

Build your own CaptchaAI Node.js SDK — get your API key and start with the client class above.

Related guides:

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.