Skip to main content
RapidDev - Software Development Agency
API AutomationsTwitchBearer Token

How to Automate Twitch Stream Alerts using the API

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.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesTwitchMay 2026RapidDev Engineering Team
TL;DR

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

Auth

App Access Token (Client Credentials)

Rate limit

800 points/minute Helix bucket

Format

JSON

SDK

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/.

Base URLhttps://api.twitch.tv/helix

Setting 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.

  1. 1Go to dev.twitch.tv/console/apps and log in with your Twitch account
  2. 2Click 'Register Your Application'. Set Name to 'StreamAlertsBot', OAuth Redirect URLs to https://localhost (placeholder for client credentials flow), and Category to 'Chat Bot'
  3. 3Set Client Type to 'Confidential' for server-side apps (this enables a Client Secret)
  4. 4Save the app — copy the Client ID immediately. Click 'New Secret' and copy the Client Secret
  5. 5Store CLIENT_ID and CLIENT_SECRET in environment variables — never in code
  6. 6Request an App Access Token via POST to https://id.twitch.tv/oauth2/token with grant_type=client_credentials
  7. 7Validate the token hourly via GET https://id.twitch.tv/oauth2/validate and on any 401 response
auth.py
1import os
2import requests
3
4CLIENT_ID = os.environ['TWITCH_CLIENT_ID']
5CLIENT_SECRET = os.environ['TWITCH_CLIENT_SECRET']
6
7def 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']
20
21def 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 == 200
28
29# Usage
30ACCESS_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

POST/eventsub/subscriptions

Creates 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.

ParameterTypeRequiredDescription
typestringrequiredEvent type — use 'stream.online' for going live and 'stream.offline' for stream end.
versionstringrequiredEvent version. Use '1' for stream.online and stream.offline.
condition.broadcaster_user_idstringrequiredThe Twitch user ID (not username) of the broadcaster to monitor. Get this via GET /users?login=<username>.
transport.secretstringrequiredA secret string between 10 and 100 characters used to verify webhook payloads via HMAC-SHA256.

Request

json
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

json
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"}}]}
GET/eventsub/subscriptions

Lists all current EventSub subscriptions. Use to audit what you have subscribed and check subscription status.

ParameterTypeRequiredDescription
statusstringoptionalFilter by status: enabled, webhook_callback_verification_pending, webhook_callback_verification_failed, notification_failures_exceeded, authorization_revoked, user_removed.

Response

json
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}
GET/users

Looks up a Twitch user by login name to get their numeric user ID, which is required for EventSub subscription conditions.

ParameterTypeRequiredDescription
loginstringoptionalTwitch username (lowercase). Returns the user's numeric ID needed for subscription conditions.

Response

json
1{"data": [{"id": "12345678", "login": "ninja", "display_name": "Ninja", "broadcaster_type": "partner"}]}
GET/streams

Fetches current live stream data including title, game, viewer count, and thumbnail URL. Call this after receiving a stream.online event to enrich your alert.

ParameterTypeRequiredDescription
user_idstringoptionalGet stream data for a specific broadcaster by their user ID.
user_loginstringoptionalAlternative to user_id — look up by Twitch username.

Response

json
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

1

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.

request.sh
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.

2

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.

request.sh
1# Subscribe to stream.online
2curl -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 }'
16
17# Subscribe to stream.offline
18curl -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.

3

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.

request.sh
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.

4

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.

request.sh
1# Fetch stream details
2curl -X GET 'https://api.twitch.tv/helix/streams?user_id=12345678' \
3 -H 'Authorization: Bearer <access_token>' \
4 -H 'Client-Id: <client_id>'
5
6# Post to Discord webhook
7curl -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.

automate_stream_alerts.py
1import os
2import hmac
3import hashlib
4import time
5import logging
6import requests
7from flask import Flask, request, Response
8
9logging.basicConfig(level=logging.INFO)
10logger = logging.getLogger(__name__)
11
12CLIENT_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']
17
18app = Flask(__name__)
19
20def 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']
27
28def helix_headers(token):
29 return {'Authorization': f'Bearer {token}', 'Client-Id': CLIENT_ID}
30
31ACCESS_TOKEN = get_access_token()
32
33def 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']
41
42def 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 return
54 resp.raise_for_status()
55 logger.info(f'Subscribed to {event_type}')
56
57def 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', ''))
62
63@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)
98
99if __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

422{"error": "Unprocessable Entity", "status": 422, "message": "subscription missing proper callback verification"}
Cause

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.

Fix

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.

Retry strategy

Not retryable. Delete the subscription (DELETE /eventsub/subscriptions?id=<id>) and resubscribe after fixing the verification endpoint.

409{"error": "Conflict", "status": 409, "message": "subscription already exists"}
Cause

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.

Fix

Check existing subscriptions via GET /eventsub/subscriptions before creating new ones. Delete stale subscriptions first.

Retry strategy

Not retryable. Use the existing subscription or delete it and recreate.

401{"status": 401, "message": "Invalid OAuth token"}
Cause

The App Access Token has expired (~60 days) or was revoked. Both Authorization: Bearer and Client-Id headers must be present and valid.

Fix

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.

Retry strategy

Request a new token immediately, then retry the original request.

403{"status": 403, "message": "Forbidden"}
Cause

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.

Fix

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.

Retry strategy

Fix the signature verification logic and redeploy. Existing subscriptions will retry delivery per the revocation timeline.

429{"status": 429, "error": "Too Many Requests", "message": "Request rate limit reached"}
Cause

The 800 points/minute Helix bucket is exhausted. This typically happens when polling GET /streams too frequently alongside other Helix calls.

Fix

Read Ratelimit-Remaining and Ratelimit-Reset headers to know your remaining budget. Use EventSub notifications instead of polling GET /streams for stream state changes.

Retry strategy

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)

ScopeLimitWindow
Helix REST (per app access token)800 pointsper minute
EventSub webhook subscriptions10,000 total cost unitsmax_total_cost per app (grows with authorized users)
EventSub WebSocket subscriptions10 total cost unitsper client_id + user_id tuple (max 3 connections, 300 subs/connection)
retry-handler.ts
1import time
2import requests
3
4def 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 continue
13 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

intermediate

Subscribe 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

advanced

Combine 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

intermediate

Subscribe 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/month

Zapier'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.

Pros
  • + No server or hosting required — Zapier handles webhook polling
  • + Works with 7,000+ apps for alert destinations
  • + Set up in under 10 minutes
Cons
  • - 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/month

Make (formerly Integromat) supports Twitch triggers for live events and can route alerts to Discord, Slack, email, Google Sheets, and hundreds of other services.

Pros
  • + 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
Cons
  • - 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/month

n8n 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.

Pros
  • + 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
Cons
  • - 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.

ChatGPT / Claude Prompt

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?

Lovable / V0 Prompt

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.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.