Gareth Wilson Gareth Wilson

Guide to Twitch Webhooks: Features and Best Practices

Published


Twitch EventSub webhooks notify your application about channel activity: a stream goes live, someone follows or subscribes, a channel updates. If you're building a bot, an overlay, an alerting service, or any Twitch integration, EventSub over webhooks is how you react in real time without polling.

This guide covers how Twitch EventSub webhooks work, the message types you'll handle, the challenge handshake, how to verify the signature, the replay window, and the best practices for production.

What are Twitch EventSub webhooks?

EventSub is Twitch's unified eventing system. Over the webhook transport, Twitch POSTs to your callback URL, and the Twitch-Eventsub-Message-Type header tells you which of three kinds of message it is: webhook_callback_verification (the setup challenge), notification (an actual event), or revocation (Twitch has revoked the subscription). You create subscriptions via the Helix API with an app access token, setting a secret used to sign deliveries.

Twitch EventSub features

FeatureDetails
ConfigurationPOST /helix/eventsub/subscriptions (app access token)
Message typesnotification, webhook_callback_verification, revocation
Signature headerTwitch-Eventsub-Message-Signature
Signature schemesha256= + hex HMAC-SHA256 over message-id + timestamp + raw body
SecretSet per subscription, 10-100 ASCII characters
CallbackHTTPS on port 443
Replay protectionReject messages with a timestamp older than 10 minutes
DeliveryAt-least-once; dedupe on message ID
Cost modelmax_total_cost (typically 10,000); user-authorized subs cost 0
SDKsCommunity only (@twurple/eventsub-http, twitchAPI)

Common subscription types

You subscribe to specific EventSub types. Common ones:

TypeFires when
stream.onlineA channel starts streaming
stream.offlineA channel stops streaming
channel.followSomeone follows the channel (version 2, requires a moderator_user_id condition)
channel.subscribeSomeone subscribes to the channel
channel.updateA channel's category, title, or settings change (version 2)
channel.cheerSomeone cheers with Bits

Note that channel.follow and channel.update are version 2. The version is a separate field in the subscription create body, not part of the type string. Subscribe to what you need, and consult Twitch's EventSub subscription-types reference for the full list.

Setting up Twitch EventSub webhooks

Create a subscription via Helix with an app access token (user tokens are rejected for the webhook transport). Set the transport.secret (10-100 ASCII characters) and an HTTPS callback on port 443:

curl -X POST "https://api.twitch.tv/helix/eventsub/subscriptions" \
  -H "Authorization: Bearer $APP_ACCESS_TOKEN" \
  -H "Client-Id: $CLIENT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "stream.online",
    "version": "1",
    "condition": { "broadcaster_user_id": "1234" },
    "transport": {
      "method": "webhook",
      "callback": "https://your-app.example.com/eventsub",
      "secret": "your-subscription-secret"
    }
  }'

The challenge handshake

When you create a subscription, Twitch sends a webhook_callback_verification message. Respond with a 200 and the raw challenge string from the payload as the body (text/plain, not JSON-wrapped). Until you do, the subscription isn't enabled.

Securing Twitch EventSub webhooks

Verify the Twitch-Eventsub-Message-Signature header on every message: it's sha256= followed by a hex HMAC-SHA256 where the signed content is the concatenation of the Twitch-Eventsub-Message-Id header, the Twitch-Eventsub-Message-Timestamp header, and the raw request body, keyed with your subscription secret. Reject on mismatch, and reject any message whose timestamp is more than 10 minutes old to guard against replays.

const crypto = require("crypto");

const SECRET = process.env.TWITCH_EVENTSUB_SECRET;

function verify(headers, rawBody) {
  const id = headers["twitch-eventsub-message-id"];
  const timestamp = headers["twitch-eventsub-message-timestamp"];
  const message = id + timestamp + rawBody;
  const expected = "sha256=" +
    crypto.createHmac("sha256", SECRET).update(message).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(headers["twitch-eventsub-message-signature"] || "");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
  // Reject messages older than 10 minutes (replay protection)
  return Date.now() - new Date(timestamp).getTime() < 10 * 60 * 1000;
}

app.post("/eventsub", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.headers, req.body.toString("utf8"))) {
    return res.sendStatus(403);
  }

  const type = req.headers["twitch-eventsub-message-type"];
  const payload = JSON.parse(req.body);

  if (type === "webhook_callback_verification") {
    return res.status(200).type("text/plain").send(payload.challenge);
  }
  if (type === "revocation") {
    // subscription revoked (user_removed, authorization_revoked, ...)
    return res.sendStatus(200);
  }

  res.sendStatus(200); // notification: acknowledge fast
  processQueue.add(payload.event); // process asynchronously
});

The same verification in Python:

import hashlib
import hmac
import os
from datetime import datetime, timezone

SECRET = os.environ["TWITCH_EVENTSUB_SECRET"].encode()


def verify(headers, raw_body: bytes) -> bool:
    message = (headers["Twitch-Eventsub-Message-Id"]
               + headers["Twitch-Eventsub-Message-Timestamp"]).encode() + raw_body
    expected = "sha256=" + hmac.new(SECRET, message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, headers.get("Twitch-Eventsub-Message-Signature", ""))

Twitch EventSub limitations and pain points

Three message types on one endpoint

The Problem: The same callback receives verification challenges, notifications, and revocations. A handler that assumes every POST is a notification never enables the subscription (it ignores the challenge) and misses revocations.

Why It Happens: EventSub multiplexes lifecycle and delivery on one URL, distinguished by Twitch-Eventsub-Message-Type.

Workarounds:

  • Branch on Twitch-Eventsub-Message-Type: echo the challenge for verification, handle revocation, process notifications.
  • Log revocations (user_removed, authorization_revoked, notification_failures_exceeded, version_removed) so you can resubscribe.

How Hookdeck Can Help: Hookdeck can answer the verification challenge and verify signatures at the edge, forwarding only clean notifications to your app while surfacing revocations.

The 10-minute replay window

The Problem: Twitch signs a timestamp, and messages older than 10 minutes should be rejected as replays. A backlogged handler can see messages age out, and skipping the timestamp check leaves you open to replay.

Why It Happens: The freshness window is what makes the signature resistant to replay.

Workarounds:

  • Check the Twitch-Eventsub-Message-Timestamp against a 10-minute window.
  • Acknowledge fast and process asynchronously so you verify inside the window.

How Hookdeck Can Help: Hookdeck verifies signatures and freshness at the edge and durably queues events, so a downstream backlog doesn't risk messages aging out.

Revocation and the cost model

The Problem: Twitch revokes subscriptions on repeated delivery failure (notification_failures_exceeded) and enforces a subscription cost model (max_total_cost, typically 10,000). A flaky endpoint loses subscriptions, and un-authorized subscriptions consume cost budget.

Why It Happens: Twitch prunes failing subscriptions and caps how many app-authorized subscriptions you can hold.

Workarounds:

  • Keep the endpoint healthy and respond promptly to avoid failure-based revocation.
  • Track your total cost and prefer user-authorized subscriptions (cost 0) where possible.

How Hookdeck Can Help: Hookdeck absorbs downstream failures and retries to your service, keeping your endpoint responsive to Twitch so subscriptions aren't revoked for delivery failures.

At-least-once delivery

The Problem: The same notification can arrive more than once, so acting on duplicates double-processes.

Why It Happens: EventSub prioritizes eventual delivery.

Workarounds:

  • Dedupe on the Twitch-Eventsub-Message-Id.
  • Make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates on the message ID at the edge, so retries don't reach your handler twice. See our guide to webhook idempotency.

Best practices

Verify signature and freshness

Compute sha256= + hex HMAC-SHA256 over message-id + timestamp + raw body, compare in constant time, and reject timestamps older than 10 minutes.

Handle all three message types

Echo the challenge for verification (text/plain), handle revocation, and process notifications. Branch on Twitch-Eventsub-Message-Type.

Acknowledge fast, process asynchronously

Respond within a few seconds and defer work to a queue. See why to process webhooks asynchronously.

Dedupe on the message ID

Use Twitch-Eventsub-Message-Id as an idempotency key so at-least-once delivery doesn't double-process.

Mind versions and cost

Use the right subscription version (channel.follow and channel.update are v2), and track your max_total_cost budget.

Make Twitch EventSub webhooks production-ready

Hookdeck answers the challenge, verifies signatures and freshness, deduplicates, and durably queues every event

Conclusion

Twitch EventSub delivers channel activity over webhooks, but the endpoint has to do more than accept notifications: it answers a webhook_callback_verification challenge, handles revocation messages, and verifies a signature computed over the message id, timestamp, and body, all while rejecting anything older than 10 minutes. Add at-least-once delivery and a subscription cost model, and there's real handling required.

That puts challenge handling, signature-and-freshness verification, deduplication, and subscription health on you. Hookdeck answers the challenge, verifies signatures and freshness, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique notifications.

Get started with Hookdeck for free and handle Twitch EventSub webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.