Node.js SDK
More
The official ZeroCaptcha client for JavaScript and TypeScript: create a Cloudflare Turnstile task or a Cloudflare challenge page’s task, wait for its result, read your balance, and check a task callback’s signature. It has no dependencies and runs in Node.js 20 and later, Deno, Bun and browsers (keep your key on a server, never in a page).
Every task is real and paid from your prepaid balance, and only a task that succeeds is charged.
Install
Section titled “Install”npm install @zerocaptcha/sdkThe package is not on npm yet. Until it is, call the API with fetch, as the
quickstart’s Node program does: it makes the same calls.
Give the client your API key (zc_live_…, from the dashboard’s API keys page) and the API’s
address. Keep both in your environment rather than in your code.
import { TaskFailedError, ZeroCaptcha } from "@zerocaptcha/sdk";
const client = new ZeroCaptcha({ apiKey: process.env.ZEROCAPTCHA_KEY!, baseUrl: process.env.ZEROCAPTCHA_API!,});
// Create a task and wait for its token: one call.try { const token = await client.solve({ websiteURL: "https://shop.example.com/login", // the page with the widget websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", // its data-sitekey // The widget's data-action and data-cdata, or the action and cData options of // turnstile.render(). Leave out any the widget does not set. action: "login", cdata: "session-7f3a9c2e", // proxy: "http://user:pass@proxy.example.net:8080", // to solve through your own proxy // callbackUrl: "https://hooks.example.com/zerocaptcha", // to be called when it ends }); console.log(token);} catch (error) { if (error instanceof TaskFailedError) console.log(error.code); // such as ERROR_CAPTCHA_UNSOLVABLE else throw error;}
// Or step by step.const task = await client.createTask( { websiteURL: "https://shop.example.com/login", websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", action: "login", // the widget's data-action, if it sets one cdata: "session-7f3a9c2e", // the widget's data-cdata, if it sets one }, // Your ID for this task, sent as the Idempotency-Key; one is made for you when you give none. { idempotencyKey: "login-2026-10-01-0001" },);const done = await client.waitForResult(task.id, { timeoutMs: 120_000 });console.log(done.solution?.token, done.cost);
// Your balance, in US dollars.const { available } = await client.getBalance();| Method | What it does |
|---|---|
new ZeroCaptcha({ apiKey, baseUrl, timeoutMs?, fetch? }) |
A client; timeoutMs bounds each request (30 seconds by default), and fetch swaps in your own, such as one with a proxy agent. |
createTask(task, { idempotencyKey?, signal? }) |
Creates a task: websiteURL, websiteKey, and optionally type, action, cdata, proxy, callbackUrl. |
getTask(id) |
Reads a task; its token is in solution while it is available. |
waitForResult(id, { timeoutMs?, intervalMs?, signal? }) |
Polls every 2 seconds, for up to 3 minutes by default, until the task ends. |
solve(task, options?) |
createTask then waitForResult: the token. |
createChallengeTask({ websiteURL, proxy, callbackUrl? }) |
Creates a Cloudflare challenge page’s task, through your proxy. |
solveChallenge({ websiteURL, proxy }, options?) |
createChallengeTask then waitForResult: { cfClearance, userAgent, tokenExpiresAt }. |
getBalance() |
{ available, held, currency }, as decimal strings. |
verifySignature(secret, header, rawBody, { toleranceSeconds?, now? }) |
Whether a callback is genuine. |
- A task with
proxy(such ashttp://user:pass@proxy.example.net:8080) is solved through your proxy, asTurnstileTask; without one it isTurnstileTaskProxyless. createTasksends anIdempotency-Keywith every call, one of its own unless you give yours, so retrying it never makes a second task.- A request the API asks you to slow down (429) or cannot serve for a moment (502, 503, 504) is
tried again after the wait it asks for, three times in all, as is one that got no answer or an
answer cut short, with the same
Idempotency-Key. Any other refusal throws aZeroCaptchaErrorwith the API’scode, such asinsufficient_funds, and itsrequestId. waitForResultnever runs pasttimeoutMs: a slow read is cut off, and a retry that would wait longer than the time left is not made. It throwsTaskFailedErrorwhen the task fails or expires, and nothing is charged; a wait that runs out throwsWaitTimeoutError, with the task as last read (task, undefined if no read finished in time), and you can wait again.
Cloudflare challenge pages
Section titled “Cloudflare challenge pages”A challenge page is passed through your proxy, and gives the cf_clearance
cookie with the user agent it is bound to. Send both, through the same proxy:
const { cfClearance, userAgent } = await client.solveChallenge({ websiteURL: "https://shop.example.com/", proxy: process.env.PROXY_URL!, // such as http://user:pass@proxy.example.net:8080});Callbacks
Section titled “Callbacks”A task that names callbackUrl is POSTed to it once it ends, with the task as JSON. Check each
call’s signature against the raw body, before you parse it:
import { verifySignature } from "@zerocaptcha/sdk";
export async function handle(request: Request): Promise<Response> { const rawBody = await request.text(); const genuine = await verifySignature( process.env.ZEROCAPTCHA_CALLBACK_SECRET!, request.headers.get("zerocaptcha-signature"), rawBody, ); if (!genuine) return new Response(null, { status: 401 }); const task = JSON.parse(rawBody); console.log(task.id, task.status); return new Response(null, { status: 204 });}A call older than five minutes does not verify, so a recorded call cannot be replayed. See Polling and callbacks.