> ## Content Index
> Fetch the complete content index at: https://roundproxies.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 6 Ways to Scrape Kick.com in 2026
- URL: https://roundproxies.com/blog/scrape-kick-com/
- Published: 2026-04-16T19:33:48.000Z
- Updated: 2026-09-23T13:04:19.000Z
- Description: Scrape Kick.com without paid APIs. 6 working methods with Python code, from the public API to WebSocket chat and Cloudflare-bypassing clients.
- Author: Marius Bernard
- Tags: Web Scraping, #dated-69e00a9cf1b8cae22148ca3e

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:

```bash
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)

****Best for:** Production apps that need to send chat, subscribe to events, or manage your own channel.  
****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:

```python
# 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:

```python
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

****Best for:** Public channel data, clips, leaderboards, chatroom IDs.  
****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](https://github.com/lexiforest/curl%5Fcffi), 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.

```python
# 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:

```python
# 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)

****Best for:** Low-volume scraping on machines where you can't install curl-impersonate.  
****Difficulty:** Easy  
****Cost:** Free  
****Against Cloudflare:** Medium — degrading over time

[Cloudscraper](https://github.com/VeNoMouS/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.

```python
# 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

****Best for:** Scraping JS-rendered widgets, emotes, or flows that require actual page interaction.  
****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

```python
# 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

****Best for:** Chat analytics, moderation bots, live sentiment tracking.  
****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:

```python
# 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:

```python
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:

```python
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

****Best for:** Recording streams, building VOD archives, bitrate analysis.  
****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.

```python
# 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`:

```python
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](https://roundproxies.com/) 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.