# Captcha Solver Callbacks: pingback and callbackUrl, No Polling

> Get Cloudflare Turnstile results pushed to your server instead of polling: callbackUrl and pingback, what each call contains, retries, and how to answer.

- Source: https://zerocaptcha.io/guides/captcha-solver-callbacks
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

Polling works, but it makes your code ask the same question every second or two. A callback turns
that around: you name a URL when you create the task, and ZeroCaptcha posts the result there as
soon as the task ends. This guide shows how to name a callback in each API format, what arrives,
how retries work, and how to answer.

## Name a callback

| Format | Where the URL goes |
| --- | --- |
| Compatible `createTask` | `callbackUrl`, beside `task` |
| REST `POST /v1/tasks` | `callbackUrl` in the body |
| 2Captcha `in.php` | `pingback` |

```json
{
  "clientKey": "zc_live_…",
  "callbackUrl": "https://example.com/zerocaptcha/callback",
  "task": {
    "type": "TurnstileTaskProxyless",
    "websiteURL": "https://shop.example.com/login",
    "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",
    "metadata": { "action": "login", "cdata": "session-7f3a9c2e" }
  }
}
```

The task itself is unchanged by the callback: send the widget's action and cData in `metadata` as
always, copied from its `data-action` and `data-cdata` (or the `action` and `cData` options of
`turnstile.render()`), and leave out any it does not set.

The URL must be `http` or `https`, at most 2,048 characters, without a username or password, and
point at a public host. A task with any other URL is refused when you create it, and nothing is
held. There is nothing to register first.

## What arrives

When the task ends, solved, failed or expired, ZeroCaptcha sends a `POST` to your URL in the
format you created the task in:

- **Compatible:** the JSON `getTaskResult` would return, with `status: "ready"` and the token, or
  `errorId` 1 and the error code.
- **REST:** the task as `GET /v1/tasks/{id}` shows it, token included.
- **2Captcha:** a form body, `id=<task id>&code=<token>`, or the error code in `code`, such as
  `ERROR_CAPTCHA_UNSOLVABLE`.

Every call carries two headers:

- `ZeroCaptcha-Signature: t=<unix seconds>,v1=<hex>`, an HMAC-SHA256 signature of the body.
- `ZeroCaptcha-Delivery`, the call's ID, the same on every retry of that call.

Check the signature before you trust the body: anyone can post to a public URL.
[Verify a webhook's HMAC signature](https://zerocaptcha.io/guides/verify-webhook-hmac-signature) shows how in Node,
Python and Go.

## Answer quickly, work later

Answer with any 2xx status as soon as you have stored the call. The body of your answer is
ignored. If you do slow work before answering, such as submitting the form the token is for, you
risk the 10-second limit, and the call is then retried.

A minimal receiver in Node:

```js
import { createServer } from "node:http";

createServer((request, response) => {
  const chunks = [];
  request.on("data", (chunk) => chunks.push(chunk));
  request.on("end", () => {
    const raw = Buffer.concat(chunks);
    if (!isGenuine(process.env.ZEROCAPTCHA_CALLBACK_SECRET, request.headers["zerocaptcha-signature"], raw)) {
      response.writeHead(401).end();
      return;
    }
    queue.push(JSON.parse(raw)); // handle it after answering
    response.writeHead(204).end();
  });
}).listen(8080);
```

`isGenuine` is the signature check from the signature guide, and `queue` is whatever your app
uses for background work.

## Retries

A call that gets a status other than 2xx, or no answer within 10 seconds, is retried after 30
seconds, then after waits that double each time, up to 32 minutes before the last, each lengthened
by up to half at random: eight attempts over roughly 65 to 95 minutes. Redirects are not followed. After the last attempt, ZeroCaptcha stops; the task and its
result stay in your task log and in the API.

Because a call can arrive more than once, for example when your answer was lost on the way back,
make your handler idempotent: record the `ZeroCaptcha-Delivery` value or the task ID, and skip
repeats.

## Callbacks and token lifetime

A Turnstile token works once, for 300 seconds. A callback delivers it the moment the task ends,
which saves the delay of a polling interval. If your receiver hands the token to a worker queue,
make sure the queue is fast: a token that waits five minutes is useless.
[Cloudflare Turnstile token expiry](https://zerocaptcha.io/guides/cloudflare-turnstile-token-expiry) explains the budget.

## Polling still works

A callback is an addition, not a replacement. The task stays readable with `getTaskResult`,
`res.php` or `GET /v1/tasks/{id}`, so a missed call never loses a result. A common pattern is to
rely on callbacks and poll once as a fallback for tasks that have not reported after a minute.

## Private addresses are never called

ZeroCaptcha resolves your callback's host name each time it calls, and calls only if every address
it resolves to is public. A name that resolves to a private, loopback or link-local address is
never called. For local development, expose your receiver through a public tunnel instead.

The full reference is in [Callbacks](https://zerocaptcha.io/docs/callbacks). For the solving flow itself, see the
[Cloudflare Turnstile solver](https://zerocaptcha.io/cloudflare-turnstile-solver) page.

## Questions

### Do I have to register my callback URL first?

No. Name any public http or https URL of up to 2,048 characters when you create the task. Nothing needs registering in advance.

### What happens if my server is down when the callback is sent?

The call is retried after 30 seconds, then with waits that double each time, up to 32 minutes: eight attempts over roughly 65 to 95 minutes. The result also stays readable by polling.

### Can a callback arrive twice?

Yes, for example when your answer was lost. Use the ZeroCaptcha-Delivery header or the task ID to handle each task once.
