CAPSOLVER
Blog
How to Solve Cloudflare Challenge in n8n with CapSolver

How to Solve Cloudflare Challenge in n8n with CapSolver

Logo of CapSolver

Ethan Collins

Pattern Recognition Specialist

12-Mar-2026

Cloudflare's bot protection goes far beyond CAPTCHA widgets. The Cloudflare Challenge โ€” the full-page "Verifying your browserโ€ฆ" screen that blocks access to a site entirely โ€” is one of the most aggressive bot defenses on the web. It runs a full JavaScript challenge in a real browser environment, checks behavioral signals, and fingerprints your TLS connection before it ever lets you through.

Standard automation tools fail here. Not because they can't solve the challenge, but because Cloudflare fingerprints the TLS handshake itself โ€” and any non-browser HTTP client gets blocked before the challenge is even served, or re-challenged immediately after, even with a valid cf_clearance cookie.

In this guide, you'll learn how to build a Cloudflare Challenge scraper in n8n that actually works โ€” combining CapSolver to solve the challenge, a local Go TLS server to spoof Chrome's TLS fingerprint, and an n8n workflow to tie it all together.

What you'll build:

  • A dual-trigger workflow (schedule + webhook) that bypasses Cloudflare Bot Management
  • A lightweight Go server that makes HTTP requests with a real Chrome TLS fingerprint
  • A reusable scraper template you can point at any CF-protected site
  • Use-case workflows for site health monitoring, price monitoring, account login, and account signup โ€” all behind Cloudflare Challenge

What Is the Cloudflare Challenge?

The Cloudflare Challenge (also called the JS challenge or Bot Management challenge) is a full-page interstitial that Cloudflare injects before a visitor can access a protected site. You've seen it: a black or white screen with "Verifying your browserโ€ฆ" or "Just a momentโ€ฆ" and a loading bar or Cloudflare logo.

Unlike Turnstile โ€” which is a small widget embedded inside a page โ€” the Cloudflare Challenge takes over the entire page. You cannot access any content until it completes.

Cloudflare Challenge Cloudflare Turnstile
Where it appears Full-page interstitial โ€” blocks site access entirely Embedded widget inside a page (e.g., login form)
What it looks like "Verifying your browserโ€ฆ" loading screen A small checkbox or invisible widget in a form
Who adds it Cloudflare adds it automatically based on security rules The website owner embeds it in their HTML
Solving approach AntiCloudflareTask โ€” requires a proxy AntiTurnstileTaskProxyLess โ€” no proxy needed
Cookie returned cf_clearance (domain-specific, IP-bound) Turnstile token (short-lived, used once)
Proxy required? Yes โ€” same IP must be used for solve and fetch No

If you see an embedded checkbox or widget inside a form, that's Turnstile โ€” not this challenge. Check the CapSolver guide on identifying challenge types if you're unsure.


Why Standard HTTP Clients Fail

Here's the problem that most guides skip over: Cloudflare checks your TLS fingerprint, not just your cookies.

When a browser connects to a website over HTTPS, it sends a TLS ClientHello that includes details about its supported cipher suites, extensions, and settings. Cloudflare records this fingerprint (called a JA3 or JA4 fingerprint) and compares it to known browser profiles.

Go's net/http, Python's requests, curl, and most HTTP libraries all have distinct TLS fingerprints. Even if CapSolver successfully solves the challenge and returns a valid cf_clearance cookie, Cloudflare will re-challenge or block the request if it detects a non-browser TLS fingerprint on the subsequent fetch.

The fix: a Go server using httpcloak โ€” a library that spoofs a real Chrome TLS stack including:

  • JA3 / JA4 fingerprint
  • HTTP/2 SETTINGS frames
  • ALPN negotiation
  • Header ordering and values

This makes the fetch look exactly like a Chrome browser request at the network level.


Architecture Overview

This solution has three components:

Copy
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     n8n Workflow                        โ”‚
โ”‚                                                         โ”‚
โ”‚  Trigger โ†’ Set Config โ†’ CapSolver (AntiCloudflareTask)  โ”‚
โ”‚         โ†’ Code Node  โ†’ HTTP Request โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                    โ”‚ POST /fetch
                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                         โ”‚   TLS Server :7878  โ”‚
                         โ”‚  (Go + httpcloak    โ”‚
                         โ”‚   Chrome preset)    โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                    โ”‚ HTTPS (Chrome TLS)
                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                         โ”‚  CF-Protected Site  โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Component Role
n8n workflow Orchestrates the solve โ†’ fetch โ†’ extract pipeline
CapSolver Runs AntiCloudflareTask through a proxy, returns cf_clearance + userAgent
TLS Server Go binary using httpcloak's latest Chrome preset to make the actual HTTPS fetch

Prerequisites

Requirement Notes
n8n self-hosted Required โ€” the TLS server must run on the same machine as n8n. n8n Cloud is not suitable for this use case.
CapSolver account Sign up here and get your API key
Go 1.21+ Must be installed on the n8n server. Check with go version.
Residential or mobile proxy Datacenter proxies will fail on most Cloudflare-protected sites. See Proxy Requirements below.

Proxy Requirements

AntiCloudflareTask requires a proxy because CapSolver needs to load the actual protected page through a browser to solve the challenge. The proxy IP is bound to the cf_clearance cookie โ€” you must use the exact same proxy for the subsequent page fetch.

The workflow uses a single proxy field in host:port:user:pass format. This is the format CapSolver expects. The Prepare TLS Request code node automatically converts it to http://user:pass@host:port URL format for the TLS server using a toProxyURL() helper โ€” you don't need to manage two separate proxy fields.

Datacenter proxies (IPs from cloud providers, VPS hosts) are flagged aggressively by Cloudflare's bot scoring system. For most Cloudflare-protected sites, you need a residential or mobile proxy with sticky/session support so the IP stays the same across the solve and fetch.


Setting Up CapSolver in n8n

CapSolver is available as an official integration in n8n โ€” no community node installation required.

Step 1: Open the Credentials Page

Go to your n8n instance and navigate to Settings โ†’ Credentials.

n8n credentials page showing CapSolver account

Step 2: Create the CapSolver Credential

  1. Click Create credential (top right)
  2. Search for "CapSolver" and select CapSolver API
  3. Enter your API Key from the CapSolver Dashboard
  4. Leave Allowed HTTP Request Domains set to All
  5. Click Save

You should see a green "Connection tested successfully" banner.

CapSolver credential configuration with successful connection test

Step 1 โ€” Build the TLS Server

This Go server receives fetch requests from n8n and executes them using httpcloak's Chrome TLS profile. It's a small, self-contained binary that you run alongside n8n.

Create the source file

Create a directory and save the following as main.go:

bash Copy
mkdir -p ~/tls-server && cd ~/tls-server
go Copy
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/sardanioss/httpcloak/client"
)

type FetchRequest struct {
	URL     string            `json:"url"`
	Method  string            `json:"method"`
	Headers map[string]string `json:"headers"`
	Proxy   string            `json:"proxy"`
	Body    string            `json:"body"`
}

type FetchResponse struct {
	Status  int                 `json:"status"`
	Body    string              `json:"body"`
	Headers map[string][]string `json:"headers"`
}

type ErrorResponse struct {
	Error string `json:"error"`
}

func writeError(w http.ResponseWriter, status int, msg string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(ErrorResponse{Error: msg})
}

func fetchHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeError(w, http.StatusMethodNotAllowed, "only POST allowed")
		return
	}

	var req FetchRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}

	if req.URL == "" {
		writeError(w, http.StatusBadRequest, "url is required")
		return
	}
	if req.Method == "" {
		req.Method = "GET"
	}

	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	c := client.NewClient("chrome-145", client.WithTimeout(60*time.Second))
	defer c.Close()

	if req.Proxy != "" {
		c.SetProxy(req.Proxy)
	}

	// Extract user-agent separately so httpcloak uses it instead of its preset value.
	headers := make(map[string][]string, len(req.Headers))
	var userAgent string
	for k, v := range req.Headers {
		lower := strings.ToLower(k)
		if lower == "user-agent" {
			userAgent = v
		} else {
			headers[k] = []string{v}
		}
	}

	var bodyReader io.Reader
	if req.Body != "" {
		bodyReader = strings.NewReader(req.Body)
	}

	hcReq := &client.Request{
		Method:    strings.ToUpper(req.Method),
		URL:       req.URL,
		Headers:   headers,
		Body:      bodyReader,
		UserAgent: userAgent,
		FetchMode: client.FetchModeNavigate,
	}

	resp, err := c.Do(ctx, hcReq)
	if err != nil {
		writeError(w, http.StatusBadGateway, "fetch failed: "+err.Error())
		return
	}

	body, err := resp.Text()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "read body failed: "+err.Error())
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(FetchResponse{
		Status:  resp.StatusCode,
		Body:    body,
		Headers: resp.Headers,
	})
}

func main() {
	const port = "7878"

	mux := http.NewServeMux()
	mux.HandleFunc("/fetch", fetchHandler)
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, `{"status":"ok"}`)
	})

	log.Printf("TLS server (httpcloak chrome-145) listening on :%s", port)
	log.Fatal(http.ListenAndServe(":"+port, mux))
}

Initialize and build

bash Copy
go mod init tls-server
go get github.com/sardanioss/httpcloak/client
go build -o main main.go

Run with PM2

bash Copy
pm2 start ./main --name tls-server
pm2 save

Verify it's running

bash Copy
curl http://localhost:7878/health

Expected: {"status":"ok"}

Note: The TLS server must run on the same machine as your n8n instance. The n8n workflow calls it at http://localhost:7878/fetch. If you're using n8n Cloud, you would need a self-hosted setup โ€” this is one reason self-hosted n8n is strongly recommended for Cloudflare Challenge scraping.


Step 2 โ€” Allow n8n to Call Localhost

By default, n8n blocks HTTP Request nodes from calling localhost addresses (SSRF protection). You need to disable this restriction.

Add the following environment variable to your n8n process and restart:

bash Copy
N8N_BLOCK_ACCESS_TO_LOCALHOST=false pm2 restart n8n --update-env

Important: The --update-env flag is required. A plain pm2 restart n8n will not pick up new environment variables from your shell session.

Verify it took effect by checking the n8n process environment:

bash Copy
cat /proc/$(pm2 jlist | python3 -c "import json,sys; procs=json.load(sys.stdin); print([p for p in procs if p['name']=='n8n'][0]['pid'])")/environ | tr '\0' '\n' | grep N8N_BLOCK

You should see N8N_BLOCK_ACCESS_TO_LOCALHOST=false.


The Cloudflare Challenge Workflow

The workflow has two parallel trigger paths โ€” schedule (runs every 6 hours automatically) and webhook (runs on demand via HTTP POST). Both paths follow identical logic and can be configured independently.

How It Works

Schedule path:

Copy
Every 6 Hours โ†’ Set Target Config [Schedule] โ†’ Solve Cloudflare Challenge
             โ†’ Prepare TLS Request โ†’ Fetch via TLS Server โ†’ Extract Result

Webhook path:

Copy
Webhook Trigger โ†’ Set Target Config [Webhook] โ†’ Solve Cloudflare Challenge
               โ†’ Prepare TLS Request โ†’ Fetch via TLS Server โ†’ Extract Result โ†’ Respond to Webhook

Node Configuration

1. Set Target Config

This node stores the three values the rest of the workflow depends on. Edit these fields to point at your target.

For the Schedule path (Set Target Config [Schedule]):

Field Example Value Description
targetURL https://protected-site.com/page The Cloudflare-protected URL to scrape
proxy host:port:user:pass Proxy in host:port:user:pass format โ€” used for both CapSolver and the TLS server

For the Webhook path (Set Target Config [Webhook]), the values come from the POST body:

Field Expression Description
targetURL ={{ $json.body.targetURL }} The Cloudflare-protected URL to scrape
proxy ={{ $json.body.proxy }} Proxy in host:port:user:pass format
userAgent ={{ $json.body.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36' }} User-Agent string โ€” defaults to a recent Chrome UA if not provided

Single proxy field with auto-conversion: You only need to provide the proxy once in host:port:user:pass format. The CapSolver node uses this format directly, and the Prepare TLS Request code node converts it internally to http://user:pass@host:port URL format for the TLS server using a toProxyURL() helper function. Both steps use the same proxy IP โ€” the cf_clearance cookie is bound to the IP that solved the challenge.

2. Solve Cloudflare Challenge (CapSolver Node)

Parameter Value
Operation Cloudflare Challenge
Type AntiCloudflareTask
Website URL ={{ $json.targetURL }}
Proxy ={{ $json.proxy }}
Credential Select your CapSolver account
On Error Continue (using regular output)

The onError: "continueRegularOutput" setting means the workflow continues even if CapSolver fails to find a challenge (e.g., the page is not currently behind a challenge). The downstream code node checks whether a solution was returned and gracefully handles both cases.

CapSolver launches a real browser through your proxy, loads the target URL, and solves the Cloudflare challenge. When successful, it returns a solution object containing:

  • cookies โ€” an object { cf_clearance: "..." } with the clearance cookie
  • userAgent โ€” the exact User-Agent string the browser used during the solve

3. Prepare TLS Request (Code Node)

This node builds the request payload for the TLS server, converting the CapSolver solution into the right format.

For the Webhook path (Prepare TLS Request [Webhook]):

javascript Copy
const config = $('Set Target Config [Webhook]').first().json;
const capResult = $input.first().json;

// Convert proxy from host:port:user:pass to http://user:pass@host:port
function toProxyURL(proxy) {
  if (!proxy) return '';
  if (proxy.startsWith('http')) return proxy;
  const parts = proxy.split(':');
  if (parts.length === 4) {
    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;
  }
  return proxy;
}

// Check if CapSolver succeeded or failed (continueOnFail)
let cookieStr = '';
let ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';

if (capResult.data && capResult.data.solution) {
  const solution = capResult.data.solution;
  const cookies = solution.cookies;
  cookieStr = cookies && typeof cookies === 'object'
    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')
    : (cookies || '');
  if (solution.userAgent) ua = solution.userAgent;
}

const chromeMatch = ua.match(/Chrome\/(\d+)/);
const chromeVer = chromeMatch ? chromeMatch[1] : '145';
const secChUa = `"Chromium";v="${chromeVer}", "Not A(Brand";v="8", "Google Chrome";v="${chromeVer}"`;

const headers = {
  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
  'accept-language': 'en-US,en;q=0.9',
  'sec-ch-ua': secChUa,
  'sec-ch-ua-mobile': '?0',
  'sec-ch-ua-platform': '"Windows"',
  'sec-fetch-dest': 'document',
  'sec-fetch-mode': 'navigate',
  'sec-fetch-site': 'none',
  'sec-fetch-user': '?1',
  'upgrade-insecure-requests': '1',
  'user-agent': ua
};

if (cookieStr) headers['cookie'] = cookieStr;

return [{ json: {
  url: config.targetURL,
  method: 'GET',
  proxy: toProxyURL(config.proxy),
  headers
}}];

For the Schedule path (Prepare TLS Request [Schedule]), the code is identical except it references a different config node:

javascript Copy
const config = $('Set Target Config [Schedule]').first().json;

Key details about this code:

  1. toProxyURL() converts the proxy format automatically. You store the proxy as host:port:user:pass (which CapSolver expects), and the function converts it to http://user:pass@host:port (which the TLS server expects). No need to maintain two separate proxy fields.

  2. Graceful fallback when CapSolver fails. With onError: "continueRegularOutput", the CapSolver node passes its output downstream even on failure. The code checks capResult.data && capResult.data.solution โ€” if there's no solution, it proceeds without cookies (useful when a page isn't currently showing a challenge).

  3. solution.cookies is an object, not a string. CapSolver's AntiCloudflareTask returns cookies as { cf_clearance: "abc123..." }. This must be serialized to "cf_clearance=abc123..." before sending as a header. The code handles both cases.

  4. solution.userAgent must be forwarded exactly. The cf_clearance cookie is tied to the specific User-Agent that was used during the solve. If no userAgent is provided (or CapSolver failed), the code defaults to a recent Chrome UA. The sec-ch-ua header is dynamically derived from the Chrome version in the User-Agent string.

4. Fetch via TLS Server (HTTP Request Node)

Setting Value
Method POST
URL http://localhost:7878/fetch
Content Type Raw
Raw Content Type application/json
Body ={{ JSON.stringify($json) }}
Timeout 60000 ms

Why contentType: "raw" and not "json"? n8n's json content type mode expects body parameters as key-value pairs. If you pass JSON.stringify($json) as a string, n8n treats the whole string as a single malformed param and sends {"": ""} to the server. Using raw mode sends the body exactly as the expression evaluates.

5. Extract Result

Pulls the three fields from the TLS server response into clean output fields:

Field Expression Type
status ={{ $json.status }} number
body ={{ $json.body }} string
fetchedAt ={{ new Date().toISOString() }} string

6. Respond to Webhook (Webhook path only)

Setting Value
Respond With JSON
Response Body ={{ JSON.stringify($json) }}

Returns the extracted result as a JSON response to the caller.


Import This Workflow

Copy the JSON below and import it into n8n via Menu โ†’ Import from JSON. After importing, update the Set Target Config [Schedule] node with your own targetURL and proxy (in host:port:user:pass format), and select your CapSolver credential in the two solver nodes. The webhook path reads its config from the POST body, so no placeholder editing is needed there.

Click to expand workflow JSON
json Copy
{
  "name": "Cloudflare Challenge โ€” CapSolver + Schedule + Webhook",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [-400, 0],
      "id": "cf111111-1111-1111-1111-111111111101",
      "name": "Every 6 Hours"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfg-s-001",
              "name": "targetURL",
              "value": "https://YOUR_CF_PROTECTED_SITE.com",
              "type": "string"
            },
            {
              "id": "cfg-s-002",
              "name": "proxy",
              "value": "YOUR_PROXY_HOST:PORT:USER:PASS",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-96, 0],
      "id": "cf111111-1111-1111-1111-111111111102",
      "name": "Set Target Config [Schedule]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [208, 0],
      "id": "cf111111-1111-1111-1111-111111111103",
      "name": "Solve Cloudflare Challenge [Schedule]",
      "credentials": {
        "capSolverApi": {
          "id": "YOUR_CREDENTIAL_ID",
          "name": "CapSolver account"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Target Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'none',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.targetURL,\n  method: 'GET',\n  proxy: toProxyURL(config.proxy),\n  headers\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [512, 0],
      "id": "cf111111-1111-1111-1111-111111111104",
      "name": "Prepare TLS Request [Schedule]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [800, 0],
      "id": "cf111111-1111-1111-1111-111111111105",
      "name": "Fetch via TLS Server [Schedule]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "out-s-001",
              "name": "status",
              "value": "={{ $json.status }}",
              "type": "number"
            },
            {
              "id": "out-s-002",
              "name": "body",
              "value": "={{ $json.body }}",
              "type": "string"
            },
            {
              "id": "out-s-003",
              "name": "fetchedAt",
              "value": "={{ new Date().toISOString() }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1104, 0],
      "id": "cf111111-1111-1111-1111-111111111106",
      "name": "Extract Result [Schedule]"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cloudflare-scraper",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-400, 420],
      "id": "cf111111-1111-1111-1111-111111111107",
      "name": "Webhook Trigger",
      "webhookId": "cf111111-aaaa-bbbb-cccc-111111111107",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfg-w-001",
              "name": "targetURL",
              "value": "={{ $json.body.targetURL }}",
              "type": "string"
            },
            {
              "id": "cfg-w-002",
              "name": "proxy",
              "value": "={{ $json.body.proxy }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-96, 420],
      "id": "cf111111-1111-1111-1111-111111111108",
      "name": "Set Target Config [Webhook]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [208, 420],
      "id": "cf111111-1111-1111-1111-111111111109",
      "name": "Solve Cloudflare Challenge [Webhook]",
      "credentials": {
        "capSolverApi": {
          "id": "YOUR_CREDENTIAL_ID",
          "name": "CapSolver account"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Target Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'none',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.targetURL,\n  method: 'GET',\n  proxy: toProxyURL(config.proxy),\n  headers\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [512, 420],
      "id": "cf111111-1111-1111-1111-111111111110",
      "name": "Prepare TLS Request [Webhook]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": {
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [800, 420],
      "id": "cf111111-1111-1111-1111-111111111111",
      "name": "Fetch via TLS Server [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "out-w-001",
              "name": "status",
              "value": "={{ $json.status }}",
              "type": "number"
            },
            {
              "id": "out-w-002",
              "name": "body",
              "value": "={{ $json.body }}",
              "type": "string"
            },
            {
              "id": "out-w-003",
              "name": "fetchedAt",
              "value": "={{ new Date().toISOString() }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1104, 420],
      "id": "cf111111-1111-1111-1111-111111111112",
      "name": "Extract Result [Webhook]"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [1408, 420],
      "id": "cf111111-1111-1111-1111-111111111113",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Every 6 Hours": {
      "main": [[{"node": "Set Target Config [Schedule]", "type": "main", "index": 0}]]
    },
    "Set Target Config [Schedule]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Schedule]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Schedule]": {
      "main": [[{"node": "Prepare TLS Request [Schedule]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Request [Schedule]": {
      "main": [[{"node": "Fetch via TLS Server [Schedule]", "type": "main", "index": 0}]]
    },
    "Fetch via TLS Server [Schedule]": {
      "main": [[{"node": "Extract Result [Schedule]", "type": "main", "index": 0}]]
    },
    "Webhook Trigger": {
      "main": [[{"node": "Set Target Config [Webhook]", "type": "main", "index": 0}]]
    },
    "Set Target Config [Webhook]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Webhook]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Webhook]": {
      "main": [[{"node": "Prepare TLS Request [Webhook]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Request [Webhook]": {
      "main": [[{"node": "Fetch via TLS Server [Webhook]", "type": "main", "index": 0}]]
    },
    "Fetch via TLS Server [Webhook]": {
      "main": [[{"node": "Extract Result [Webhook]", "type": "main", "index": 0}]]
    },
    "Extract Result [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}

Test It

Once the TLS server is running and the workflow is active, trigger the webhook path:

bash Copy
curl -X POST https://your-n8n-instance.com/webhook/cloudflare-scraper \
  -H "Content-Type: application/json" \
  -d '{
    "targetURL": "https://protected-site.com",
    "proxy": "host:port:user:pass"
  }'

You can also pass a custom userAgent in the body โ€” if omitted, it defaults to a recent Chrome UA.

Expected response:

json Copy
{
  "status": 200,
  "body": "<!DOCTYPE html><html>...",
  "fetchedAt": "2026-03-10T19:30:00.000Z"
}

A status: 200 with real HTML content confirms the full pipeline worked โ€” Cloudflare challenge solved, TLS fingerprint passed, page content retrieved.


Use-Case Workflows

The basic scraper workflow above retrieves raw page content. The following workflows extend the same Cloudflare Challenge pattern โ€” Set Target Config โ†’ Solve CF Challenge โ†’ Prepare TLS Request โ†’ Fetch via TLS Server โ€” to specific use cases. Each requires the same prerequisites: a self-hosted n8n instance, the TLS server running on port 7878, a residential proxy, and a CapSolver credential. The Solver API is the exception โ€” it doesn't need the TLS server since it returns the solution without fetching.

All of these templates are available in the repo's workflows/use-cases/ directory and are importable into n8n.

Workflow Purpose
Cloudflare Challenge Site Health Monitor โ€” CapSolver + Schedule + Webhook Monitors a CF-protected site every 30 minutes, tracks uptime in $workflow.staticData, alerts after 2+ consecutive failures
Cloudflare Challenge Price Monitor / Scrape Data โ€” CapSolver + Schedule + Webhook Scrapes price and product name from a CF-protected page every 6 hours, compares against previous values, alerts on changes
Cloudflare Challenge Account Login โ€” CapSolver + Schedule + Webhook Logs into your own account on a CF-protected site by solving the challenge first, then POSTing credentials through the TLS server
Cloudflare Challenge Account Signup โ€” CapSolver + Schedule + Webhook Registers with your own email on a CF-protected site, same two-phase approach as login
Cloudflare Challenge โ€” Solver API Webhook-only API that returns raw cf_clearance + userAgent โ€” no page fetch, no TLS server needed. Your external app handles the fetching.

Site Health Monitor

This workflow checks whether a Cloudflare-protected site is responding correctly. It runs every 30 minutes (schedule) or on demand (webhook), solves the Cloudflare Challenge, fetches the page through the TLS server, and evaluates the HTTP status code.

Schedule path:

Copy
Every 30 Minutes โ†’ Set Target Config โ†’ Solve CF Challenge โ†’ Prepare TLS Request
               โ†’ Fetch via TLS Server โ†’ Site Up? โ†’ Record Uptime / Record Downtime
               โ†’ 2+ Consecutive Failures? โ†’ Create Incident Alert / Skip Alert

The Site Up? node checks $json.status < 400 (note: the TLS server returns status, not statusCode). The uptime/downtime code nodes use $workflow.staticData to track:

  • Total checks and successes
  • Uptime percentage
  • Consecutive failure count
  • Alert suppression for single failures (avoids false positives)

An incident alert is only created after 2 or more consecutive failures, reducing noise from transient network issues.

Click to expand full workflow JSON
json Copy
{
  "name": "Cloudflare Challenge Site Health Monitor โ€” CapSolver + Schedule + Webhook",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [-600, 0],
      "id": "cf555555-5555-5555-5555-555555555501",
      "name": "Every 30 Minutes"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "cfg-s-001", "name": "targetURL", "value": "https://YOUR_CF_PROTECTED_SITE.com", "type": "string" },
            { "id": "cfg-s-002", "name": "proxy", "value": "YOUR_PROXY_HOST:PORT:USER:PASS", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-300, 0],
      "id": "cf555555-5555-5555-5555-555555555502",
      "name": "Set Target Config [Schedule]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [0, 0],
      "id": "cf555555-5555-5555-5555-555555555503",
      "name": "Solve Cloudflare Challenge [Schedule]",
      "onError": "continueRegularOutput",
      "credentials": {
        "capSolverApi": {
          "id": "YOUR_CREDENTIAL_ID",
          "name": "CapSolver account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Target Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'none',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.targetURL,\n  method: 'GET',\n  proxy: toProxyURL(config.proxy),\n  headers\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [300, 0],
      "id": "cf555555-5555-5555-5555-555555555504",
      "name": "Prepare TLS Request [Schedule]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [600, 0],
      "id": "cf555555-5555-5555-5555-555555555505",
      "name": "Fetch via TLS Server [Schedule]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "health-if-001", "leftValue": "={{ $json.status }}", "rightValue": 400, "operator": { "type": "number", "operation": "lt" } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [900, 0],
      "id": "cf555555-5555-5555-5555-555555555506",
      "name": "Site Up?"
    },
    {
      "parameters": {
        "jsCode": "const staticData = $workflow.staticData;\nif (!staticData.checks) staticData.checks = 0;\nif (!staticData.successes) staticData.successes = 0;\n\nstaticData.checks++;\nstaticData.successes++;\nstaticData.lastSuccess = new Date().toISOString();\nstaticData.consecutiveFailures = 0;\n\nconst uptime = ((staticData.successes / staticData.checks) * 100).toFixed(2);\n\nreturn [{\n  json: {\n    status: 'up',\n    statusCode: $input.first().json.status,\n    uptime: `${uptime}%`,\n    totalChecks: staticData.checks,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1200, -100],
      "id": "cf555555-5555-5555-5555-555555555507",
      "name": "Record Uptime"
    },
    {
      "parameters": {
        "jsCode": "const staticData = $workflow.staticData;\nif (!staticData.checks) staticData.checks = 0;\nif (!staticData.successes) staticData.successes = 0;\nif (!staticData.consecutiveFailures) staticData.consecutiveFailures = 0;\n\nstaticData.checks++;\nstaticData.consecutiveFailures++;\nstaticData.lastFailure = new Date().toISOString();\n\nconst uptime = ((staticData.successes / staticData.checks) * 100).toFixed(2);\n\nreturn [{\n  json: {\n    status: 'down',\n    statusCode: $input.first().json.status,\n    uptime: `${uptime}%`,\n    consecutiveFailures: staticData.consecutiveFailures,\n    totalChecks: staticData.checks,\n    alertMessage: `ALERT: CF-protected site has been down for ${staticData.consecutiveFailures} consecutive checks (${staticData.consecutiveFailures * 30} minutes)`,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1200, 100],
      "id": "cf555555-5555-5555-5555-555555555508",
      "name": "Record Downtime"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "alert-if-001", "leftValue": "={{ $json.consecutiveFailures }}", "rightValue": 2, "operator": { "type": "number", "operation": "gte" } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1500, 100],
      "id": "cf555555-5555-5555-5555-555555555509",
      "name": "2+ Consecutive Failures?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "inc-001", "name": "incident", "value": "=INCIDENT: {{ $json.alertMessage }}", "type": "string" },
            { "id": "inc-002", "name": "severity", "value": "critical", "type": "string" },
            { "id": "inc-003", "name": "checkedAt", "value": "={{ $json.checkedAt }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1800, 0],
      "id": "cf555555-5555-5555-5555-555555555510",
      "name": "Create Incident Alert"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "skip-001", "name": "note", "value": "Single failure โ€” monitoring, no alert yet", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1800, 200],
      "id": "cf555555-5555-5555-5555-555555555511",
      "name": "Skip Alert (1st Failure)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cloudflare-site-health-monitor",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-600, 500],
      "id": "cf555555-5555-5555-5555-555555555512",
      "name": "Webhook Trigger",
      "webhookId": "cf555555-aaaa-bbbb-cccc-555555555512",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "cfg-w-001", "name": "targetURL", "value": "={{ $json.body.targetURL }}", "type": "string" },
            { "id": "cfg-w-002", "name": "proxy", "value": "={{ $json.body.proxy }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-300, 500],
      "id": "cf555555-5555-5555-5555-555555555513",
      "name": "Set Target Config [Webhook]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [0, 500],
      "id": "cf555555-5555-5555-5555-555555555514",
      "name": "Solve Cloudflare Challenge [Webhook]",
      "onError": "continueRegularOutput",
      "credentials": {
        "capSolverApi": {
          "id": "YOUR_CREDENTIAL_ID",
          "name": "CapSolver account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Target Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'none',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.targetURL,\n  method: 'GET',\n  proxy: toProxyURL(config.proxy),\n  headers\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [300, 500],
      "id": "cf555555-5555-5555-5555-555555555515",
      "name": "Prepare TLS Request [Webhook]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [600, 500],
      "id": "cf555555-5555-5555-5555-555555555516",
      "name": "Fetch via TLS Server [Webhook]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "health-if-002", "leftValue": "={{ $json.status }}", "rightValue": 400, "operator": { "type": "number", "operation": "lt" } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [900, 500],
      "id": "cf555555-5555-5555-5555-555555555517",
      "name": "Site Up? [Webhook]"
    },
    {
      "parameters": {
        "jsCode": "const staticData = $workflow.staticData;\nif (!staticData.checks) staticData.checks = 0;\nif (!staticData.successes) staticData.successes = 0;\n\nstaticData.checks++;\nstaticData.successes++;\nstaticData.lastSuccess = new Date().toISOString();\nstaticData.consecutiveFailures = 0;\n\nconst uptime = ((staticData.successes / staticData.checks) * 100).toFixed(2);\n\nreturn [{\n  json: {\n    status: 'up',\n    statusCode: $input.first().json.status,\n    uptime: `${uptime}%`,\n    totalChecks: staticData.checks,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1200, 400],
      "id": "cf555555-5555-5555-5555-555555555518",
      "name": "Record Uptime [Webhook]"
    },
    {
      "parameters": {
        "jsCode": "const staticData = $workflow.staticData;\nif (!staticData.checks) staticData.checks = 0;\nif (!staticData.successes) staticData.successes = 0;\nif (!staticData.consecutiveFailures) staticData.consecutiveFailures = 0;\n\nstaticData.checks++;\nstaticData.consecutiveFailures++;\nstaticData.lastFailure = new Date().toISOString();\n\nconst uptime = ((staticData.successes / staticData.checks) * 100).toFixed(2);\n\nreturn [{\n  json: {\n    status: 'down',\n    statusCode: $input.first().json.status,\n    uptime: `${uptime}%`,\n    consecutiveFailures: staticData.consecutiveFailures,\n    totalChecks: staticData.checks,\n    alertMessage: `ALERT: CF-protected site has been down for ${staticData.consecutiveFailures} consecutive checks (${staticData.consecutiveFailures * 30} minutes)`,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1200, 600],
      "id": "cf555555-5555-5555-5555-555555555519",
      "name": "Record Downtime [Webhook]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "alert-if-002", "leftValue": "={{ $json.consecutiveFailures }}", "rightValue": 2, "operator": { "type": "number", "operation": "gte" } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1500, 600],
      "id": "cf555555-5555-5555-5555-555555555520",
      "name": "2+ Consecutive Failures? [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "inc-004", "name": "incident", "value": "=INCIDENT: {{ $json.alertMessage }}", "type": "string" },
            { "id": "inc-005", "name": "severity", "value": "critical", "type": "string" },
            { "id": "inc-006", "name": "checkedAt", "value": "={{ $json.checkedAt }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1800, 500],
      "id": "cf555555-5555-5555-5555-555555555521",
      "name": "Create Incident Alert [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "skip-002", "name": "note", "value": "Single failure โ€” monitoring, no alert yet", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1800, 700],
      "id": "cf555555-5555-5555-5555-555555555522",
      "name": "Skip Alert (1st Failure) [Webhook]"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [2100, 500],
      "id": "cf555555-5555-5555-5555-555555555523",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Every 30 Minutes": {
      "main": [[{"node": "Set Target Config [Schedule]", "type": "main", "index": 0}]]
    },
    "Set Target Config [Schedule]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Schedule]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Schedule]": {
      "main": [[{"node": "Prepare TLS Request [Schedule]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Request [Schedule]": {
      "main": [[{"node": "Fetch via TLS Server [Schedule]", "type": "main", "index": 0}]]
    },
    "Fetch via TLS Server [Schedule]": {
      "main": [[{"node": "Site Up?", "type": "main", "index": 0}]]
    },
    "Site Up?": {
      "main": [
        [{"node": "Record Uptime", "type": "main", "index": 0}],
        [{"node": "Record Downtime", "type": "main", "index": 0}]
      ]
    },
    "Record Downtime": {
      "main": [[{"node": "2+ Consecutive Failures?", "type": "main", "index": 0}]]
    },
    "2+ Consecutive Failures?": {
      "main": [
        [{"node": "Create Incident Alert", "type": "main", "index": 0}],
        [{"node": "Skip Alert (1st Failure)", "type": "main", "index": 0}]
      ]
    },
    "Webhook Trigger": {
      "main": [[{"node": "Set Target Config [Webhook]", "type": "main", "index": 0}]]
    },
    "Set Target Config [Webhook]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Webhook]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Webhook]": {
      "main": [[{"node": "Prepare TLS Request [Webhook]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Request [Webhook]": {
      "main": [[{"node": "Fetch via TLS Server [Webhook]", "type": "main", "index": 0}]]
    },
    "Fetch via TLS Server [Webhook]": {
      "main": [[{"node": "Site Up? [Webhook]", "type": "main", "index": 0}]]
    },
    "Site Up? [Webhook]": {
      "main": [
        [{"node": "Record Uptime [Webhook]", "type": "main", "index": 0}],
        [{"node": "Record Downtime [Webhook]", "type": "main", "index": 0}]
      ]
    },
    "Record Downtime [Webhook]": {
      "main": [[{"node": "2+ Consecutive Failures? [Webhook]", "type": "main", "index": 0}]]
    },
    "2+ Consecutive Failures? [Webhook]": {
      "main": [
        [{"node": "Create Incident Alert [Webhook]", "type": "main", "index": 0}],
        [{"node": "Skip Alert (1st Failure) [Webhook]", "type": "main", "index": 0}]
      ]
    },
    "Record Uptime [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    },
    "Create Incident Alert [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    },
    "Skip Alert (1st Failure) [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Also available at workflows/use-cases/cloudflare-challenge-site-health-monitor.json.

Price Monitor

This workflow scrapes price and product data from a Cloudflare-protected page. It runs every 6 hours or on demand, compares the extracted price against the previous value stored in $workflow.staticData, and returns an alert payload if the price changed.

Schedule path:

Copy
Every 6 Hours โ†’ Set Target Config โ†’ Solve CF Challenge โ†’ Prepare TLS Request
             โ†’ Fetch via TLS Server โ†’ Extract Price โ†’ Compare Price
             โ†’ Price Changed? โ†’ Build Alert / No Change

Critical detail โ€” HTML extraction uses body, not data:

The reCAPTCHA version of this workflow uses dataPropertyName: "data" because n8n's HTTP Request node puts the response body in $json.data. The Cloudflare version fetches through the TLS server, which returns { status, body, headers } โ€” so the HTML content is in $json.body. The HTML extraction node must use dataPropertyName: "body".

json Copy
{
  "operation": "extractHtmlContent",
  "sourceData": "json",
  "dataPropertyName": "body",
  "extractionValues": {
    "values": [
      { "key": "price", "cssSelector": ".product-price, [data-price], .price" },
      { "key": "productName", "cssSelector": "h1, .product-title" }
    ]
  }
}
Click to expand full workflow JSON
json Copy
{
  "name": "Cloudflare Challenge Price Monitor / Scrape Data โ€” CapSolver + Schedule + Webhook",
  "nodes": [
    {
      "parameters": {
        "rule": { "interval": [{ "field": "hours", "hoursInterval": 6 }] }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [-500, 0],
      "id": "cf333333-3333-3333-3333-333333333301",
      "name": "Every 6 Hours"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "cfg-s-001", "name": "targetURL", "value": "https://YOUR_CF_PROTECTED_SITE.com/product-page", "type": "string" },
            { "id": "cfg-s-002", "name": "proxy", "value": "YOUR_PROXY_HOST:PORT:USER:PASS", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-200, 0],
      "id": "cf333333-3333-3333-3333-333333333302",
      "name": "Set Target Config [Schedule]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [100, 0],
      "id": "cf333333-3333-3333-3333-333333333303",
      "name": "Solve Cloudflare Challenge [Schedule]",
      "onError": "continueRegularOutput",
      "credentials": { "capSolverApi": { "id": "YOUR_CREDENTIAL_ID", "name": "CapSolver account" } }
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Target Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'none',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.targetURL,\n  method: 'GET',\n  proxy: toProxyURL(config.proxy),\n  headers\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [400, 0],
      "id": "cf333333-3333-3333-3333-333333333304",
      "name": "Prepare TLS Request [Schedule]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [700, 0],
      "id": "cf333333-3333-3333-3333-333333333305",
      "name": "Fetch via TLS Server [Schedule]"
    },
    {
      "parameters": {
        "operation": "extractHtmlContent",
        "sourceData": "json",
        "dataPropertyName": "body",
        "extractionValues": {
          "values": [
            { "key": "price", "cssSelector": ".product-price, [data-price], .price", "returnValue": "text", "returnArray": false },
            { "key": "productName", "cssSelector": "h1, .product-title", "returnValue": "text", "returnArray": false }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [1000, 0],
      "id": "cf333333-3333-3333-3333-333333333306",
      "name": "Extract Price"
    },
    {
      "parameters": {
        "jsCode": "const staticData = $workflow.staticData;\nconst currentPrice = $input.first().json.price;\nconst previousPrice = staticData.lastPrice;\nconst productName = $input.first().json.productName || 'Product';\n\nconst parsePrice = (str) => {\n  if (!str) return null;\n  const match = str.match(/[\\d,]+\\.?\\d*/);\n  return match ? parseFloat(match[0].replace(',', '')) : null;\n};\n\nconst currentNum = parsePrice(currentPrice);\nconst previousNum = parsePrice(previousPrice);\n\nstaticData.lastPrice = currentPrice;\nstaticData.lastChecked = new Date().toISOString();\n\nconst changed = previousNum !== null && currentNum !== null && currentNum !== previousNum;\nconst direction = changed ? (currentNum < previousNum ? 'dropped' : 'increased') : 'unchanged';\nconst diff = changed ? Math.abs(currentNum - previousNum).toFixed(2) : '0';\n\nreturn [{\n  json: {\n    productName,\n    currentPrice,\n    previousPrice: previousPrice || 'first check',\n    changed,\n    direction,\n    diff: changed ? `$${diff}` : null,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1300, 0],
      "id": "cf333333-3333-3333-3333-333333333307",
      "name": "Compare Price"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "price-if-001", "leftValue": "={{ $json.changed }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1600, 0],
      "id": "cf333333-3333-3333-3333-333333333308",
      "name": "Price Changed?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "alert-001", "name": "alert", "value": "=Price {{ $json.direction }} for {{ $json.productName }}: {{ $json.previousPrice }} โ†’ {{ $json.currentPrice }} ({{ $json.direction === 'dropped' ? '-' : '+' }}{{ $json.diff }})", "type": "string" },
            { "id": "alert-002", "name": "severity", "value": "={{ $json.direction === 'dropped' ? 'deal' : 'info' }}", "type": "string" },
            { "id": "alert-003", "name": "checkedAt", "value": "={{ $json.checkedAt }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1900, -80],
      "id": "cf333333-3333-3333-3333-333333333309",
      "name": "Build Alert"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "nc-001", "name": "status", "value": "no_change", "type": "string" },
            { "id": "nc-002", "name": "currentPrice", "value": "={{ $json.currentPrice }}", "type": "string" },
            { "id": "nc-003", "name": "checkedAt", "value": "={{ $json.checkedAt }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1900, 128],
      "id": "cf333333-3333-3333-3333-333333333310",
      "name": "No Change"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cloudflare-price-monitor",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-500, 500],
      "id": "cf333333-3333-3333-3333-333333333311",
      "name": "Webhook Trigger",
      "webhookId": "cf333333-aaaa-bbbb-cccc-333333333311",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "cfg-w-001", "name": "targetURL", "value": "={{ $json.body.targetURL }}", "type": "string" },
            { "id": "cfg-w-002", "name": "proxy", "value": "={{ $json.body.proxy }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-200, 500],
      "id": "cf333333-3333-3333-3333-333333333312",
      "name": "Set Target Config [Webhook]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [100, 500],
      "id": "cf333333-3333-3333-3333-333333333313",
      "name": "Solve Cloudflare Challenge [Webhook]",
      "onError": "continueRegularOutput",
      "credentials": { "capSolverApi": { "id": "YOUR_CREDENTIAL_ID", "name": "CapSolver account" } }
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Target Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'none',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.targetURL,\n  method: 'GET',\n  proxy: toProxyURL(config.proxy),\n  headers\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [400, 500],
      "id": "cf333333-3333-3333-3333-333333333314",
      "name": "Prepare TLS Request [Webhook]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [700, 500],
      "id": "cf333333-3333-3333-3333-333333333315",
      "name": "Fetch via TLS Server [Webhook]"
    },
    {
      "parameters": {
        "operation": "extractHtmlContent",
        "sourceData": "json",
        "dataPropertyName": "body",
        "extractionValues": {
          "values": [
            { "key": "price", "cssSelector": ".product-price, [data-price], .price", "returnValue": "text", "returnArray": false },
            { "key": "productName", "cssSelector": "h1, .product-title", "returnValue": "text", "returnArray": false }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.html",
      "typeVersion": 1.2,
      "position": [1000, 500],
      "id": "cf333333-3333-3333-3333-333333333316",
      "name": "Extract Price [Webhook]"
    },
    {
      "parameters": {
        "jsCode": "const staticData = $workflow.staticData;\nconst currentPrice = $input.first().json.price;\nconst previousPrice = staticData.lastPrice;\nconst productName = $input.first().json.productName || 'Product';\n\nconst parsePrice = (str) => {\n  if (!str) return null;\n  const match = str.match(/[\\d,]+\\.?\\d*/);\n  return match ? parseFloat(match[0].replace(',', '')) : null;\n};\n\nconst currentNum = parsePrice(currentPrice);\nconst previousNum = parsePrice(previousPrice);\n\nstaticData.lastPrice = currentPrice;\nstaticData.lastChecked = new Date().toISOString();\n\nconst changed = previousNum !== null && currentNum !== null && currentNum !== previousNum;\nconst direction = changed ? (currentNum < previousNum ? 'dropped' : 'increased') : 'unchanged';\nconst diff = changed ? Math.abs(currentNum - previousNum).toFixed(2) : '0';\n\nreturn [{\n  json: {\n    productName,\n    currentPrice,\n    previousPrice: previousPrice || 'first check',\n    changed,\n    direction,\n    diff: changed ? `$${diff}` : null,\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1300, 500],
      "id": "cf333333-3333-3333-3333-333333333317",
      "name": "Compare Price [Webhook]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "price-if-002", "leftValue": "={{ $json.changed }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1600, 500],
      "id": "cf333333-3333-3333-3333-333333333318",
      "name": "Price Changed? [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "alert-004", "name": "alert", "value": "=Price {{ $json.direction }} for {{ $json.productName }}: {{ $json.previousPrice }} โ†’ {{ $json.currentPrice }} ({{ $json.direction === 'dropped' ? '-' : '+' }}{{ $json.diff }})", "type": "string" },
            { "id": "alert-005", "name": "severity", "value": "={{ $json.direction === 'dropped' ? 'deal' : 'info' }}", "type": "string" },
            { "id": "alert-006", "name": "checkedAt", "value": "={{ $json.checkedAt }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1900, 420],
      "id": "cf333333-3333-3333-3333-333333333319",
      "name": "Build Alert [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "nc-004", "name": "status", "value": "no_change", "type": "string" },
            { "id": "nc-005", "name": "currentPrice", "value": "={{ $json.currentPrice }}", "type": "string" },
            { "id": "nc-006", "name": "checkedAt", "value": "={{ $json.checkedAt }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1900, 628],
      "id": "cf333333-3333-3333-3333-333333333320",
      "name": "No Change [Webhook]"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [2200, 500],
      "id": "cf333333-3333-3333-3333-333333333321",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Every 6 Hours": {
      "main": [[{"node": "Set Target Config [Schedule]", "type": "main", "index": 0}]]
    },
    "Set Target Config [Schedule]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Schedule]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Schedule]": {
      "main": [[{"node": "Prepare TLS Request [Schedule]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Request [Schedule]": {
      "main": [[{"node": "Fetch via TLS Server [Schedule]", "type": "main", "index": 0}]]
    },
    "Fetch via TLS Server [Schedule]": {
      "main": [[{"node": "Extract Price", "type": "main", "index": 0}]]
    },
    "Extract Price": {
      "main": [[{"node": "Compare Price", "type": "main", "index": 0}]]
    },
    "Compare Price": {
      "main": [[{"node": "Price Changed?", "type": "main", "index": 0}]]
    },
    "Price Changed?": {
      "main": [
        [{"node": "Build Alert", "type": "main", "index": 0}],
        [{"node": "No Change", "type": "main", "index": 0}]
      ]
    },
    "Webhook Trigger": {
      "main": [[{"node": "Set Target Config [Webhook]", "type": "main", "index": 0}]]
    },
    "Set Target Config [Webhook]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Webhook]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Webhook]": {
      "main": [[{"node": "Prepare TLS Request [Webhook]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Request [Webhook]": {
      "main": [[{"node": "Fetch via TLS Server [Webhook]", "type": "main", "index": 0}]]
    },
    "Fetch via TLS Server [Webhook]": {
      "main": [[{"node": "Extract Price [Webhook]", "type": "main", "index": 0}]]
    },
    "Extract Price [Webhook]": {
      "main": [[{"node": "Compare Price [Webhook]", "type": "main", "index": 0}]]
    },
    "Compare Price [Webhook]": {
      "main": [[{"node": "Price Changed? [Webhook]", "type": "main", "index": 0}]]
    },
    "Price Changed? [Webhook]": {
      "main": [
        [{"node": "Build Alert [Webhook]", "type": "main", "index": 0}],
        [{"node": "No Change [Webhook]", "type": "main", "index": 0}]
      ]
    },
    "Build Alert [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    },
    "No Change [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Also available at workflows/use-cases/cloudflare-challenge-price-monitor-scheduled.json.

Account Login

This workflow logs into your own account on a Cloudflare-protected site. It's a two-phase approach: first solve the Cloudflare Challenge to get cf_clearance + userAgent, then POST the login form through the TLS server with those credentials attached.

Schedule path:

Copy
Every 24 Hours โ†’ Set Login Config โ†’ Solve CF Challenge
             โ†’ Prepare TLS Login Request โ†’ Submit Login via TLS Server
             โ†’ Login Successful? โ†’ Mark Success / Mark Failed

The key difference from reCAPTCHA login: The reCAPTCHA version submits a g-recaptcha-response token in the form body. The Cloudflare version doesn't submit a token at all โ€” instead, it sends the cf_clearance cookie and the exact userAgent from the solve as HTTP headers. The form body contains only the login credentials.

Prepare TLS Login Request code:

javascript Copy
const config = $('Set Login Config [Schedule]').first().json;
const capResult = $input.first().json;

// Convert proxy from host:port:user:pass to http://user:pass@host:port
function toProxyURL(proxy) {
  if (!proxy) return '';
  if (proxy.startsWith('http')) return proxy;
  const parts = proxy.split(':');
  if (parts.length === 4) return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;
  return proxy;
}

let cookieStr = '';
let ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';

if (capResult.data && capResult.data.solution) {
  const solution = capResult.data.solution;
  const cookies = solution.cookies;
  cookieStr = cookies && typeof cookies === 'object'
    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')
    : (cookies || '');
  if (solution.userAgent) ua = solution.userAgent;
}

const chromeMatch = ua.match(/Chrome\/(\d+)/);
const chromeVer = chromeMatch ? chromeMatch[1] : '145';
const secChUa = `"Chromium";v="${chromeVer}", "Not A(Brand";v="8", "Google Chrome";v="${chromeVer}"`;

const params = new URLSearchParams();
params.set(config.usernameField, config.usernameValue);
params.set(config.passwordField, config.passwordValue);

const headers = {
  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
  'accept-language': 'en-US,en;q=0.9',
  'content-type': 'application/x-www-form-urlencoded',
  'sec-ch-ua': secChUa,
  'sec-ch-ua-mobile': '?0',
  'sec-ch-ua-platform': '"Windows"',
  'sec-fetch-dest': 'document',
  'sec-fetch-mode': 'navigate',
  'sec-fetch-site': 'same-origin',
  'sec-fetch-user': '?1',
  'upgrade-insecure-requests': '1',
  'user-agent': ua
};

if (cookieStr) headers['cookie'] = cookieStr;

return [{ json: {
  url: config.loginActionURL,
  method: 'POST',
  proxy: toProxyURL(config.proxy),
  headers,
  body: params.toString()
}}];

The Login Successful? node checks both the TLS server response status and whether the response body contains the configured successMarker string (e.g., "account-dashboard").

Click to expand full workflow JSON
json Copy
{
  "name": "Cloudflare Challenge Account Login โ€” CapSolver + Schedule + Webhook",
  "nodes": [
    {
      "parameters": {
        "rule": { "interval": [{ "field": "hours", "hoursInterval": 24 }] }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [-640, 0],
      "id": "cf666666-6666-6666-6666-666666666601",
      "name": "Every 24 Hours"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "login-001", "name": "targetURL", "value": "https://YOUR_CF_PROTECTED_SITE.com/login", "type": "string" },
            { "id": "login-002", "name": "loginActionURL", "value": "https://YOUR_CF_PROTECTED_SITE.com/login", "type": "string" },
            { "id": "login-003", "name": "proxy", "value": "YOUR_PROXY_HOST:PORT:USER:PASS", "type": "string" },
            { "id": "login-004", "name": "usernameField", "value": "email", "type": "string" },
            { "id": "login-005", "name": "passwordField", "value": "password", "type": "string" },
            { "id": "login-006", "name": "usernameValue", "value": "[email protected]", "type": "string" },
            { "id": "login-007", "name": "passwordValue", "value": "YOUR_ACCOUNT_PASSWORD", "type": "string" },
            { "id": "login-008", "name": "successMarker", "value": "account-dashboard", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-336, 0],
      "id": "cf666666-6666-6666-6666-666666666602",
      "name": "Set Login Config [Schedule]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [-32, 0],
      "id": "cf666666-6666-6666-6666-666666666603",
      "name": "Solve Cloudflare Challenge [Schedule]",
      "credentials": { "capSolverApi": { "id": "YOUR_CREDENTIAL_ID", "name": "CapSolver account" } },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Login Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst params = new URLSearchParams();\nparams.set(config.usernameField, config.usernameValue);\nparams.set(config.passwordField, config.passwordValue);\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'content-type': 'application/x-www-form-urlencoded',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'same-origin',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.loginActionURL,\n  method: 'POST',\n  proxy: toProxyURL(config.proxy),\n  headers,\n  body: params.toString()\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [272, 0],
      "id": "cf666666-6666-6666-6666-666666666604",
      "name": "Prepare TLS Login Request [Schedule]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [576, 0],
      "id": "cf666666-6666-6666-6666-666666666605",
      "name": "Submit Login via TLS Server [Schedule]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "login-if-001", "leftValue": "={{ $json.status < 400 && String($json.body || '').includes($('Set Login Config [Schedule]').item.json.successMarker) }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [880, 0],
      "id": "cf666666-6666-6666-6666-666666666606",
      "name": "Login Successful? [Schedule]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "login-010", "name": "action", "value": "account_login", "type": "string" },
            { "id": "login-011", "name": "status", "value": "success", "type": "string" },
            { "id": "login-012", "name": "message", "value": "Configured account login flow succeeded (via Cloudflare Challenge bypass)", "type": "string" },
            { "id": "login-013", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, -80],
      "id": "cf666666-6666-6666-6666-666666666607",
      "name": "Mark Login Success [Schedule]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "login-014", "name": "action", "value": "account_login", "type": "string" },
            { "id": "login-015", "name": "status", "value": "failed", "type": "string" },
            { "id": "login-016", "name": "statusCode", "value": "={{ $json.status }}", "type": "number" },
            { "id": "login-017", "name": "message", "value": "Login response did not match the configured success marker", "type": "string" },
            { "id": "login-018", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, 120],
      "id": "cf666666-6666-6666-6666-666666666608",
      "name": "Mark Login Failed [Schedule]"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cloudflare-account-login",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-640, 420],
      "id": "cf666666-6666-6666-6666-666666666609",
      "name": "Webhook Trigger",
      "webhookId": "cf666666-aaaa-bbbb-cccc-666666666609",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "login-019", "name": "targetURL", "value": "={{ $json.body.targetURL }}", "type": "string" },
            { "id": "login-020", "name": "loginActionURL", "value": "={{ $json.body.loginActionURL }}", "type": "string" },
            { "id": "login-021", "name": "proxy", "value": "={{ $json.body.proxy }}", "type": "string" },
            { "id": "login-022", "name": "usernameField", "value": "={{ $json.body.usernameField }}", "type": "string" },
            { "id": "login-023", "name": "passwordField", "value": "={{ $json.body.passwordField }}", "type": "string" },
            { "id": "login-024", "name": "usernameValue", "value": "={{ $json.body.usernameValue }}", "type": "string" },
            { "id": "login-025", "name": "passwordValue", "value": "={{ $json.body.passwordValue }}", "type": "string" },
            { "id": "login-026", "name": "successMarker", "value": "={{ $json.body.successMarker }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-336, 420],
      "id": "cf666666-6666-6666-6666-666666666610",
      "name": "Set Login Config [Webhook]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [-32, 420],
      "id": "cf666666-6666-6666-6666-666666666611",
      "name": "Solve Cloudflare Challenge [Webhook]",
      "credentials": { "capSolverApi": { "id": "YOUR_CREDENTIAL_ID", "name": "CapSolver account" } },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Login Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst params = new URLSearchParams();\nparams.set(config.usernameField, config.usernameValue);\nparams.set(config.passwordField, config.passwordValue);\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'content-type': 'application/x-www-form-urlencoded',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'same-origin',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.loginActionURL,\n  method: 'POST',\n  proxy: toProxyURL(config.proxy),\n  headers,\n  body: params.toString()\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [272, 420],
      "id": "cf666666-6666-6666-6666-666666666612",
      "name": "Prepare TLS Login Request [Webhook]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [576, 420],
      "id": "cf666666-6666-6666-6666-666666666613",
      "name": "Submit Login via TLS Server [Webhook]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "login-if-002", "leftValue": "={{ $json.status < 400 && String($json.body || '').includes($('Set Login Config [Webhook]').item.json.successMarker) }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [880, 420],
      "id": "cf666666-6666-6666-6666-666666666614",
      "name": "Login Successful? [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "login-028", "name": "action", "value": "account_login", "type": "string" },
            { "id": "login-029", "name": "status", "value": "success", "type": "string" },
            { "id": "login-030", "name": "message", "value": "Configured account login flow succeeded (via Cloudflare Challenge bypass)", "type": "string" },
            { "id": "login-031", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, 340],
      "id": "cf666666-6666-6666-6666-666666666615",
      "name": "Mark Login Success [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "login-032", "name": "action", "value": "account_login", "type": "string" },
            { "id": "login-033", "name": "status", "value": "failed", "type": "string" },
            { "id": "login-034", "name": "statusCode", "value": "={{ $json.status }}", "type": "number" },
            { "id": "login-035", "name": "message", "value": "Login response did not match the configured success marker", "type": "string" },
            { "id": "login-036", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, 540],
      "id": "cf666666-6666-6666-6666-666666666616",
      "name": "Mark Login Failed [Webhook]"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [1488, 420],
      "id": "cf666666-6666-6666-6666-666666666617",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Every 24 Hours": {
      "main": [[{"node": "Set Login Config [Schedule]", "type": "main", "index": 0}]]
    },
    "Set Login Config [Schedule]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Schedule]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Schedule]": {
      "main": [[{"node": "Prepare TLS Login Request [Schedule]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Login Request [Schedule]": {
      "main": [[{"node": "Submit Login via TLS Server [Schedule]", "type": "main", "index": 0}]]
    },
    "Submit Login via TLS Server [Schedule]": {
      "main": [[{"node": "Login Successful? [Schedule]", "type": "main", "index": 0}]]
    },
    "Login Successful? [Schedule]": {
      "main": [
        [{"node": "Mark Login Success [Schedule]", "type": "main", "index": 0}],
        [{"node": "Mark Login Failed [Schedule]", "type": "main", "index": 0}]
      ]
    },
    "Webhook Trigger": {
      "main": [[{"node": "Set Login Config [Webhook]", "type": "main", "index": 0}]]
    },
    "Set Login Config [Webhook]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Webhook]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Webhook]": {
      "main": [[{"node": "Prepare TLS Login Request [Webhook]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Login Request [Webhook]": {
      "main": [[{"node": "Submit Login via TLS Server [Webhook]", "type": "main", "index": 0}]]
    },
    "Submit Login via TLS Server [Webhook]": {
      "main": [[{"node": "Login Successful? [Webhook]", "type": "main", "index": 0}]]
    },
    "Login Successful? [Webhook]": {
      "main": [
        [{"node": "Mark Login Success [Webhook]", "type": "main", "index": 0}],
        [{"node": "Mark Login Failed [Webhook]", "type": "main", "index": 0}]
      ]
    },
    "Mark Login Success [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    },
    "Mark Login Failed [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Also available at workflows/use-cases/cloudflare-challenge-account-login-scheduled-webhook.json.

Account Signup

Same two-phase pattern as login but for registering with your own email. The Config node includes nameField, emailField, passwordField and their corresponding values. The Prepare TLS Signup Request code builds a form POST with three fields instead of two.

Warning: The schedule trigger is named "Every 24 Hours (Use Carefully)" because repeated scheduled execution can create duplicate registrations. Leave the workflow inactive until you've configured and tested it.

Click to expand full workflow JSON
json Copy
{
  "name": "Cloudflare Challenge Account Signup โ€” CapSolver + Schedule + Webhook",
  "nodes": [
    {
      "parameters": {
        "rule": { "interval": [{ "field": "hours", "hoursInterval": 24 }] }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [-640, 0],
      "id": "cf777777-7777-7777-7777-777777777701",
      "name": "Every 24 Hours (Use Carefully)"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "signup-001", "name": "targetURL", "value": "https://YOUR_CF_PROTECTED_SITE.com/signup", "type": "string" },
            { "id": "signup-002", "name": "signupActionURL", "value": "https://YOUR_CF_PROTECTED_SITE.com/signup", "type": "string" },
            { "id": "signup-003", "name": "proxy", "value": "YOUR_PROXY_HOST:PORT:USER:PASS", "type": "string" },
            { "id": "signup-004", "name": "nameField", "value": "name", "type": "string" },
            { "id": "signup-005", "name": "emailField", "value": "email", "type": "string" },
            { "id": "signup-006", "name": "passwordField", "value": "password", "type": "string" },
            { "id": "signup-007", "name": "nameValue", "value": "Your Name", "type": "string" },
            { "id": "signup-008", "name": "emailValue", "value": "[email protected]", "type": "string" },
            { "id": "signup-009", "name": "passwordValue", "value": "YOUR_ACCOUNT_PASSWORD", "type": "string" },
            { "id": "signup-010", "name": "successMarker", "value": "welcome", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-336, 0],
      "id": "cf777777-7777-7777-7777-777777777702",
      "name": "Set Signup Config [Schedule]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [-32, 0],
      "id": "cf777777-7777-7777-7777-777777777703",
      "name": "Solve Cloudflare Challenge [Schedule]",
      "credentials": { "capSolverApi": { "id": "YOUR_CREDENTIAL_ID", "name": "CapSolver account" } },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Signup Config [Schedule]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst params = new URLSearchParams();\nparams.set(config.nameField, config.nameValue);\nparams.set(config.emailField, config.emailValue);\nparams.set(config.passwordField, config.passwordValue);\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'content-type': 'application/x-www-form-urlencoded',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'same-origin',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.signupActionURL,\n  method: 'POST',\n  proxy: toProxyURL(config.proxy),\n  headers,\n  body: params.toString()\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [272, 0],
      "id": "cf777777-7777-7777-7777-777777777704",
      "name": "Prepare TLS Signup Request [Schedule]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [576, 0],
      "id": "cf777777-7777-7777-7777-777777777705",
      "name": "Submit Signup via TLS Server [Schedule]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "signup-if-001", "leftValue": "={{ $json.status < 400 && String($json.body || '').includes($('Set Signup Config [Schedule]').item.json.successMarker) }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [880, 0],
      "id": "cf777777-7777-7777-7777-777777777706",
      "name": "Signup Successful? [Schedule]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "signup-012", "name": "action", "value": "account_signup", "type": "string" },
            { "id": "signup-013", "name": "status", "value": "success", "type": "string" },
            { "id": "signup-014", "name": "message", "value": "Configured account signup flow succeeded (via Cloudflare Challenge bypass)", "type": "string" },
            { "id": "signup-015", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, -80],
      "id": "cf777777-7777-7777-7777-777777777707",
      "name": "Mark Signup Success [Schedule]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "signup-016", "name": "action", "value": "account_signup", "type": "string" },
            { "id": "signup-017", "name": "status", "value": "failed", "type": "string" },
            { "id": "signup-018", "name": "statusCode", "value": "={{ $json.status }}", "type": "number" },
            { "id": "signup-019", "name": "message", "value": "Signup response did not match the configured success marker", "type": "string" },
            { "id": "signup-020", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, 120],
      "id": "cf777777-7777-7777-7777-777777777708",
      "name": "Mark Signup Failed [Schedule]"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cloudflare-account-signup",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [-640, 420],
      "id": "cf777777-7777-7777-7777-777777777709",
      "name": "Webhook Trigger",
      "webhookId": "cf777777-aaaa-bbbb-cccc-777777777709",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "signup-021", "name": "targetURL", "value": "={{ $json.body.targetURL }}", "type": "string" },
            { "id": "signup-022", "name": "signupActionURL", "value": "={{ $json.body.signupActionURL }}", "type": "string" },
            { "id": "signup-023", "name": "proxy", "value": "={{ $json.body.proxy }}", "type": "string" },
            { "id": "signup-024", "name": "nameField", "value": "={{ $json.body.nameField }}", "type": "string" },
            { "id": "signup-025", "name": "emailField", "value": "={{ $json.body.emailField }}", "type": "string" },
            { "id": "signup-026", "name": "passwordField", "value": "={{ $json.body.passwordField }}", "type": "string" },
            { "id": "signup-027", "name": "nameValue", "value": "={{ $json.body.nameValue }}", "type": "string" },
            { "id": "signup-028", "name": "emailValue", "value": "={{ $json.body.emailValue }}", "type": "string" },
            { "id": "signup-029", "name": "passwordValue", "value": "={{ $json.body.passwordValue }}", "type": "string" },
            { "id": "signup-030", "name": "successMarker", "value": "={{ $json.body.successMarker }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [-336, 420],
      "id": "cf777777-7777-7777-7777-777777777710",
      "name": "Set Signup Config [Webhook]"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.targetURL }}",
        "proxy": "={{ $json.proxy }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [-32, 420],
      "id": "cf777777-7777-7777-7777-777777777711",
      "name": "Solve Cloudflare Challenge [Webhook]",
      "credentials": { "capSolverApi": { "id": "YOUR_CREDENTIAL_ID", "name": "CapSolver account" } },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const config = $('Set Signup Config [Webhook]').first().json;\nconst capResult = $input.first().json;\n\nfunction toProxyURL(proxy) {\n  if (!proxy) return '';\n  if (proxy.startsWith('http')) return proxy;\n  const parts = proxy.split(':');\n  if (parts.length === 4) {\n    return `http://${parts[2]}:${parts[3]}@${parts[0]}:${parts[1]}`;\n  }\n  return proxy;\n}\n\nlet cookieStr = '';\nlet ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';\n\nif (capResult.data && capResult.data.solution) {\n  const solution = capResult.data.solution;\n  const cookies = solution.cookies;\n  cookieStr = cookies && typeof cookies === 'object'\n    ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n    : (cookies || '');\n  if (solution.userAgent) ua = solution.userAgent;\n}\n\nconst chromeMatch = ua.match(/Chrome\\/(\\d+)/);\nconst chromeVer = chromeMatch ? chromeMatch[1] : '145';\nconst secChUa = `\"Chromium\";v=\"${chromeVer}\", \"Not A(Brand\";v=\"8\", \"Google Chrome\";v=\"${chromeVer}\"`;\n\nconst params = new URLSearchParams();\nparams.set(config.nameField, config.nameValue);\nparams.set(config.emailField, config.emailValue);\nparams.set(config.passwordField, config.passwordValue);\n\nconst headers = {\n  'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',\n  'accept-language': 'en-US,en;q=0.9',\n  'content-type': 'application/x-www-form-urlencoded',\n  'sec-ch-ua': secChUa,\n  'sec-ch-ua-mobile': '?0',\n  'sec-ch-ua-platform': '\"Windows\"',\n  'sec-fetch-dest': 'document',\n  'sec-fetch-mode': 'navigate',\n  'sec-fetch-site': 'same-origin',\n  'sec-fetch-user': '?1',\n  'upgrade-insecure-requests': '1',\n  'user-agent': ua\n};\n\nif (cookieStr) headers['cookie'] = cookieStr;\n\nreturn [{ json: {\n  url: config.signupActionURL,\n  method: 'POST',\n  proxy: toProxyURL(config.proxy),\n  headers,\n  body: params.toString()\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [272, 420],
      "id": "cf777777-7777-7777-7777-777777777712",
      "name": "Prepare TLS Signup Request [Webhook]"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://localhost:7878/fetch",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000 }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [576, 420],
      "id": "cf777777-7777-7777-7777-777777777713",
      "name": "Submit Signup via TLS Server [Webhook]"
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            { "id": "signup-if-002", "leftValue": "={{ $json.status < 400 && String($json.body || '').includes($('Set Signup Config [Webhook]').item.json.successMarker) }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true } }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [880, 420],
      "id": "cf777777-7777-7777-7777-777777777714",
      "name": "Signup Successful? [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "signup-032", "name": "action", "value": "account_signup", "type": "string" },
            { "id": "signup-033", "name": "status", "value": "success", "type": "string" },
            { "id": "signup-034", "name": "message", "value": "Configured account signup flow succeeded (via Cloudflare Challenge bypass)", "type": "string" },
            { "id": "signup-035", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, 340],
      "id": "cf777777-7777-7777-7777-777777777715",
      "name": "Mark Signup Success [Webhook]"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "signup-036", "name": "action", "value": "account_signup", "type": "string" },
            { "id": "signup-037", "name": "status", "value": "failed", "type": "string" },
            { "id": "signup-038", "name": "statusCode", "value": "={{ $json.status }}", "type": "number" },
            { "id": "signup-039", "name": "message", "value": "Signup response did not match the configured success marker", "type": "string" },
            { "id": "signup-040", "name": "checkedAt", "value": "={{ new Date().toISOString() }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1184, 540],
      "id": "cf777777-7777-7777-7777-777777777716",
      "name": "Mark Signup Failed [Webhook]"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [1488, 420],
      "id": "cf777777-7777-7777-7777-777777777717",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Every 24 Hours (Use Carefully)": {
      "main": [[{"node": "Set Signup Config [Schedule]", "type": "main", "index": 0}]]
    },
    "Set Signup Config [Schedule]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Schedule]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Schedule]": {
      "main": [[{"node": "Prepare TLS Signup Request [Schedule]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Signup Request [Schedule]": {
      "main": [[{"node": "Submit Signup via TLS Server [Schedule]", "type": "main", "index": 0}]]
    },
    "Submit Signup via TLS Server [Schedule]": {
      "main": [[{"node": "Signup Successful? [Schedule]", "type": "main", "index": 0}]]
    },
    "Signup Successful? [Schedule]": {
      "main": [
        [{"node": "Mark Signup Success [Schedule]", "type": "main", "index": 0}],
        [{"node": "Mark Signup Failed [Schedule]", "type": "main", "index": 0}]
      ]
    },
    "Webhook Trigger": {
      "main": [[{"node": "Set Signup Config [Webhook]", "type": "main", "index": 0}]]
    },
    "Set Signup Config [Webhook]": {
      "main": [[{"node": "Solve Cloudflare Challenge [Webhook]", "type": "main", "index": 0}]]
    },
    "Solve Cloudflare Challenge [Webhook]": {
      "main": [[{"node": "Prepare TLS Signup Request [Webhook]", "type": "main", "index": 0}]]
    },
    "Prepare TLS Signup Request [Webhook]": {
      "main": [[{"node": "Submit Signup via TLS Server [Webhook]", "type": "main", "index": 0}]]
    },
    "Submit Signup via TLS Server [Webhook]": {
      "main": [[{"node": "Signup Successful? [Webhook]", "type": "main", "index": 0}]]
    },
    "Signup Successful? [Webhook]": {
      "main": [
        [{"node": "Mark Signup Success [Webhook]", "type": "main", "index": 0}],
        [{"node": "Mark Signup Failed [Webhook]", "type": "main", "index": 0}]
      ]
    },
    "Mark Signup Success [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    },
    "Mark Signup Failed [Webhook]": {
      "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Also available at workflows/use-cases/cloudflare-challenge-account-signup-scheduled-webhook.json.

Challenge Solver API

The other use-case workflows solve the Cloudflare Challenge and fetch the page in one pipeline. But what if your application already has its own HTTP client with proper TLS fingerprinting โ€” and you just need the cf_clearance cookie and userAgent?

The Solver API workflow exposes a single webhook endpoint that accepts a website URL and proxy, solves the challenge via CapSolver, and returns the raw solution. No TLS server, no page fetch โ€” just the credentials your app needs to make its own request.

This follows the same minimal pattern as the other Solver APIs in the repo (reCAPTCHA v2 โ€” Solver API, reCAPTCHA v3 โ€” Solver API, Turnstile โ€” Solver API). The only difference is a Format Solution code node โ€” needed because AntiCloudflareTask returns cookies as an object that must be serialized, unlike reCAPTCHA/Turnstile which return a simple token string.

Flow:

Copy
Webhook (POST /solver-cloudflare-challenge) โ†’ Cloudflare Challenge (CapSolver)
                                             โ†’ Format Solution โ†’ Respond to Webhook

4 nodes, webhook-only, no schedule path, no TLS server dependency.

Request:

bash Copy
curl -X POST https://your-n8n-instance.com/webhook/solver-cloudflare-challenge \
  -H "Content-Type: application/json" \
  -d '{
    "websiteURL": "https://protected-site.com",
    "proxy": "host:port:user:pass"
  }'

Note: The field is websiteURL (not targetURL) โ€” matching the field name used by all other Solver APIs and the CapSolver node itself.

Successful response:

json Copy
{
  "success": true,
  "cf_clearance": "abc123...",
  "cookies": "cf_clearance=abc123...",
  "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/145.0.0.0 ...",
  "solvedAt": "2026-03-11T12:00:00.000Z"
}

Failed response (no challenge found, bad proxy, etc.):

json Copy
{
  "success": false,
  "error": "No solution returned โ€” site may not be showing a challenge"
}

Why the Format Solution node is needed:

The other Solver APIs (reCAPTCHA v2, v3, Turnstile) can pass the CapSolver output directly to the Respond to Webhook node with $json.data โ€” because the solution is a simple object with a token string. Cloudflare Challenge is different:

  1. solution.cookies is an object ({ cf_clearance: "..." }), not a string โ€” it needs to be serialized to "cf_clearance=..." for use as a Cookie header
  2. The workflow uses continueOnFail: true on the CapSolver node so it can return a structured error response instead of crashing
  3. The response extracts just cf_clearance and cookies (the full cookie string) for convenience

Format Solution code:

javascript Copy
const input = $input.first().json;

if (input.error || !input.data || !input.data.solution) {
  const errorMsg = input.error
    ? (input.error.message || JSON.stringify(input.error))
    : 'No solution returned โ€” site may not be showing a challenge';
  return [{ json: { success: false, error: errorMsg } }];
}

const solution = input.data.solution;
const cookies = solution.cookies;
const cfClearance = (cookies && typeof cookies === 'object')
  ? (cookies.cf_clearance || '') : '';
const cookieString = (cookies && typeof cookies === 'object')
  ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')
  : (cookies || '');

return [{ json: {
  success: true,
  cf_clearance: cfClearance,
  cookies: cookieString,
  userAgent: solution.userAgent || '',
  solvedAt: new Date().toISOString()
}}];

Key differences from the scraper workflow:

  • No TLS server required โ€” the workflow doesn't fetch the page, so no Chrome TLS fingerprinting is needed on the n8n side
  • No N8N_BLOCK_ACCESS_TO_LOCALHOST needed โ€” no localhost HTTP calls
  • Caller is responsible for TLS fingerprinting โ€” if the caller's HTTP client has a non-browser TLS fingerprint, Cloudflare will still re-challenge even with a valid cf_clearance
  • Webhook-only โ€” no schedule path (an API is called on demand, not on a timer)
  • Uses websiteURL โ€” matching the other Solver APIs and CapSolver's native field name (the scraper workflows use targetURL because they also handle the fetch)
Click to expand full workflow JSON
json Copy
{
  "name": "Cloudflare Challenge โ€” Solver API",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "solver-cloudflare-challenge",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-208, 0],
      "id": "cf770001-7777-7777-7777-777777777701",
      "name": "Webhook",
      "webhookId": "cf770001-aaaa-bbbb-cccc-777777777701"
    },
    {
      "parameters": {
        "operation": "Cloudflare Challenge",
        "type": "AntiCloudflareTask",
        "websiteURL": "={{ $json.body.websiteURL }}",
        "proxy": "={{ $json.body.proxy }}",
        "userAgent": "={{ $json.body.userAgent }}",
        "html": "={{ $json.body.html }}"
      },
      "type": "n8n-nodes-capsolver.capSolver",
      "typeVersion": 1,
      "position": [112, 0],
      "id": "cf770001-7777-7777-7777-777777777702",
      "name": "Cloudflare Challenge",
      "credentials": {
        "capSolverApi": {
          "id": "YOUR_CREDENTIAL_ID",
          "name": "CapSolver account"
        }
      },
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nif (input.error || !input.data || !input.data.solution) {\n  const errorMsg = input.error\n    ? (input.error.message || JSON.stringify(input.error))\n    : 'No solution returned โ€” site may not be showing a challenge';\n  return [{ json: { success: false, error: errorMsg } }];\n}\n\nconst solution = input.data.solution;\nconst cookies = solution.cookies;\nconst cfClearance = (cookies && typeof cookies === 'object') ? (cookies.cf_clearance || '') : '';\nconst cookieString = (cookies && typeof cookies === 'object')\n  ? Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ')\n  : (cookies || '');\n\nreturn [{ json: {\n  success: true,\n  cf_clearance: cfClearance,\n  cookies: cookieString,\n  userAgent: solution.userAgent || '',\n  solvedAt: new Date().toISOString()\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [400, 0],
      "id": "cf770001-7777-7777-7777-777777777703",
      "name": "Format Solution"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [700, 0],
      "id": "cf770001-7777-7777-7777-777777777704",
      "name": "Respond to Webhook"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [[{ "node": "Cloudflare Challenge", "type": "main", "index": 0 }]]
    },
    "Cloudflare Challenge": {
      "main": [[{ "node": "Format Solution", "type": "main", "index": 0 }]]
    },
    "Format Solution": {
      "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" }
}

Also available at workflows/use-cases/cloudflare-challenge-solver-api.json.


Troubleshooting

CapSolver returns error 1002 ("Failed to solve")

Symptom CapSolver node fails with Solve failed: Failed to solve the captcha: 1002
Cause Datacenter proxy IP flagged by Cloudflare's bot scoring โ€” the solve attempt fails before completing
Fix Switch to a residential or mobile proxy with sticky session support

CapSolver times out after 120 seconds

Symptom Get task result timeout: unable to solve within 120000 seconds
Cause Proxy is too slow, or the site uses Cloudflare's stricter UAM (Under Attack Mode)
Fix Try a different proxy endpoint or a proxy provider with better routing to the target site's region

"SSRF protection: Localhost access is blocked in strict mode"

Symptom Fetch via TLS Server node fails with SSRF protection error
Cause N8N_BLOCK_ACCESS_TO_LOCALHOST is still set to its default value
Fix Run: N8N_BLOCK_ACCESS_TO_LOCALHOST=false pm2 restart n8n --update-env โ€” the --update-env flag is required

Got a challenge page back instead of content (status 403)

Symptom HTTP 200 from workflow, but the response body is a Cloudflare challenge page
Cause 1 The target site redirected to a subdomain โ€” cf_clearance is domain-specific and doesn't transfer
Cause 2 The site has additional bot protection on top of Cloudflare (proprietary layer)
Fix Confirm the exact URL that serves the content. If it's a different domain or subdomain, you need to solve the challenge for that specific URL

TLS server returns {"error": "url is required"} or empty body

Symptom Fetch via TLS Server receives {"": ""} as the request body
Cause HTTP Request node using contentType: "json" with JSON.stringify() โ€” n8n serializes incorrectly
Fix Set contentType to raw and rawContentType to application/json as shown in the node configuration above

TLS server not responding

Symptom Connection refused on port 7878
Cause The Go binary is not running
Fix Run pm2 status โ€” if tls-server is not listed or shows stopped, run pm2 start ~/tls-server/main --name tls-server && pm2 save

"ERROR_KEY_DOES_NOT_EXIST" or "ERROR_ZERO_BALANCE"

Symptom CapSolver node fails immediately with an API error
Cause Invalid API key or insufficient account balance
Fix Check your API key in Settings โ†’ Credentials and verify your balance at the CapSolver Dashboard

Best Practices

  1. Use residential or mobile proxies โ€” Datacenter IPs are flagged by Cloudflare's bot scoring. For AntiCloudflareTask to succeed on most protected sites, you need a proxy with residential or mobile IP ranges.

  2. Use the same proxy IP for solve and fetch โ€” cf_clearance is bound to the IP address that solved the challenge. If your proxy rotates IPs between requests, Cloudflare will re-challenge. Use sticky session mode on your proxy provider.

  3. Forward userAgent exactly โ€” CapSolver selects a specific User-Agent for each solve. The cf_clearance cookie is validated against this UA. Any change โ€” even a minor version difference โ€” can cause Cloudflare to reject the clearance.

  4. Fetch the page immediately after solving โ€” cf_clearance can expire within minutes on strictly-configured sites. The workflow is designed to chain these steps, but avoid adding delays between the CapSolver node and the TLS server fetch.

  5. Keep the TLS server alive with PM2 โ€” Run pm2 save and pm2 startup to ensure the TLS server restarts automatically after reboots.

  6. Use self-hosted n8n โ€” The TLS server must run on the same machine as n8n. n8n Cloud does not support running local servers alongside workflows.


Conclusion

You've built a complete Cloudflare Challenge bypass pipeline in n8n โ€” no browser automation, no Puppeteer, no Playwright. Just three components working together: CapSolver to solve the challenge, a Go TLS server to spoof Chrome's network fingerprint, and an n8n workflow to orchestrate everything.

The key insight is that solving the challenge is only half the problem. Without matching TLS fingerprints on the subsequent fetch, cf_clearance is useless โ€” Cloudflare inspects the handshake, not just the cookie. The httpcloak TLS server handles that layer, making the fetch indistinguishable from a real Chrome browser at the network level.

The repo now gives you actual starting templates for Cloudflare-protected sites:

  • Basic scraping โ€” retrieve raw page content
  • Site health monitoring โ€” track uptime with consecutive-failure alerting
  • Price monitoring โ€” extract and compare prices over time
  • Account login โ€” log into your own account behind Cloudflare
  • Account signup โ€” register with your own email behind Cloudflare
  • Challenge Solver API โ€” expose a webhook that returns raw cf_clearance + userAgent for external apps

All of them use the same two-tier pattern: CapSolver solves the challenge, the TLS server makes the actual request. The Solver API is the exception โ€” it skips the TLS server entirely and returns just the solution, letting your own application handle the fetch. Configure the placeholders, keep the workflows inactive until they match your target, then activate.


Ready to get started? Sign up for CapSolver and use bonus code n8n for an extra 8% bonus on your first recharge!

CapSolver bonus code banner

Frequently Asked Questions

What is AntiCloudflareTask, and how is it different from AntiTurnstileTaskProxyLess?

AntiCloudflareTask solves the full-page Cloudflare Bot Management challenge โ€” the "Just a momentโ€ฆ" screen that blocks site access entirely. It requires a proxy because CapSolver must load the actual protected page through a browser. AntiTurnstileTaskProxyLess solves Turnstile widgets embedded inside pages (login forms, signup forms) and does not require a proxy. Different challenges, different task types.

Why can't I just use n8n's built-in HTTP Request node to fetch the page?

n8n's HTTP Request node uses Go's standard net/http library, which has a distinct TLS fingerprint that Cloudflare detects. Even with a valid cf_clearance cookie, Cloudflare will re-challenge any request that doesn't match a known browser TLS profile. The TLS server solves this by using httpcloak to spoof a real Chrome TLS stack.

Why does my datacenter proxy keep failing?

Cloudflare's bot scoring assigns risk scores to IP addresses. Datacenter IPs (from AWS, GCP, VPS providers, etc.) are well-known and get high risk scores. AntiCloudflareTask uses your proxy to load the challenge page, and if Cloudflare detects the IP as a datacenter, it either serves a harder challenge that CapSolver can't solve, or fails the challenge entirely. Residential and mobile IPs have lower risk scores and pass more reliably.

Does cf_clearance expire?

Yes. The expiration depends on the site's Cloudflare configuration โ€” it can range from a few minutes to 24 hours. For recurring scraping jobs, the scheduled workflow (every 6 hours) re-solves the challenge regularly. For on-demand scraping, the webhook path solves a fresh challenge on every request.

What if the site uses both Turnstile and the Cloudflare Challenge?

Some sites have Cloudflare Bot Management active at the domain level (which shows the challenge page before you can access anything) and also embed a Turnstile widget inside a specific form. In this case, you need to handle them separately: use this workflow to bypass the initial Cloudflare Challenge and retrieve the page, then use a Turnstile solver for the form submission. Each is a distinct step.

Can I use this with n8n Cloud?

Not easily. The TLS server is a local Go binary that must run on the same machine as n8n so the workflow can call http://localhost:7878/fetch. n8n Cloud does not allow running local services alongside workflows. You would need a self-hosted n8n instance, or run the TLS server as an externally accessible service and update the URL in the workflow โ€” though that adds latency and infrastructure overhead.

What sites does this work on?

This approach works on sites protected by Cloudflare Bot Management that show the standard "Verifying your browserโ€ฆ" interstitial. It may not work on sites that use Cloudflare's stricter UAM (Under Attack Mode) or that combine Cloudflare with additional proprietary bot protection layers. Success also depends on proxy quality โ€” a residential proxy that works well for one site may not work for another depending on regional Cloudflare rules.

Compliance Disclaimer: The information provided on this blog is for informational purposes only. CapSolver is committed to compliance with all applicable laws and regulations. The use of the CapSolver network for illegal, fraudulent, or abusive activities is strictly prohibited and will be investigated. Our captcha-solving solutions enhance user experience while ensuring 100% compliance in helping solve captcha difficulties during public data crawling. We encourage responsible use of our services. For more information, please visit our Terms of Service and Privacy Policy.

More