# SDKs

> The official Noptcha client libraries for Python and Node.js / TypeScript - solve a captcha in one call, typed errors, webhook verification.

Source: https://noptcha.com/docs/sdks/

Official client libraries for the [native API (v1)](https://noptcha.com/docs/native-api.md). Both are small layers over the HTTP API with **no runtime dependencies**, and both work with test keys, so every example on this page runs in the [sandbox](https://noptcha.com/docs/sandbox.md) unchanged.

|  | Python | Node.js / TypeScript |
| --- | --- | --- |
| Install | `pip install noptcha` | `npm install noptcha` |
| Requires | Python 3.11 or later | Node.js 22 or later; ES modules, standard web APIs only (`fetch`, Web Crypto) |
| Clients | `Noptcha` (blocking), `AsyncNoptcha` (asyncio) | `Noptcha` (promises) |
| Types | `TypedDict`s for every parameter, solution and response | TypeScript types; parameters and solutions typed by task type |

Both read the key from the `NOPTCHA_KEY` environment variable unless you pass one (surrounding whitespace, such as the newline at the end of a key file, is removed; a key with other invalid characters is rejected before any request). Keep the key on your server; never ship it to a browser. The API address must be `https://` (plain `http://` is only accepted for `localhost`, `127.0.0.1` and `::1`).

## Solve a captcha

`solve` creates the task with `Prefer: wait=60`, so most tasks come back solved from a single request; otherwise it long-polls `GET /v1/tasks/{id}` until the task is final, and returns the solved task or raises / rejects with `TaskFailedError`.

**Python**

```python
from noptcha import Noptcha, TaskFailedError

client = Noptcha()  # reads NOPTCHA_KEY; a nop_test_ key runs in the sandbox

try:
    task = client.solve(
        "recaptcha.v2",
        {
            "website_url": "https://www.google.com/recaptcha/api2/demo",
            "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        },
    )
    print(task["solution"])
except TaskFailedError as e:  # unsolvable, timeout, proxy_error... (not charged)
    print("failed:", e.code)
```


**Node.js**

```js
import { Noptcha, TaskFailedError } from 'noptcha';

const client = new Noptcha(); // reads NOPTCHA_KEY; a nop_test_ key runs in the sandbox

try {
  const task = await client.solve('recaptcha.v2', {
    website_url: "https://www.google.com/recaptcha/api2/demo",
    website_key: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  });
  console.log(task.solution);
} catch (err) {
  if (!(err instanceof TaskFailedError)) throw err;
  console.log('failed:', err.code); // unsolvable, timeout, proxy_error... (not charged)
}
```

What `solve` does for you:

-   **Idempotency.** Every create request carries an `Idempotency-Key` (a new UUID unless you pass your own), and a retry sends the same bytes, so a retry after a network error returns the same task instead of a second one. If the retry arrives while the first attempt is still being processed (`409 idempotency_conflict`), the client waits for it with back-off. An error thrown while creating carries the key that was sent (`e.idempotency_key` / `e.idempotencyKey`): call again with `idempotency_key=` / `idempotencyKey:` set to it and the same arguments to get the same task back. To be able to retry after your process crashes too, generate the key yourself and store it before the call.
-   **Retries.** Connection errors, `429` and `5xx` responses (including a proxy’s `502` or `504`) are retried (2 times by default), waiting `Retry-After` when the response has one.
-   **Waiting.** Long polls of 60 seconds (`poll_wait` / `pollWait`); when the server returns early because too many requests are waiting, the client pauses before asking again, as [Rate limits](https://noptcha.com/docs/limits.md) asks.
-   **Timeout and cancellation.** `solve` gives up after 5 minutes by default (`timeout` in seconds in Python, `timeoutMs` in Node.js), including the create request; once the task exists it throws `WaitTimeoutError` with the task ID, and the task itself goes on. Cancel with a `threading.Event` (`cancel=`) or an `AbortSignal` (`signal`): `solve` then throws `OperationCancelledError` with the task ID (or, if the task was not created yet, the idempotency key). With `AsyncNoptcha`, cancel the coroutine as usual.
-   **asyncio.** `AsyncNoptcha` runs its requests in its own pool of threads (`max_concurrency`, 64 by default), not in the event loop’s default executor, so long polls never hold up the rest of your application. Close it when you are done (`async with AsyncNoptcha() as client:` or `client.close()`).

The parameters of each captcha are on its [task type page](https://noptcha.com/docs/task-types.md). In TypeScript they are checked against the task type, and `task.solution` has the type’s solution type.

## Step by step

The steps `solve` combines are available on their own: create a task, wait for it (this returns a failed task instead of throwing), and [report](https://noptcha.com/docs/native-api/tasks.md) whether the answer worked.

**Python**

```python
from noptcha import Noptcha

client = Noptcha()
params = {
    "website_url": "https://www.google.com/recaptcha/api2/demo",
    "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
}

task = client.create_task("recaptcha.v2", params, metadata={"order": "A-17"})
print(task["id"], task["status"])  # usually "processing"

task = client.wait_for_task(task["id"], timeout=120)  # solved or failed
print(task["status"], task["solution"])
if task["status"] == "solved":
    # after you used the answer: did the site accept it?
    print(client.send_feedback(task["id"], correct=True))
```


**Node.js**

```js
import { Noptcha } from 'noptcha';

const client = new Noptcha();
const params = {
  website_url: "https://www.google.com/recaptcha/api2/demo",
  website_key: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
};

let task = await client.createTask('recaptcha.v2', params, { metadata: { order: 'A-17' } });
console.log(task.id, task.status); // usually "processing"

task = await client.waitForTask(task.id, { timeoutMs: 120_000 }); // solved or failed
console.log(task.status, task.solution);
if (task.status === 'solved') {
  // after you used the answer: did the site accept it?
  console.log(await client.sendFeedback(task.id, true));
}
```

`metadata` and `callback_url` (`callbackUrl` in Node.js) are passed through as in the [API](https://noptcha.com/docs/native-api/tasks.md). The clients also cover `GET /v1/balance`, `/v1/me`, `/v1/types` and `/v1/usage` (`get_balance`, `get_me`, `list_types`, `get_usage`; `getBalance`, `getMe`, `listTypes`, `getUsage` in Node.js), and the public `GET /v1/catalog` (`get_catalog`, `getCatalog`), which never sends the key: `Noptcha.public()` makes a client without a key for it. In Node.js `getCatalog` sends only `Accept`, so it also works in a browser without a CORS preflight.

## Errors

Each [error code](https://noptcha.com/docs/errors.md) has its own error class (`InvalidParamsError`, `InsufficientBalanceError`, `RateLimitError`, …), grouped by HTTP status (`PaymentRequiredError`, `TooManyRequestsError`, …), all derived from `NoptchaError`. API errors carry the status, the `code`, the `detail`, the request ID and `Retry-After`. Codes added to the API later map to the class of their HTTP status.

**Python**

```python
from noptcha import InvalidParamsError, Noptcha

client = Noptcha()

try:
    client.create_task("recaptcha.v2", {
        "website_url": "https://www.google.com/recaptcha/api2/demo",
    })  # website_key is missing
except InvalidParamsError as e:
    for err in e.errors:
        print(err["pointer"], err["message"])
    print("request id:", e.request_id)  # quote it when you contact support
```


**Node.js**

```js
import { InvalidParamsError, Noptcha } from 'noptcha';

const client = new Noptcha();

try {
  await client.createTask('recaptcha.v2', {
    website_url: "https://www.google.com/recaptcha/api2/demo",
  }); // website_key is missing
} catch (err) {
  if (!(err instanceof InvalidParamsError)) throw err;
  for (const e of err.errors) console.log(e.pointer, e.message);
  console.log('request id:', err.requestId); // quote it when you contact support
}
```

| Class | Raised when |
| --- | --- |
| `TaskFailedError` | `solve`: the task ended without a solution; `code` is the task’s `error.code` |
| `WaitTimeoutError` | `solve`, `wait_for_task` / `waitForTask`: the task was not final in time |
| `OperationCancelledError` | `solve`, `wait_for_task` / `waitForTask` (and `createTask` in Node.js) were cancelled |
| `APIConnectionError`, `APITimeoutError` | no response (network error, timeout) after the retries |
| `APIError` and its subclasses | the API answered with an error status |
| `WebhookVerificationError` | a webhook’s signature or timestamp is not valid |

## Webhooks

Both libraries verify [webhook](https://noptcha.com/docs/webhooks.md) signatures as described there: the raw body, any of several `v1,` signatures, and a timestamp at most 5 minutes from your clock. `verify(body, headers)` returns the parsed event; the one-off form takes the same order after the secret: `verify_webhook(secret, body, headers)` / `verifyWebhook(secret, body, headers)`.

**Python**

```python
import os

from noptcha import Webhook, WebhookVerificationError

webhook = Webhook(os.environ["NOPTCHA_WEBHOOK_SECRET"])  # whsec_...


def handle(headers, body):
    """headers: the request headers; body: the raw request body (bytes). Returns the status to answer."""
    try:
        event = webhook.verify(body, headers)
    except WebhookVerificationError:
        return 401
    if event["type"] == "task.solved":
        print("solved:", event["data"]["id"], event["data"]["solution"])
    return 204  # acknowledge every event, including types you do not handle
```


**Node.js**

```js
import { Webhook, WebhookVerificationError } from 'noptcha';

const webhook = new Webhook(process.env.NOPTCHA_WEBHOOK_SECRET); // whsec_...

// headers: the request headers; body: the raw request body (Buffer or string). Returns the status to answer.
export async function handle(headers, body) {
  let event;
  try {
    event = await webhook.verify(body, headers);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return 401;
    throw err;
  }
  if (event.type === 'task.solved') console.log('solved:', event.data.id, event.data.solution);
  return 204; // acknowledge every event, including types you do not handle
}
```

> **Note:** Pass the body exactly as received (bytes or the raw string), not a parsed and re-serialized object, or the signature will not match. In Express, for example, use `express.raw({ type: 'application/json' })` for this route.

## Versions

The libraries follow [Semantic Versioning](https://semver.org/). New task types, fields and error codes arrive in minor releases; the API stays backward compatible within `/v1` ([Versioning](https://noptcha.com/docs/versioning.md)). The compatibility API and the 2captcha legacy API are not covered: they exist for software that already speaks those protocols.
