# 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.

- Source: https://zerocaptcha.io/guides/idempotency-keys-for-captcha-tasks
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

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_reused` in 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:

```python
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 sending
```

Starting 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

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

```sh
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](https://zerocaptcha.io/docs/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](https://zerocaptcha.io/guides/rate-limits-and-concurrency) for when retries happen, and
the [Cloudflare Turnstile solver](https://zerocaptcha.io/cloudflare-turnstile-solver) page for complete programs.

## Questions

### How long does ZeroCaptcha remember an idempotency key?

24 hours from the first createTask that used it. Within that time the same key and request return the first reply and the same task.

### Is the idempotency key a secret?

No. It only names one attempt to create one task. Anyone would still need your API key to use it.

### Should I reuse a key after a task fails?

No. Reuse a key only to retry exactly the same request whose reply you lost. A new task, including a retry after a failure, gets a new key.
