Node.js SDK
Official CaptchaSonic library for Node.js and TypeScript — modern, Promise-based, multi-transport (gRPC / ConnectRPC / HTTP).
The CaptchaSonic Node.js SDK (captchasonic) is a modern, Promise-based, TypeScript-first library for solving CAPTCHAs at scale. Every method returns a Promise, ships full type definitions, and runs over three interchangeable transports — native gRPC (fastest, Node.js only), ConnectRPC (works in Node.js and browsers), and plain HTTP/JSON. Transient errors are retried automatically with exponential backoff.
It works in Node.js ≥ 18 and modern browsers, and is compatible with Express, Fastify, Next.js, React, Vue, and Vite.
Installation
npm install captchasonic
# or
yarn add captchasonic
# or
pnpm add captchasonic
Requirements
| Requirement | Notes |
|---|---|
| Node.js | ≥ 18.0.0 (required for the grpc transport) |
| Module format | ES Module — the package ships "type": "module" |
| Browser | Any modern browser with fetch; use the connect or http transport |
The package is published as ESM only. In an ESM project ("type": "module" in your package.json, or a .mjs file) import it directly:
import { CaptchaSonic } from "captchasonic";
From a CommonJS file, load it with a dynamic import():
// CommonJS (.cjs / "type": "commonjs")
const { CaptchaSonic } = await import("captchasonic");
TIP
If you are on TypeScript, set "module": "NodeNext" (or "ESNext") and "moduleResolution": "NodeNext" in tsconfig.json so the bundled type definitions resolve correctly.
Authentication
Create an API key in the CaptchaSonic dashboard and pass it as the first argument to the constructor:
import { CaptchaSonic } from "captchasonic";
const solver = new CaptchaSonic("YOUR_API_KEY");
Never hard-code keys in source control. Read the key from the environment instead:
import { CaptchaSonic } from "captchasonic";
const solver = new CaptchaSonic(process.env.CAPTCHASONIC_API_KEY);
Quick Start
Each captcha type has a dedicated solve* helper that submits the task and resolves with the result. There is no separate polling step for image tasks — the SDK handles it for you.
solveRecaptchaV2TokentokenreCAPTCHA v2 (token) — Solve a reCAPTCHA v2 token challenge via browser automation (async — polls until ready).
solveRecaptchaV2Token(…)returnssolution.tokenimport { CaptchaSonic } from "captchasonic";
const solver = new CaptchaSonic(process.env.CAPTCHASONIC_API_KEY);
const result = await solver.solveRecaptchaV2Token({
websiteURL: "https://example.com/login",
websiteKey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
});
console.log(result.solution.gRecaptchaResponse);
// 03AGdBq25SxXT... ← drop into the g-recaptcha-response fieldfrom captchasonic import CaptchaSonic
solver = CaptchaSonic("YOUR_API_KEY")
result = solver.solve_recaptcha_v2_token(
website_url="https://example.com/login",
website_key="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
)
print(result["token"])
# 03AGdBq25SxXT... ← drop this into the g-recaptcha-response fieldReturns: image methods resolve a result whose answer lives under
typedSolution(e.g.typedSolution.grid.objects, the selected tile indices); token methods return the token in thesolutionmap.
TIP
Token-based methods (Turnstile, Cloudflare, reCAPTCHA v2/v3 token, popular-captcha token) submit a browser-automation task and poll internally until a token is ready — by default up to 120 seconds.
Supported CAPTCHA Types
All image methods take a single typed options object. Images may be Uint8Array, Node Buffer, or a string file path (file paths are Node.js only).
reCAPTCHA v2 (image)
await solver.solveRecaptchaV2({
images: tiles, // typically 9 tiles for a 3×3 grid
question: "traffic lights", // plain text, or a Google class code like "/m/015qff"
});
// → result.typedSolution.grid.objects (number[] — selected tile indices)
PopularCaptcha (hCaptcha-style image)
await solver.solvePopularCaptcha({
images: tiles, // 1–64 tiles
question: "Click each image with a cat",
questionType: "objectClassify", // "objectClassify" | "grid" | "objectClick" | "objectDrag"
examples, // optional reference images for objectClick
websiteURL: "https://example.com",
});
// objectClassify/grid → typedSolution.grid.objects
// objectClick → typedSolution.click
// objectDrag → typedSolution.drag
GeeTest
// nine-grid (question + images required)
await solver.solveGeetest({ type: "nine", question: "Select all bicycles", images: tiles });
// click / icon (question + images required)
await solver.solveGeetest({ type: "click", question: "the bear", images: tiles });
// slide puzzle (piece + background)
await solver.solveGeetest({ type: "slide", images: [piece], examples: [background] });
// swap puzzles
await solver.solveGeetest({ type: "match" });
await solver.solveGeetest({ type: "winlinze" });
Returns: grid/click types land in typedSolution.grid.objects (or typedSolution.click); slide returns typedSolution.slide.x.
Accepted type aliases:
| Canonical | Aliases |
|---|---|
| nine-grid | "nine", "geetest_nine", "9" |
| click / icon | "click", "geetest_click", "icon" |
| slide | "slide", "geetest_slide" |
| match | "match", "geetest_match" |
| winlinze | "winlinze", "geetest_winlinze" |
OCR / image-to-text
await solver.solveOcr({ images: [img] }); // general OCR
await solver.solveOcr({ images: [img], module: "mtcaptcha", maxLength: 4 });
await solver.solveOcr({ images: imgs, module: "bls", numeric: true, maxLength: 3 });
// → result.typedSolution.text.texts[0]
| Option | Type | Notes |
|---|---|---|
module | "common" | "mtcaptcha" | "bls" | "morocco" | Default "common" |
numeric | boolean | Digits only (auto-set for "bls") |
caseSensitive | boolean | Preserve letter case |
minLength / maxLength | number | Length bounds |
TikTok
await solver.solveTikTok({ type: "click", question: "Select the shape", images });
await solver.solveTikTok({ type: "whirl", question: "Rotate to match", images, examples }); // examples required
await solver.solveTikTok({ type: "slide", question: "Slide to fit", images, examples }); // examples required
Returns: click lands in typedSolution.click; whirl/slide return typedSolution.slide.x.
type accepts "click"/"tiktok_click", "whirl"/"tiktok_whirl", "slide"/"tiktok_slide".
Binance
await solver.solveBinance({ type: "grid", question: "Select the bicycle", images });
await solver.solveBinance({ type: "slide", images: [puzzle], examples: [background] });
Returns: grid lands in typedSolution.grid.objects; slide returns typedSolution.slide.x.
type accepts "grid"/"binance_grid" and "slide"/"binance_slide".
AWS WAF
solveAwsWaf takes positional arguments. The question is formatted as "type:category:target".
await solver.solveAwsWaf(tiles, "grid:vehicles:cars");
Returns: selected tile indices in typedSolution.grid.objects.
Slide image (local, no AI)
Detects the slide offset locally using contour detection. Accepts one combined image, or [background, piece].
const r = await solver.solveSlideImage({ images: ["slide_bg.png", "piece.png"] });
console.log(r.typedSolution?.slide?.x); // pixel offset, e.g. 142
Returns: the slide offset in pixels at typedSolution.slide.x.
Token methods (browser automation)
These submit a task and poll internally until a token is returned (up to the polling timeout, 120s by default).
solveTurnstiletokenCloudflare Turnstile — Solve a Cloudflare Turnstile token challenge (async — polls until ready).
solveTurnstile(…)returnssolution.tokenawait solver.solveTurnstile({ websiteURL, websiteKey, proxy }); // proxy optional
await solver.solveRecaptchaV2Token({ websiteURL, websiteKey, proxy }); // proxy optional
await solver.solveRecaptchaV3Token({ websiteURL, websiteKey, proxy }); // proxy optional
await solver.solvePopularCaptchaToken({ websiteURL, websiteKey, proxy, metadata }); // proxy optional
await solver.solveCloudflare({ websiteURL, websiteKey, proxy }); // proxy REQUIREDsolver.solve_turnstile(website_url=..., website_key=..., proxy=...) # proxy optional
solver.solve_recaptcha_v2_token(website_url=..., website_key=..., proxy=...) # proxy optional
solver.solve_recaptcha_v3_token(website_url=..., website_key=..., proxy=...) # proxy optional
solver.solve_popular_captcha_token(website_url=..., website_key=..., proxy=...) # proxy optional
solver.solve_cloudflare(website_url=..., website_key=..., proxy=...) # proxy REQUIREDReturns: the token in the solution map of the response (for example result.solution.token or result.solution.gRecaptchaResponse, depending on the captcha).
TypeScript Usage
The SDK exports types for every option object and response. Import them with import type.
import { CaptchaSonic } from "captchasonic";
import type { SolveGeetestOptions, GetTaskResultResponse } from "captchasonic";
const solver = new CaptchaSonic(process.env.CAPTCHASONIC_API_KEY!);
const opts: SolveGeetestOptions = {
type: "nine",
question: "Select all bicycles",
images: tiles,
};
const result = await solver.solveGeetest(opts);
console.log(result.typedSolution?.grid?.objects);
Exported types
import type {
CaptchaSonicOptions,
ImageInput,
SolvePopularCaptchaOptions,
SolveRecaptchaV2Options,
SolveGeetestOptions,
SolveOcrOptions,
SolveTikTokOptions,
SolveBinanceOptions,
SolveTurnstileOptions,
SolvePopularCaptchaTokenOptions,
SolveRecaptchaV2TokenOptions,
SolveRecaptchaV3TokenOptions,
SolveCloudflareOptions,
SolveSlideImageOptions,
GeetestSubtype,
TikTokSubtype,
BinanceSubtype,
Task,
CreateTaskResponse,
GetTaskResultResponse,
} from "captchasonic";
ImageInput is Uint8Array | Buffer | string.
Proxy Support
Token / browser-automation methods accept an optional proxy string in the form http://user:pass@host:port:
await solver.solveTurnstile({
websiteURL: "https://example.com",
websiteKey: "0x4AAAAAAA...",
proxy: "http://user:[email protected]:8080",
});
WARNING
solveCloudflare always requires a proxy — the proxy field is mandatory for that method. Other token methods run proxyless when proxy is omitted.
For enterprise hCaptcha you can also pass metadata (rqdata, rqtoken, fingerprint) to solvePopularCaptchaToken.
Configuration
Pass an options object as the second constructor argument. Only the options below are supported.
const solver = new CaptchaSonic("YOUR_API_KEY", {
transport: "connect", // "grpc" (default) | "connect" | "http"
timeout: 180_000, // max poll wait in ms (alias: timeoutMs); per-call default 30000
pollingInterval: 5_000, // polling frequency in ms (default 2000)
baseUrl: "https://api.captchasonic.com", // override endpoint (alias: url)
});
| Option | Type | Default | Description |
|---|---|---|---|
transport | "grpc" | "connect" | "http" | "grpc" | Wire protocol (see table below) |
timeout / timeoutMs | number | 30000 per call | Request timeout; timeout also caps polling wait |
pollingInterval | number | 2000 | How often token tasks are polled |
url / baseUrl | string | per-transport | Override the API endpoint |
Transports
| Transport | Environments | Protocol |
|---|---|---|
grpc | Node.js only | gRPC binary over HTTP/2 — lowest latency, sends images as raw binary |
connect | Node.js and browsers | ConnectRPC over fetch |
http | Node.js and browsers | Plain REST/JSON over fetch |
TIP
In the browser use connect (recommended) or http. Native gRPC requires Node.js. With connect/http, images are auto-encoded to base64 before sending; with grpc they are sent as raw binary with zero overhead.
Error Handling
All API-level failures throw a SonicError, which extends the built-in Error. It carries a numeric errorId and sets name to the matching error name.
import { CaptchaSonic, SonicError } from "captchasonic";
try {
const result = await solver.solveGeetest({ type: "nine", question: "bicycles", images });
} catch (err) {
if (err instanceof SonicError) {
console.error(err.errorId); // 1–6
console.error(err.name); // e.g. "InvalidApiKeyError"
console.error(err.message);
} else {
throw err; // network / unexpected
}
}
| id | Python exception | Node errorName | Meaning | Action |
|---|---|---|---|---|
0 | SonicError | SonicError | Base class for every SDK error — catches anything below. | Inspect `error.errorId` (Node) / `err.error_id` (Python) and branch. |
1 | InvalidApiKeyError | InvalidApiKeyError | API key is missing, malformed, or revoked. | Verify the key in your dashboard. |
2 | InsufficientBalanceError | InsufficientBalanceError | Account balance can't cover the task. | Add funds. |
3 | DailyLimitExceededError | DailyLimitExceededError | Daily quota exhausted. | Wait for the daily reset or upgrade your plan. |
4 | MinuteLimitExceededError | MinuteLimitExceededError | Per-minute rate limit hit. | Slow down requests / add exponential backoff. |
5 | QuotaExceededError | QuotaExceededError | Plan quota exhausted. | Upgrade your plan. |
6 | PlanExpiredError | PlanExpiredError | Subscription has expired. | Renew your subscription. |
Every concrete row inherits from SonicError — catch it once to handle all SDK failures, or branch on errorId / exception subclass for granular control. Transient gRPC errors are retried automatically with exponential backoff (up to 3 attempts).
TIP
Transient gRPC errors are retried automatically with exponential backoff (up to 3 attempts), so you usually only need to handle the SonicError cases above.
Account Helpers
const balance = await solver.getBalance(); // → number (USD)
const health = await solver.healthCheck(); // → { healthy: boolean, version: string }
Low-level task methods are available if you need direct control over submission and polling:
const created = await solver.createTask(task); // submit a Partial<Task>
const result = await solver.getTaskResult(taskId); // poll for a result
Troubleshooting
require is not defined / Cannot use import statement outside a module — the package is ESM only. Use import in an ESM project, or await import("captchasonic") from CommonJS. See Installation.
grpc transport fails in the browser — native gRPC is Node.js only. Switch to transport: "connect" or transport: "http".
InvalidApiKeyError on every call — confirm the key is passed as the first constructor argument and not accidentally undefined (e.g. a missing env var).
Token method times out — token tasks poll up to timeout ms (default 120000). Increase timeout and verify the websiteURL / websiteKey match the target page. For Cloudflare, a valid proxy is required.
File-path images don't load — string file paths are read synchronously and are Node.js only; in the browser pass a Uint8Array instead.