How to Bypass Kasada When Web Scraping
Bypass Kasada when scraping public data: how KPSDK proof-of-work, the x-kpsdk-ct and x-kpsdk-cd headers, and the 429 challenge work, plus real API code.

A plain requests.get() against a Kasada-protected site comes back 429 before your parser sees a byte of HTML, and it ships a pile of x-kpsdk-* headers to tell you exactly who blocked you. To bypass Kasada when you collect public data you first have to accept that it scored your request as a bot before the page existed, using a client-side proof-of-work, a polymorphic JavaScript payload, and a fingerprint your bare HTTP client can't produce. This guide explains how Kasada bot detection actually works, how to read its 429 challenge and the x-kpsdk-ct and x-kpsdk-cd headers, and the ethical ways to gather public data without pretending your traffic is something it isn't. There's no permanent bypass and no magic flag. What follows are the signals that decide the outcome, and how to make legitimate automation look like the real browser sessions it already is.
Scrape responsibly: what "bypass" really means
"Bypass" here means one thing: making legitimate, automated access to public data look like the ordinary browser traffic it already is, so a heuristic doesn't wrongly flag it. It does not mean breaking into anything protected. Kasada sits in front of ticketing queues, sneaker drops, account creation, and checkout for a reason, and those are precisely the flows to leave alone. Set the ground rules before you write a line of code.
- Public data only. Anything behind a login, a paywall, or that exposes someone's personal information is off-limits without explicit permission. Kasada guards fraud-sensitive paths; don't touch them.
- Read robots.txt and the Terms of Service. If a path is disallowed or the ToS prohibits automated collection, respect it. A
Crawl-delayis a rate the site is asking you to honor, not a hint. - Rate-limit yourself. Bot defenses exist in part because scrapers hammer origins. Slow, considerate crawling is both more ethical and, conveniently, far less detectable.
- Prefer the official API. If the site publishes a feed or API, use it. It's faster, cleaner, and it's the access path the site actually sanctions.
- No guarantees. Kasada ships detection updates continuously and rotates its payload on nearly every load. A technique that works today can stop working next week. Anyone selling a "permanent Kasada bypass" is selling snake oil.
The goal is not to defeat security. It's to stop a bot filter from misclassifying a well-behaved, public-data crawler as an attacker. If a site clearly does not want to be scraped, the right answer is to stop, not to escalate.
Everything below is about fingerprint realism and good manners, not about defeating authentication or collecting data that was never meant to be public.
What Kasada Bot Defense actually is
Kasada is a bot-mitigation vendor founded in Sydney in 2015, and its product is Kasada Bot Defense. You'll meet it most often on ticketing platforms, sneaker and streetwear launch sites, streaming services, and retail brands that get hammered by scalping and credential-stuffing bots. Engineers usually recognize it by its client toolkit, KPSDK (the Kasada SDK), which stamps a set of x-kpsdk-* headers on requests and responses.
Two design choices make Kasada scraping harder than a generic firewall, and they're what set it apart from Cloudflare, DataDome, Akamai, or PerimeterX. First, Kasada is invisible by default. It does not usually show a CAPTCHA or a "press and hold" widget. Instead it runs a silent client-side test and either lets you through or quietly rate-limits you, so there's no puzzle to solve and often no obvious block page to read. Second, its core weapon is a client-side proof-of-work: a small computational puzzle your browser must solve before it earns a token. Solving it once in a real browser costs a couple of milliseconds. Solving it thousands of times per minute from a bot farm costs real CPU, and Kasada ramps the difficulty as it gets more suspicious, so scale itself becomes the cost.
The upshot: you can't bypass Kasada bot detection by faking a header or blocking a third-party host. The proof-of-work has to actually run, and only a JavaScript runtime can run it.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
How Kasada detects bots
Kasada scores several independent layers on every request and folds them into a trust decision. A single strong mismatch can trigger the 429 challenge, which is why swapping in a "better proxy" rarely fixes anything on its own. The IP is one row in the table.
| Detection layer | What Kasada inspects | What gives a bot away |
|---|---|---|
| Client-side proof-of-work | Whether the KPSDK puzzle was solved and how long it took (`x-kpsdk-cd`) | No solved token, an impossible solve time, or a replayed answer |
| Sensor / fingerprint (p.js) | Canvas, WebGL, audio, fonts, screen geometry, `navigator.webdriver`, automation properties | Headless Chromium tells, or the payload never executed at all |
| IP reputation and ASN | Address history, whether the ASN is a known datacenter (AWS, GCP, Azure, OVH, Hetzner) | A clean-looking request from a flagged datacenter range |
| TLS fingerprint (JA3/JA4) | Cipher suites, extensions, and curves in the Client Hello | Python `urllib3` or Go defaults that match no real browser |
| HTTP/2 fingerprint and headers | Frame settings, header and pseudo-header order, `sec-ch-ua` client hints | A Chrome User-Agent over an HTTP/2 profile no Chrome would send |
| Request cadence | Volume and timing per IP and per token | Bursts that trip the difficulty ramp, or one token reused everywhere |
The mental model that matters: Kasada cross-checks these layers against each other. A Chrome User-Agent paired with a Python TLS handshake and no solved proof-of-work is far more suspicious than an honest Python client, because the inconsistency is the tell. To pass, every layer has to agree on the same story, and one of those layers is a computation only a real JS engine can perform. That's the same principle behind the broader proxy-block avoidance playbook, and it applies double when a proof-of-work is in the mix.
KPSDK, the proof-of-work, and the polymorphic p.js
Kasada's client script goes by a couple of names: p.js (older docs call it ips.js). It's heavily obfuscated and polymorphic, meaning the code changes on almost every load, so you can't hard-code a parser against it. Reverse-engineering last week's payload buys you nothing this week.
Here's the flow that decides whether your scraper lives or dies:
- Your first request to a protected path returns a challenge (usually
429) withx-kpsdk-*headers and a small bootstrap that pulls inp.js. p.jsloads inside a custom JavaScript virtual machine, fingerprints the device, and runs the proof-of-work: a hashing puzzle whose difficulty Kasada controls.- The VM POSTs the solved answer plus the fingerprint to a Kasada telemetry endpoint. The answer and its timing travel in the
x-kpsdk-cdheader. - If the score is clean, Kasada mints a client token and returns it as
x-kpsdk-ct. Subsequent requests that carry a validx-kpsdk-ctsail through.
One detail is easy to miss and worth knowing: Kasada's script path carries two fixed UUID segments, historically 149e9513-01fa-4fb0-aad4-566afd725d1b and 2d206a39-8ed7-437e-a3be-862e0f06eea3, before the p.js filename. Those UUIDs are consistent across many Kasada deployments, so together with the x-kpsdk- headers they're a reliable way to confirm you're actually looking at Kasada* rather than guessing. A quick, non-intrusive probe tells you what you're up against:
import requests
def detect_kasada(url):
"""Confirm Kasada by its response headers and challenge code. Read-only probe."""
r = requests.get(url, timeout=20)
kpsdk = {k: v for k, v in r.headers.items() if k.lower().startswith("x-kpsdk")}
is_kasada = bool(kpsdk) or "149e9513-01fa-4fb0-aad4-566afd725d1b" in r.text
print(f"status={r.status_code} kasada={is_kasada} headers={kpsdk}")
return is_kasada
detect_kasada("https://example.com/")
If you see a 429 (or 403) alongside any x-kpsdk-* header, or that UUID in the page source, you're dealing with Kasada and the proof-of-work is your real obstacle, not the HTTP status.
Decode x-kpsdk-ct, x-kpsdk-cd, and the 429 challenge
Most scrapers treat "not 200" as one undifferentiated failure. Kasada hands you a diagnostic if you read the status code together with the KPSDK headers. Start with the headers, because they name the mechanism.
| Header | Direction | What it carries | The tell when it's wrong |
|---|---|---|---|
| `x-kpsdk-ct` | response, then sent on requests | The **client token** minted after the proof-of-work is accepted; your session pass | Absent means you never solved the challenge, so every request stays a challenge |
| `x-kpsdk-cd` | sent on requests | The **challenge data**: an encoded blob with the proof-of-work answer plus timing (work time, duration, answer set) | A fabricated or replayed blob fails the server-side re-check |
| `x-kpsdk-v` | both | The KPSDK **version** string for the current `p.js` | A stale version against a rotated payload gets rejected |
| `x-kpsdk-dv` | sent on requests | The **device validation** payload from the fingerprint pass | Missing means no fingerprint was submitted |
The x-kpsdk-cd value is the one people try to fake. It base64-decodes to a small JSON object whose fields describe how the puzzle was solved, so tampering shows up immediately. Treat the field names below as illustrative of the shape, not a spec to hard-code against, because Kasada rotates them:
import base64, json
def peek_cd(cd_header):
"""Illustrative: show the shape of an x-kpsdk-cd blob. Do not hard-code fields."""
decoded = json.loads(base64.b64decode(cd_header + "=="))
# shape resembles: {"workTime": ..., "id": ..., "answers": [...], "duration": ...}
return decoded
The point isn't to forge one. It's to understand that the server re-computes whether the work is real and whether the timing is plausible, so a hand-built x-kpsdk-cd never survives. Now the response side, which tells you what to do next.
| Response | What you'll see | What Kasada decided | Correct action |
|---|---|---|---|
| **200 (real content)** | Your HTML/JSON, and an `x-kpsdk-ct` token for the session | Proof-of-work accepted, trust granted | Persist `x-kpsdk-ct`, reuse it for this session, keep the same exit IP |
| **429 (challenge)** | A short body plus `x-kpsdk-*` headers, `p.js` referenced | No valid token yet: solve the puzzle | Render in a real browser so `p.js` runs and mints `x-kpsdk-ct` |
| **403 (hard block)** | Denied repeatedly, no solvable challenge | Reputation plus fingerprint failed outright | Rotate residential IP and fix TLS/JS fingerprint; don't replay the same request |
| **429 that keeps escalating** | Same code, growing latency, token stops working | The difficulty ramp kicked in against your volume | Slow down, one token per session, cut concurrency |
| **p.js never executes** | `429` loop even inside a browser, no `x-kpsdk-ct` minted | The script path or the telemetry POST was blocked | Allow the two-UUID `p.js` path and its POST to load and run |
| **5xx from a scraping API** | Returned by the API when upstream attempts fail | The proxy or render layer exhausted its retries | Retry with backoff; escalate to premium proxy plus stealth |
The distinction that trips people up: Kasada's normal challenge is a 429, not the 403 you might expect from Cloudflare or PerimeterX. A 429 with x-kpsdk-* headers means "run the proof-of-work," while a bare 403 on retry means "your reputation and fingerprint are already burned." Retrying a burned request the same way just wastes IPs.
Run a real browser that solves the proof-of-work
The proof-of-work is the layer a plain HTTP library cannot satisfy, because there's no JS runtime to execute p.js and no VM to run the puzzle. Without that computation, x-kpsdk-ct is never minted, so you stay in the 429 loop no matter how clean your headers look. Satisfying this reliably means running an actual browser.
Launch Chromium through Playwright or Puppeteer, patch the obvious automation leaks with a stealth layer, and give p.js time to solve the puzzle and set the token before you read the page. Here's Playwright in Python routing through a residential proxy:
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
proxy={"server": "http://gate.sparkproxy.io:10000",
"username": "user", "password": "pass"},
)
context = browser.new_context(
viewport={"width": 1366, "height": 900},
locale="en-US",
timezone_id="America/New_York", # match the proxy's country
)
page = context.new_page()
stealth_sync(page) # hides navigator.webdriver and headless tells
page.goto("https://example.com/", wait_until="networkidle") # p.js solves the PoW, sets x-kpsdk-ct
page.wait_for_timeout(1200) # let the VM finish the puzzle before you navigate
page.goto("https://example.com/catalog", wait_until="networkidle")
html = page.content()
browser.close()
Two details matter. The first goto lands on a light page so p.js executes and mints x-kpsdk-ct before you request the target, exactly as a real visitor's browser would. The wait_for_timeout gives the VM room to finish the proof-of-work, because navigating away too fast can abort the puzzle and leave you tokenless. Set the viewport, locale, and timezone to consistent, human values rather than the headless defaults (800x600, UTC) that push suspicion up. If you're on Puppeteer, the same proxy-plus-stealth-plus-warm-up pattern applies.
A real browser is heavier than an HTTP client. Run one only for targets that genuinely challenge you, and reserve lightweight clients for the easy pages.
Match your TLS and HTTP/2 fingerprint
Once you hold a valid x-kpsdk-ct, some follow-up API calls behind the same enforcer only score the network and header layers, and there you can skip a full browser. The request still has to survive TLS fingerprinting. Python's requests rides on urllib3, whose Client Hello matches no browser on earth. Kasada computes a JA3/JA4 hash from that handshake and reads "Python," no matter what User-Agent you set.
The fix is curl_cffi, which impersonates a real browser's TLS stack and HTTP/2 profile:
from curl_cffi import requests as cffi
session = cffi.Session(impersonate="chrome124") # real Chrome TLS + HTTP/2
session.proxies = {
"http": "http://user:pass@gate.sparkproxy.io:10000",
"https": "http://user:pass@gate.sparkproxy.io:10000",
}
session.headers.update({"x-kpsdk-ct": "TOKEN_FROM_THE_BROWSER_STEP"})
resp = session.get("https://example.com/api/products")
print(resp.status_code) # 200 if the token, fingerprint, and IP all hold up
impersonate="chrome124" sends Chrome 124's exact cipher suites, extensions, and HTTP/2 settings, so the JA3/JA4 hash and the User-Agent finally tell the same story. That consistency is the whole point. For TLS realism layer by layer see the proxy-block avoidance guide, and for doing this at volume the async scraping with requests and aiohttp walkthrough covers concurrency without tripping rate limits.
Be honest about which path a target needs. If a fresh session has no token, no TLS trick alone mints one. That's a job for a real browser or a rendering API that runs one.
Use residential and mobile IPs with clean reputation
IP reputation is the fastest way to earn an instant block and, worse with Kasada, a steeper proof-of-work. Requests from well-known datacenter ASNs (AWS, GCP, Azure, OVH, Hetzner) start with a heavy suspicion penalty, because almost no ordinary fan queues for concert tickets from an AWS address. You can solve a perfect puzzle and still get throttled purely on the exit IP.
Residential and mobile proxies route through real consumer ISPs and carrier networks, so the exit IP carries the reputation of an ordinary home or phone connection. That single change often turns a reliable 429 ramp into a clean 200. If you're new to the differences between IP families, the residential proxy explainer breaks down when each type fits.
Two rules keep a good IP good:
- One session, one IP. Bind the
x-kpsdk-cttoken to the exit IP that earned it. Don't replay one "golden" token across a rotating pool; that token-to-IP mismatch is exactly what Kasada watches for. - Geo-match the audience. Scraping a US ticketing site? Exit from a US residential IP. A German IP hitting a US-only drop is an easy anomaly.
Residential IPs are not a standalone bypass. They clear the reputation layer, but the proof-of-work and fingerprint layers still have to pass. Pair a clean IP with a real browser or a matched TLS fingerprint, never one alone.
Pacing: why Kasada punishes speed with compute
This is where Kasada differs from token-only defenses in a way that changes your whole crawl budget. Kasada's difficulty ramp means that the faster and more suspiciously you hit an origin, the harder the proof-of-work becomes, so speed literally costs you CPU. A crawler that fires ten requests a second doesn't just risk a block, it makes every one of its own puzzles more expensive to solve.
- Warm up. Load a light page first and let
p.jsmintx-kpsdk-ctbefore you request deep pages. Jumping straight to a checkout or drop URL with no prior token is a classic bot pattern. - Add jittered delays. Random pauses between requests beat a fixed interval. A perfectly regular heartbeat is itself robotic.
- Cap concurrency per IP at a handful of parallel requests, not hundreds, and spread load across the pool so no single token or IP draws the difficulty ramp.
- Reuse the token, not the request. One good
x-kpsdk-ctcan serve a whole session's worth of same-IP requests. Solve once, reuse, and you spend far less compute than re-solving on every call. - Follow a plausible path. Category, then listing, then detail. Real users don't teleport across a hundred unrelated URLs in a minute.
import random, time
def polite_delay(lo=1.5, hi=5.0):
"""Human-like jittered pause between requests."""
time.sleep(random.uniform(lo, hi))
If the site publishes a Crawl-delay in robots.txt, treat it as a hard floor. Slower crawling costs you throughput today and saves you from a blanket ban tomorrow. Against Kasada it also saves you literal CPU cycles, which makes it the cheapest anti-detection technique there is, and the most ethical.
Bypass Kasada with the SparkProxy Scraping API
Maintaining a real browser farm that solves a rotating proof-of-work, a residential pool, current TLS fingerprints, and careful pacing is a standing engineering job, because Kasada keeps moving its payload. A scraping API collapses those layers into request parameters and keeps them current on the provider's side. The scraping API vs self-managed proxies comparison covers when that trade is worth it. For a proof-of-work target like Kasada, it usually is.
The SparkProxy Scraping API takes a target URL and handles the proxy, the real browser render, the p.js execution, and the puzzle solve for you. Each parameter maps to one of the detection layers above:
curl -G "https://scrape.sparkproxy.io/api/v1" \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "url=https://example.com/catalog" \
--data-urlencode "render_js=true" \
--data-urlencode "premium_proxy=true" \
--data-urlencode "country_code=US" \
--data-urlencode "stealth=true" \
--data-urlencode "human=true" \
--data-urlencode "wait_for=.product-grid" \
--data-urlencode "format=json"
The same request in Python, with each flag annotated by the layer it satisfies:
import requests
def fetch(url):
r = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "YOUR_API_KEY"},
params={
"url": url,
"render_js": "true", # real Chromium runs p.js and solves the KPSDK proof-of-work
"premium_proxy": "true", # residential exit, not a flagged datacenter ASN
"country_code": "US", # geo-match the site's expected audience
"stealth": "true", # extra automation-leak patching the p.js fingerprint reads
"human": "true", # simulated mouse movement to round out the fingerprint
"wait_for": ".product-grid",# hold until content past the 429 challenge renders
"format": "json", # envelope with status_code + body + credits_used
},
timeout=120,
)
return r.json()
render_js=true runs a genuine browser, so p.js executes and a valid x-kpsdk-ct gets minted. premium_proxy=true routes through a residential IP, which clears the reputation layer and keeps the difficulty ramp low. stealth=true adds extra automation-leak patching so the fingerprint reads as a real device. human=true simulates mouse movement and interaction delays. wait_for holds until your real content appears, so you don't capture a challenge page by mistake. With format=json, the reply is an envelope carrying status_code, body, and credits_used. On the SparkProxy price sheet a residential request with JS rendering costs 25 credits, and stealth and country_code add 5 credits each, so turn them on for hard targets like Kasada and leave them off for easy ones.
Kasada isn't the only vendor you'll meet. The fundamentals carry across Cloudflare, DataDome, Akamai Bot Manager, and PerimeterX, but Kasada stands out for its invisible proof-of-work and the 429-plus-x-kpsdk-* signature, so the render_js flag that runs the puzzle carries more weight here than a flag that only replays a cookie.
A retry loop that reads Kasada signals
Tie it together with a loop that branches on the decoded status instead of blindly retrying. With format=json, the envelope's status_code mirrors what the target returned, so you can act on a 429, a 403, or a 5xx differently.
import time
import requests
API = "https://scrape.sparkproxy.io/api/v1"
KEY = "YOUR_API_KEY"
def scrape(url, max_tries=4):
params = {
"url": url,
"render_js": "true",
"premium_proxy": "true",
"country_code": "US",
"stealth": "true",
"human": "true",
"wait_for": ".product-grid",
"format": "json",
}
for attempt in range(1, max_tries + 1):
env = requests.get(API, headers={"X-API-Key": KEY},
params=params, timeout=120).json()
code = env.get("status_code")
if code == 200:
return env["body"] # rendered HTML, past the proof-of-work
if code == 429: # challenge or difficulty ramp: back off, retry fresh
time.sleep(2 ** attempt)
continue
if code == 403 or code >= 500: # hard block or render failed: retry, already max-stealth
time.sleep(1.5 * attempt)
continue
raise RuntimeError(f"Unexpected Kasada status {code} for {url}")
raise RuntimeError(f"Gave up on {url} after {max_tries} tries")
The loop already runs with premium proxy, stealth, and human interaction on, so a 429 retry gets a fresh residential IP and a new browser session that solves the puzzle again rather than replaying a doomed request. Exponential backoff on 429 matters more against Kasada than against most vendors, because hammering the origin drives the difficulty ramp up and makes the next solve harder. When nothing clears after a few tries, the honest move is to stop, respect the site's signal, and revisit whether the data is worth pursuing at all.
Frequently asked questions
FAQ
Scraping public data is broadly permitted in many jurisdictions, but bypassing Kasada does not grant a legal exemption. Legality depends on what you collect and how: stay on public pages, honor robots.txt and the site's Terms of Service, avoid personal data, and never touch content behind a login. When in doubt, get written permission or use the site's official API.
Because a 429 with x-kpsdk-* headers is Kasada's challenge, not a plain rate limit. It means no valid client token was presented, so it's asking your client to run the KPSDK proof-of-work and prove it's a real browser. A bare HTTP client never executes p.js, so it never solves the puzzle, never earns an x-kpsdk-ct, and stays in the 429 loop.
They are KPSDK's session headers. x-kpsdk-ct is the client token Kasada mints after it accepts your proof-of-work, and you send it on later requests as your session pass. x-kpsdk-cd carries the challenge data: an encoded blob with the puzzle answer and its timing, which the server re-checks so a fabricated value fails. You also see x-kpsdk-v (the SDK version) and x-kpsdk-dv (device validation).
Not reliably. The proof-of-work runs inside a polymorphic JavaScript VM that changes on nearly every load, so hand-porting the math is a treadmill. The workable path is a genuine browser (Playwright or Puppeteer) that executes p.js and solves the puzzle for you, or a rendering API that runs one. Once you hold a valid x-kpsdk-ct, you can reuse it on lighter HTTP calls in the same session.
No. A residential IP clears the reputation layer, which is often the fastest block and also keeps the difficulty ramp low, but Kasada still requires a solved proof-of-work, a clean fingerprint, and a matching TLS handshake. A residential IP behind a Python client that never runs p.js still gets a 429. Combine a clean IP with a real browser and human-like pacing.
All three score IP reputation, TLS, and a JavaScript fingerprint, so the basics carry over. Kasada's difference is its client-side proof-of-work: it's usually invisible (no visible CAPTCHA), it challenges with a 429 plus x-kpsdk-* headers rather than a 403, and it punishes speed by raising the puzzle difficulty. That makes actually running the KPSDK computation, not just passing a token, the core requirement.
Get 50% off your first purchase
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon — claim it before it's gone
Related articles

How to Handle Cookies and Sessions When Web Scraping
Handle cookies and sessions when web scraping: cookie jars, Set-Cookie parsing, CSRF tokens, login state, disk persistence, and sticky proxy IPs, in Python.

Async Web Scraping in Python: A Concurrency Guide
Master async web scraping in Python with asyncio, httpx and aiohttp: bound concurrency with semaphores, size connection pools, and isolate failures.

How to Scrape Google News: RSS, Headlines, and Feeds
Scrape Google News with its free RSS feeds: pull headlines and articles, decode the redirected article links, and geo-target editions with hl, gl, and ceid.
