API
Idempotency Keys: Retry Captcha Tasks Without Paying Twice
Use an Idempotency-Key header so a retried createTask never makes a second task: how keys work for 24 hours, the two key errors, and finding a lost task.
3 min readPublished Updated
Networks lose replies. When a createTask call times out, your code cannot tell whether the task
was created. Retrying blindly may make a second task, and if both are solved, you pay twice. An
idempotency key removes the doubt: you name the attempt, and the API promises that the same
name makes at most one task. This guide shows how ZeroCaptcha’s keys work and how to use them.
How it works
Send an Idempotency-Key header with createTask (compatible format), in.php (2Captcha format)
or POST /v1/tasks (REST). For 24 hours from the first request with that key:
- The same key with the same request returns the first reply, with the same task ID, instead of creating a new task.
- The same key with a different request is refused with
ERROR_IDEMPOTENCY_KEY_REUSED(idempotency_key_reusedin REST). A key names one task, not a slot you can reuse. - The same key while the first request is still being served is answered with
ERROR_IDEMPOTENCY_KEY_IN_USE. Wait a moment and send it again: the retry gets the first request’s reply.
A refused request never costs anything. The one task a key creates is charged only if it is solved.
Make the key before the first request
The key must exist before you send anything, so a crash between the request and the reply still leaves you knowing it. A good key includes the time and something random:
import datetime, uuid
intent_key = f"{datetime.datetime.now(datetime.timezone.utc):%Y-%m-%dT%H:%M:%SZ}-{uuid.uuid4()}"print(f"intent key: {intent_key}", flush=True) # log it before sendingStarting with the time lets you tell later whether the API may have forgotten the key: after 24 hours, a retry with it could create a new task.
Retry with the same key
import os, time, requests
API, KEY = os.environ["ZEROCAPTCHA_API"], os.environ["ZEROCAPTCHA_KEY"]RETRY = {"ERROR_RATE_LIMIT", "ERROR_SERVICE_UNAVAILABLE", "ERROR_NO_SLOT_AVAILABLE", "ERROR_IDEMPOTENCY_KEY_IN_USE"}
def create_task(task: dict, intent_key: str) -> str: delay = 1 for _ in range(6): try: reply = requests.post(f"{API}/createTask", timeout=15, headers={"Idempotency-Key": intent_key}, json={"clientKey": KEY, "task": task}) except requests.RequestException: reply = None # lost: retry with the same key if reply is not None and reply.status_code < 500 and reply.status_code != 429: body = reply.json() if body.get("errorId") == 0: return body["taskId"] code = body.get("errorCode", f"HTTP {reply.status_code}") if code not in RETRY: raise RuntimeError(code) time.sleep(delay) delay = min(delay * 2, 16) raise RuntimeError(f"createTask did not answer; retry later with key {intent_key}")Every retry sends the same key. Whether the first attempt got through or not, you end up with
exactly one task. When the API sends a Retry-After header, wait that long instead of the
doubling pause; the quickstart samples do both.
Finding a task whose reply you lost
If your process died before it saw the task ID, look the task up by its key instead of creating it again. The REST API lists your tasks filtered by key:
curl "$ZEROCAPTCHA_API/v1/tasks?idempotencyKey=2026-09-30T10:00:00Z-4f6d0c1e-…" \ -H "Authorization: Bearer $ZEROCAPTCHA_KEY"The list holds the task that key created, with its id and status, or nothing if the first
request never arrived. Then keep polling that task instead of starting another.
When to start a new key
- After a task fails or expires: that task is over; the next attempt is a new task.
- After you have used the token: the next form needs a new task.
- When the key is close to 24 hours old: look the task up first, as above.
Reusing a key for a new task is the one misuse the API refuses outright, with
ERROR_IDEMPOTENCY_KEY_REUSED, precisely so that a bug cannot silently hand you an old result.
The SDKs do this for you
The JavaScript, Python and Go SDKs send an Idempotency-Key with every task they
create and retry 429, 502, 503 and 504 with the same key. If you use one of them, you already
have this behavior.
See Rate limits and concurrency for when retries happen, and the Cloudflare Turnstile solver page for complete programs.