# v1: tasks

> Create tasks with the Noptcha v1 API - synchronous waiting with Prefer, idempotency keys, metadata, long polling and feedback.

Source: https://noptcha.com/docs/native-api/tasks/

## Create a task

`POST /v1/tasks` with `type` (a [task type](https://noptcha.com/docs/task-types.md)), `params` (that type’s parameters) and optional `metadata`:

**curl**

```bash
export NOPTCHA_KEY=nop_test_...  # your test or live key
IDEMPOTENCY_KEY=$(uuidgen)  # one key per task you want; a retry of the same request reuses it
curl -s -X POST "https://api.noptcha.com/v1/tasks" \
  -H "Authorization: Bearer $NOPTCHA_KEY" \
  -H 'Prefer: wait=60' \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<EOF
{
  "type": "recaptcha.v2",
  "params": {
    "website_url": "https://www.google.com/recaptcha/api2/demo",
    "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
  },
  "metadata": {
    "order": "A-17"
  }
}
EOF
```


**Python**

```python
import os
import uuid

import requests

API = "https://api.noptcha.com"
KEY = os.environ["NOPTCHA_KEY"]
idempotency_key = str(uuid.uuid4())  # one key per task you want; a retry of the same request reuses it

r = requests.post(
    f"{API}/v1/tasks",
    headers={
        "Authorization": f"Bearer {KEY}",
        "Prefer": "wait=60",
        "Idempotency-Key": idempotency_key,
    },
    json={
        "type": "recaptcha.v2",
        "params": {
            "website_url": "https://www.google.com/recaptcha/api2/demo",
            "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
        },
        "metadata": {
            "order": "A-17",
        },
    },
    timeout=100,
)
print(r.status_code, r.json())
```


**Node.js**

```js
import { randomUUID } from 'node:crypto';

const API = 'https://api.noptcha.com';
const KEY = process.env.NOPTCHA_KEY;
const idempotencyKey = randomUUID(); // one key per task you want; a retry of the same request reuses it

const res = await fetch(`${API}/v1/tasks`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${KEY}`,
    Prefer: "wait=60",
    "Idempotency-Key": idempotencyKey,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "recaptcha.v2",
    params: {
      website_url: "https://www.google.com/recaptcha/api2/demo",
      website_key: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    },
    metadata: {
      order: "A-17",
    },
  }),
});
console.log(res.status, await res.json());
```

| Response | Meaning |
| --- | --- |
| `200` | The task is final: `status` is `solved` or `failed` |
| `202` | The task is still `processing`: poll it |
| `4xx`, `503` | The request was rejected; no task was created and nothing was charged ([Errors](https://noptcha.com/docs/errors.md)) |

Both 200 and 202 carry `Location: /v1/tasks/{id}`. A task that cannot be solved is a successful request: 200 with `status: "failed"`, an `error` and zero cost.

Set `callback_url` to also receive the final result as a [webhook](https://noptcha.com/docs/webhooks.md) (`task.solved` or `task.failed`).

## Synchronous waiting

Add `Prefer: wait=N` (or `?wait=N`) to wait up to N seconds, at most **90**, for the result in the same request:

-   the task finishes in time: `200` and the final task;
-   otherwise: `202` and the task in `processing`; continue with `GET /v1/tasks/{id}`.

When the wait was applied the response has `Preference-Applied: wait=N`. If your organization already has too many requests waiting, the request returns `202` at once without that header. Types that are solved synchronously return the result immediately in any case.

## Get a task and long polling

`GET /v1/tasks/{id}` returns the task. With `?wait=N` (at most 90) the request waits until the task is final or N seconds pass, so you do not need a tight polling loop:

**curl**

```bash
export NOPTCHA_KEY=nop_test_...  # your test or live key
TASK_ID=7259140378796032  # the task ID you got when creating the task
curl -s "https://api.noptcha.com/v1/tasks/$TASK_ID?wait=30" \
  -H "Authorization: Bearer $NOPTCHA_KEY"
```


**Python**

```python
import os

import requests

API = "https://api.noptcha.com"
KEY = os.environ["NOPTCHA_KEY"]
task_id = "7259140378796032"  # the task ID you got when creating the task

r = requests.get(
    f"{API}/v1/tasks/{task_id}",
    params={
        "wait": "30",
    },
    headers={
        "Authorization": f"Bearer {KEY}",
    },
    timeout=100,
)
print(r.status_code, r.json())
```


**Node.js**

```js
const API = 'https://api.noptcha.com';
const KEY = process.env.NOPTCHA_KEY;
const taskId = '7259140378796032'; // the task ID you got when creating the task

const res = await fetch(`${API}/v1/tasks/${taskId}?wait=30`, {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${KEY}`,
  },
});
console.log(res.status, await res.json());
```

Polling one task is limited to 5 requests per second (bursts of 10); a long poll counts once. If a long poll returns `processing` immediately (the waiting slots are full), wait a second before the next poll instead of retrying at once.

## Idempotency

Send an `Idempotency-Key` header (1–64 visible ASCII characters, for example a UUID) with `POST /v1/tasks`. Use one new key for each task you want, and the same key for every retry of that request. If the request fails on the network and you retry it **with the same key and a byte-for-byte identical body**, you get the same task back instead of a second, separately charged task. Replayed responses carry `Idempotent-Replayed: true`.

The body is compared as raw bytes, not as JSON: a body with the same fields in a different order, with different whitespace, or with `0.7` written as `0.70` counts as a different body. Serialize the body once and resend those exact bytes; do not rebuild it for each attempt:

**curl**

```bash
export NOPTCHA_KEY=nop_test_...  # your test or live key
IDEMPOTENCY_KEY=$(uuidgen)  # one key per task you want; a retry of the same request reuses it
# --retry resends the same bytes with the same key after a timeout or a 408, 429 or 5xx response
curl -s --retry 3 -X POST "https://api.noptcha.com/v1/tasks" \
  -H "Authorization: Bearer $NOPTCHA_KEY" \
  -H 'Prefer: wait=60' \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<EOF
{
  "type": "recaptcha.v2",
  "params": {
    "website_url": "https://www.google.com/recaptcha/api2/demo",
    "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
  },
  "metadata": {
    "order": "A-17"
  }
}
EOF
```


**Python**

```python
import json
import os
import time
import uuid

import requests

API = "https://api.noptcha.com"
KEY = os.environ["NOPTCHA_KEY"]

# Serialize the body once: every attempt must send the same key and the same bytes,
# or the retry is 409 idempotency_conflict.
idempotency_key = str(uuid.uuid4())  # one key per task you want; a retry of the same request reuses it
body = json.dumps({
    "type": "recaptcha.v2",
    "params": {
        "website_url": "https://www.google.com/recaptcha/api2/demo",
        "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    },
    "metadata": {
        "order": "A-17",
    },
})

for attempt in range(4):
    try:
        r = requests.post(
            f"{API}/v1/tasks",
            headers={
                "Authorization": f"Bearer {KEY}",
                "Prefer": "wait=60",
                "Idempotency-Key": idempotency_key,
                "Content-Type": "application/json",
            },
            data=body,
            timeout=100,
        )
        break
    except (requests.ConnectionError, requests.Timeout):
        if attempt == 3:
            raise
        time.sleep(2**attempt)  # then the same request again
print(r.status_code, r.headers.get("Idempotent-Replayed"), r.json())
```


**Node.js**

```js
import { randomUUID } from 'node:crypto';

const API = 'https://api.noptcha.com';
const KEY = process.env.NOPTCHA_KEY;

// Serialize the body once: every attempt must send the same key and the same bytes,
// or the retry is 409 idempotency_conflict.
const idempotencyKey = randomUUID(); // one key per task you want; a retry of the same request reuses it
const body = JSON.stringify({
  type: "recaptcha.v2",
  params: {
    website_url: "https://www.google.com/recaptcha/api2/demo",
    website_key: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
  },
  metadata: {
    order: "A-17",
  },
});

let res;
for (let attempt = 0; ; attempt++) {
  try {
    res = await fetch(`${API}/v1/tasks`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${KEY}`,
        Prefer: "wait=60",
        "Idempotency-Key": idempotencyKey,
        "Content-Type": "application/json",
      },
      body,
      signal: AbortSignal.timeout(100_000),
    });
    break;
  } catch (err) {
    if (attempt === 3) throw err;
    await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); // then the same request again
  }
}
console.log(res.status, res.headers.get('idempotent-replayed'), await res.json());
```

-   Keys are kept for 24 hours per organization.
-   The same key with a different body (any difference in the bytes) is `409 idempotency_conflict`; so is a retry while the first request with that key is still being admitted (retry shortly).
-   If the first request did not create a task (insufficient balance, no capacity, invalid parameters…), the key is released and you can use it again.
-   After the task’s result has expired (10 minutes after completion), a replay is `404 not_found`; the task is not created again. Use a new key.

## Metadata

`metadata` is your own string key-value pairs, returned with the task: at most 16 keys; keys 1–64 characters, values at most 256 characters. A value that is not a string is `422 invalid_params` with pointer `/metadata/<key>`.

## Proxies

Pass `proxy` in `params` for types that accept one; see [Proxies](https://noptcha.com/docs/proxies.md).

**curl**

```bash
export NOPTCHA_KEY=nop_test_...  # your test or live key
curl -s -X POST "https://api.noptcha.com/v1/tasks" \
  -H "Authorization: Bearer $NOPTCHA_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<EOF
{
  "type": "recaptcha.v2",
  "params": {
    "website_url": "https://example.com/login",
    "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
    "proxy": {
      "type": "http",
      "host": "203.0.113.7",
      "port": 8080,
      "login": "proxyuser",
      "password": "proxypass"
    }
  }
}
EOF
```


**Python**

```python
import os

import requests

API = "https://api.noptcha.com"
KEY = os.environ["NOPTCHA_KEY"]

r = requests.post(
    f"{API}/v1/tasks",
    headers={
        "Authorization": f"Bearer {KEY}",
    },
    json={
        "type": "recaptcha.v2",
        "params": {
            "website_url": "https://example.com/login",
            "website_key": "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
            "proxy": {
                "type": "http",
                "host": "203.0.113.7",
                "port": 8080,
                "login": "proxyuser",
                "password": "proxypass",
            },
        },
    },
    timeout=30,
)
print(r.status_code, r.json())
```


**Node.js**

```js
const API = 'https://api.noptcha.com';
const KEY = process.env.NOPTCHA_KEY;

const res = await fetch(`${API}/v1/tasks`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "recaptcha.v2",
    params: {
      website_url: "https://example.com/login",
      website_key: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
      proxy: {
        type: "http",
        host: "203.0.113.7",
        port: 8080,
        login: "proxyuser",
        password: "proxypass",
      },
    },
  }),
});
console.log(res.status, await res.json());
```

## Feedback

After using a solution, report whether it worked:

**curl**

```bash
export NOPTCHA_KEY=nop_test_...  # your test or live key
TASK_ID=7259140378796032  # the task ID you got when creating the task
curl -s -X POST "https://api.noptcha.com/v1/tasks/$TASK_ID/feedback" \
  -H "Authorization: Bearer $NOPTCHA_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<EOF
{
  "correct": false,
  "reason": "token rejected by the site"
}
EOF
```


**Python**

```python
import os

import requests

API = "https://api.noptcha.com"
KEY = os.environ["NOPTCHA_KEY"]
task_id = "7259140378796032"  # the task ID you got when creating the task

r = requests.post(
    f"{API}/v1/tasks/{task_id}/feedback",
    headers={
        "Authorization": f"Bearer {KEY}",
    },
    json={
        "correct": False,
        "reason": "token rejected by the site",
    },
    timeout=30,
)
print(r.status_code, r.json())
```


**Node.js**

```js
const API = 'https://api.noptcha.com';
const KEY = process.env.NOPTCHA_KEY;
const taskId = '7259140378796032'; // the task ID you got when creating the task

const res = await fetch(`${API}/v1/tasks/${taskId}/feedback`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    correct: false,
    reason: "token rejected by the site",
  }),
});
console.log(res.status, await res.json());
```

`correct` is required; `reason` is optional and is not stored. Feedback is accepted for solved tasks within 10 minutes after completion and returns `{"id": "…", "accepted": true}`; only the first report per task is recorded, later ones still succeed. A task that is still processing or failed returns `409 task_not_solved`. Feedback is recorded as a quality signal; it does **not** refund the task.

## Complete example

**curl**

```bash
export NOPTCHA_KEY=nop_test_...  # your test or live key

# waits up to 60 s; 200 = finished (solved or failed), 202 = still processing
IDEMPOTENCY_KEY=$(uuidgen)  # one key per task you want; a retry of the same request reuses it
curl -s -X POST "https://api.noptcha.com/v1/tasks" \
  -H "Authorization: Bearer $NOPTCHA_KEY" \
  -H 'Prefer: wait=60' \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H 'Content-Type: application/json' \
  -d @- <<EOF
{
  "type": "turnstile",
  "params": {
    "website_url": "https://example.com/signup",
    "website_key": "1x00000000000000000000AA",
    "action": "signup"
  }
}
EOF

# on 202: long-poll with the "id" from the response
TASK_ID=...
curl -s "https://api.noptcha.com/v1/tasks/$TASK_ID?wait=60" \
  -H "Authorization: Bearer $NOPTCHA_KEY"
```


**Python**

```python
import os
import time
import uuid

import requests

API = "https://api.noptcha.com"
HEADERS = {"Authorization": f"Bearer {os.environ['NOPTCHA_KEY']}"}  # a nop_test_ key runs in the sandbox

# One Idempotency-Key per task you want. If you retry this request after a network error, send the same key and
# the same body bytes: you get the same task back, not a second one (see Idempotency on the Tasks page).
idempotency_key = str(uuid.uuid4())
r = requests.post(
    f"{API}/v1/tasks",
    headers={**HEADERS, "Prefer": "wait=60", "Idempotency-Key": idempotency_key},
    json={
        "type": "turnstile",
        "params": {
            "website_url": "https://example.com/signup",
            "website_key": "1x00000000000000000000AA",
            "action": "signup",
        },
    },
    timeout=100,
)
task = r.json()
if r.status_code >= 400:
    raise RuntimeError(f"{task['code']}: {task.get('detail', '')}")
while task["status"] == "processing":  # 202: not finished within the wait
    time.sleep(1)  # a long poll can return early (for example when wait slots are full)
    r = requests.get(f"{API}/v1/tasks/{task['id']}", params={"wait": 60}, headers=HEADERS, timeout=100)
    task = r.json()
    if r.status_code >= 400:
        raise RuntimeError(f"{task['code']}: {task.get('detail', '')}")

if task["status"] == "solved":
    print(task["solution"])
else:
    print("failed:", task["error"]["code"], task["error"]["message"])
```


**Node.js**

```js
import { randomUUID } from 'node:crypto';

const API = 'https://api.noptcha.com';
const HEADERS = { Authorization: `Bearer ${process.env.NOPTCHA_KEY}` }; // a nop_test_ key runs in the sandbox

async function v1(path, init = {}) {
  const res = await fetch(`${API}${path}`, { ...init, headers: { ...HEADERS, ...init.headers } });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.code}: ${data.detail ?? ''}`);
  return data;
}

// One Idempotency-Key per task you want. If you retry this request after a network error, send the same key and
// the same body bytes: you get the same task back, not a second one (see Idempotency on the Tasks page).
const idempotencyKey = randomUUID();
let task = await v1('/v1/tasks', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Prefer: 'wait=60', 'Idempotency-Key': idempotencyKey },
  body: JSON.stringify({
    type: "turnstile",
    params: {
      website_url: "https://example.com/signup",
      website_key: "1x00000000000000000000AA",
      action: "signup",
    },
  }),
});
while (task.status === 'processing') {
  await new Promise((r) => setTimeout(r, 1000)); // a long poll can return early (for example when wait slots are full)
  task = await v1(`/v1/tasks/${task.id}?wait=60`); // long poll
}
if (task.status === 'solved') console.log(task.solution);
else console.log('failed:', task.error.code, task.error.message);
```
