Twitch EventSub verification failing? Here's the challenge, decoded

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.

What Twitch actually sends

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:

Pass it with zero code (and see the traffic)

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:

  1. Create a bin (no signup needed) and open its settings.
  2. Set the custom response — Status 200, Content-Type text/plain, Body:
{{body.challenge}}
  1. Use your 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.

Verifying the signature (the part everyone gets wrong)

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.

Testing locally with the Twitch CLI

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.

Why subscriptions die later (revocation statuses)

Passing verification once isn't the end. Subscriptions don't expire, but Twitch revokes them — a revocation message arrives with one of these statuses:

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

← All guides · Docs · Hookden vs webhook.site