v1: tasks
Create a task
Section titled “Create a task”POST /v1/tasks with type (a task type), params (that type’s parameters) and optional
metadata:
export NOPTCHA_KEY=nop_test_... # your test or live keyIDEMPOTENCY_KEY=$(uuidgen) # one key per task you want; a retry of the same request reuses itcurl -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" }}EOFimport osimport 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())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) |
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 (task.solved or task.failed).
Synchronous waiting
Section titled “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:
200and the final task; - otherwise:
202and the task inprocessing; continue withGET /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
Section titled “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:
export NOPTCHA_KEY=nop_test_... # your test or live keyTASK_ID=7259140378796032 # the task ID you got when creating the taskcurl -s "https://api.noptcha.com/v1/tasks/$TASK_ID?wait=30" \ -H "Authorization: Bearer $NOPTCHA_KEY"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())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
Section titled “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:
export NOPTCHA_KEY=nop_test_... # your test or live keyIDEMPOTENCY_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 responsecurl -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" }}EOFimport jsonimport osimport timeimport 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 itbody = 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 againprint(r.status_code, r.headers.get("Idempotent-Replayed"), r.json())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 itconst 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
Section titled “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
Section titled “Proxies”Pass proxy in params for types that accept one; see Proxies.
export NOPTCHA_KEY=nop_test_... # your test or live keycurl -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" } }}EOFimport 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())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
Section titled “Feedback”After using a solution, report whether it worked:
export NOPTCHA_KEY=nop_test_... # your test or live keyTASK_ID=7259140378796032 # the task ID you got when creating the taskcurl -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"}EOFimport 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())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
Section titled “Complete example”export NOPTCHA_KEY=nop_test_... # your test or live key
# waits up to 60 s; 200 = finished (solved or failed), 202 = still processingIDEMPOTENCY_KEY=$(uuidgen) # one key per task you want; a retry of the same request reuses itcurl -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 responseTASK_ID=...curl -s "https://api.noptcha.com/v1/tasks/$TASK_ID?wait=60" \ -H "Authorization: Bearer $NOPTCHA_KEY"import osimport timeimport 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"])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);