You create an EventSub subscription, Twitch answers 202 Accepted with status
webhook_callback_verification_pending — and then nothing. The subscription
either stays pending or flips to webhook_callback_verification_failed, and no
events ever arrive. This page walks through exactly what Twitch sends to your callback, the
handful of ways the challenge response goes wrong, and how to pass the whole check with zero
code while you look at the real traffic.
Immediately after you subscribe, Twitch POSTs a verification request to your callback with the header:
Twitch-Eventsub-Message-Type: webhook_callback_verification
The JSON body carries a challenge field at the top level:
{
"challenge": "pogchamp-kappa-360noscope-vohiyo",
"subscription": {
"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
"status": "webhook_callback_verification_pending",
"type": "channel.follow",
...
}
}
Per Twitch's docs, your response must be status 200 with a body containing the
raw challenge value — and nothing else. Two requirements trip people up:
"pogchamp-…" arrives with quotes around it and verification fails. Twitch's own
docs call this out: some web frameworks default to converting responses into JSON. Send it as
text/plain.localhost can't receive real EventSub traffic at all (the Twitch CLI exists for
that — below).Chicken-and-egg again: you want to see what EventSub sends before you've built the endpoint, but Twitch won't enable the subscription until an endpoint answers the challenge. A Hookden bin can answer it:
200, Content-Type text/plain,
Body:{{body.challenge}}
https://hookden.pages.dev/h/… URL as the transport.callback when
you create the subscription (it's HTTPS on 443, so it qualifies).The template echoes the challenge byte-exact with no JSON quoting, the subscription flips
to enabled, and every event after that lands in your dashboard as inspectable
JSON — headers included. Anonymous bins expire after 24 hours; sign in
(free) to keep one around longer.
Every EventSub message — including the verification request itself — is signed:
Twitch-Eventsub-Message-Signature: sha256=<hex HMAC>
The HMAC-SHA256 key is the secret you passed when creating the subscription
(an ASCII string, 10–100 characters). The signed message is the concatenation of
three things, in order, with no separators:
Twitch-Eventsub-Message-Id + Twitch-Eventsub-Message-Timestamp + raw request body
Classic bugs: HMAC-ing only the body (GitHub habit), inserting dots between the parts
(Svix habit), using the parsed-and-re-serialized body instead of the raw bytes, and
case-sensitive header lookups (many runtimes lowercase header names — Twitch's own sample
code calls .toLowerCase() first). Compare with a timing-safe function and return
a 4xx when it doesn't match.
Hookden verifies this scheme natively: pick Twitch EventSub in the bin's
signature-verification setting, paste your subscription secret, and every captured message
gets a ✓/✗ badge computed over message-id + timestamp + raw body — so you can
tell instantly whether your secret is wrong or your own verifier is.
The Twitch CLI can exercise your handler without SSL and without creating real subscriptions:
twitch event verify-subscription subscribe -F http://localhost:8080/eventsub -s your10charsecret twitch event trigger subscribe -F http://localhost:8080/eventsub -s your10charsecret
verify-subscription checks exactly the two things Twitch checks: 200 status
and the raw challenge in the body. It works against a bin URL too — handy for confirming the
template above before pointing a real subscription at it.
Passing verification once isn't the end. Subscriptions don't expire, but Twitch revokes
them — a revocation message arrives with one of these statuses:
notification_failures_exceeded — your callback timed out or errored too many
times. Twitch expects a response within a few seconds; do slow work after responding
2xx.authorization_revoked — the user revoked the token or changed their
password.user_removed — the user in the subscription no longer exists.version_removed — the subscription type/version was retired.Also note delivery is at-least-once: if Twitch is unsure you received a
notification it resends it, with Twitch-Eventsub-Message-Retry counting the
attempt — deduplicate on Twitch-Eventsub-Message-Id if double-processing would
hurt.
Related handshakes: Slack URL verification (echo a JSON field — Twitch's is the same shape), Zoom's CRC (hash it, don't echo it), Meta's GET hub.challenge, and Discord's Ed25519 check.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new