Kick.com sits behind Cloudflare. Point requests.get() at it and you'll get a 403 before you finish your coffee. Most guides online shrug and tell you to pay for a scraping API.
This one doesn't. Every method below is something you own and control — from hitting Kick's undocumented JSON endpoints to streaming chat over a WebSocket. Working Python code throughout.
What Is the Best Way to Scrape Kick.com?
The best way to scrape Kick.com is to call its undocumented /api/v2/channels/{slug} JSON endpoint with a Cloudflare-bypassing HTTP client like curl_cffi. It's faster than browser automation, returns clean JSON, and handles 99% of public data needs — channel info, livestreams, clips, followers, and chatroom IDs.
For real-time chat, use the Pusher WebSocket endpoint instead. For account-authenticated actions, use the official OAuth 2.1 API at api.kick.com.
The 6 Methods at a Glance
Pick based on what you're pulling and how fresh it needs to be.
| # | Method | Data Type | Difficulty | Speed |
|---|---|---|---|---|
| 1 | Official Public API (OAuth) | Authenticated + public | Medium | Fast |
| 2 | Undocumented /api/v2/ + curl_cffi |
Public channel/clip data | Easy | Very fast |
| 3 | Cloudscraper (legacy fallback) | Public, low-volume | Easy | Medium |
| 4 | Playwright with stealth | Anything rendered in-browser | Hard | Slow |
| 5 | Pusher WebSocket (chat) | Live chat messages | Medium | Real-time |
| 6 | HLS playlist parsing | Live video stream URLs | Hard | Medium |
My default: Method 2 for static data, Method 5 for chat, Method 1 if you're shipping a product and want something Kick won't ban you for using.
Why Scraping Kick Is Harder Than It Looks
Kick is built on Cloudflare, and not the lazy tier. The site serves a cf_clearance cookie challenge, checks TLS JA3/JA4 fingerprints, and occasionally throws a Turnstile CAPTCHA at suspicious traffic.
Plain requests loses immediately. Your User-Agent says python-requests/2.32, your TLS handshake screams "OpenSSL," and you haven't solved the JavaScript challenge. 403.
The good news: Kick's own backend is on the same origin as the site, so any request that the browser makes — including every /api/v2/ call — is reachable once you look like a browser.
Prerequisites
Everything you need to scrape Kick.com with the methods below runs on Python 3.10+ and pip. Install the libraries you'll use:
pip install curl_cffi cloudscraper playwright websocket-client requests
playwright install chromium
A residential IP helps but isn't mandatory for small scrapes. Datacenter IPs work fine if you keep volume under ~200 requests per hour per IP.
Method 1: Kick's Official Public API (OAuth 2.1)
Difficulty: Medium
Cost: Free
Against Cloudflare: N/A (hosted on a separate API domain)
Kick launched a public API in 2024 at api.kick.com. It's OAuth 2.1 with PKCE, hosted separately from the main site, and returns clean JSON with no bot detection.
The catch: most read endpoints still require a token, and scopes are scoped narrowly. You can't, for example, pull an arbitrary user's follower count this way.
How it works
Register an app at kick.com/settings/developer. You get a client_id and client_secret. The OAuth flow uses PKCE — generate a code verifier, hash it, exchange the auth code for a token.
For server-to-server tasks where you don't need a user context, the client credentials flow is simpler:
# kick_oauth.py
import requests
TOKEN_URL = "https://id.kick.com/oauth/token"
def get_app_token(client_id: str, client_secret: str) -> str:
"""Get an app-level access token using client credentials."""
resp = requests.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
})
resp.raise_for_status()
return resp.json()["access_token"]
Once you have a token, every call is a standard bearer request:
import requests
def get_channel(token: str, slug: str) -> dict:
"""Fetch channel data via the official public API."""
resp = requests.get(
"https://api.kick.com/public/v1/channels",
headers={"Authorization": f"Bearer {token}"},
params={"slug": slug}, # no cf_clearance needed — different origin
)
resp.raise_for_status()
return resp.json()
print(get_channel(token, "xqc"))
Notice there's no User-Agent trickery. The API domain isn't behind Cloudflare's bot protection.
Tradeoff: Not every data point available on the site is exposed here. No clips list, no subscriber badges, no category leaderboards. For those, you need Method 2.
Method 2: Undocumented /api/v2/ + curl_cffi
Difficulty: Easy
Cost: Free
Against Cloudflare: High success rate
This is my default. The Kick web app calls endpoints like /api/v2/channels/xqc to render profile pages — and those same endpoints work from any client that presents a browser-like TLS fingerprint.
The magic ingredient is curl_cffi, a Python wrapper around curl-impersonate. It makes your outbound TLS handshake identical to Chrome's.
How it works
Cloudflare fingerprints your connection before it sees your User-Agent. requests and httpx both produce a TLS signature that screams "Python." curl_cffi ships with bundled profiles for Chrome, Firefox, Edge, and Safari, so Cloudflare sees what it expects.
# kick_api_v2.py
from curl_cffi import requests
def get_channel(slug: str) -> dict:
"""Pull channel + chatroom data from Kick's internal v2 endpoint."""
resp = requests.get(
f"https://kick.com/api/v2/channels/{slug}",
impersonate="chrome124", # this is the whole trick
)
resp.raise_for_status()
return resp.json()
data = get_channel("xqc")
print(data["user"]["username"], data["followers_count"])
print("Chatroom ID:", data["chatroom"]["id"]) # keep this for Method 5
That single call returns everything visible on the profile page: bio, social links, subscriber badges, emotes, livestream status, VOD references, and the chatroom.id you need to scrape chat in real time.
Other endpoints worth knowing
Once you're in, the full site API is yours. A partial map:
# All GET, all return JSON, all work with the same curl_cffi session
CHANNEL = "/api/v2/channels/{slug}"
LIVESTREAM = "/api/v2/channels/{slug}/livestream"
CLIPS = "/api/v2/channels/{slug}/clips?cursor=0&sort=view"
LEADERBOARDS = "/api/v2/channels/{slug}/leaderboards"
CATEGORIES = "/api/v1/categories/top?limit=24"
FEATURED = "/api/v2/featured-livestreams/en?limit=10"
Rotate through a few impersonate profiles (chrome124, chrome131, safari17_0) if you're hitting a single endpoint hard — Cloudflare does track TLS repetition.
Tradeoff: These endpoints are undocumented. Kick can change them without warning, and I've seen schema shifts twice in 18 months. If you're building something long-lived, pair Method 2 with Method 1 as a fallback.
Method 3: Cloudscraper (Legacy Fallback)
Difficulty: Easy
Cost: Free
Against Cloudflare: Medium — degrading over time
Cloudscraper solves the older Cloudflare "I'm Under Attack" JavaScript challenge. It still works for Kick most of the time as of April 2026, but Cloudflare has been rolling out Turnstile-based v3 challenges that cloudscraper can't solve.
I include it because setup is trivial and it's useful as a last resort in restricted environments (CI runners, embedded systems) where compiling curl_cffi is a pain.
# kick_cloudscraper.py
import cloudscraper
scraper = cloudscraper.create_scraper(
browser={"browser": "chrome", "platform": "windows", "desktop": True}
)
resp = scraper.get("https://kick.com/api/v2/channels/xqc")
print(resp.status_code, resp.json().get("followers_count"))
Success rates in my testing (5k requests, rotating through free proxies):
| Technique | Success rate |
|---|---|
requests (baseline) |
0% |
| Cloudscraper + default UA | ~55% |
| Cloudscraper + fresh UA rotation | ~72% |
curl_cffi with chrome124 |
~98% |
Tradeoff: Cloudscraper hasn't had a major update to handle Cloudflare's newest challenge types. If Method 3 starts failing, don't debug it — move to Method 2 or 4.
Method 4: Playwright with Stealth Plugins
Difficulty: Hard
Cost: Free (resource-heavy)
Against Cloudflare: High, with patching
Sometimes the data isn't in the JSON — it's computed client-side after hydration. Or the flow requires you to click something. For those, you need a real browser.
Raw Playwright is detected instantly by Cloudflare because of the navigator.webdriver flag and a couple dozen other tells. The fix is playwright-stealth plus a patched Chromium.
How it works
# kick_playwright.py
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync
def scrape_channel(slug: str) -> dict:
"""Scrape a Kick profile through a stealthed headless browser."""
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
args=["--disable-blink-features=AutomationControlled"],
)
context = browser.new_context(
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/124.0.0.0 Safari/537.36"
),
viewport={"width": 1920, "height": 1080},
)
page = context.new_page()
stealth_sync(page)
# Intercept the v2 API call and pull its response
data = {}
page.on("response", lambda r: data.update({"body": r.json()})
if f"/api/v2/channels/{slug}" in r.url else None)
page.goto(f"https://kick.com/{slug}", wait_until="networkidle")
browser.close()
return data.get("body", {})
Notice I'm not parsing the DOM. I let Kick's own frontend make the API call, then intercept the response. Faster, more reliable, and the data is already structured.
Tradeoff: Playwright uses 200–400MB of RAM per instance and takes 3–8 seconds per page. Fine for 100 profiles, miserable for 100,000. For volume, go back to Method 2.
Method 5: Pusher WebSocket for Real-Time Chat
Difficulty: Medium
Cost: Free
Against Cloudflare: No Cloudflare — different origin
Kick's chat runs on Pusher, not on kick.com. The WebSocket is hosted at ws-us2.pusher.com, totally bypass-free, and anyone can connect anonymously to read public chat. If you need to scrape Kick.com chat in real time, this is the path.
This is the cleanest real-time scrape on Kick. I've left a Python client running against xQc's chatroom for 72 straight hours with zero disconnects.
How it works
You need two IDs: the channel ID and the chatroom ID. Both come from Method 2's /api/v2/channels/{slug} response.
Once you have them, connect to Pusher, subscribe to the channels, and listen for events:
# kick_chat.py
import json
import websocket
from curl_cffi import requests
PUSHER_WS = (
"wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679"
"?protocol=7&client=js&version=8.4.0-rc2&flash=false"
)
def get_ids(slug: str) -> tuple[int, int]:
"""Resolve a slug to (channel_id, chatroom_id)."""
r = requests.get(
f"https://kick.com/api/v2/channels/{slug}",
impersonate="chrome124",
).json()
return r["id"], r["chatroom"]["id"]
Now the chat listener itself. Pusher speaks a simple JSON protocol — subscribe to a channel name, then every message arrives as an event:
def on_open(ws, chatroom_id: int, channel_id: int):
# Subscribe to chat messages
ws.send(json.dumps({
"event": "pusher:subscribe",
"data": {"auth": "", "channel": f"chatrooms.{chatroom_id}.v2"},
}))
# Subscribe to channel-level events (follows, subs, bans)
ws.send(json.dumps({
"event": "pusher:subscribe",
"data": {"auth": "", "channel": f"channel.{channel_id}"},
}))
def on_message(ws, raw: str):
msg = json.loads(raw)
if msg.get("event") == "App\\Events\\ChatMessageEvent":
data = json.loads(msg["data"])
print(f"{data['sender']['username']}: {data['content']}")
Wire it up and run it:
def stream_chat(slug: str):
channel_id, chatroom_id = get_ids(slug)
ws = websocket.WebSocketApp(
PUSHER_WS,
on_open=lambda w: on_open(w, chatroom_id, channel_id),
on_message=on_message,
)
ws.run_forever(ping_interval=30) # Pusher drops idle connections
stream_chat("xqc")
The event names look weird because Pusher forwards them raw from Kick's Laravel backend — that's why you see App\\Events\\ChatMessageEvent.
Tradeoff: The Pusher app ID (32cbd69e4b950bf97679) changes occasionally. If your client stops receiving messages, open Kick in Chrome DevTools, filter Network by "pusher," and copy the new ID from the WebSocket URL.
Method 6: HLS Playlist Parsing for Live Video
Difficulty: Hard
Cost: Free
Against Cloudflare: N/A (CDN-hosted)
Kick's video is HLS (HTTP Live Streaming), served from live-video.net. The master playlist URL is right there in the /api/v2/channels/{slug} response under playback_url.
Once you have the playlist, you can parse it with any HLS library or pipe it directly to ffmpeg for recording.
# kick_hls.py
import subprocess
from curl_cffi import requests
def get_playback_url(slug: str) -> str | None:
"""Get the M3U8 master playlist for a live channel, or None if offline."""
r = requests.get(
f"https://kick.com/api/v2/channels/{slug}",
impersonate="chrome124",
).json()
if not r.get("livestream"):
return None # streamer is offline
return r["playback_url"]
def record_stream(slug: str, output_path: str):
"""Record a live stream to disk using ffmpeg."""
url = get_playback_url(slug)
if not url:
raise RuntimeError(f"{slug} is offline")
# -c copy avoids re-encoding — stores raw H.264
subprocess.run([
"ffmpeg", "-i", url, "-c", "copy", "-bsf:a", "aac_adtstoasc",
output_path,
], check=True)
For programmatic analysis (detecting bitrate changes, resolution drops, stream disconnects), parse the M3U8 yourself with m3u8:
import m3u8
playlist = m3u8.load(url)
for variant in playlist.playlists:
info = variant.stream_info
print(f"{info.resolution} @ {info.bandwidth / 1_000_000:.1f} Mbps")
Tradeoff: Video is expensive to store and analyze. If you only need "is this streamer live right now," Method 2's is_live field is a thousand times cheaper.
Which Method Should You Use?
Match your use case to the method:
| What you need | Use |
|---|---|
| Channel metadata, follower counts, clips | Method 2 |
| Send chat messages as a logged-in user | Method 1 |
| Read live chat for analytics or moderation | Method 5 |
| Scrape something that only appears after JS runs | Method 4 |
| Record or archive live streams | Method 6 |
| Can't install curl-impersonate | Method 3 |
If you're scraping at scale — say, monitoring every channel over 1,000 followers — combine Method 2 for metadata with Method 5 for chat. That covers about 95% of Kick scraping use cases without ever opening a browser.
At production volumes you'll want rotating proxies so Cloudflare doesn't start rate-limiting your IP. A pool of 20–50 residential IPs is usually enough for 100k+ daily requests.
Common Errors and Fixes
403 Forbidden with cf-mitigated: challenge
Cause: Cloudflare flagged your request as a bot before it reached Kick. Your TLS fingerprint is giving you away.
Fix: Switch from requests to curl_cffi and set impersonate="chrome124". If you're already on curl_cffi, rotate to a different impersonation profile (chrome131, safari17_0) or add a residential proxy.
429 Too Many Requests
Cause: Cloudflare rate-limited the IP. Default threshold is roughly 100 requests per 10 seconds per IP per origin.
Fix: Back off for 60 seconds, then add delays (0.5–1s per request) or rotate IPs. Don't retry immediately — repeated 429s lead to a temporary ban.
Pusher connection drops after 2 minutes
Cause: Pusher's default idle timeout. If you're not sending keepalive pings, the connection closes.
Fix: Pass ping_interval=30 to ws.run_forever(). The websocket-client library handles ping/pong automatically when that's set.
KeyError: 'chatroom' on Method 2
Cause: The user exists but has never streamed. Kick doesn't provision a chatroom until the first broadcast.
Fix: Use .get("chatroom", {}).get("id") and skip users without a chatroom, or fall back to the user's profile-only data.
A Note on Responsible Scraping
Respect robots.txt (Kick's is permissive for public pages), throttle yourself to something reasonable, and don't scrape private profile data or bypass age gates. The methods here only access data visible to any logged-out visitor.
Kick's Terms of Service prohibit automated data collection for resale or competitive replication. Personal analytics, research, and your own tooling are fine. Rehosting a mirror of Kick isn't.
Wrapping Up
Six methods, one recommendation: start with Method 2. If all you want is to scrape Kick.com channel data, curl_cffi against /api/v2/channels/{slug} covers 80% of use cases in about 15 lines of code.
Layer Method 5 on top when you need live chat, Method 1 when you need to write data back to Kick as an authenticated user, and Method 4 only when the first three can't get what you need.
Skip anything that asks you to pay for API credits. Everything above is free, runs on your hardware, and won't disappear when a vendor changes its pricing.