Skip to main content

AntiBot & WAF Cheatsheet

Identify and unlock Akamai Bot Manager, DataDome, Kasada, Imperva Incapsula, Cloudflare, PerimeterX, and Fastly. TLS fingerprinting, header order, status codes, and BrowserSolver mode workflows with copy-ready snippets.

Mental model: identify, then unlock

Anti-bot walls fail scrapers on TLS fingerprint, header order, cookies, and IP long before parsers run. BrowserSolver Website modes own that session for you on allowlisted domains.

Two ways teams scrape protected sites.

Plain textCopy
DIY request client
  Your code talks to the target with a browser-grade TLS stack,
  keeps a clean cookie jar, and replays challenge posts.
  Most “payload looks fine” failures are TLS / headers / IP / cookies.

BrowserSolver Website modes
  One HTTP API: AntiBot (classify), then Crawler / Rendering / WebUnlocker.
  Unlock playbooks run server-side on allowlisted hosts.
  You parse HTML/JSON; you do not maintain sensor posts.

Non-negotiables for any local browser-grade client.

Plain textCopy
1. Chrome TLS profile (latest available) + random extension order
2. HTTP/3 off when proxies do not support it
3. Exact browser header order (incl. HTTP/2 pseudo-headers)
4. Same UA + TLS + IP + hints for the whole flow
5. Sticky/session proxies (never rotate mid-challenge)
6. Clean cookie jar (no duplicate names / wrong domain)

Which anti-bot is this? Quick routing

Identify the vendor from public response signals, then open the matching section or bypass guide.

Public identification cheat sheet.

Plain textCopy
Akamai Bot Manager
  Cookies _abck + bm_sz; dynamic <script src="/xxxx/yyyy/..."> near body end
  428 = SEC-CPT; 429 body with {"t":...} often SBSD

Imperva / Incapsula
  reese84 cookie or x-d-token: reese84
  /_Incapsula_Resource?SWJIYLWA=...: utmvc (___utmvc cookie)
  "Pardon Our Interruption": reese84 dynamic (+ possible PoW)

DataDome
  403 with inline var dd={...}; datadome cookie
  i.js / rt:'i': interstitial; c.js / rt:'c': slider
  tags = proactive /js telemetry (not a block page)

Kasada
  429 HTML referencing ips.js; x-kpsdk-* / tkrm_alpekz_* cookies
  x-is-human: Vercel BotID variant

Cloudflare
  Managed Challenge / Turnstile; cf-ray; __cf_bm / cf_clearance

PerimeterX / HUMAN
  px-cdn / client.perimeterx.net; _px* cookies; Access Denied walls

Fastly
  Edge 403 / empty bodies before origin; CDN JS challenges; Fastly headers

Detect vendor signals from HTML and headers

Use these heuristics in triage scripts before you call Website AntiBot at scale. Prefer AntiBot in production for named classification.

TypeScript heuristics over status, headers, and body.

TypeScriptCopy
type VendorGuess =
  | "akamai" | "datadome" | "kasada" | "imperva"
  | "cloudflare" | "perimeterx" | "fastly" | "unknown"

function guessVendor(status: number, headers: Headers, body: string): VendorGuess {
  const setCookie = headers.get("set-cookie") ?? ""
  const server = (headers.get("server") ?? "").toLowerCase()
  const cfRay = headers.has("cf-ray")

  if (/_abck=|bm_sz=/.test(setCookie) || /sec-cp-challenge/.test(body)) return "akamai"
  if (status === 403 && /var\s+dd\s*=\s*\{/.test(body)) return "datadome"
  if (status === 429 && /ips\.js|x-kpsdk-|tkrm_alpekz_/.test(body + setCookie)) return "kasada"
  if (/reese84=|x-d-token|_Incapsula_Resource|Pardon Our Interruption/i.test(body + setCookie))
    return "imperva"
  if (cfRay || /__cf_bm=|cf_clearance=|cdn-cgi\/challenge/.test(body + setCookie))
    return "cloudflare"
  if (/perimeterx|px-cdn|_pxvid=|_pxhd=/i.test(body + setCookie)) return "perimeterx"
  if (server.includes("fastly") || /x-fastly|fastly-/.test([...headers.keys()].join(" ")))
    return "fastly"
  return "unknown"
}

Why stock HTTP clients fail anti-bot checks

requests, httpx, axios, node-fetch, and Go net/http expose non-browser TLS and header order. Prefer Website modes when you want BrowserSolver to own the session.

Fingerprint gaps that look like “vendor bugs”.

Plain textCopy
Bad (stock client fingerprint):
  TLS ClientHello ≠ Chrome
  Header order alphabetical or framework-default
  sec-ch-ua / UA / platform version drift
  Datacenter IP on a residential-only target

Good (browser-grade session):
  Chrome TLS profile + random extension order
  Wire-captured header order for that request class
  Sticky IP for the whole challenge flow
  Clean cookie jar (no duplicates / wrong domain)

Chrome HTTP/2 pseudo-header order is stable. Firefox and Safari differ. Capture the wire order for the request you replay.

Plain textCopy
Chrome  :method :authority :scheme :path
Firefox :method :path :authority :scheme
Safari  :method :scheme :path :authority

Example Chrome navigation header order (illustrative):
:method, :authority, :scheme, :path,
sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform,
upgrade-insecure-requests, user-agent, accept,
sec-fetch-site, sec-fetch-mode, sec-fetch-user, sec-fetch-dest,
accept-encoding, accept-language, cookie, priority

TLS fingerprinting, header order, and client hints

Header order and HTTP/2 pseudo-headers are among the strongest bot signals. DevTools does not show true wire order.

Hard rules

Checklist before blaming a vendor challenge.

Plain textCopy
1. Header case: lowercase on HTTP/2, Title-Case on HTTP/1.1
2. Never set Content-Length manually (duplicate length = hard fail)
3. sec-ch-ua must match the UA Chrome major + GREASE brands
4. sec-ch-ua-platform must match the UA OS ("Windows" / "macOS")
5. If priority is last, cookie must sit immediately before it
6. accept-encoding for Chrome ≈ gzip, deflate, br, zstd
7. Disable HTTP/3 when proxies do not support it
8. Prefer sticky/session proxies; egress IP stays constant
9. Never hardcode sec-ch-ua-full-version-list (stale GREASE = block)
10. No DevTools “Disable cache” artifacts (cache-control + pragma no-cache)

Chrome version coherence

UA, sec-ch-ua, and platform must agree.

Plain textCopy
Keep these three in lockstep on every request:
  User-Agent  … Chrome/<MAJOR>.…
  sec-ch-ua   "Google Chrome";v="<MAJOR>", "Chromium";v="<MAJOR>", "Not A(Brand";v="…"
  sec-ch-ua-platform  "Windows" | "macOS"  (quoted, matching UA OS)
  sec-ch-ua-mobile    ?0 for desktop

Windows Chrome is the usual baseline; macOS Chrome is also common.
Bump MAJOR together. Never bump only the UA string.

Cookie transport

Multiple cookie headers on HTTP/2 are normal Chrome behavior. On HTTP/1.1 cookies coalesce into one Cookie header.

Plain textCopy
HTTP/2  cookie: a=1
           cookie: b=2   (correct; do not “fix”)
HTTP/1.1 Cookie: a=1; b=2

Jar bugs that look like WAF blocks:
  - same cookie name twice with different values
  - missing domain/path after Set-Cookie
  - rotating IP mid-challenge while reusing cookies

IP, sticky proxies, and session consistency

Rotating IPs mid-challenge is one of the most common silent block causes. BrowserSolver Website modes manage egress for you; apply this when debugging a local client.

Proxy rules of thumb.

Plain textCopy
Use sticky / session proxies for the entire challenge + protected action
Never rotate mid-flow (sensor, then verify, then checkout)
Residential or mobile when datacenter IPs are scored to zero
One clean cookie jar per sticky IP / session
If DataDome slider shows t=bv: IP hard-ban; change sticky IP first
Re-probe with Website AntiBot after an IP pool change

Anti-bot HTTP status code reference

Quick map from status codes to challenge families.

Cross-vendor status codes you will see in the wild.

Plain textCopy
200 + challenge HTML     Cloudflare / soft blocks / empty shells
403 + var dd={...}       DataDome interstitial or slider
403 edge empty           Fastly / generic CDN ACL
428                      Akamai SEC-CPT (provider=crypto|behavioral|adaptive)
429 + ips.js             Kasada
429 + {"t":"..."}        Akamai SBSD hard block
429 / 503 rate shapes    vendor rate limits (retry with backoff + sticky IP)
401 from api.*           missing/wrong x-api-key on BrowserSolver

Authenticate BrowserSolver API calls

Every Website mode call uses your dashboard API key as x-api-key. Never ship the key in browser bundles.

Self-contained crawler probe (export the key once per shell).

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/crawler" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://example.com"}'

Node helper you can paste above other TypeScript snippets on this page.

TypeScriptCopy
const API = "https://api.browsersolver.com/v1"
const KEY = process.env.BROWSERSOLVER_API_KEY!

async function websitePost(path: string, body: Record<string, unknown>) {
  const res = await fetch(`${API}${path}`, {
    method: "POST",
    headers: {
      "x-api-key": KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    throw new Error(`${path} ${res.status}: ${await res.text()}`)
  }
  return res.json()
}

Python helper for the same pattern.

PythonCopy
import os
import requests

API = "https://api.browsersolver.com/v1"
KEY = os.environ["BROWSERSOLVER_API_KEY"]

def website_post(path: str, body: dict):
    r = requests.post(
        f"{API}{path}",
        headers={"x-api-key": KEY, "Content-Type": "application/json"},
        json=body,
        timeout=120,
    )
    r.raise_for_status()
    return r.json()

Bypass Akamai Bot Manager: signals and status codes

Public signals for Akamai Bot Manager: _abck sensors, SEC-CPT 428, SBSD 429, and optional pixel. Unlock on allowlisted domains with Website WebUnlocker.

Identify Akamai

Cookies, scripts, and challenge families.

Plain textCopy
Cookies: _abck, bm_sz (sec_cpt on SEC-CPT flows)
Script: dynamic <script src="/xxxx/yyyy/..."> near end of body
        (path is per-response; never hardcode)
UA: Chrome Windows/macOS expected by sensor flows

Status codes:
  200 + sensor script    : standard _abck sensor flow
  428                    : SEC-CPT ({"sec-cp-challenge":"true","provider":…})
  429 + {"t":…}          : SBSD (state-based scraping detection)
  Pixel script present   : optional; presence ≠ enforcement

_abck validity (site-dependent):
  Often contains ~0~ when ready; some sites need exactly 3 sensor posts
  After a protected action, cookie may invalidate (~0~-1~-1): re-sensor
  If still invalid after 3 sensors: TLS/headers/IP, not “more sensors”

SEC-CPT providers (HTTP 428)

provider selects the challenge family. Success: sec_cpt contains ~3~.

Plain textCopy
crypto      mandatory wait (chlg_duration) + PoW, then /_sec/verify?provider=crypto
behavioral  1-3 sensor posts + dynamic verify_url
adaptive    wait + PoW (answers count), then behavioral sensors
            verify on static /_sec/cp_challenge/verify

Keep sec-ch-ua / mobile / platform coherent for the whole solve.
SEC-CPT can appear on first load or mid-session on an API call.

SBSD modes

Three shapes of State Based Scraping Detection.

Plain textCopy
(a) Passive: page loads; SBSD script has v UUID, no t
    Post index 0 then index 1 to the script path

(b) Hard challenge: blocking page with v and t in the script URL
    Single payload POST to /[path]?t=[t]

(c) 429 block: protected call returns {"t":"..."} only
    Reuse stored path + UUID + script from an earlier solve
    POST fresh payload to /[path]?t=<token>, then retry

Always store path/UUID/script before you start protected traffic.

BrowserSolver path

Self-contained: probe then unlock (inspect JSON fields in docs).

BashCopy
export BROWSERSOLVER_API_KEY="your-key"
URL='https://shop.example.com/product/1'

curl -sS "https://api.browsersolver.com/v1/website/antibot" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"query\":\"$URL\"}"

curl -sS "https://api.browsersolver.com/v1/website/webunlocker" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"query\":\"$URL\"}"

Bypass DataDome: interstitial, slider, and tags

DataDome is aggressive on retail and ecommerce catalogs. Hard IP bans (t=bv) need a new sticky residential IP, not another solve.

Identify DataDome

Block page vs proactive tags.

Plain textCopy
HTTP 403 with inline `var dd={...}`
Keep from dd: cid, hsh, s, b/t/e (type-dependent) + blocked URL as referer
Cookie: datadome=... on the 403

Challenge types:
  rt:'i' + https://ct.captcha-delivery.com/i.js: interstitial
  rt:'c' + https://ct.captcha-delivery.com/c.js: slider (captcha)
  tags: NOT a block page; proactive POST to the site's /js endpoint
         (often type=ch then type=le) to raise trust

Hard IP ban:
  slider field t=bv: proxy hard-blocked; solving will not help
  Rotate sticky residential / mobile IP before retrying

BrowserSolver path

Self-contained unlock call.

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/webunlocker" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://shop.example.com/product/sku-42"}'

Bypass Kasada and Vercel BotID

Kasada surfaces as 429 + ips.js. Vercel BotID adds x-is-human. Prefer WebUnlocker on allowlisted domains instead of hand-rolling POW.

Public identification checklist.

Plain textCopy
HTTP 429 HTML referencing ips.js
Headers/cookies: x-kpsdk-*, tkrm_alpekz_s1.3, tkrm_alpekz_s1.3-ssn
Script path with two GUID segments, e.g.
  /149e9513-.../2d206a39-.../{ips.js|fp|tl|mfc}

Common flows:
  Flow 1: 429 on homepage, then GET ips.js, then POST /tl, then reload
  Flow 2: /fp fingerprint endpoint, then same ips.js /tl path
            (referer = /fp URL); protected calls may need fresh x-kpsdk-cd

Vercel BotID:
  x-is-human header; script often named c.js
  Stale token or IP change: regenerate after re-fetching c.js

Payload bodies are large: usually gzip/br/zstd + content-encoding

Self-contained unlock call.

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/webunlocker" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://shop.example.com/collections/sneakers"}'

Bypass Imperva Incapsula: reese84 and utmvc

Distinguish reese84 vs utmvc before you scale unlock traffic.

Which Imperva flavor?

Plain textCopy
reese84
  Cookie named reese84 and/or x-d-token header
  Script path often static per site (POSTs with ?d=)
  Token JSON includes renewInSec: renew before expiry

reese84 dynamic
  "Pardon Our Interruption" page
  May require a PoW step before the token is accepted

utmvc
  Script like /_Incapsula_Resource?SWJIYLWA=...
  Cookie name is ___utmvc (three underscores)
  Large body: compress (gzip/br/zstd)

Also watch for Incapsula captcha blocks (hCaptcha / Geetest)
  after sensors when trust is still low

Self-contained unlock call.

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/webunlocker" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://retail.example.com/catalog"}'

Bypass Cloudflare Challenge Pages and Turnstile

Challenge Pages, Turnstile, and Bot Fight Mode are the usual walls. Escalate only after Crawler or Rendering already failed.

Common Cloudflare block signals.

Plain textCopy
Managed Challenge / Turnstile interstitial instead of product HTML
cf-ray response header
__cf_bm / cf_clearance cookies after a successful browser challenge
Bot Fight Mode / Under Attack soft blocks (empty or cookie-gated body)
Soft “200 OK” pages that omit the data you need
cdn-cgi/challenge paths in HTML or network waterfalls

Self-contained mode ladder with curl.

BashCopy
export BROWSERSOLVER_API_KEY="your-key"
URL='https://shop.example.com/collections/all'

# 1) cheapest
curl -sS -o /tmp/out.html -w "%{http_code}" "https://api.browsersolver.com/v1/website/crawler" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" -d "{\"query\":\"$URL\"}"

# 2) JS-heavy but reachable: /website/rendering
# 3) challenge wall on allowlisted host: /website/webunlocker

Bypass PerimeterX / HUMAN bot defense

PerimeterX (HUMAN) mixes device fingerprinting with edge scores. Confirm with AntiBot, unlock with WebUnlocker on allowlisted hosts.

Signals to watch for.

Plain textCopy
Scripts/hosts referencing px-cdn, client.perimeterx.net, or human security
Cookies: _px*, _pxhd, _pxvid (names vary by integration)
Block pages mentioning Access Denied with PX/HUMAN branding
Often paired with App / bot score on XHR after first paint
First paint can succeed while XHR data calls fail with PX scores

Self-contained AntiBot probe.

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/antibot" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://store.example.com/"}'

Bypass Fastly Bot Management and edge WAF

Fastly often protects high-traffic catalogs at the CDN edge before origin HTML. Unlock on allowlisted hosts with Website WebUnlocker.

Common Fastly edge signals.

Plain textCopy
Edge 403 or empty bodies before origin content
JS challenges served from the CDN
Rate and ACL denials on catalog paths
Server / via headers mentioning Fastly
x-fastly-* or Fastly-specific cache headers on blocks

Self-contained unlock call.

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/webunlocker" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://catalog.example.com/"}'

Mode ladder: AntiBot, Crawler, Rendering, Unlock

Burn the cheapest mode that works. AntiBot is for classification; WebUnlocker is for allowlisted vendor-hardened hosts.

Decision matrix.

Plain textCopy
Website AntiBot    : name vendor / challenge / fingerprint gap
Website Crawler    : static HTML & JSON APIs (cheapest)
Website Rendering  : JS-heavy pages you can already reach
Website WebUnlocker: Cloudflare / DataDome / Akamai / PX / Kasada /
                      Imperva / Fastly on allowlisted domains

Rules:
  - Do not start on WebUnlocker for every URL
  - Re-probe with AntiBot when overnight pass rates collapse
  - Confirm domain support before production unlock volume
  - Failed unlock retries after exhaustion are not billed (see product docs)

Website AntiBot probe before you burn unlock credits

One URL per call. Inspect the JSON response in the docs for vendor / challenge / mode hints, then route traffic.

Self-contained probe (print raw JSON).

BashCopy
export BROWSERSOLVER_API_KEY="your-key"

curl -sS "https://api.browsersolver.com/v1/website/antibot" \
  -H "x-api-key: $BROWSERSOLVER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"https://example.com"}'

TypeScript wrapper (fields follow current API docs).

TypeScriptCopy
const API = "https://api.browsersolver.com/v1"
const KEY = process.env.BROWSERSOLVER_API_KEY!

const res = await fetch(`${API}/website/antibot`, {
  method: "POST",
  headers: { "x-api-key": KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ query: process.argv[2] ?? "https://example.com" }),
})
console.log(await res.json())

Unlock once, parse forever

WebUnlocker returns scrape-ready markup with the same redirect / raw-source controls as Crawler. Keep parsers on one response shape (see current docs).

Self-contained unlock + Cheerio parse (adapt body field to your docs).

TypeScriptCopy
import * as cheerio from "cheerio"

const API = "https://api.browsersolver.com/v1"
const KEY = process.env.BROWSERSOLVER_API_KEY!

const res = await fetch(`${API}/website/webunlocker`, {
  method: "POST",
  headers: { "x-api-key": KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ query: "https://shop.example.com/p/sku-1" }),
})
const page = await res.json()
const html = typeof page === "string" ? page : String(page.html ?? page.body ?? page)
const $ = cheerio.load(html)
console.log({ title: $("h1").first().text().trim() })

Production pipeline pattern for protected catalogs

Classify once per host, cache the mode, unlock only when needed.

Host-to-mode cache sketch.

TypeScriptCopy
type Mode = "crawler" | "rendering" | "webunlocker"
const modeByHost = new Map<string, Mode>()

async function fetchHtml(url: string): Promise<unknown> {
  const host = new URL(url).host
  const mode = modeByHost.get(host) ?? "crawler"
  const path = `/website/${mode}`
  try {
    return await websitePost(path, { query: url })
  } catch {
    if (mode === "crawler") {
      modeByHost.set(host, "rendering")
      return fetchHtml(url)
    }
    if (mode === "rendering") {
      modeByHost.set(host, "webunlocker")
      return fetchHtml(url)
    }
    // Optional: website/antibot once, then force webunlocker if allowlisted
    throw new Error(`exhausted modes for ${host}`)
  }
}

// websitePost: copy from “Authenticate BrowserSolver API calls”

Vendor unlock guides on browsersolver.com

Deep-dives for each WAF playbook. Use them when pitching coverage or onboarding a new domain.

Canonical product routes.

Plain textCopy
/products/website-antibot
/products/website-webunlocker
/products/website-crawler
/products/website-rendering
/products/website-technology  : stack signals alongside AntiBot

Triage: first five questions when scraping is blocked

Work top-down. Cross-cutting session bugs are far more common than “wrong vendor playbook”.

Ask these before opening a support ticket.

Plain textCopy
1. Does AntiBot return a named vendor, or is the URL unreachable?
2. Did Crawler / Rendering already fail on the same URL?
3. Is the host on the WebUnlocker allowlist (or did support enable it)?
4. Are you reusing a sticky session/IP for the whole job, or rotating mid-flow?
5. Is the failure a challenge wall, empty soft-block, rate limit, or 5xx?

If 2–4 are “no / unsure”, fix routing before blaming unlock.

Symptom, cause, and fix for anti-bot blocks

Cross-cutting matrix for scrapers and local clients.

Plain textCopy
Symptom                              Likely cause                         Fix
Works locally, fails in prod         rotating proxy / IP drift            sticky session proxies
Intermittent blocks                  dirty cookie jar or mid-flow rotate  one jar + one IP per session
Blocked immediately                  DC IP or UA/sec-ch-ua mismatch       residential + coherent UA
DataDome rejects despite UA          hardcoded full-version-list          do not hardcode client hints
Unlock returns challenge HTML        domain not allowlisted / wrong mode  confirm coverage; re-probe
Crawler empty, browser works         JS-rendered content                  Website Rendering
Rendering challenge wall             vendor bot manager                   Website WebUnlocker
Pass rate drops overnight            vendor changed protection            AntiBot re-probe + mode bump
401 / missing key                    x-api-key absent or wrong            set header from dashboard key

Vendor-shaped symptoms (when you still debug locally).

Plain textCopy
Akamai
  _abck never valid after 3 sensors: TLS/headers/IP, not “more sensors”
  428 mid-session: SEC-CPT; keep hints coherent; honor chlg_duration waits
  429 {"t":…}: SBSD; reuse path/UUID/script, post a fresh payload

DataDome
  t=bv on slider: IP hard-ban; change sticky IP
  Still blocked after interstitial/slider: tags trust may be low

Imperva
  reese84 persists: token/IP issue; renew before renewInSec
  utmvc rejected: cookie must be ___utmvc; compress large bodies

Kasada
  429 loops: stale x-kpsdk-cd POW reused across requests
  x-is-human rejected: re-fetch BotID c.js after IP/UA change

Fastly
  Edge 403 with empty body: unlock / allowlist before origin parsers

What to capture when debugging WAF blocks

BrowserSolver owns the unlock session; still keep a small HAR or response dump when filing tickets. Prefer wire captures when you debug a local TLS client: HAR alone lacks true header order and TLS fingerprint.

Minimum evidence pack.

Plain textCopy
Include:
  - target URL + timestamp (UTC)
  - AntiBot JSON (vendor / challenge / mode hints)
  - mode used (crawler | rendering | webunlocker)
  - HTTP status + first 2–4 KB of body (redact PII)
  - response headers: cf-ray, set-cookie names (not values), server
  - whether the domain is allowlisted
  - for local clients: TLS profile id + header-order list from a wire proxy

Skip:
  - full cookie values in public channels
  - API keys
  - unrelated analytics / ad noise

Redact helper before pasting into Slack/email.

TypeScriptCopy
function redact(html: string, max = 4000) {
  return html
    .replace(/datadome=[^;\s]+/gi, "datadome=REDACTED")
    .replace(/_abck=[^;\s]+/gi, "_abck=REDACTED")
    .replace(/reese84=[^;\s]+/gi, "reese84=REDACTED")
    .replace(/___utmvc=[^;\s]+/gi, "___utmvc=REDACTED")
    .replace(/cf_clearance=[^;\s]+/gi, "cf_clearance=REDACTED")
    .slice(0, max)
}

DIY TLS client vs BrowserSolver Website modes

Use this when deciding whether to maintain a local browser-grade client or route through Website AntiBot / WebUnlocker.

When to stay on BrowserSolver modes.

Plain textCopy
Use Website AntiBot + WebUnlocker when:
  - you want one HTTP API for crawl / render / unlock
  - you do not want to maintain TLS clients + challenge posts
  - the domain is (or can be) allowlisted

Use a local browser-grade client only when:
  - you are researching a new host before requesting allowlist
  - you need interactive debugging of a single challenge flow

Do not expect Website modes to return raw sensor payloads.
Do not post DIY challenge bodies through Website Crawler / Unlock.