# Cloudflare WAF and 5-second challenges

> Pass a Cloudflare WAF or 5-second challenge page ("Just a moment...") through your proxy, then use its cf_clearance cookie with its user agent.

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

Some sites answer a first visit with a Cloudflare challenge page instead of the page itself. It goes
by several names, all one thing to solve:

- **A Cloudflare WAF challenge:** a WAF rule (a custom, rate limiting or IP Access rule) whose
  action is Managed Challenge, Non-Interactive Challenge or Interactive Challenge. Bot Fight Mode and
  Under Attack mode show the same page.
- **"Just a moment..." or "Checking your browser":** the interstitial page itself, answered with
  HTTP 403 and the header `cf-mitigated: challenge`.
- **The 5-second challenge:** the old name for Cloudflare's JavaScript challenge, after the few
  seconds its page took. Today it is a Managed or Non-Interactive Challenge (`js_challenge`), which
  Cloudflare says typically takes less than five seconds.

A `CloudflareChallengeTask` passes that page for you and returns what a browser gets for passing it:
the `cf_clearance` cookie, and the user agent the cookie is bound to. Send both with your requests
to the site, through the same proxy, and the site serves its pages. A Cloudflare block, such as
error 1020, is a refusal rather than a challenge: no task passes it.

A challenge task is priced, held and charged like any other task: the price shows in the
[price list](https://zerocaptcha.io/pricing), and nothing is charged unless the task is solved.

## What the clearance is

`cf_clearance` is the cookie Cloudflare sets when a visitor passes a challenge. While it is valid,
the site lets that visitor through without challenging it again. Cloudflare ties it to "the
specific visitor and device it was issued to", so in practice it is accepted only:

- **from the same IP address** that earned it: so a challenge task always runs through your proxy,
  and you use the clearance through that proxy;
- **with the same user agent** that earned it: the `User-Agent` header in `solution.userAgent`,
  exactly;
- **for as long as the site allows:** its Challenge Passage setting, 30 minutes by default. We serve
  the clearance for 30 minutes after it is issued (`tokenExpiresAt`), then delete it 10 minutes
  later.

A client whose TLS handshake does not look like the browser the user agent names may be challenged
again, whatever its cookie: Cloudflare's bot detection looks at TLS fingerprints as well as
headers. A plain HTTP library's handshake does not look
like Chrome's. For sites that check, use a client that impersonates the browser, such as
curl-impersonate, or a real browser.

## Why it needs your proxy

A cookie earned from our solver's address would be refused from yours, so there is no proxyless
challenge task: a `CloudflareChallengeTaskProxyless` is refused, with
[`validation_failed`](https://zerocaptcha.io/docs/reference/errors#validation_failed) on REST and
`ERROR_TASK_NOT_SUPPORTED` in the createTask format, and nothing is held. Your proxy also looks the
page up and connects to it, so the solve comes from the address you will use.

## Create a task

`POST /v1/tasks` with the page and your proxy. A challenge page has no widget, so the task takes
no `websiteKey`, `action` or `cdata`; sending one is refused.

| Field | Required | What it is |
| --- | --- | --- |
| `type` | Yes | `CloudflareChallengeTask`. CapSolver's name, `AntiCloudflareTask`, works too. |
| `websiteURL` | Yes | The page behind the challenge: a public `http` or `https` page. |
| `proxy` | Yes | Your proxy, `http` or `https`, with its port, such as `http://user:pass@proxy.example.net:8080`. The same rules apply as for `TurnstileTask`: a public host, and a port no other protocol reserves. |
| `callbackUrl` | No | Where to POST the result when the task ends. See [Polling and callbacks](https://zerocaptcha.io/docs/callbacks). |

The same checks as a Turnstile task apply before the task is held and again before each attempt:
the page must be on a public domain and not on the blocklist, and your proxy must resolve to public
addresses only. Your proxy's password is never logged, and it is deleted when the task ends.

**curl**

```sh
curl "$ZEROCAPTCHA_API/v1/tasks" \
  -H "Authorization: Bearer $ZEROCAPTCHA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"type": "CloudflareChallengeTask", "websiteURL": "https://shop.example.com/",
       "proxy": "http://user:pass@proxy.example.net:8080",
       "callbackUrl": "https://example.com/zerocaptcha/callback"}'
```

**Node**

```js
const api = process.env.ZEROCAPTCHA_API;
const headers = { Authorization: `Bearer ${process.env.ZEROCAPTCHA_KEY}` };

let task = await fetch(`${api}/v1/tasks`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    type: "CloudflareChallengeTask",
    websiteURL: "https://shop.example.com/",
    proxy: process.env.PROXY_URL, // such as http://user:pass@proxy.example.net:8080
    callbackUrl: "https://example.com/zerocaptcha/callback", // optional: POSTed when the task ends
  }),
}).then((response) => response.json());

while (task.status === "queued" || task.status === "running") {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  task = await fetch(`${api}/v1/tasks/${task.id}`, { headers }).then((response) => response.json());
}
if (!task.solution) throw new Error(`${task.errorCode}: ${task.errorDescription}`);
const { userAgent, cookie } = task.solution; // cookie.name is "cf_clearance"
```

**Python**

```python
import os
import time
import uuid

import requests

api = os.environ["ZEROCAPTCHA_API"]
headers = {"Authorization": f"Bearer {os.environ['ZEROCAPTCHA_KEY']}"}

task = requests.post(
    f"{api}/v1/tasks",
    headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
    json={
        "type": "CloudflareChallengeTask",
        "websiteURL": "https://shop.example.com/",
        "proxy": os.environ["PROXY_URL"],  # such as http://user:pass@proxy.example.net:8080
        "callbackUrl": "https://example.com/zerocaptcha/callback",  # optional: POSTed when the task ends
    },
    timeout=15,
).json()
while task["status"] in ("queued", "running"):
    time.sleep(2)
    task = requests.get(f"{api}/v1/tasks/{task['id']}", headers=headers, timeout=15).json()
if not task.get("solution"):
    raise SystemExit(f"{task['errorCode']}: {task['errorDescription']}")
user_agent = task["solution"]["userAgent"]
clearance = task["solution"]["cookie"]["value"]
```

**Go**

```go
type challengeTask struct {
	ID               string `json:"id"`
	Status           string `json:"status"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Solution         *struct {
		UserAgent string `json:"userAgent"`
		Cookie    struct {
			Name  string `json:"name"`
			Value string `json:"value"`
		} `json:"cookie"`
	} `json:"solution"`
}

func solveChallenge(pageURL, proxy string) (*challengeTask, error) {
	body, _ := json.Marshal(map[string]string{
		"type": "CloudflareChallengeTask", "websiteURL": pageURL, "proxy": proxy,
		// Optional: POSTed when the task ends.
		"callbackUrl": "https://example.com/zerocaptcha/callback",
	})
	req, _ := http.NewRequest(http.MethodPost, os.Getenv("ZEROCAPTCHA_API")+"/v1/tasks", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("ZEROCAPTCHA_KEY"))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", fmt.Sprint(time.Now().UnixNano()))
	var task challengeTask
	for {
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		err = json.NewDecoder(resp.Body).Decode(&task)
		resp.Body.Close()
		if err != nil {
			return nil, err
		}
		if task.Status != "queued" && task.Status != "running" {
			break
		}
		time.Sleep(2 * time.Second)
		req, _ = http.NewRequest(http.MethodGet, os.Getenv("ZEROCAPTCHA_API")+"/v1/tasks/"+task.ID, nil)
		req.Header.Set("Authorization", "Bearer "+os.Getenv("ZEROCAPTCHA_KEY"))
	}
	if task.Solution == nil {
		return nil, fmt.Errorf("%s: %s", task.ErrorCode, task.ErrorDescription)
	}
	return &task, nil
}
```

**PHP**

```php
<?php
// The createTask format, as CapSolver's clients send it.
$body = json_encode([
    'clientKey' => getenv('ZEROCAPTCHA_KEY'),
    'task' => [
        'type' => 'AntiCloudflareTask',
        'websiteURL' => 'https://shop.example.com/',
        'proxy' => getenv('PROXY_URL'),
    ],
    // Optional: POSTed when the task ends.
    'callbackUrl' => 'https://example.com/zerocaptcha/callback',
]);
// The same Idempotency-Key on a retry returns the same task instead of a second, paid one.
$headers = "Content-Type: application/json\r\nIdempotency-Key: " . bin2hex(random_bytes(16));
$context = stream_context_create(['http' => ['method' => 'POST', 'header' => $headers, 'content' => $body]]);
$created = json_decode(file_get_contents(getenv('ZEROCAPTCHA_API') . '/createTask', false, $context), true);

do {
    sleep(2);
    $poll = json_encode(['clientKey' => getenv('ZEROCAPTCHA_KEY'), 'taskId' => $created['taskId']]);
    $context = stream_context_create(['http' => ['method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => $poll]]);
    $result = json_decode(file_get_contents(getenv('ZEROCAPTCHA_API') . '/getTaskResult', false, $context), true);
} while ($result['errorId'] === 0 && $result['status'] === 'processing');

$userAgent = $result['solution']['userAgent'];
$clearance = $result['solution']['cookies']['cf_clearance'];
```

The [SDKs](https://zerocaptcha.io/docs/sdks) do all of this in one call: `solveChallenge` in Node, `solve_challenge` in
Python and `SolveChallenge` in Go return the clearance and its user agent.

## Read the result

Read the task with `GET /v1/tasks/{id}` until it ends. A solved one carries the clearance:

```json
{
  "id": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b",
  "type": "CloudflareChallengeTask",
  "kind": "cloudflare",
  "status": "succeeded",
  "websiteURL": "https://shop.example.com/",
  "websiteKey": null,
  "usesProxy": true,
  "solution": {
    "token": "Dyw1BhDnEAGRy5fh…",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
    "cookie": { "name": "cf_clearance", "value": "Dyw1BhDnEAGRy5fh…", "expiresAt": null }
  },
  "tokenState": "available",
  "tokenIssuedAt": "2026-09-30T14:02:14Z",
  "tokenExpiresAt": "2026-09-30T14:32:14Z"
}
```

| Field | What it is |
| --- | --- |
| `solution.cookie.value` | The `cf_clearance` cookie's value. `solution.token` holds the same value. |
| `solution.userAgent` | The `User-Agent` to send with the cookie. |
| `solution.cookie.expiresAt` | When the site stops accepting the cookie, if it is known; `null` when it is not, as today. The site's own setting decides (its Challenge Passage, 30 minutes by default), and the solver does not learn it. |
| `tokenExpiresAt` | Until when the API serves the clearance: 30 minutes after it was issued. It is deleted 10 minutes after that. |

In the createTask format, `getTaskResult` answers as CapSolver's `AntiCloudflareTask` does:

```json
{
  "errorId": 0,
  "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b",
  "status": "ready",
  "solution": {
    "token": "Dyw1BhDnEAGRy5fh…",
    "type": "cloudflare",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
    "cookies": { "cf_clearance": "Dyw1BhDnEAGRy5fh…" }
  },
  "cost": "0.001200",
  "createTime": 1790776925,
  "endTime": 1790776934,
  "solveCount": 1,
  "expiresAt": "2026-09-30T14:32:14Z"
}
```

A `createTask` for a challenge page ignores the fields other providers take for one, such as
`userAgent` and `html`, and any `websiteKey`, `action` or `cdata`.

> **Caution**
>
>   The [2Captcha format](https://zerocaptcha.io/docs/2captcha) does not serve challenge pages: `in.php` takes
>   `method=turnstile` only, and `res.php` refuses a challenge task made in another format with
>   `ERROR_BAD_PARAMETERS`, as its replies have no place for the user agent the cookie needs.

## Use the clearance

Send the cookie and the user agent with each request to the site, through the proxy that earned
them. Reuse them for every request until the site challenges you again, then create a new task.

**curl**

```sh
curl "https://shop.example.com/" \
  --proxy "$PROXY_URL" \
  -H "User-Agent: $USER_AGENT" \
  -H "Cookie: cf_clearance=$CF_CLEARANCE"
```

**Node**

```js
// got 14, through the same proxy with hpagent.
import got from "got";
import { HttpsProxyAgent } from "hpagent";

const site = got.extend({
  agent: { https: new HttpsProxyAgent({ proxy: process.env.PROXY_URL }) },
  headers: { "user-agent": userAgent, cookie: `cf_clearance=${cookie.value}` },
});
const page = await site("https://shop.example.com/");
console.log(page.statusCode);
```

**Python**

```python
import os

import httpx
import requests

proxy = os.environ["PROXY_URL"]

# requests
with requests.Session() as session:
    session.proxies = {"http": proxy, "https": proxy}
    session.headers["User-Agent"] = user_agent
    session.cookies.set("cf_clearance", clearance, domain="shop.example.com")
    print(session.get("https://shop.example.com/", timeout=30).status_code)

# httpx
with httpx.Client(proxy=proxy, headers={"User-Agent": user_agent}, cookies={"cf_clearance": clearance}) as client:
    print(client.get("https://shop.example.com/", timeout=30).status_code)
```

**Go**

```go
// Through the same proxy, with the cookie in a jar and the user agent on every request.
proxy, _ := url.Parse(os.Getenv("PROXY_URL"))
jar, _ := cookiejar.New(nil)
site, _ := url.Parse("https://shop.example.com/")
jar.SetCookies(site, []*http.Cookie{{Name: "cf_clearance", Value: task.Solution.Cookie.Value}})
client := &http.Client{
	Jar:       jar,
	Transport: &http.Transport{Proxy: http.ProxyURL(proxy)},
	Timeout:   30 * time.Second,
}
req, _ := http.NewRequest(http.MethodGet, site.String(), nil)
req.Header.Set("User-Agent", task.Solution.UserAgent)
resp, err := client.Do(req)
```

**PHP**

```php
<?php
$curl = curl_init('https://shop.example.com/');
curl_setopt_array($curl, [
    CURLOPT_PROXY => getenv('PROXY_URL'),
    CURLOPT_USERAGENT => $userAgent,
    CURLOPT_COOKIE => 'cf_clearance=' . $clearance,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$page = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;
```

A client that impersonates the browser's TLS handshake, such as curl-impersonate, keeps the
clearance accepted where plain HTTP libraries may be challenged again. When the site challenges you
again, its clearance has ended: create a new task.

In a browser you drive, set the cookie for the site's domain and the browser's user agent to
`solution.userAgent` before loading the page, and route the browser through the same proxy. See
[Browser automation](https://zerocaptcha.io/docs/browser-automation).

## Test against a live challenge page

The [Cloudflare WAF managed challenge test page](https://zerocaptcha.io/captcha-test/cloudflare-managed-challenge), the
[Cloudflare 5-second JS challenge test page](https://zerocaptcha.io/captcha-test/cloudflare-js-challenge) and the
[Cloudflare WAF interactive challenge test page](https://zerocaptcha.io/captcha-test/cloudflare-interactive-challenge) each sit
behind a Cloudflare WAF rule of that kind, and show whether a request carried a clearance. Point a
challenge task at one to see the whole flow, then load the page with its clearance. Every test
page is on the [Cloudflare Turnstile demo and CAPTCHA test pages](https://zerocaptcha.io/captcha-test).

## When it fails

A challenge that is not passed after every attempt fails with `ERROR_CAPTCHA_UNSOLVABLE`, and one
still unsolved at its deadline expires with `ERROR_TASK_TIMEOUT`; neither is charged. A proxy that
resolves to a private address by the time the task runs fails with `ERROR_PROXY_NOT_ALLOWED`. See
[error codes](https://zerocaptcha.io/docs/reference/errors) for every code.
