Automate Twitch subscriber notifications using EventSub subscription types channel.subscribe and channel.subscription.gift — PubSub is gone (decommissioned April 14, 2025), EventSub is the only real-time path. Use a User Access Token with channel:read:subscriptions scope. For the full subscriber list, call GET /helix/subscriptions (same scope). Validate tokens hourly — users can revoke at any time. Rate limit: 800 points/minute Helix bucket. Every Helix request needs both Authorization: Bearer and Client-Id headers.
API Quick Reference
User Access Token (OAuth 2.0)
800 points/minute Helix bucket
JSON
Available
Understanding the Twitch Helix API for Subscriber Notifications
The Twitch Helix REST API (base URL: https://api.twitch.tv/helix) requires two headers on every request: Authorization: Bearer <token> and Client-Id: <client_id>. Subscriber automation uses EventSub for real-time events and GET /helix/subscriptions for current subscriber lists.
EventSub replaced PubSub entirely — PubSub was fully decommissioned on April 14, 2025. EventSub supports three transport types: Webhook (Twitch POSTs to your HTTPS endpoint, requires App Access Token), WebSocket (your app connects to wss://eventsub.wss.twitch.tv/ws, requires User Access Token), and Conduits (for large-scale multi-channel bots). For subscriber notifications, you need User Access Tokens because the relevant scopes (channel:read:subscriptions) are user-context scopes.
Key EventSub types for subscribers: channel.subscribe (new subscription), channel.subscription.gift (gift subs), channel.subscription.message (resub with message), channel.cheer (Bits — separate from subs but often paired). GET /helix/subscriptions requires channel:read:subscriptions scope with a user token from the broadcaster. Validate tokens hourly via GET https://id.twitch.tv/oauth2/validate since user tokens can be revoked without notice. The official documentation is at https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelsubscribe.
https://api.twitch.tv/helixSetting Up Twitch API Authentication for Subscriber Notifications
Subscriber automation requires a User Access Token from the broadcaster with the channel:read:subscriptions scope — App Access Tokens from Client Credentials flow do not work for this scope. Use Authorization Code Flow to get a user token. Tokens last ~60 days but should be validated hourly and refreshed when needed. User tokens for Confidential clients use non-expiring refresh tokens.
- 1Go to dev.twitch.tv/console/apps and register an application
- 2Set OAuth Redirect URL to your callback (e.g., https://yourapp.com/auth/callback)
- 3Select Client Type: Confidential for server-side applications
- 4Copy your Client ID and Client Secret
- 5Redirect the broadcaster to: https://id.twitch.tv/oauth2/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&scope=channel:read:subscriptions
- 6Exchange the authorization code for tokens at POST https://id.twitch.tv/oauth2/token
- 7Store access_token and refresh_token securely in environment variables or secrets manager
- 8Validate token hourly with GET https://id.twitch.tv/oauth2/validate
1import requests2import os34CLIENT_ID = os.environ['TWITCH_CLIENT_ID']5CLIENT_SECRET = os.environ['TWITCH_CLIENT_SECRET']67def exchange_code(auth_code, redirect_uri):8 r = requests.post('https://id.twitch.tv/oauth2/token', data={9 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET,10 'code': auth_code, 'grant_type': 'authorization_code', 'redirect_uri': redirect_uri11 })12 return r.json() # {access_token, refresh_token, expires_in, scope}1314def refresh_access_token(refresh_token):15 r = requests.post('https://id.twitch.tv/oauth2/token', data={16 'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET,17 'grant_type': 'refresh_token', 'refresh_token': refresh_token18 })19 if r.status_code != 200:20 raise Exception(f'Token refresh failed: {r.json()}')21 return r.json()2223def validate_token(access_token):24 r = requests.get('https://id.twitch.tv/oauth2/validate',25 headers={'Authorization': f'OAuth {access_token}'})26 if r.status_code != 200:27 raise Exception('Token invalid — needs refresh')28 return r.json()2930# Standard headers for all Helix requests31def get_headers(access_token):32 return {'Authorization': f'Bearer {access_token}', 'Client-Id': CLIENT_ID}Security notes
- •Never expose Client Secret in frontend code — it is server-side only
- •Store access_token and refresh_token in encrypted environment variables
- •Validate tokens hourly — user revocation is silent and causes immediate 401 errors
- •For EventSub webhooks, use a 10-100 char secret and verify HMAC-SHA256 signatures on all incoming events
- •Refresh tokens for Confidential clients do not expire — but treat them as long-lived secrets
Key endpoints
/eventsub/subscriptionsCreate an EventSub subscription for channel.subscribe to receive real-time new subscriber events. Also create channel.subscription.gift for gift subs and channel.subscription.message for resubs with messages.
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | required | channel.subscribe, channel.subscription.gift, channel.subscription.message, or channel.cheer |
version | string | required | 1 for all subscriber types |
condition.broadcaster_user_id | string | required | Broadcaster's numeric user ID |
transport.method | string | required | websocket (with session_id) or webhook (with callback URL and secret) |
transport.session_id | string | optional | WebSocket session ID from the welcome message |
Request
1{"type": "channel.subscribe", "version": "1", "condition": {"broadcaster_user_id": "12345678"}, "transport": {"method": "websocket", "session_id": "YOUR_SESSION_ID"}}Response
1{"data": [{"id": "sub_abc123", "status": "enabled", "type": "channel.subscribe", "version": "1", "condition": {"broadcaster_user_id": "12345678"}, "created_at": "2026-01-15T10:00:00Z"}]}/subscriptions?broadcaster_id={id}&first=100Gets the list of users subscribed to the broadcaster's channel with tier and gifted status. Requires channel:read:subscriptions scope. Returns up to 100 per page with cursor pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
broadcaster_id | string | required | Broadcaster's numeric user ID |
first | number | optional | Max 100 per page |
after | string | optional | Pagination cursor from previous response |
Response
1{"data": [{"broadcaster_id": "12345678", "user_id": "99999", "user_name": "Subscriber1", "tier": "1000", "is_gift": false}], "pagination": {"cursor": "abc"}, "total": 4523, "points": 4523}/eventsub/subscriptionsLists your active EventSub subscriptions and their status. Use this to verify subscriptions are enabled and to detect any that failed verification.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | optional | Filter by status: enabled, webhook_callback_verification_pending, webhook_callback_verification_failed, etc. |
type | string | optional | Filter by subscription type |
Response
1{"data": [{"id": "sub_abc", "status": "enabled", "type": "channel.subscribe", "cost": 0}], "total": 3, "total_cost": 0, "max_total_cost": 10}Step-by-step automation
Connect to EventSub WebSocket and Subscribe
Why: WebSocket transport is the simplest real-time option for subscriber notifications — no public URL needed and no webhook verification complexity.
Connect to wss://eventsub.wss.twitch.tv/ws. The server sends a session_welcome message with your session_id. Use this session_id to create EventSub subscriptions via POST /helix/eventsub/subscriptions. Subscribe to channel.subscribe, channel.subscription.gift, and channel.subscription.message for complete coverage.
1# After connecting WebSocket and getting session_id from welcome message,2# create the subscription via REST3curl -X POST "https://api.twitch.tv/helix/eventsub/subscriptions" \4 -H "Authorization: Bearer USER_ACCESS_TOKEN" \5 -H "Client-Id: YOUR_CLIENT_ID" \6 -H "Content-Type: application/json" \7 -d '{8 "type": "channel.subscribe",9 "version": "1",10 "condition": {"broadcaster_user_id": "12345678"},11 "transport": {"method": "websocket", "session_id": "YOUR_SESSION_ID"}12 }'Pro tip: The EventSub WebSocket has a max_total_cost of 10 per client_id+user_id pair — 3 subscription types each with cost 0 (since the broadcaster authorized your app) means you have plenty of headroom.
Expected result: WebSocket connected, subscriptions created for channel.subscribe, channel.subscription.gift, and channel.subscription.message. Events arrive in real-time as subscribers join.
Format and Send Subscriber Alerts
Why: Raw EventSub events need formatting before they are useful as alerts — different sub types have different event fields.
EventSub delivers different event shapes for each subscription type. channel.subscribe includes user_name, is_gift, and tier. channel.subscription.gift includes gifter_user_name, total (subs gifted this event), cumulative_total, and is_anonymous. channel.subscription.message includes a message and cumulative_months for resubs. Format each appropriately and post to Discord/Slack.
1# Post new subscriber alert to Discord webhook2curl -X POST "https://discord.com/api/webhooks/WEBHOOK_ID/TOKEN" \3 -H "Content-Type: application/json" \4 -d '{5 "embeds": [{6 "title": "New Subscriber!",7 "description": "viewer123 just subscribed at Tier 1!",8 "color": 95208959 }]10 }'Pro tip: Handle gift sub bombs separately from individual gifts — channel.subscription.gift fires once per gift bomb with a total count, while channel.subscribe fires individually for each gifted sub. Avoid duplicate alerts.
Expected result: Formatted subscriber alert messages are posted to Discord with appropriate context for new subs, gift subs, and resubs.
Fetch Current Subscriber List for Reports
Why: EventSub gives you real-time events, but you need GET /helix/subscriptions to query historical subscribers and total subscriber count.
Call GET /helix/subscriptions with the broadcaster_id. Each page returns up to 100 subscribers with tier, is_gift, and gifter info. The response also includes total (subscriber count) and points (subscriber points for affiliate/partner tier rewards). Paginate until no cursor is returned.
1# Get subscriber list and total count2curl "https://api.twitch.tv/helix/subscriptions?broadcaster_id=12345678&first=100" \3 -H "Authorization: Bearer USER_ACCESS_TOKEN" \4 -H "Client-Id: YOUR_CLIENT_ID"Pro tip: The points field in the response shows subscriber points (used for Affiliate/Partner tier tracking). A Tier 1 sub = 1 point, Tier 2 = 2, Tier 3 = 6. Points affect partner perks and are separate from Bits.
Expected result: Complete subscriber list with tier breakdown and gifted subscriber count. The total field gives the official subscriber count.
Complete working code
Complete subscriber notification bot using EventSub WebSocket that connects on startup, subscribes to three event types (new sub, gift sub, resub), formats context-aware alerts, and posts to Discord in real-time. Includes token validation and WebSocket reconnection logic.
1import websocket2import requests3import json, threading, os, time4from datetime import datetime56CLIENT_ID = os.environ['TWITCH_CLIENT_ID']7ACCESS_TOKEN = os.environ['TWITCH_ACCESS_TOKEN']8BROADCASTER_ID = os.environ['TWITCH_BROADCASTER_ID']9DISCORD_WEBHOOK = os.environ.get('DISCORD_WEBHOOK_URL')1011HEADERS = {'Authorization': f'Bearer {ACCESS_TOKEN}', 'Client-Id': CLIENT_ID}12TIER = {'1000': 'Tier 1', '2000': 'Tier 2', '3000': 'Tier 3'}13SUB_TYPES = ['channel.subscribe', 'channel.subscription.gift', 'channel.subscription.message']1415def validate_token():16 r = requests.get('https://id.twitch.tv/oauth2/validate', headers={'Authorization': f'OAuth {ACCESS_TOKEN}'})17 if r.status_code != 200: raise Exception('Token invalid')18 print('Token validated')1920def subscribe_eventsub(session_id):21 for stype in SUB_TYPES:22 r = requests.post('https://api.twitch.tv/helix/eventsub/subscriptions',23 headers={**HEADERS, 'Content-Type': 'application/json'},24 json={'type': stype, 'version': '1',25 'condition': {'broadcaster_user_id': BROADCASTER_ID},26 'transport': {'method': 'websocket', 'session_id': session_id}})27 print(f'Subscribed {stype}: {r.status_code}')2829def send_alert(title, desc):30 if DISCORD_WEBHOOK:31 requests.post(DISCORD_WEBHOOK, json={'embeds': [{'title': title, 'description': desc, 'color': 9520895}]})3233def handle_event(sub_type, event):34 if sub_type == 'channel.subscribe':35 tier = TIER.get(event.get('tier', '1000'), 'Tier 1')36 if not event.get('is_gift'):37 send_alert('⭐ New Subscriber!', f"{event['user_name']} just subscribed at {tier}!")38 elif sub_type == 'channel.subscription.gift':39 n = event.get('total', 1)40 gifter = 'Anonymous' if event.get('is_anonymous') else event.get('gifter_user_name', '?')41 tier = TIER.get(event.get('tier', '1000'), 'Tier 1')42 send_alert('🎁 Gift Subs!', f"{gifter} gifted {n}x {tier} sub{'s' if n>1 else ''}!")43 elif sub_type == 'channel.subscription.message':44 months = event.get('cumulative_months', 1)45 tier = TIER.get(event.get('tier', '1000'), 'Tier 1')46 msg = event.get('message', {}).get('text', '')47 desc = f"{event['user_name']} resubbed for {months} months ({tier})!"48 if msg: desc += f' "{msg[:100]}"'49 send_alert('🔄 Resub!', desc)5051def on_message(ws, message):52 data = json.loads(message)53 mtype = data.get('metadata', {}).get('message_type')54 if mtype == 'session_welcome':55 sid = data['payload']['session']['id']56 threading.Thread(target=subscribe_eventsub, args=(sid,), daemon=True).start()57 elif mtype == 'notification':58 handle_event(data['metadata']['subscription_type'], data['payload']['event'])59 elif mtype == 'session_keepalive':60 pass # heartbeat, no action needed6162def on_error(ws, error):63 print(f'WebSocket error: {error}')6465def on_close(ws, code, reason):66 print(f'Disconnected: {code} {reason}. Reconnecting in 5s...')67 time.sleep(5)68 start_bot()6970def start_bot():71 validate_token()72 ws = websocket.WebSocketApp('wss://eventsub.wss.twitch.tv/ws',73 on_message=on_message, on_error=on_error, on_close=on_close)74 ws.run_forever(ping_interval=30, ping_timeout=10)7576if __name__ == '__main__':77 start_bot()Error handling
Must provide a valid Client-ID or OAuth tokenMissing or invalid Authorization or Client-Id header. Both headers are required on every Helix request. User tokens can also be revoked by the broadcaster at any time.
Validate the token first via GET https://id.twitch.tv/oauth2/validate. If expired, use the refresh token. If revoked, trigger re-authorization with the broadcaster.
Refresh token and retry once. If refresh fails, require re-authorization.
Missing required scope: channel:read:subscriptionsThe user access token lacks channel:read:subscriptions scope, or the token belongs to a user who is not the broadcaster.
Re-authorize the broadcaster with the required scope included in the authorization URL. The token must come from the broadcaster's account, not a moderator.
No retry — fix the authorization scope and re-authenticate the broadcaster.
Subscription validation failedFor webhook transport: your callback did not echo the challenge within the timeout. For WebSocket: the session_id is invalid or the WebSocket connection dropped before the subscription was created.
For WebSocket: reconnect and use the new session_id from the welcome message. For webhook: ensure your callback server responds within 10 seconds with the exact challenge value.
Reconnect WebSocket or fix webhook callback, then recreate the subscription.
Too many requests800 points/minute Helix bucket exhausted. Usually happens when paginating large subscriber lists rapidly.
Add delays between paginated requests (100ms minimum). Check Ratelimit-Remaining header and throttle when it drops below 50.
Wait until Ratelimit-Reset timestamp, then retry with added delays.
Rate Limits for Twitch Helix API
| Scope | Limit | Window |
|---|---|---|
| Per access token (Helix) | 800 points | per minute |
| EventSub WebSocket subscriptions | max_total_cost = 10 per client_id+user_id | Subscriptions to broadcaster's own authorized channel cost 0 |
| GET /subscriptions pagination | 1 point per page | 100 subscribers per page |
1import time, requests23def helix_with_retry(url, headers, params=None, max_retries=5):4 for attempt in range(max_retries):5 r = requests.get(url, headers=headers, params=params or {})6 if r.status_code == 429:7 reset = int(r.headers.get('Ratelimit-Reset', time.time() + 60))8 wait = max(reset - int(time.time()) + 1, 1)9 print(f'Rate limited, waiting {wait}s')10 time.sleep(wait)11 elif r.status_code == 401:12 raise Exception('Token needs refresh')13 else:14 r.raise_for_status()15 return r.json()16 raise Exception('Max retries hit')- EventSub subscriptions to a broadcaster's own authorized channel cost 0 points — use multiple event types freely
- Add 100ms delays between subscriber list pagination requests
- Validate tokens hourly to catch revocations before they cause silent failures
- Monitor max_total_cost in EventSub subscription responses — stay under 10 for WebSocket transport
- For multi-channel bots, use Webhook transport with an App Access Token instead of WebSocket
Security checklist
- Store Client ID, Client Secret, and user tokens in environment variables — never hardcode
- Validate user tokens hourly via GET https://id.twitch.tv/oauth2/validate — users can revoke at any time
- For webhook transport, use a 10-100 char secret and verify HMAC-SHA256 signatures on all EventSub notifications
- Limit OAuth scope to channel:read:subscriptions only — do not request unnecessary permissions
- Implement WebSocket reconnection logic — connections can drop and you must reconnect and re-subscribe
- Use HTTPS for OAuth redirect URIs and EventSub webhook callbacks
- Log all subscription events for audit trail and debugging
Automation use cases
Real-Time Stream Overlay Alerts
intermediateDisplay custom animated subscriber, gift sub, and resub alerts on your stream overlay in real-time using EventSub WebSocket events.
Monthly Subscriber Report
intermediateOn the first of each month, pull the full subscriber list, calculate tier distribution, MRR estimate, and growth rate, then post to Discord.
Top Gifter Leaderboard
advancedTrack channel.subscription.gift events and maintain a cumulative top-gifter leaderboard that updates in real-time on your channel's Discord.
Sub Milestone Celebrations
beginnerTrack total subscriber count and trigger special alerts when hitting milestones (100, 500, 1K, 5K subs) using a polling job combined with EventSub.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier (100 tasks/month); paid from $20/monthZapier's Twitch integration can trigger on new subscriber events and post to Discord, Slack, or Google Sheets for basic subscriber notification workflows.
- + No code required
- + Easy multi-platform connection
- + Handles authentication automatically
- - Limited to basic subscriber events
- - No gift sub or resub message support
- - 15-minute polling delay
Make (formerly Integromat)
Free tier (1,000 operations/month); paid from $9/monthMake's Twitch module supports subscriber triggers with better conditional logic and data routing than Zapier for multi-destination alerts.
- + More flexible than Zapier
- + Visual scenario builder
- + Lower cost
- - Polling-based, not real-time EventSub
- - Limited event type coverage
n8n
Free self-hosted; Cloud from $20/monthn8n's Twitch node can monitor subscriber events and chain into Discord/Slack notifications with full data transformation capabilities and a self-hosted free option.
- + Self-hosted = free
- + Full workflow customization
- + Supports complex alert logic
- - Requires self-hosting setup
- - Polling-based in most configurations
Best practices
- Always include both Authorization: Bearer and Client-Id headers on every Helix request — missing either causes 401
- Use EventSub WebSocket for real-time alerts — PubSub is decommissioned (April 14, 2025) and cannot be used
- Handle channel.subscribe gift events carefully — when a gift bomb occurs, channel.subscribe fires for each recipient AND channel.subscription.gift fires once for the total; avoid double-counting
- Validate tokens hourly — channel owners can revoke your app access at any time without notice
- Subscribe to channel.subscription.message (not just channel.subscribe) to capture resub messages with their community content
- Store subscriber snapshots weekly to track net subscriber growth beyond what real-time events capture
- Implement WebSocket reconnection with exponential backoff — connections drop and you must re-subscribe using the new session_id
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Twitch subscriber notification bot using EventSub WebSocket in Python. I successfully connect and receive the session_welcome message with session_id, but when I create the channel.subscribe subscription via POST /helix/eventsub/subscriptions, I get a 403 error. Here's my code: [paste code]. I have a user access token with channel:read:subscriptions scope. What am I missing?
Build a Twitch Subscriber Alert Dashboard that shows real-time subscriber events using EventSub WebSocket. Display a live feed of new subs, gift subs, and resubs with the subscriber name, tier, and timestamp. Show a counter for today's new subs and total current subscribers (from GET /helix/subscriptions). Include a Discord alert toggle. Use Supabase for authentication and to persist the subscriber event history.
Frequently asked questions
Is the Twitch API free?
Yes, the Twitch Helix API is completely free. You need a free developer account at dev.twitch.tv to register an app. There are no paid API tiers — all developers get the same 800 points/minute rate limit.
What is the difference between channel.subscribe and channel.subscription.message?
channel.subscribe fires on any new subscription, including gifted subs. channel.subscription.message fires specifically when a subscriber sends a resub message (with months accumulated and an optional chat message). Subscribe to both to capture all subscription events comprehensively.
Does PubSub still work for subscriber notifications?
No. Twitch fully decommissioned PubSub on April 14, 2025. Any code using PubSub for subscriber events stopped working on that date. EventSub is now the only supported real-time event system. Use EventSub WebSocket (no public URL needed) or EventSub Webhook.
What happens when I hit the rate limit?
You receive HTTP 429 from Helix REST calls. Check the Ratelimit-Reset header (Unix timestamp) and wait until that time before retrying. EventSub WebSocket events themselves are not rate-limited in the traditional sense — events are pushed to you as they occur.
Can I use an App Access Token instead of a User Access Token?
For EventSub WebSocket transport, you need a User Access Token because WebSocket connections are user-context. For webhook transport, you use an App Access Token for the subscription creation. However, GET /helix/subscriptions always requires a User Access Token from the broadcaster.
How do I distinguish a new subscriber from someone who was gifted a sub?
In the channel.subscribe event payload, check the is_gift boolean field — true means it was a gifted sub. channel.subscription.gift fires separately with gifter information. This lets you send different alert messages for self-subscriptions vs. received gifts.
Can RapidDev build a Twitch subscriber tracking system?
Yes — RapidDev has built 600+ integrations including Twitch bots and dashboards. We can build real-time subscriber alerts, monthly reporting, leaderboards, and stream overlay integrations. Book a free consultation at rapidevelopers.com.
What EventSub subscription types cover all subscriber-related events?
For complete coverage: channel.subscribe (new subs and gifted subs received), channel.subscription.gift (gift sub bombs with total count and gifter info), channel.subscription.message (resubs with messages), and channel.cheer (Bits — separate from subs but often tracked together). All require channel:read:subscriptions scope.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation