# Migrate a createTask client

> Move a CapSolver or Anti-Captcha style createTask client to ZeroCaptcha: change the host and key, then check its field names and replies.

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

Clients written for CapSolver's or Anti-Captcha's JSON API call `createTask`, then `getTaskResult`
until the task is ready. ZeroCaptcha answers the same calls, with the same shapes, for Cloudflare
Turnstile and Cloudflare challenge pages. The change is the host and the key, plus a look at the
field names your client sends.

ZeroCaptcha is not affiliated with CapSolver or Anti-Captcha. This guide names them only to show
what to change.

## Before you start

1. [Sign up](https://zerocaptcha.io/docs/quickstart), create a key and add funds. Keep the key in your environment, as
   `ZEROCAPTCHA_KEY`, and the API's address as `ZEROCAPTCHA_API`.
2. Check that your code keeps task IDs as text: ZeroCaptcha's are UUIDs.
3. Check the task types you send. ZeroCaptcha takes `TurnstileTaskProxyless`, `TurnstileTask`,
   `CloudflareChallengeTask`, and the same names with CapSolver's `Anti` prefix
   (`AntiTurnstileTaskProxyLess`, `AntiTurnstileTask` and `AntiCloudflareTask`), in any case. Any other type is `ERROR_TASK_NOT_SUPPORTED`.

## Change the host and the key

The task stays as your client sends it, with the widget's action and cData in `metadata`: many
sites check both when they verify the token, so keep sending them. See
[action and cData](https://zerocaptcha.io/docs/action-and-cdata). An `Idempotency-Key` header, new to most of these
clients, makes a retried create return the same task.

**curl**

```sh
# Before: https://api.capsolver.com/createTask or https://api.anti-captcha.com/createTask
# After. 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.
curl "$ZEROCAPTCHA_API/createTask" -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"clientKey\": \"$ZEROCAPTCHA_KEY\", \"task\": {\"type\": \"AntiTurnstileTaskProxyLess\",
       \"websiteURL\": \"https://shop.example.com/login\", \"websiteKey\": \"0x4AAAAAAAB1cD2eF3gH4iJ5\",
       \"metadata\": {\"action\": \"login\", \"cdata\": \"session-7f3a9c2e\"}}}"
```

**Node**

```js
// Before
// const BASE = "https://api.capsolver.com";
// const clientKey = process.env.CAPSOLVER_KEY;

// After
const BASE = process.env.ZEROCAPTCHA_API;
const clientKey = process.env.ZEROCAPTCHA_KEY;

const created = await fetch(`${BASE}/createTask`, {
  method: "POST",
  // One Idempotency-Key per task: a retried create with it returns the same task.
  headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    clientKey,
    task: {
      type: "AntiTurnstileTaskProxyLess",
      websiteURL: "https://shop.example.com/login",
      websiteKey: "0x4AAAAAAAB1cD2eF3gH4iJ5",
      // 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" },
    },
  }),
}).then((response) => response.json());
console.log(created.errorId === 0 ? created.taskId : created.errorCode);
```

**Python**

```python
import os
import uuid

import requests

# Before
# BASE = "https://api.anti-captcha.com"
# CLIENT_KEY = os.environ["ANTICAPTCHA_KEY"]

# After
BASE = os.environ["ZEROCAPTCHA_API"]
CLIENT_KEY = os.environ["ZEROCAPTCHA_KEY"]

created = requests.post(
    f"{BASE}/createTask",
    json={
        "clientKey": CLIENT_KEY,
        "task": {
            "type": "TurnstileTaskProxyless",
            "websiteURL": "https://shop.example.com/login",
            "websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",
            # 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"},
        },
    },
    # One Idempotency-Key per task: a retried create with it returns the same task.
    headers={"Idempotency-Key": str(uuid.uuid4())},
    timeout=15,
).json()
print(created["taskId"] if created["errorId"] == 0 else created["errorCode"])
```

**Go**

```go
// Before:
// base, clientKey := "https://api.capsolver.com", os.Getenv("CAPSOLVER_KEY")

// After:
base, clientKey := os.Getenv("ZEROCAPTCHA_API"), os.Getenv("ZEROCAPTCHA_KEY")

body, _ := json.Marshal(map[string]any{
	"clientKey": clientKey,
	"task": map[string]any{
		"type":       "AntiTurnstileTaskProxyLess",
		"websiteURL": "https://shop.example.com/login",
		"websiteKey": "0x4AAAAAAAB1cD2eF3gH4iJ5",
		// 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"},
	},
})
req, _ := http.NewRequest(http.MethodPost, base+"/createTask", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// One Idempotency-Key per task: a retried create with it returns the same task.
req.Header.Set("Idempotency-Key", rand.Text())
resp, err := http.DefaultClient.Do(req)
```

**PHP**

```php
<?php
// Before
// $base = 'https://api.anti-captcha.com';
// $clientKey = getenv('ANTICAPTCHA_KEY');

// After
$base = getenv('ZEROCAPTCHA_API');
$clientKey = getenv('ZEROCAPTCHA_KEY');

$created = json_decode(file_get_contents("$base/createTask", false, stream_context_create(['http' => [
    'method' => 'POST',
    // One Idempotency-Key per task: a retried create with it returns the same task.
    'header' => "Content-Type: application/json\r\nIdempotency-Key: " . bin2hex(random_bytes(16)),
    'content' => json_encode([
        'clientKey' => $clientKey,
        'task' => [
            'type' => 'TurnstileTaskProxyless',
            'websiteURL' => 'https://shop.example.com/login',
            'websiteKey' => '0x4AAAAAAAB1cD2eF3gH4iJ5',
            // 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'],
        ],
    ]),
]])), true);
echo $created['errorId'] === 0 ? $created['taskId'] : $created['errorCode'], PHP_EOL;
```

`getTaskResult` and `getBalance` move the same way: the same bodies, on ZeroCaptcha's host. If you
use a client library, point it at the API's address if it lets you set the host; if you are not sure
it does, call the API over HTTP as above.

## Check the field names

ZeroCaptcha reads the spellings these clients send, and ignores fields it does not use, so a field
under a name it does not know is silently left out. Check yours against these:

| What | Names ZeroCaptcha reads |
| --- | --- |
| The page | `websiteURL`, `websiteUrl` |
| The site key | `websiteKey` |
| The action | `action`, `pageAction`, `metadata.action` |
| The cData | `cdata`, `cData`, `data`, `turnstileCData`, `metadata.cdata` (or `metadata.cData`) |
| A proxy | `proxy` as a URL, or `proxyType`, `proxyAddress`, `proxyPort`, `proxyLogin`, `proxyPassword` |
| A callback | `callbackUrl`, beside `task` |

Each of these is read as the clients send it, Anti-Captcha's `cData` included. A cData under any
other name is left out, and the site may then refuse the token.

CapMonster Cloud's clients send `pageAction` and `data`, both read. Its `cloudflareTaskType` may be
`token` or left out; its `cf_clearance` mode is refused with `ERROR_TASK_NOT_SUPPORTED`, as the
clearance would be tied to a user agent the client did not choose. Use `CloudflareChallengeTask`
with your proxy instead: its reply gives the clearance and the user agent to send it with.

## Replies

- A created task: `{"errorId": 0, "taskId": "…"}`.
- While it runs: `{"errorId": 0, "status": "processing"}`.
- Solved: `status: "ready"`, `solution.token`, and `cost`, `createTime`, `endTime`, `solveCount` and
  `expiresAt`. A challenge page's solution also has `userAgent` and `cookies.cf_clearance`, as
  CapSolver's `AntiCloudflareTask` answers.
- Failed: `errorId: 1` with `errorCode`, such as `ERROR_CAPTCHA_UNSOLVABLE`, and nothing charged.
- Balance: `{"errorId": 0, "balance": 12.3456}`, in US dollars.
- Reports: `reportIncorrect`, `reportCorrect`, Anti-Captcha's `reportIncorrectRecaptcha` and
  `reportCorrectRecaptcha`, and CapSolver's `feedbackTask` answer `{"errorId": 0, "status":
  "success"}`. Each is recorded for our staff and refunds nothing, as every charge is final.

Every reply is HTTP 200; a failure is `errorId: 1`. See [createTask format](https://zerocaptcha.io/docs/createtask) for
every field and the [errors reference](https://zerocaptcha.io/docs/reference/errors#compatible-codes) for every code.

## What to check after you switch

- **Proxies:** `http` and `https` only; SOCKS is not supported yet. A challenge page always needs
  your proxy: there is no proxyless challenge task.
- **Errors:** `ERROR_RATE_LIMIT` asks you to slow your polling; `ERROR_NO_SLOT_AVAILABLE` means your
  account's share of the queue is full for a moment. Both come with a wait: see
  [Errors and retries](https://zerocaptcha.io/docs/errors-and-retries).
- **Money:** prepaid US dollars, charged only when a task is solved. See [pricing](https://zerocaptcha.io/pricing).
- **Idempotency:** send an `Idempotency-Key` header with each `createTask`, so a retry after a lost
  reply returns the first task instead of creating a second.

Roll it out one service at a time, with a key of its own and a daily
[spend cap](https://zerocaptcha.io/docs/keys#cap-a-keys-daily-spend), and watch its tasks in the dashboard.
