API
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.
3 min readPublished Updated
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 |
{ "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
getTaskResultwould return, withstatus: "ready"and the token, orerrorId1 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 incode, such asERROR_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 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:
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 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. For the solving flow itself, see the Cloudflare Turnstile solver page.