# createTask format

> ZeroCaptcha's createTask, getTaskResult and getBalance, the JSON format other CAPTCHA APIs use: every field and spelling, reply, code and sample.

Source: https://zerocaptcha.io/docs/createtask

ZeroCaptcha speaks the `createTask` format that CapSolver, Anti-Captcha and 2Captcha's JSON API
use, so a client written for one of them solves Turnstile and Cloudflare challenge pages here after
two changes: its base URL, to our API's address, and its key, to your ZeroCaptcha key. A task made
this way is priced, held and charged exactly as one made with REST.

ZeroCaptcha is not affiliated with CapSolver, Anti-Captcha or 2Captcha. Their names appear here only
to say which format this is.

## The calls

| Call | Body | Reply |
| --- | --- | --- |
| `POST /createTask` | `{"clientKey": "…", "task": {…}}`, and optionally `callbackUrl` | `{"errorId": 0, "taskId": "…"}` |
| `POST /getTaskResult` | `{"clientKey": "…", "taskId": "…"}` | `processing`, then `ready` with the solution |
| `POST /getBalance` | `{"clientKey": "…"}` | `{"errorId": 0, "balance": 12.3456}` |
| `POST /reportIncorrect`, `POST /reportCorrect` | `{"clientKey": "…", "taskId": "…"}` | `{"errorId": 0, "status": "success"}` |
| `POST /reportIncorrectRecaptcha`, `POST /reportCorrectRecaptcha` | The same, as Anti-Captcha's clients send it | The same |
| `POST /feedbackTask` | `{"clientKey": "…", "taskId": "…", "result": {"invalid": true}}`, as CapSolver's clients send it | The same |

- **Every reply is HTTP 200.** Success is `errorId: 0`; a failure is `errorId: 1` with `errorCode`
  and `errorDescription`. Only a failure outside the format, such as a body over 64 KiB, a server
  timeout or the service shedding load, answers with an HTTP error and a
  [problem document](https://zerocaptcha.io/docs/reference/errors), so check the status too.
- **The body is read leniently,** as these clients send it: JSON whatever the `Content-Type` (some
  send `text/plain`), a number wherever text is expected, `null` for an absent field, and fields it
  does not use are ignored.
- **`Idempotency-Key`** works on `createTask` as on REST: send the same header again within 24 hours
  and you get the first task back instead of a second one.
- **`clientKey`** is your API key, `zc_live_…`, 41 characters.

## Cloudflare Turnstile task

| Field | Required | Other spellings | What it is |
| --- | --- | --- | --- |
| `type` | Yes | | `TurnstileTaskProxyless`, or `TurnstileTask` through your proxy; CapSolver's `AntiTurnstileTaskProxyLess` too, and `AntiTurnstileTask` for the proxy variant. Case does not matter. |
| `websiteURL` | Yes | `websiteUrl` | The page with the widget. |
| `websiteKey` | Yes | | The widget's site key. |
| `action` | When the widget sets one | `pageAction` (CapMonster Cloud's), `metadata.action` | The widget's action: its `data-action`, or the `action` option of `turnstile.render()`. |
| `cdata` | When the widget sets one | `cData`, `data`, `turnstileCData`, `metadata.cdata`, `metadata.cData` | The widget's cData: its `data-cdata`, or the `cData` option of `turnstile.render()`. |
| `proxy` | With `TurnstileTask` | the `proxyAddress` fields | Your proxy as a URL: `http://user:pass@proxy.example.net:8080`. |
| `proxyType`, `proxyAddress`, `proxyPort`, `proxyLogin`, `proxyPassword` | Instead of `proxy` | | Your proxy in parts: `proxyType` `http` (the default) or `https`, a public host, a port (a number or a string of digits), and an optional login and password of at most 255 bytes each. |
| `cloudflareTaskType` | No | | CapMonster Cloud's mode: `token`, or absent, for the widget's token. Its `cf_clearance` and `wait_room` modes are refused with `ERROR_TASK_NOT_SUPPORTED`, before anything is made or charged: for a challenge page's clearance, use the [challenge page task](#challenge-page-task). |

Most createTask clients send the action and cData nested in the task's `metadata`, as
`"metadata": {"action": "login", "cdata": "session-7f3a9c2e"}`, and the samples below do too.
Many sites check both when they verify the token, so send them whenever the widget sets them:
see [Cloudflare Turnstile action and cData](https://zerocaptcha.io/docs/action-and-cdata).

Where a field has more than one spelling, the first one sent wins, in the order listed. The limits
are REST's: see [Solving Cloudflare Turnstile](https://zerocaptcha.io/docs/cloudflare-turnstile#the-tasks-fields). SOCKS proxies are not
supported yet.

## Challenge page task

| Field | Required | What it is |
| --- | --- | --- |
| `type` | Yes | `CloudflareChallengeTask`, or CapSolver's `AntiCloudflareTask`. |
| `websiteURL` or `websiteUrl` | Yes | The page behind the challenge. |
| `proxy`, or the `proxyAddress` fields | Yes | Your proxy: the clearance works only from its address. |

A `websiteKey`, `action` or `cdata` is ignored, as are fields other providers take for a challenge
page, such as `userAgent` and `html`. A proxyless challenge task, such as
`CloudflareChallengeTaskProxyless` or `AntiCloudflareTaskProxyLess`, is refused with
`ERROR_TASK_NOT_SUPPORTED`. See [Cloudflare WAF and 5-second challenges](https://zerocaptcha.io/docs/challenges).

## Replies of getTaskResult

While the task runs:

```json
{ "errorId": 0, "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "status": "processing" }
```

Solved, a Turnstile task:

```json
{
  "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"
}
```

Solved, a challenge page: `solution` has `"type": "cloudflare"`, `userAgent`, and
`"cookies": {"cf_clearance": "…"}` beside `token`, as CapSolver's `AntiCloudflareTask` answers.

Failed, or not solved in time (nothing is charged):

```json
{
  "errorId": 1,
  "errorCode": "ERROR_CAPTCHA_UNSOLVABLE",
  "errorDescription": "Every attempt to solve the challenge failed. Nothing was charged.",
  "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b",
  "status": "failed",
  "cost": "0.000000"
}
```

Solved, but read after its token expired: `errorId: 1`, `ERROR_TOKEN_EXPIRED`, `"status": "ready"`
and the `cost` it was charged. Use tokens as soon as they are ready.

| Field | Meaning |
| --- | --- |
| `status` | `processing` until the task ends, then `ready`; `failed` on an error reply for a task that did not succeed |
| `solution.token` | The Turnstile token, or the `cf_clearance` cookie's value |
| `expiresAt` | When the token stops being accepted, in UTC |
| `cost` | What the task cost, in US dollars with six decimals |
| `createTime`, `endTime` | When it was created and solved, in Unix seconds |
| `solveCount` | The solve attempts it took |

## Samples

**curl**

```sh
# metadata holds the widget's data-action and data-cdata, or the action and cData options of
# turnstile.render(): leave out any the widget does not set. For your own proxy, make the type
# TurnstileTask and add "proxy": "http://user:pass@proxy.example.net:8080" to the task; to be
# called when it ends, add "callbackUrl": "https://hooks.example.com/zerocaptcha" beside it.
reply=$(curl -sS "$ZEROCAPTCHA_API/createTask" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"task\": {\"type\": \"TurnstileTaskProxyless\",
       \"websiteURL\": \"https://example.com/login\", \"websiteKey\": \"0x4AAAAAAAB1cD2eF3gH4iJ5\",
       \"metadata\": {\"action\": \"login\", \"cdata\": \"session-7f3a9c2e\"}}}")
# errorId 1 is a refusal; its errorCode and errorDescription say why.
if [ "$(jq -r .errorId <<<"$reply")" != 0 ]; then echo "$reply" >&2; exit 1; fi
TASK_ID=$(jq -r .taskId <<<"$reply")

# Every 2 seconds, until "status" is ready, or errorId 1 says the task failed:
curl "$ZEROCAPTCHA_API/getTaskResult" -H "Content-Type: application/json" \
  -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"taskId\": \"$TASK_ID\"}"
```

**Node**

```js
const api = process.env.ZEROCAPTCHA_API;
const clientKey = process.env.ZEROCAPTCHA_KEY;
const post = async (path, body, headers = {}) => {
  const response = await fetch(`${api}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json", ...headers },
    body: JSON.stringify({ clientKey, ...body }),
  });
  if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`);
  const reply = await response.json();
  if (reply.errorId !== 0) throw new Error(`${reply.errorCode}: ${reply.errorDescription}`);
  return reply;
};

const { taskId } = await post(
  "/createTask",
  {
    task: {
      type: "TurnstileTaskProxyless", // or "TurnstileTask", with proxy below
      websiteURL: "https://example.com/login", // the page with the widget
      websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5", // its data-sitekey
      // The widget's data-action and data-cdata, or the action and cData options of
      // turnstile.render(). Leave out any the widget does not set.
      metadata: { action: "login", cdata: "session-7f3a9c2e" },
      // proxy: "http://user:pass@proxy.example.net:8080", // TurnstileTask only
    },
    // callbackUrl: "https://hooks.example.com/zerocaptcha", // to be called when it ends
  },
  { "Idempotency-Key": crypto.randomUUID() },
);
let result;
do {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  result = await post("/getTaskResult", { taskId });
} while (result.status === "processing");
console.log(result.solution.token);
```

**Python**

```python
import os
import time
import uuid

import requests

API = os.environ["ZEROCAPTCHA_API"]
KEY = os.environ["ZEROCAPTCHA_KEY"]

def post(path, body, headers=None):
    response = requests.post(f"{API}{path}", json={"clientKey": KEY, **body}, headers=headers, timeout=15)
    response.raise_for_status()
    reply = response.json()
    if reply["errorId"] != 0:
        raise RuntimeError(f"{reply['errorCode']}: {reply['errorDescription']}")
    return reply

task_id = post(
    "/createTask",
    {
        "task": {
            "type": "TurnstileTaskProxyless",  # or "TurnstileTask", with proxy below
            "websiteURL": "https://example.com/login",  # the page with the widget
            "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",  # its data-sitekey
            # The widget's data-action and data-cdata, or the action and cData options of
            # turnstile.render(). Leave out any the widget does not set.
            "metadata": {"action": "login", "cdata": "session-7f3a9c2e"},
            # "proxy": "http://user:pass@proxy.example.net:8080",  # TurnstileTask only
        },
        # "callbackUrl": "https://hooks.example.com/zerocaptcha",  # to be called when it ends
    },
    headers={"Idempotency-Key": str(uuid.uuid4())},
)["taskId"]
while True:
    time.sleep(2)
    result = post("/getTaskResult", {"taskId": task_id})
    if result["status"] != "processing":
        break
print(result["solution"]["token"])
```

**Go**

```go
package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

type reply struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
	Status           string `json:"status"`
	Solution         struct {
		Token string `json:"token"`
	} `json:"solution"`
}

func post(path string, body map[string]any, idempotencyKey string) (reply, error) {
	var r reply
	body["clientKey"] = os.Getenv("ZEROCAPTCHA_KEY")
	payload, _ := json.Marshal(body)
	req, err := http.NewRequest(http.MethodPost, os.Getenv("ZEROCAPTCHA_API")+path, bytes.NewReader(payload))
	if err != nil {
		return r, err
	}
	req.Header.Set("Content-Type", "application/json")
	if idempotencyKey != "" {
		req.Header.Set("Idempotency-Key", idempotencyKey)
	}
	resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
	if err != nil {
		return r, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return r, fmt.Errorf("%s: HTTP %d", path, resp.StatusCode)
	}
	if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
		return r, err
	}
	if r.ErrorID != 0 {
		return r, fmt.Errorf("%s: %s", r.ErrorCode, r.ErrorDescription)
	}
	return r, nil
}

func main() {
	created, err := post("/createTask", map[string]any{
		"task": map[string]any{
			"type":       "TurnstileTaskProxyless",    // or "TurnstileTask", with proxy below
			"websiteURL": "https://example.com/login", // the page with the widget
			"websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",  // its data-sitekey
			// The widget's data-action and data-cdata, or the action and cData options of
			// turnstile.render(). Leave out any the widget does not set.
			"metadata": map[string]string{"action": "login", "cdata": "session-7f3a9c2e"},
			// "proxy": "http://user:pass@proxy.example.net:8080", // TurnstileTask only
		},
		// "callbackUrl": "https://hooks.example.com/zerocaptcha", // to be called when it ends
	}, rand.Text())
	result := created
	for err == nil && (result.Status == "" || result.Status == "processing") {
		time.Sleep(2 * time.Second)
		result, err = post("/getTaskResult", map[string]any{"taskId": created.TaskID}, "")
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println(result.Solution.Token)
}
```

**PHP**

```php
<?php
function zerocaptcha(string $path, array $body, array $headers = []): array
{
    $body['clientKey'] = getenv('ZEROCAPTCHA_KEY');
    $curl = curl_init(getenv('ZEROCAPTCHA_API') . $path);
    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json'], $headers),
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 15,
    ]);
    $raw = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);
    if ($raw === false || $status !== 200) {
        throw new RuntimeException("$path: HTTP $status");
    }
    $reply = json_decode($raw, true);
    if ($reply['errorId'] !== 0) {
        throw new RuntimeException("{$reply['errorCode']}: {$reply['errorDescription']}");
    }
    return $reply;
}

$created = zerocaptcha('/createTask', [
    'task' => [
        'type' => 'TurnstileTaskProxyless', // or 'TurnstileTask', with 'proxy' below
        'websiteURL' => 'https://example.com/login', // the page with the widget
        'websiteKey' => '0x4AAAAAAAB1cD2eF3gH4iJ5', // its data-sitekey
        // The widget's data-action and data-cdata, or the action and cData options of
        // turnstile.render(). Leave out any the widget does not set.
        'metadata' => ['action' => 'login', 'cdata' => 'session-7f3a9c2e'],
        // 'proxy' => 'http://user:pass@proxy.example.net:8080', // TurnstileTask only
    ],
    // 'callbackUrl' => 'https://hooks.example.com/zerocaptcha', // to be called when it ends
], ['Idempotency-Key: ' . bin2hex(random_bytes(16))]);
do {
    sleep(2);
    $result = zerocaptcha('/getTaskResult', ['taskId' => $created['taskId']]);
} while ($result['status'] === 'processing');
echo $result['solution']['token'], PHP_EOL;
```

> **Note**
>
> Poll every 2 seconds. `getTaskResult` and `getBalance` share the read budgets with REST: over one,
> the reply is `ERROR_RATE_LIMIT` with `Retry-After`. `createTask` has no rate budget; your balance and
> your account's share of the queue bound it (`ERROR_NO_SLOT_AVAILABLE`). See
> [Rate limits](https://zerocaptcha.io/docs/rate-limits).

## Reports

A report says whether the site took a solved task's token: `reportIncorrect` (or Anti-Captcha's
`reportIncorrectRecaptcha`, or CapSolver's `feedbackTask` with `"invalid": true`) that it refused
it, and `reportCorrect` (or `reportCorrectRecaptcha`, or `"invalid": false`) that it took it. Each is
recorded against the task, where our staff read it to find sites and settings that fail.

Nothing is refunded: a task is charged only when it is solved, and every charge is final. A task
takes one report: a second is `ERROR_DUPLICATE_REPORT`, and a report of a task that was not solved is
`ERROR_REPORT_NOT_RECORDED`. A key needs `tasks:write` to report.

## Error codes

Every `errorCode` of this format, with whether a retry helps and what it costs, is in the
[errors reference](https://zerocaptcha.io/docs/reference/errors#compatible-codes), and a task's own failures under
[task outcomes](https://zerocaptcha.io/docs/reference/errors#task-outcomes). The ones clients meet most:

| Code | What to do |
| --- | --- |
| `ERROR_KEY_DOES_NOT_EXIST`, `ERROR_KEY_REVOKED` | Use a working key from the dashboard. |
| `ERROR_ZERO_BALANCE` | Add funds, then create the task again. |
| `ERROR_TASK_NOT_SUPPORTED`, `ERROR_INVALID_TASK_DATA` | Fix the task as `errorDescription` says. |
| `ERROR_NO_SLOT_AVAILABLE`, `ERROR_RATE_LIMIT` | Wait a few seconds (or `Retry-After`), then retry. |
| `ERROR_CAPTCHA_UNSOLVABLE`, `ERROR_TASK_TIMEOUT` | The task failed and cost nothing: create a new one. |
| `ERROR_TOKEN_EXPIRED` | The task was solved and charged; create a new one and use its token at once. |
| `ERROR_NO_SUCH_CAPCHA_ID` | Poll with the `taskId` `createTask` gave, with a key of the same account. |

## What differs from other providers

- Task IDs are ZeroCaptcha's own, UUIDs such as `0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b`, not numbers.
  A client that keeps the ID as text passes it back unchanged.
- Only Turnstile and Cloudflare challenge pages are offered; other task types are refused with
  `ERROR_TASK_NOT_SUPPORTED`.
- `callbackUrl` beside `task` names a [callback](https://zerocaptcha.io/docs/callbacks), signed so you can check it.
- There is no free trial or test key: every task is real, and charged only when it is solved.
