Automate Twitch stream-live alerts using EventSub subscriptions — PubSub was fully decommissioned April 14, 2025, so EventSub is now mandatory. Subscribe to stream.online and stream.offline event types using an App Access Token, then your webhook or WebSocket receives a notification and posts the alert to Discord, Slack, or email. Webhook transport handles up to 10,000 subscription cost units; WebSocket is simpler for hobby projects.
API Quick Reference
App Access Token (Client Credentials)
800 points/minute Helix bucket
JSON
Available
Understanding the Twitch EventSub API for Stream Alerts
The Twitch Helix REST API (base URL: https://api.twitch.tv/helix) is the current official API, replacing the deprecated v5/Kraken API. Every Helix request must include two headers: Authorization: Bearer <token> and Client-Id: <your_client_id>. Omitting either returns a 401 or 400 error. EventSub is Twitch's event delivery system for real-time notifications — PubSub, the previous system, was fully decommissioned on April 14, 2025 and can no longer be used.
EventSub supports three transport types: Webhook (Twitch POSTs to your HTTPS endpoint), WebSocket (your app maintains a WebSocket connection to wss://eventsub.wss.twitch.tv/ws), and Conduits (a newer pooled webhook architecture for very large bots). For stream alerts, webhook transport scales to thousands of channels (max_total_cost starts at 10,000) while WebSocket is simpler for hobby projects following a single channel but is limited to max_total_cost of 10 per client_id+user_id tuple.
The critical step for webhook transport is the challenge verification: when you create an EventSub subscription, Twitch immediately sends a POST with a webhook_callback_verification message type containing a challenge string. Your endpoint must respond within 10 seconds with the challenge value as the raw response body (Content-Type: text/plain) or the subscription is revoked. Once verified, Twitch delivers stream.online and stream.offline events to the same URL. The official documentation is at https://dev.twitch.tv/docs/eventsub/.
https://api.twitch.tv/helixSetting Up Twitch API Authentication for Stream Alerts
Stream alert EventSub subscriptions use an App Access Token obtained via the Client Credentials OAuth flow — no user authorization is needed for stream.online/offline events. Every request to Helix requires both the Authorization: Bearer <token> header and the Client-Id: <client_id> header. App Access Tokens have a ~60-day lifespan and are not refreshable; request a new one via the same Client Credentials flow when the token expires.
- 1Go to dev.twitch.tv/console/apps and log in with your Twitch account
- 2Click 'Register Your Application'. Set Name to 'StreamAlertsBot', OAuth Redirect URLs to https://localhost (placeholder for client credentials flow), and Category to 'Chat Bot'
- 3Set Client Type to 'Confidential' for server-side apps (this enables a Client Secret)
- 4Save the app — copy the Client ID immediately. Click 'New Secret' and copy the Client Secret
- 5Store CLIENT_ID and CLIENT_SECRET in environment variables — never in code
- 6Request an App Access Token via POST to https://id.twitch.tv/oauth2/token with grant_type=client_credentials
- 7Validate the token hourly via GET https://id.twitch.tv/oauth2/validate and on any 401 response
1import os2import requests34CLIENT_ID = os.environ['TWITCH_CLIENT_ID']5CLIENT_SECRET = os.environ['TWITCH_CLIENT_SECRET']67def get_app_access_token() -> str:8 """Request a new App Access Token via Client Credentials."""9 response = requests.post(10 'https://id.twitch.tv/oauth2/token',11 data={12 'client_id': CLIENT_ID,13 'client_secret': CLIENT_SECRET,14 'grant_type': 'client_credentials'15 }16 )17 response.raise_for_status()18 token_data = response.json()19 return token_data['access_token']2021def validate_token(token: str) -> bool:22 """Validate token - call hourly and on every 401."""23 resp = requests.get(24 'https://id.twitch.tv/oauth2/validate',25 headers={'Authorization': f'OAuth {token}'}26 )27 return resp.status_code == 2002829# Usage30ACCESS_TOKEN = get_app_access_token()31print(f'Token valid: {validate_token(ACCESS_TOKEN)}')Security notes
- •Store CLIENT_ID, CLIENT_SECRET, and your EventSub webhook secret in environment variables — never hardcode them
- •Verify the Twitch-Eventsub-Message-Signature header on every incoming webhook to prevent spoofed events
- •App Access Tokens are not user-specific — do not use them to perform actions on behalf of a user (use User Access Tokens for that)
- •Call GET https://id.twitch.tv/oauth2/validate hourly; if it returns 401, request a new token before the next API call
- •Use a separate secret for each EventSub subscription you create — makes it easy to rotate one without invalidating others
- •Never log the full access token — log only the first 8 characters for debugging
Key endpoints
/eventsub/subscriptionsCreates an EventSub subscription for stream.online or stream.offline events. The response includes a status of webhook_callback_verification_pending until your endpoint echoes the challenge.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | required | Event type — use 'stream.online' for going live and 'stream.offline' for stream end. |
version | string | required | Event version. Use '1' for stream.online and stream.offline. |
condition.broadcaster_user_id | string | required | The Twitch user ID (not username) of the broadcaster to monitor. Get this via GET /users?login=<username>. |
transport.secret | string | required | A secret string between 10 and 100 characters used to verify webhook payloads via HMAC-SHA256. |
Request
1{"type": "stream.online", "version": "1", "condition": {"broadcaster_user_id": "12345678"}, "transport": {"method": "webhook", "callback": "https://yourdomain.com/twitch/eventsub", "secret": "your_webhook_secret_10_to_100_chars"}}Response
1{"data": [{"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", "status": "webhook_callback_verification_pending", "type": "stream.online", "version": "1", "condition": {"broadcaster_user_id": "12345678"}, "transport": {"method": "webhook", "callback": "https://yourdomain.com/twitch/eventsub"}}]}/eventsub/subscriptionsLists all current EventSub subscriptions. Use to audit what you have subscribed and check subscription status.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | optional | Filter by status: enabled, webhook_callback_verification_pending, webhook_callback_verification_failed, notification_failures_exceeded, authorization_revoked, user_removed. |
Response
1{"data": [{"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4", "status": "enabled", "type": "stream.online", "condition": {"broadcaster_user_id": "12345678"}}], "total": 1, "total_cost": 1, "max_total_cost": 10000}/usersLooks up a Twitch user by login name to get their numeric user ID, which is required for EventSub subscription conditions.
| Parameter | Type | Required | Description |
|---|---|---|---|
login | string | optional | Twitch username (lowercase). Returns the user's numeric ID needed for subscription conditions. |
Response
1{"data": [{"id": "12345678", "login": "ninja", "display_name": "Ninja", "broadcaster_type": "partner"}]}/streamsFetches current live stream data including title, game, viewer count, and thumbnail URL. Call this after receiving a stream.online event to enrich your alert.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | optional | Get stream data for a specific broadcaster by their user ID. |
user_login | string | optional | Alternative to user_id — look up by Twitch username. |
Response
1{"data": [{"id": "987654321", "user_id": "12345678", "user_name": "Ninja", "game_name": "Fortnite", "title": "RANKED GRIND", "viewer_count": 45000, "started_at": "2026-05-22T10:00:00Z", "thumbnail_url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_ninja-{width}x{height}.jpg"}]}Step-by-step automation
Resolve Broadcaster Username to User ID
Why: EventSub subscriptions require the numeric broadcaster_user_id, not the username — and user IDs do not change even if the broadcaster renames their account.
Call GET /users?login=<username> with your App Access Token and both required headers. Parse the id field from the first item in the data array. Store this ID in your database rather than looking it up every time.
1curl -X GET 'https://api.twitch.tv/helix/users?login=ninja' \2 -H 'Authorization: Bearer <app_access_token>' \3 -H 'Client-Id: <your_client_id>'Pro tip: Store the user ID in a database. User IDs are stable — they never change even if the broadcaster changes their display name or username.
Expected result: Returns the broadcaster's numeric ID (e.g., '12345678'). Use this ID in the condition field of all subsequent EventSub subscriptions.
Create EventSub Subscriptions for stream.online and stream.offline
Why: EventSub is the only supported real-time event system for Twitch since PubSub was decommissioned April 14, 2025 — there is no polling-based equivalent for stream start/end.
POST to /eventsub/subscriptions twice: once for type 'stream.online' and once for 'stream.offline'. Both require version '1', the broadcaster_user_id condition, and webhook transport configuration with your HTTPS callback URL and a 10-100 character secret. Use your App Access Token for this — no user authorization is needed.
1# Subscribe to stream.online2curl -X POST 'https://api.twitch.tv/helix/eventsub/subscriptions' \3 -H 'Authorization: Bearer <app_access_token>' \4 -H 'Client-Id: <your_client_id>' \5 -H 'Content-Type: application/json' \6 --data '{7 "type": "stream.online",8 "version": "1",9 "condition": {"broadcaster_user_id": "12345678"},10 "transport": {11 "method": "webhook",12 "callback": "https://yourdomain.com/twitch/eventsub",13 "secret": "my_webhook_secret_at_least_10_chars"14 }15 }'1617# Subscribe to stream.offline18curl -X POST 'https://api.twitch.tv/helix/eventsub/subscriptions' \19 -H 'Authorization: Bearer <app_access_token>' \20 -H 'Client-Id: <your_client_id>' \21 -H 'Content-Type: application/json' \22 --data '{23 "type": "stream.offline",24 "version": "1",25 "condition": {"broadcaster_user_id": "12345678"},26 "transport": {27 "method": "webhook",28 "callback": "https://yourdomain.com/twitch/eventsub",29 "secret": "my_webhook_secret_at_least_10_chars"30 }31 }'Pro tip: If you get a 409 Conflict, the subscription already exists. Call GET /eventsub/subscriptions to audit what is active and delete stale ones with DELETE /eventsub/subscriptions?id=<id>.
Expected result: Both subscriptions return status 'webhook_callback_verification_pending'. Twitch immediately sends a verification request to your callback URL — your endpoint must echo the challenge to activate them.
Handle Webhook Verification and Event Delivery
Why: Twitch sends a challenge verification immediately after subscription creation — if your endpoint does not echo it within 10 seconds, the subscription is revoked and you will miss all events.
Your callback endpoint receives three types of POST requests, distinguished by the Twitch-Eventsub-Message-Type header: 'webhook_callback_verification' (echo the challenge), 'notification' (process the event), and 'revocation' (log and resubscribe). For each notification, verify the HMAC-SHA256 signature in Twitch-Eventsub-Message-Signature before processing — compute it from the raw body, not a parsed object.
1# Simulate a Twitch challenge (your server must respond with the challenge value)2# This is what Twitch sends to your endpoint:3curl -X POST https://yourdomain.com/twitch/eventsub \4 -H 'Content-Type: application/json' \5 -H 'Twitch-Eventsub-Message-Type: webhook_callback_verification' \6 --data '{"challenge": "pogchamp-monkeys-f00bar", "subscription": {"id": "abc123"}}'Pro tip: Use express.raw() (Node.js) or request.get_data() (Flask/Python) to access the raw body bytes — you must compute the HMAC over the raw bytes, not a re-serialized JSON string, or the signature will not match.
Expected result: The subscription status changes from 'webhook_callback_verification_pending' to 'enabled' after your endpoint successfully echoes the challenge. Verify via GET /eventsub/subscriptions.
Enrich the Alert with Stream Data and Post to Discord or Slack
Why: The stream.online event only contains the broadcaster ID — you need to call GET /streams to get the current title, game, viewer count, and thumbnail for a useful alert.
After receiving a stream.online notification, call GET /streams?user_id=<broadcaster_id> to fetch live stream details. Format the alert as a Discord embed or Slack Block Kit message including the stream title, game, thumbnail, and a link to the channel. Post to Discord via their Incoming Webhook URL or to Slack via chat.postMessage.
1# Fetch stream details2curl -X GET 'https://api.twitch.tv/helix/streams?user_id=12345678' \3 -H 'Authorization: Bearer <access_token>' \4 -H 'Client-Id: <client_id>'56# Post to Discord webhook7curl -X POST 'https://discord.com/api/webhooks/YOUR_ID/YOUR_TOKEN' \8 -H 'Content-Type: application/json' \9 --data '{10 "embeds": [{11 "title": "Ninja is now LIVE on Twitch!",12 "description": "Playing Fortnite — RANKED GRIND",13 "url": "https://twitch.tv/ninja",14 "color": 9520895,15 "image": {"url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_ninja-320x180.jpg?t=12345"}16 }]17 }'Pro tip: Twitch thumbnail URLs include a {width}x{height} placeholder — replace it with actual pixel dimensions (e.g., 320x180) before using the URL in embeds. Also append a cache-busting query parameter like ?t=<timestamp> to force Discord and Slack to load the current frame rather than a cached old one.
Expected result: A formatted Discord embed or Slack message appears in your notification channel showing the streamer's name, game, title, live viewer count, and stream thumbnail.
Complete working code
This complete script registers EventSub subscriptions for stream.online and stream.offline, handles Twitch's webhook verification challenge, verifies all incoming payloads with HMAC-SHA256, fetches stream details when a broadcaster goes live, and posts a formatted Discord embed alert.
1import os2import hmac3import hashlib4import time5import logging6import requests7from flask import Flask, request, Response89logging.basicConfig(level=logging.INFO)10logger = logging.getLogger(__name__)1112CLIENT_ID = os.environ['TWITCH_CLIENT_ID']13CLIENT_SECRET = os.environ['TWITCH_CLIENT_SECRET']14WEBHOOK_SECRET = os.environ['TWITCH_WEBHOOK_SECRET']15CALLBACK_URL = os.environ['TWITCH_CALLBACK_URL']16DISCORD_WEBHOOK_URL = os.environ['DISCORD_WEBHOOK_URL']1718app = Flask(__name__)1920def get_access_token():21 resp = requests.post('https://id.twitch.tv/oauth2/token', data={22 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET,23 'grant_type': 'client_credentials'24 })25 resp.raise_for_status()26 return resp.json()['access_token']2728def helix_headers(token):29 return {'Authorization': f'Bearer {token}', 'Client-Id': CLIENT_ID}3031ACCESS_TOKEN = get_access_token()3233def get_broadcaster_id(username):34 resp = requests.get('https://api.twitch.tv/helix/users',35 headers=helix_headers(ACCESS_TOKEN), params={'login': username})36 resp.raise_for_status()37 data = resp.json()['data']38 if not data:39 raise ValueError(f'User not found: {username}')40 return data[0]['id']4142def subscribe(event_type, broadcaster_id):43 resp = requests.post('https://api.twitch.tv/helix/eventsub/subscriptions',44 headers={**helix_headers(ACCESS_TOKEN), 'Content-Type': 'application/json'},45 json={46 'type': event_type, 'version': '1',47 'condition': {'broadcaster_user_id': broadcaster_id},48 'transport': {'method': 'webhook', 'callback': CALLBACK_URL, 'secret': WEBHOOK_SECRET}49 }50 )51 if resp.status_code == 409:52 logger.info(f'{event_type} subscription already exists')53 return54 resp.raise_for_status()55 logger.info(f'Subscribed to {event_type}')5657def verify_signature(raw_body, headers):58 msg = (headers.get('Twitch-Eventsub-Message-Id', '').encode() +59 headers.get('Twitch-Eventsub-Message-Timestamp', '').encode() + raw_body)60 expected = 'sha256=' + hmac.new(WEBHOOK_SECRET.encode(), msg, hashlib.sha256).hexdigest()61 return hmac.compare_digest(expected, headers.get('Twitch-Eventsub-Message-Signature', ''))6263@app.route('/twitch/eventsub', methods=['POST'])64def eventsub():65 raw = request.get_data()66 if not verify_signature(raw, dict(request.headers)):67 return Response('Forbidden', status=403)68 data = request.get_json()69 msg_type = request.headers.get('Twitch-Eventsub-Message-Type')70 if msg_type == 'webhook_callback_verification':71 return Response(data['challenge'], content_type='text/plain', status=200)72 if msg_type == 'notification':73 event = data['event']74 sub_type = data['subscription']['type']75 if sub_type == 'stream.online':76 username = event['broadcaster_user_name']77 bid = event['broadcaster_user_id']78 stream = requests.get('https://api.twitch.tv/helix/streams',79 headers=helix_headers(ACCESS_TOKEN),80 params={'user_id': bid}).json().get('data', [])81 if stream:82 s = stream[0]83 thumb = s['thumbnail_url'].replace('{width}','320').replace('{height}','180')84 thumb += f'?t={int(time.time())}'85 requests.post(DISCORD_WEBHOOK_URL, json={'embeds': [{86 'title': f'{username} is LIVE!',87 'description': f'Playing **{s["game_name"]}**\n{s["title"]}',88 'url': f'https://twitch.tv/{username}',89 'color': 9520895, 'image': {'url': thumb},90 'fields': [{'name': 'Viewers', 'value': str(s['viewer_count']), 'inline': True}]91 }]})92 logger.info(f'Alert posted for {username}')93 elif sub_type == 'stream.offline':94 requests.post(DISCORD_WEBHOOK_URL, json={95 'content': f':red_circle: {event["broadcaster_user_name"]} has gone offline.'96 })97 return Response(status=204)9899if __name__ == '__main__':100 broadcaster_id = get_broadcaster_id(os.environ.get('TWITCH_CHANNEL', 'ninja'))101 subscribe('stream.online', broadcaster_id)102 subscribe('stream.offline', broadcaster_id)103 app.run(port=8080)Error handling
{"error": "Unprocessable Entity", "status": 422, "message": "subscription missing proper callback verification"}Your webhook endpoint did not echo the challenge field within 10 seconds, or returned it as JSON instead of plain text. The subscription is revoked.
Return the challenge string with Content-Type: text/plain and HTTP 200 — not wrapped in JSON. Also ensure your endpoint is publicly reachable over HTTPS (not localhost). Delete the failed subscription and recreate.
Not retryable. Delete the subscription (DELETE /eventsub/subscriptions?id=<id>) and resubscribe after fixing the verification endpoint.
{"error": "Conflict", "status": 409, "message": "subscription already exists"}You are attempting to create a subscription for the same type, version, and condition that already exists. Up to 3 subscriptions with identical type+condition are allowed, but duplicates beyond that return 409.
Check existing subscriptions via GET /eventsub/subscriptions before creating new ones. Delete stale subscriptions first.
Not retryable. Use the existing subscription or delete it and recreate.
{"status": 401, "message": "Invalid OAuth token"}The App Access Token has expired (~60 days) or was revoked. Both Authorization: Bearer and Client-Id headers must be present and valid.
Request a new App Access Token via POST to https://id.twitch.tv/oauth2/token with grant_type=client_credentials. Validate tokens hourly via GET https://id.twitch.tv/oauth2/validate.
Request a new token immediately, then retry the original request.
{"status": 403, "message": "Forbidden"}The signature computed on incoming webhook payloads does not match the Twitch-Eventsub-Message-Signature header, causing your endpoint to return 403. Or your app does not have permission for this subscription type.
Verify HMAC-SHA256 over message_id + timestamp + raw_body bytes (not re-serialized JSON). Ensure WEBHOOK_SECRET matches the secret used when creating the subscription.
Fix the signature verification logic and redeploy. Existing subscriptions will retry delivery per the revocation timeline.
{"status": 429, "error": "Too Many Requests", "message": "Request rate limit reached"}The 800 points/minute Helix bucket is exhausted. This typically happens when polling GET /streams too frequently alongside other Helix calls.
Read Ratelimit-Remaining and Ratelimit-Reset headers to know your remaining budget. Use EventSub notifications instead of polling GET /streams for stream state changes.
Wait until the Ratelimit-Reset Unix timestamp before retrying. Exponential backoff with initial delay of 60 seconds for extreme cases.
Rate Limits for Twitch Helix API (Stream Alerts)
| Scope | Limit | Window |
|---|---|---|
| Helix REST (per app access token) | 800 points | per minute |
| EventSub webhook subscriptions | 10,000 total cost units | max_total_cost per app (grows with authorized users) |
| EventSub WebSocket subscriptions | 10 total cost units | per client_id + user_id tuple (max 3 connections, 300 subs/connection) |
1import time2import requests34def helix_get_with_retry(url, headers, params=None, max_retries=3):5 for attempt in range(max_retries):6 resp = requests.get(url, headers=headers, params=params)7 if resp.status_code == 429:8 reset_ts = int(resp.headers.get('Ratelimit-Reset', time.time() + 60))9 wait = max(reset_ts - int(time.time()), 1)10 print(f'Rate limited. Waiting {wait}s')11 time.sleep(wait)12 continue13 if resp.status_code == 401:14 raise Exception('Token expired — request a new one')15 resp.raise_for_status()16 return resp.json()17 raise Exception(f'Failed after {max_retries} retries')- Use EventSub webhook notifications instead of polling GET /streams — EventSub is push-based and consumes no Helix quota while idle
- Cache stream data (title, game, viewer count) for at least 30 seconds after fetching to avoid hammering the streams endpoint during an alert burst
- Monitor Ratelimit-Remaining on every Helix response and back off proactively when it drops below 100
- Delete EventSub subscriptions for channels you no longer monitor — subscription cost accumulates against your max_total_cost limit
Security checklist
- Verify the Twitch-Eventsub-Message-Signature HMAC-SHA256 on every incoming webhook payload before processing — reject with 403 if invalid
- Use a random, unique secret (10-100 chars) per EventSub subscription — rotate it if compromised by deleting and recreating the subscription
- Store CLIENT_ID, CLIENT_SECRET, and WEBHOOK_SECRET in environment variables — never commit them to version control
- Validate your App Access Token hourly via GET https://id.twitch.tv/oauth2/validate and request a new one on any 401 response
- Use HTTPS for your webhook callback URL — Twitch refuses HTTP endpoints for EventSub
- Implement idempotency: Twitch may redeliver notifications (e.g., after revocation recovery). Use Twitch-Eventsub-Message-Id to deduplicate already-processed events
Automation use cases
Multi-Channel Alert Bot for Discord Servers
intermediateSubscribe to stream.online for an entire roster of streamers and fan out alerts to different Discord channels based on the broadcaster's game category or custom tags.
Stream Schedule Dashboard
advancedCombine GET /schedule (channel stream schedule endpoint) with stream.online EventSub to display upcoming and live streams in a web dashboard with real-time status updates.
Automatic VOD Archiver
intermediateSubscribe to stream.offline events, then call GET /videos?user_id= to fetch the VOD immediately after the stream ends and post the VOD link to Discord or save it to a database.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier (100 tasks/month); Starter from $19.99/monthZapier's Twitch integration can trigger a Zap when a specific channel goes live and post the alert to Discord, Slack, email, or any of its 7,000+ supported apps.
- + No server or hosting required — Zapier handles webhook polling
- + Works with 7,000+ apps for alert destinations
- + Set up in under 10 minutes
- - Zapier polls Twitch every 5-15 minutes — not instant like EventSub webhooks
- - Free plan limited to 100 tasks/month and 2-step Zaps
- - Less control over alert formatting and logic
Make
Free tier (1,000 ops/month); Core from $9/monthMake (formerly Integromat) supports Twitch triggers for live events and can route alerts to Discord, Slack, email, Google Sheets, and hundreds of other services.
- + 1,000 free operations/month on the free plan
- + More powerful data transformation than Zapier for formatting rich alerts
- + Visual scenario editor with branching logic
- - Twitch polling interval is not real-time — typically checks every few minutes
- - Native Twitch module may not surface all EventSub event types
- - Complex scenarios with multiple branches add operational overhead
n8n
Self-hosted free; Cloud Starter from €20/monthn8n supports a Twitch node with stream-live triggers and can route to Discord, Slack, email, or any HTTP endpoint — self-hosted with no per-task cost.
- + Self-hosted option with no per-execution cost
- + Native Twitch and Discord nodes for zero-code stream-to-alert pipelines
- + Full webhook support for real-time EventSub integration with custom HTTP nodes
- - Self-hosted requires a VPS and maintenance overhead
- - Cloud version free tier limited to 5 active workflows
- - Native Twitch node may lag behind API changes
Best practices
- Always verify the Twitch-Eventsub-Message-Signature on incoming webhook payloads using HMAC-SHA256 computed over raw bytes — never trust unverified events
- Echo the challenge as plain text (Content-Type: text/plain), not JSON — this is the most common mistake when setting up EventSub for the first time
- Call GET /streams after a stream.online event rather than relying on the event payload alone — the event only contains the broadcaster ID, while /streams provides title, game, and viewer count
- Append a cache-busting query parameter (e.g., ?t=<timestamp>) to Twitch thumbnail URLs before embedding them in Discord or Slack to force a fresh image load
- Store EventSub subscription IDs in a database so you can audit, delete, and recreate them without losing track of active subscriptions
- Delete subscriptions for channels you no longer need to alert on — subscription cost accumulates against max_total_cost, and Twitch deactivates subscriptions with repeated delivery failures
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Twitch stream alert system using EventSub webhook transport. I've registered stream.online and stream.offline subscriptions and my endpoint handles the webhook_callback_verification challenge correctly. Issue: [describe your problem — e.g., 'subscriptions stay in pending status', 'receiving 422 errors', 'HMAC verification fails']. My verification code: [paste code]. Twitch API response: [paste response]. What is wrong with my EventSub webhook setup?
Build a Twitch stream alert dashboard UI. Show a list of monitored Twitch channels with their current status (LIVE / Offline) as a colored badge, current game, stream title, and viewer count. For live channels, show the stream thumbnail (Twitch thumbnail URL with 320x180 dimensions and a cache-busting ?t=<timestamp> suffix). Include a card for each channel with a Watch Live button linking to https://twitch.tv/{username}. Add a form to add or remove channels from the monitoring list. Use Tailwind, React hooks, and connect to a REST API at /api/channels for channel state.
Frequently asked questions
Can I still use PubSub for stream alerts?
No. Twitch fully decommissioned PubSub on April 14, 2025. Any code still using PubSub will receive no events. EventSub (webhook, WebSocket, or Conduit transport) is the only supported real-time event system. The migration guide is at dev.twitch.tv/docs/pubsub/.
Do I need a public URL to use EventSub?
Only for webhook transport. If you do not have a public HTTPS server, use WebSocket transport instead: connect to wss://eventsub.wss.twitch.tv/ws and create subscriptions using a User Access Token. WebSocket is simpler for hobby projects following a single channel but is limited to a max_total_cost of 10 per client_id+user_id tuple (roughly 10 subscriptions).
Why do my EventSub subscriptions stay in 'pending' status?
Your webhook endpoint did not return the challenge value within 10 seconds of receiving the webhook_callback_verification message. Make sure your endpoint: (1) is publicly accessible over HTTPS, (2) returns Content-Type: text/plain with HTTP 200, (3) returns the raw challenge string (not wrapped in JSON). After fixing, delete the pending subscription and resubscribe.
How many channels can I monitor for stream alerts?
With webhook transport, the default max_total_cost is 10,000 and grows as more users authorize your app. Each stream.online subscription costs 1 unit, so you can theoretically monitor up to 10,000 channels by default. With WebSocket transport, you are limited to max_total_cost of 10 per client_id+user_id tuple.
Is the Twitch API free to use?
Yes, the Twitch Helix API and EventSub are free with no per-request charges. You need a free Twitch account and a registered developer application at dev.twitch.tv/console/apps. The only cost is hosting your own webhook server (or using a paid serverless platform if you prefer).
What happens if my webhook server is down and I miss events?
Twitch retries failed event deliveries for a period before marking the subscription as notification_failures_exceeded and revoking it. Subscribe to the revocation message type in your handler and set up alerting for it. To catch up on missed events, call GET /streams after your server recovers to check current live status directly.
Can RapidDev help build a custom Twitch alert integration?
Yes. RapidDev has built 600+ apps including real-time Twitch alert systems, multi-channel Discord bots, and streamer dashboards. Book a free consultation at rapidevelopers.com.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation