Tutorials 9 min read

Node.js CAPTCHA Solving: Complete Integration Guide

Integrate CAPTCHA solving into Node.js applications using fetch or axios. Covers async/await patterns, TypeScript types, and a reusable client module.

Node.js CAPTCHA Solving: Complete Integration Guide

A Node.js CAPTCHA solver integration lets you handle reCAPTCHA, hCaptcha, Turnstile, and other challenges from any JavaScript or TypeScript backend. This guide builds a reusable client module with full TypeScript types, async/await polling, and error handling — ready to drop into Express APIs, scraping scripts, or serverless functions.

For the language-agnostic overview of the API, see the CAPTCHA Solver API Integration Tutorial.

Setup

No external HTTP libraries are required. Node.js 18+ includes a built-in fetch API. If you prefer axios, the pattern is nearly identical.

Set your API key as an environment variable:

export UCAPTCHA_KEY="ucap_your_key_here"

Install TypeScript types if you are using TypeScript:

npm install -D typescript @types/node

TypeScript Interfaces

Define the request and response shapes so your IDE can autocomplete and your compiler can catch mistakes:

// ucaptcha.types.ts

export interface CaptchaTask {
  type: string;
  websiteURL?: string;
  websiteKey?: string;
  body?: string; // base64 image for ImageToTextTask
  [key: string]: unknown; // allow provider-specific fields
}

export interface CreateTaskRequest {
  clientKey: string;
  task: CaptchaTask;
  callbackUrl?: string;
}

export interface CreateTaskResponse {
  errorId: number;
  errorCode?: string;
  errorDescription?: string;
  taskId?: string;
}

export interface GetTaskResultRequest {
  clientKey: string;
  taskId: string;
}

export interface TaskSolution {
  gRecaptchaResponse?: string;
  token?: string;
  text?: string;
  [key: string]: unknown;
}

export interface GetTaskResultResponse {
  errorId: number;
  errorCode?: string;
  errorDescription?: string;
  status: "processing" | "ready";
  solution?: TaskSolution;
}

export interface BalanceResponse {
  errorId: number;
  balance: number;
}

These interfaces cover the core API surface. The TaskSolution type uses an index signature because different CAPTCHA types return different solution fields.

The Client Module

Here is the complete ucaptcha.ts module:

// ucaptcha.ts

import type {
  CaptchaTask,
  CreateTaskResponse,
  GetTaskResultResponse,
  TaskSolution,
  BalanceResponse,
} from "./ucaptcha.types";

const API_URL = "https://api.ucaptcha.net";
const RETRYABLE_ERRORS = new Set(["ERROR_NO_SLOT_AVAILABLE"]);

export class UCaptchaError extends Error {
  constructor(
    public code: string,
    public description: string = ""
  ) {
    super(`${code}: ${description}`);
    this.name = "UCaptchaError";
  }
}

interface ClientOptions {
  clientKey?: string;
  maxRetries?: number;
  pollInterval?: number;
  timeout?: number;
}

async function postJSON<T>(
  endpoint: string,
  body: Record<string, unknown>,
  maxRetries: number
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const resp = await fetch(`${API_URL}/${endpoint}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(15_000),
      });

      if (!resp.ok) {
        throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
      }

      const data = (await resp.json()) as T & {
        errorId?: number;
        errorCode?: string;
        errorDescription?: string;
      };

      if (data.errorId && data.errorId !== 0) {
        const code = data.errorCode ?? "UNKNOWN";
        if (RETRYABLE_ERRORS.has(code)) {
          await sleep(3000 * (attempt + 1));
          continue;
        }
        throw new UCaptchaError(code, data.errorDescription ?? "");
      }

      return data;
    } catch (err) {
      lastError = err as Error;
      if (err instanceof UCaptchaError) throw err;
      if (attempt < maxRetries - 1) {
        await sleep(2000 * (attempt + 1));
      }
    }
  }

  throw lastError ?? new Error("Max retries exceeded");
}

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

export async function createTask(
  task: CaptchaTask,
  options: ClientOptions = {}
): Promise<string> {
  const clientKey = options.clientKey ?? process.env.UCAPTCHA_KEY;
  if (!clientKey) throw new Error("UCAPTCHA_KEY not set");

  const data = await postJSON<CreateTaskResponse>(
    "createTask",
    { clientKey, task },
    options.maxRetries ?? 3
  );

  if (!data.taskId) throw new Error("No taskId in response");
  return data.taskId;
}

export async function getTaskResult(
  taskId: string,
  options: ClientOptions = {}
): Promise<TaskSolution> {
  const clientKey = options.clientKey ?? process.env.UCAPTCHA_KEY;
  if (!clientKey) throw new Error("UCAPTCHA_KEY not set");

  const timeout = options.timeout ?? 120_000;
  const interval = options.pollInterval ?? 4000;
  const deadline = Date.now() + timeout;
  const maxRetries = options.maxRetries ?? 3;

  await sleep(5000); // initial wait

  while (Date.now() < deadline) {
    const data = await postJSON<GetTaskResultResponse>(
      "getTaskResult",
      { clientKey, taskId },
      maxRetries
    );

    if (data.status === "ready") {
      if (!data.solution) throw new Error("Status ready but no solution");
      return data.solution;
    }

    await sleep(interval);
  }

  throw new Error(`Task ${taskId} timed out after ${timeout}ms`);
}

export async function solve(
  task: CaptchaTask,
  options: ClientOptions = {}
): Promise<TaskSolution> {
  const taskId = await createTask(task, options);
  return getTaskResult(taskId, options);
}

export async function getBalance(
  options: ClientOptions = {}
): Promise<number> {
  const clientKey = options.clientKey ?? process.env.UCAPTCHA_KEY;
  if (!clientKey) throw new Error("UCAPTCHA_KEY not set");

  const data = await postJSON<BalanceResponse>(
    "getBalance",
    { clientKey },
    options.maxRetries ?? 3
  );

  return data.balance;
}

The module exports four functions: createTask, getTaskResult, solve (combines both), and getBalance. All functions accept an optional ClientOptions object, but default to reading the API key from process.env.UCAPTCHA_KEY.

Solving an hCaptcha Challenge

Here is a complete example solving an hCaptcha challenge:

import { solve } from "./ucaptcha";

async function main() {
  const solution = await solve({
    type: "HardCaptchaTaskProxyless",
    websiteURL: "https://example.com/signup",
    websiteKey: "a5f74b19-9e45-40e0-b45d-47ff91b7a6c2",
  });

  console.log("hCaptcha token:", solution.gRecaptchaResponse);

  // Use the token in a form submission
  const resp = await fetch("https://example.com/signup", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      email: "user@example.com",
      "h-captcha-response": solution.gRecaptchaResponse,
    }),
  });

  console.log("Signup response:", resp.status);
}

main().catch(console.error);

Error Handling

The postJSON helper retries network errors and ERROR_NO_SLOT_AVAILABLE automatically. Your calling code handles the rest:

import { solve, UCaptchaError } from "./ucaptcha";

try {
  const solution = await solve({
    type: "RecaptchaV2TaskProxyless",
    websiteURL: "https://example.com",
    websiteKey: "SITE_KEY",
  });
  // use solution
} catch (err) {
  if (err instanceof UCaptchaError) {
    switch (err.code) {
      case "ERROR_ZERO_BALANCE":
        console.error("Balance empty. Top up at ucaptcha.net");
        break;
      case "ERROR_CAPTCHA_UNSOLVABLE":
        console.error("CAPTCHA unsolvable. Retrying...");
        // retry logic
        break;
      default:
        console.error(`API error: ${err.code} - ${err.description}`);
    }
  } else {
    console.error("Network or timeout error:", err);
  }
}

Concurrent Solving

Node.js excels at concurrent I/O. Solve multiple CAPTCHAs in parallel with Promise.all:

import { solve } from "./ucaptcha";

const tasks = [
  { type: "RecaptchaV2TaskProxyless", websiteURL: "https://site-a.com", websiteKey: "KEY_A" },
  { type: "HardCaptchaTaskProxyless", websiteURL: "https://site-b.com", websiteKey: "KEY_B" },
  { type: "TurnstileTaskProxyless", websiteURL: "https://site-c.com", websiteKey: "KEY_C" },
];

const solutions = await Promise.all(tasks.map((task) => solve(task)));
console.log("All solved:", solutions.length);

For rate-limited scenarios, use Promise.allSettled and process results individually so one failure does not reject the batch.

Using with Express

A common pattern is wrapping the solver in an Express route so internal services can request CAPTCHA solutions over HTTP:

import express from "express";
import { solve } from "./ucaptcha";

const app = express();
app.use(express.json());

app.post("/solve", async (req, res) => {
  try {
    const solution = await solve(req.body.task);
    res.json({ success: true, solution });
  } catch (err) {
    const message = err instanceof Error ? err.message : "Unknown error";
    res.status(500).json({ success: false, error: message });
  }
});

app.listen(3001, () => console.log("CAPTCHA service on :3001"));

For a more robust version of this pattern with queuing, callbacks, and caching, see Building a CAPTCHA Solving Microservice.

Using with the Legacy 2Captcha Format

If you are migrating from a 2Captcha Node.js library, you can point it directly at api.ucaptcha.net. The legacy /in.php and /res.php endpoints are fully supported:

// With any 2captcha-compatible library
const solver = new TwoCaptcha.Solver("ucap_your_key_here");
solver.apiUrl = "https://api.ucaptcha.net";
// All existing calls work without changes

This makes migration a two-line change: swap the API key and base URL.

Summary

The ucaptcha.ts module gives you a typed, async-first client for solving CAPTCHAs from any Node.js or TypeScript project. The solve() function handles task creation and polling in a single await, while createTask() and getTaskResult() give you finer control when you need it.

uCaptcha routes your requests across CapSolver, 2Captcha, AntiCaptcha, CapMonster, and Multibot based on your routing configuration. The client code does not change when you switch routing strategies — configure it once in the dashboard at ucaptcha.net and the API handles the rest.

Related Articles