# Node.js SDK

> The official ZeroCaptcha client for JavaScript and TypeScript: solve Cloudflare Turnstile and WAF challenge pages, handle errors and check callbacks.

Source: https://zerocaptcha.io/docs/sdks/node

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

```sh
npm install @zerocaptcha/sdk
```

The package is not on npm yet. Until it is, call the API with `fetch`, as the
[quickstart](https://zerocaptcha.io/docs/quickstart)'s Node program does: it makes the same calls.

## Use

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.

```ts
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 as `http://user:pass@proxy.example.net:8080`) is solved through your
  proxy, as `TurnstileTask`; without one it is `TurnstileTaskProxyless`.
- `createTask` sends an `Idempotency-Key` with 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 a
  `ZeroCaptchaError` with the API's `code`, such as `insufficient_funds`, and its `requestId`.
- `waitForResult` never runs past `timeoutMs`: a slow read is cut off, and a retry that would wait
  longer than the time left is not made. It throws `TaskFailedError` when the task fails or
  expires, and nothing is charged; a wait that runs out throws `WaitTimeoutError`, with the task as
  last read (`task`, undefined if no read finished in time), and you can wait again.

## Cloudflare challenge pages

A [challenge page](https://zerocaptcha.io/docs/challenges) 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:

```ts
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

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:

```ts
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](https://zerocaptcha.io/docs/callbacks).
