How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a Twitch webhook?
A Twitch webhook is an HTTP POST from EventSub, the system that replaced the older PubSubHubbub-style webhooks. You create a subscription through the Twitch API naming an event type, a condition such as a broadcaster id, and a transport: your HTTPS callback URL on port 443 plus a secret you choose. Twitch then posts to that callback whenever the event occurs.
Every message carries a Twitch-Eventsub-Message-Type header, and a handler has to branch on it before reading the body. webhook_callback_verification is the one-time challenge sent when a subscription is created. notification is an event. revocation tells you the subscription has been cancelled, for example because the user withdrew authorisation or your endpoint failed too often.
What a Twitch webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
Twitch-Eventsub-Message-Type: notification
Twitch-Eventsub-Message-Id: e76c6bd4-55c9-4987-8304-da1588d8988b
Twitch-Eventsub-Message-Timestamp: 2026-01-01T12:00:00.123456789Z
Twitch-Eventsub-Message-Signature: sha256=b0b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091
Twitch-Eventsub-Message-Retry: 0
Twitch-Eventsub-Subscription-Type: stream.online
Twitch-Eventsub-Subscription-Version: 1
Content-Type: application/json
{
"subscription": {
"id": "f1c2a387-161a-49f9-a165-0f21d7a4e1c4",
"type": "stream.online",
"version": "1",
"status": "enabled",
"cost": 0,
"condition": {
"broadcaster_user_id": "1337"
},
"transport": {
"method": "webhook",
"callback": "https://your-server.example/webhooks/twitch"
},
"created_at": "2025-12-20T10:11:12.123456789Z"
},
"event": {
"id": "9001",
"broadcaster_user_id": "1337",
"broadcaster_user_login": "cool_user",
"broadcaster_user_name": "Cool_User",
"type": "live",
"started_at": "2026-01-01T12:00:00Z"
}
}
Every notification repeats the subscription that produced it, followed by an event whose fields depend on the type. Ids are strings even when they look numeric. A challenge message has the same subscription object and a top-level challenge string instead of event; a revocation has only subscription, with its status giving the reason.
Headers Twitch sends
| Header | Example | What it is |
|---|---|---|
Twitch-Eventsub-Message-Type | notification | notification, webhook_callback_verification or revocation. Decides how to treat the body. |
Twitch-Eventsub-Message-Id | e76c6bd4-55c9-4987-8304-da1588d8988b | Unique id of the message. Unchanged on a retry, and part of the signed string. |
Twitch-Eventsub-Message-Timestamp | 2026-01-01T12:00:00.123456789Z | When the message was sent, RFC 3339. Part of the signed string, used exactly as given. |
Twitch-Eventsub-Message-Signature | sha256=b0b3c4d5e6f708192a3b4c5d6e7f8091a2b3… | HMAC-SHA256, hex, prefixed sha256=. |
Twitch-Eventsub-Message-Retry | 0 | How many times this message has been re-sent. |
Twitch-Eventsub-Subscription-Type | stream.online | The event type of the subscription. |
Twitch-Eventsub-Subscription-Version | 1 | The version of that event type. |
Content-Type | application/json | The body is JSON. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How Twitch signs a message
Twitch concatenates three things with nothing between them — the value of Twitch-Eventsub-Message-Id, the value of Twitch-Eventsub-Message-Timestamp and the raw request body — and computes an HMAC-SHA256 keyed with the secret you supplied when creating the subscription. The hex digest, prefixed sha256=, is sent in Twitch-Eventsub-Message-Signature. All three message types are signed this way, the challenge included.
Use the timestamp header as the literal string you received when rebuilding the message; reformatting it changes the bytes. Twitch's guidance is also to discard messages whose timestamp is more than ten minutes old and to remember recent message ids so a repeat is ignored. Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function.
const crypto = require('crypto');
// rawBody: string of the body exactly as received
// headers: lower-cased request headers
function verifyTwitch(rawBody, headers, secret) {
const id = headers['twitch-eventsub-message-id'];
const ts = headers['twitch-eventsub-message-timestamp'];
if (!id || !ts) return false;
// the timestamp has nanosecond precision; trim it for Date.parse only
const sentAt = Date.parse(ts.replace(/(\.\d{3})\d+/, '$1'));
if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > 600000) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret)
.update(id + ts + rawBody).digest('hex');
const a = Buffer.from(headers['twitch-eventsub-message-signature'] || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The challenge, response time and revocation
A new subscription starts as pending. Twitch posts a webhook_callback_verification message whose JSON contains a challenge string, and the endpoint must reply 200 with that string alone as the body, as plain text rather than JSON. Only then does the subscription become enabled. The webhook URLs created on this site do this automatically: a message with that header gets its challenge returned as text/plain. They do not check the signature first, which your own endpoint should.
Notifications must be acknowledged with a 2xx within a few seconds, per Twitch's documentation at the time of writing. A message that is not acknowledged is sent again with Twitch-Eventsub-Message-Retry counting up and the same message id. An endpoint that keeps failing has its subscription revoked with the status notification_failures_exceeded; it is not re-enabled automatically, so you create it again.
Catch a real Twitch EventSub message
- Press Create webhook URL at the top of this page and copy the address.
- Quickest check, with the Twitch CLI:
twitch event verify-subscription subscribe -F <the address> -s test-secret-1234sends a challenge and reports whether the reply was accepted. The same command withtriggerin place ofverify-subscriptionsends a mock notification. - For a real subscription, get an app access token for a test application and send
POST https://api.twitch.tv/helix/eventsub/subscriptionswithClient-IdandAuthorization: Bearerheaders. - In the JSON body set
type,versionandcondition, and atransportof{"method":"webhook","callback":"<the address>","secret":"<10 to 100 characters>"}. - The challenge arrives in the inbox above and is answered for you; afterwards each event of that type is delivered here.
Delete the subscription through the API when you are finished. While it points at this inbox, anyone holding the inbox URL can read the events, and once the inbox expires Twitch will see failures and revoke it anyway.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- The challenge reply is the bare string. Wrapping it in JSON or quotes, or adding a newline, leaves the subscription stuck in a failed verification state.
- Mock events from the CLI use made-up ids. They are signed correctly with the secret you pass, but the users and subscriptions in them do not exist.
- Subscriptions cost points. Each has a
costcounted against a per-application limit, and some event types need the broadcaster to have authorised your app first.
api-tester.com is independent and is not affiliated with Twitch. Providers change header names, limits and retry schedules; treat the official Twitch documentation as the source of truth.
Related examples and tools
Zoom Webhook Example
Zoom will not save a webhook endpoint until it has answered a validation challenge with a hash made from the app's Secret Token, and it repeats that check from time to time.
WhatsApp Webhook Example
WhatsApp Cloud API webhooks come from Meta's shared webhook system: a GET challenge when you register the URL, then deeply nested JSON for every incoming message and every delivery status.
Dropbox Webhook Example
A Dropbox webhook tells you who has changes and nothing about what changed.
Asana Webhook Example
Asana hands you the signing secret instead of asking for one: it arrives once, in a header, during a handshake your endpoint has to answer.
Okta Webhook Example
An Okta event hook delivers System Log events to your endpoint, authenticated by a header and secret you choose rather than by a signature.
Idempotency
Why a re-sent message must not be processed twice, and how to recognise a repeat.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks with a timestamp in the signature
References
- Official Twitch documentation — The provider's own guide to the requests described on this page.
- Twitch Developers: EventSub subscription types — Every subscription type, its version and its conditions.
- Twitch Developers: testing webhook events with the CLI — Sending mock events to an endpoint from a terminal.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
Twitch webhook FAQ
What does a Twitch EventSub webhook payload look like?
A JSON object with a subscription describing the subscription that fired, including its type, version, condition and transport, and an event object whose fields depend on the event type, such as broadcaster_user_id and started_at for stream.online.
How do I respond to the Twitch webhook challenge?
When Twitch-Eventsub-Message-Type is webhook_callback_verification, reply with status 200 and the challenge value from the JSON body as the raw response body, with a plain-text content type.
How do I verify a Twitch EventSub signature?
Concatenate the Twitch-Eventsub-Message-Id header, the Twitch-Eventsub-Message-Timestamp header and the raw body, compute an HMAC-SHA256 with your subscription secret, hex-encode it, prefix it with sha256= and compare it with Twitch-Eventsub-Message-Signature.
Why was my Twitch EventSub subscription revoked?
Twitch revokes a subscription when the user removes authorisation, the user no longer exists, the event version is retired, or the callback failed too many times. The revocation message gives the reason in subscription.status.
Can I test Twitch webhooks without going live?
Yes. The Twitch CLI can send a verification challenge and mock notifications to any URL, signed with a secret you choose, without a real event happening.