Quick answer: Kick.com exposes no API for past chat and no download button. A kick chat scraper connects to Kick's public Pusher WebSocket — the same one the kick.com website uses to render chat — subscribes to one or more channels by slug, and archives every message in real time with sender, role, badges, hex color, and timestamps. The Apify Actor below costs $0.001 per message (~$1.05 per 1,000). Once a stream ends, that chat is gone. You have one window to capture it: while the stream is live.
Kick is the 4th-largest live-streaming platform as of Q3 2025, and it has a problem Twitch and YouTube solved years ago: no native chat history. On Twitch, GET /v2/channels/{channel}/videos/{video}/comments hands you past chat line by line; on YouTube, Live Chat Replay is baked into the VOD. On Kick, once the stream ends, the chat is gone from every surface — website, mobile app, and any publicly documented API endpoint.
So the streamer who ran a giveaway and wants to verify the winner, the moderation team auditing a harassment incident after the fact, and the researcher building a Kick toxicity classifier all have zero official options — unless the chat was captured live.
This post explains what the underlying surface is, why connecting to it is harder than it looks, and how to run the Kick Chat Archive Actor.
What is Kick.com? 🎮
Kick.com is a live-streaming platform launched in 2022, positioned as a Twitch alternative with a higher streamer revenue share (95/5 vs Twitch's 50/50). As of Q3 2025 it ranks 4th globally behind Twitch, YouTube Live, and Facebook Gaming by concurrent viewer count.
What Kick does not have: a documented REST API for past chat, a VOD-attached replay, or a bulk export. The official Kick developer resources cover OAuth and webhooks, but chat history is not among them.
Does Kick have a chat history API?
No. Kick has no public API endpoint that returns past chat. The only programmatic surface is the live Pusher WebSocket the kick.com frontend uses to render chat in real time. Once a ChatMessageEvent is delivered, Kick does not persist it anywhere accessible to third parties. Scraping Kick chat isn't a supplement to an official API — it's the only option.
What the data looks like
Every archived message becomes one flat, typed row:
{
"channel_slug": "abuswe7l",
"chatroom_id": 31118945,
"message_id": "8049a026-5c2e-4619-b60d-393b7217f4da",
"sender_id": 5666938,
"sender_username": "Soud_x5",
"sender_slug": "soud-x5",
"sender_color": "#FF9D00",
"sender_role": "moderator",
"sender_badges": ["moderator", "subscriber"],
"content": "[emote:2506823:azzzjh] hello chat",
"message_type": "message",
"sent_at": "2026-05-16T20:40:16+00:00",
"scraped_at": "2026-05-16T20:40:17+00:00"
}
Thirteen fields per message: channel slug, Pusher chatroom ID, Kick's message UUID, the sender's numeric ID, display name, URL slug, hex chat color, a canonical role label, the raw badge list, the message text (emote tokens preserved), message type, the UTC send timestamp, and the UTC timestamp we wrote the row — all Pydantic-validated before they hit the dataset. The sender_role collapses the badge list into one label via the chain broadcaster > moderator > staff > founder > og > vip > subscriber > user, with the raw sender_badges array kept alongside it.
The naive approach (and why it falls apart) 🔧
The WebSocket endpoint is technically public — your browser is already connected to it when you watch a Kick stream. So the first attempt usually looks like: open DevTools, find the Pusher subscription for chatroom.{N}, connect a generic WebSocket client, parse the JSON frames, write to CSV. It breaks in three distinct places:
1. Cloudflare on the channel-lookup hop. Before subscribing to a chatroom you need its numeric ID for a given slug, which comes from Kick's channel REST endpoint behind Cloudflare. A generic Python requests session gets a 403 because it emits a non-browser TLS fingerprint. We rotate curl-cffi browser fingerprints (Chrome / Firefox / Safari TLS + HTTP/2 ALPN frames) on every lookup and thread Apify residential proxies behind it, so the handshake looks like a real browser on a real residential IP.
2. Connection drops and the cold path. Pusher connections drop, subscription confirmations arrive out of order, and the App\Events\ChatMessageEvent name is undocumented. We reconnect on transient drops, wait for the pusher:connection_established frame before subscribing, and retry with exponential backoff on 408 / 429 / 5xx — up to 5 attempts, Retry-After honored. If the channel is offline, we exit non-zero with a clear status message rather than a green run with an empty dataset.
3. The role-derivation problem. Kick sends a badges array with one object per badge type, so a sender can hold moderator, subscriber, and og at once. Downstream analytics want one clean sender_role per row. We derive the canonical role and keep the raw array alongside it — both the clean label and the original signal.
We do the dirty work. What lands in your dataset is validated, ISO-8601-stamped, stable-UUID rows — no partial frames, no silent empties.
The Actor ⚡
The Actor is on the Apify Store: apify.com/DevilScrapes/kick-chat-archive. Paste a channel slug in the Console and click Start, or run it from Python:
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("DevilScrapes/kick-chat-archive").call(
run_input={
"channelSlugs": ["xqc", "trainwreckstv"],
"maxDurationSeconds": 600, # 10-min window
"maxMessagesTotal": 5000, # exits early if hit first
"proxyConfiguration": {"useApifyProxy": True},
}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["sender_username"], ":", item["content"])
Key input parameters (the run exits as soon as the first cap fires):
| Parameter | Default | Range | Notes |
|---|---|---|---|
channelSlugs |
["xqc"] |
1–20 slugs | Full URLs accepted and normalized |
maxDurationSeconds |
300 |
5–3 600 | Hard 1-hour ceiling |
maxMessagesTotal |
1000 |
1–50 000 | Across all channels combined |
proxyConfiguration |
Apify Proxy on | — | Used for the REST channel-lookup only |
What you would actually use this for
Five concrete scenarios, not abstract "data collection":
Streamer self-archive. You ran a 3-hour stream with a giveaway and want a permanent record for VOD highlights and winner verification. Schedule the Actor to start with your stream; it runs until the message cap fires. Export JSON, search it, done.
Brand monitoring during a launch. Announcing a product during a sponsored stream? Start the Actor on the channel 5 minutes before the drop and let it run for 2 hours. You get every mention — including ones the integration manager will never catch live.
Toxicity / sentiment model training. For a classifier that needs cross-platform signal, the sender_role and sender_badges fields are already engineered features — no separate preprocessing step.
Stream highlight detection. Message-per-minute spikes are a strong proxy for moments worth clipping. Record the volume curve, threshold it, ship the timestamps to your VOD editor.
Moderator coverage audit. Record message counts alongside moderator badge holders across channels. Where the moderator role accounts for under 2% of messages during high-velocity windows, the channel is under-moderated — the data makes the case in a spreadsheet rather than anecdote.
Pricing — exact numbers 💰
Pay-Per-Event. You pay for messages that land in your dataset — no data, no charge beyond the run warm-up.
| Event | Price |
|---|---|
actor-start (one per run) |
$0.05 |
result-row (per archived message) |
$0.001 |
In practice:
| Messages archived | Total cost |
|---|---|
| 100 | $0.15 |
| 1,000 | $1.05 |
| 5,000 | $5.05 |
| 50,000 | $50.05 |
Apify's $5 free trial credit covers your first 4,950 messages with no credit card.
The technically interesting bit
Kick's chat is delivered via Pusher — a hosted real-time messaging service — rather than a custom WebSocket server. The event name is App\Events\ChatMessageEvent and channels follow the pattern chatroom.{chatroom_id}.
The wrinkle: getting from a slug like xqc to the numeric chatroom_id needs a REST call to Kick's Cloudflare-protected channel endpoint, so that hop needs browser-level TLS fingerprinting and proxy rotation. The WebSocket itself is a Pusher-hosted direct connection that bypasses the proxy — proxying a WebSocket resolving to a CDN edge just adds latency without changing the fingerprint Cloudflare inspects. So the Actor splits the paths: proxy + curl-cffi for the REST lookup, direct connection for the WebSocket. Up to 20 channels then multiplex over that single socket, so one run can archive a full esports bracket in parallel.
Limitations
- Real-time only. Kick stores no publicly accessible chat. This Actor archives only chat sent while the run is active — there is no retroactive recovery.
- Public chatrooms only. Followers-only and subscribers-only modes require a Kick login token; out of scope for v1.
-
Single Pusher cluster. Kick currently routes through
ws-us2.pusher.com. If Kick migrates clusters, the Actor will need an update. -
No moderation events. Bans, deletions, slow-mode toggles, and pinned messages arrive on the WebSocket but are intentionally dropped; only
ChatMessageEventrows are written. A sibling Actor will cover moderation later. -
No emote image resolution.
[emote:N:name]tokens are preserved verbatim. Resolve them to PNG/SVG via Kick's emote endpoints separately.
FAQ
Is archiving Kick chat legal?
The Pusher WebSocket we connect to is the same one every public viewer's browser connects to. We read only what Kick delivers to all viewers of a public stream — we do not log in, send messages, cast votes, or touch any authenticated surface. As with any data project, verify compliance for your jurisdiction and use case before deploying at scale.
Can I export the archived chat to Google Sheets or a warehouse?
Yes. From Apify Console, export JSON, CSV, or Excel directly from the dataset. For automation, webhook the dataset on ACTOR.RUN.SUCCEEDED to Make, Zapier, or n8n, or pull it via the Apify API.
Does Kick have an API I could use instead?
Not for chat. The official Kick developer portal covers OAuth and certain event webhooks, but chat history is absent. This Actor is the only programmatic path to live Kick chat data.
Why is sender_role a single value when the user has multiple badges?
We apply the priority chain broadcaster > moderator > staff > founder > og > vip > subscriber > user and pick the highest one per message, so sender_role is immediately usable for GROUP BY. The full badge list stays in sender_badges if you need it.
Try it
The Actor is live: apify.com/DevilScrapes/kick-chat-archive.
Free $5 trial credit, no credit card. Point it at any public live stream — xqc is live most evenings — and you'll have rows in your dataset within seconds of the run starting. Want a use case not covered here, or a field the output is missing? Leave it in the comments — the roadmap is driven by what people actually need.
Built by Devil Scrapes — Apify Actors with attitude. Pay-per-event, transparent pricing. 😈
Top comments (0)