Skip to content

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

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.

Read next

This guide is part of the Cloudflare Turnstile solver hub. Every task is charged only when a token is ready.

Get an API key