Callback payload
More
When a task that named a callback URL ends, we POST its result to that URL, once it is final: succeeded, failed or expired. This page is the reference for that request. For how to name a URL and check a call, see Polling and callbacks.
The request
Section titled “The request”| Part | Value |
|---|---|
| Method | POST |
| URL | The task’s callbackUrl, or pingback in the 2Captcha format |
Content-Type |
application/json for REST and createTask tasks; application/x-www-form-urlencoded for 2Captcha tasks |
User-Agent |
ZeroCaptcha-Callbacks/1.0 |
ZeroCaptcha-Signature |
t=<unix seconds>,v1=<64 hex digits> |
ZeroCaptcha-Delivery |
The delivery’s ID, a UUID, the same on every attempt |
The body is read from the task when each attempt is made, in the format the task was created in.
The signature
Section titled “The signature”v1 is the HMAC-SHA256, in lowercase hex, of the timestamp t, a full stop, and the raw body, keyed
with your account’s callback secret (zcsig_…):
v1 = hex(HMAC-SHA256(key = callback secret, message = t + "." + body))t is when the attempt was made, so it changes on each retry, and so does v1. Accept a call only
if v1 matches in a constant-time comparison and t is within five minutes of your clock. Code in
four languages is in Polling and callbacks.
REST tasks
Section titled “REST tasks”The task as GET /v1/tasks/{id} shows it, with solution while its token is valid:
{ "id": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "type": "TurnstileTaskProxyless", "kind": "turnstile", "status": "succeeded", "websiteURL": "https://shop.example.com/login", "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5", "action": null, "cdata": null, "usesProxy": false, "price": "0.000800", "held": "0.000000", "cost": "0.000800", "attempts": 1, "maxAttempts": 3, "errorCode": null, "errorDescription": null, "solution": { "token": "0.Zm9vYmFy…", "userAgent": null, "cookie": null }, "tokenState": "available", "tokenIssuedAt": "2026-09-30T14:02:14Z", "tokenExpiresAt": "2026-09-30T14:07:14Z", "idempotencyKey": null, "createdAt": "2026-09-30T14:02:05Z", "startedAt": "2026-09-30T14:02:06Z", "finishedAt": "2026-09-30T14:02:14Z", "deadline": "2026-09-30T14:04:35Z", "updatedAt": "2026-09-30T14:02:14Z", "version": 3}A failed or expired task has status failed or expired, its errorCode and errorDescription,
cost 0.000000 and no solution. A challenge page’s solution has userAgent and cookie too;
see Cloudflare WAF and 5-second challenges. Every field is in the
API reference.
createTask tasks
Section titled “createTask tasks”The reply getTaskResult would give at that moment:
{ "errorId": 0, "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "status": "ready", "solution": { "token": "0.Zm9vYmFy…", "type": "turnstile" }, "cost": "0.000800", "createTime": 1790776925, "endTime": 1790776934, "solveCount": 1, "expiresAt": "2026-09-30T14:07:14Z"}A task that did not succeed sends errorId: 1 with errorCode, errorDescription, taskId,
"status": "failed" and "cost": "0.000000". See createTask format.
2Captcha tasks
Section titled “2Captcha tasks”A form with the task’s ID and its token:
id=0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b&code=0.Zm9vYmFy...or, when the task was not solved, the code in place of the token, such as
code=ERROR_CAPTCHA_UNSOLVABLE. See 2Captcha format.
Deliveries and retries
Section titled “Deliveries and retries”| Success | Any 2xx status. The body of your answer is ignored. |
| Time limit | 10 seconds per attempt |
| Retries | After 30 seconds, then doubling (1, 2, 4, 8, 16 and 32 minutes), each wait lengthened by up to half at random |
| Attempts | 8 in all, over roughly 65 to 95 minutes; then the delivery stops |
| Redirects | Not followed: a 3xx is a failed attempt |
| Addresses | The URL’s name is resolved on every attempt; a private, loopback or link-local address is never called, and the delivery stops at once |
A delivery can arrive more than once: use ZeroCaptcha-Delivery or the task ID to handle each task
once. Because each attempt reads the task afresh, a retry made after a Turnstile token expired
carries the task without its token; read failed deliveries’ tasks from the API instead.