Skip to content
ZeroCaptcha

Callback payload

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.

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.

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.

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.

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.

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.

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.