Twitch Webhook: Example Payload, Headers and Signature

Twitch EventSub sends three kinds of message to the same URL, told apart by a header: a challenge to prove you own the endpoint, the notifications themselves, and a revocation when a subscription ends. Here is a stream.online notification and how each kind is handled.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by Twitch, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

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

HeaderExampleWhat it is
Twitch-Eventsub-Message-Typenotificationnotification, webhook_callback_verification or revocation. Decides how to treat the body.
Twitch-Eventsub-Message-Ide76c6bd4-55c9-4987-8304-da1588d8988bUnique id of the message. Unchanged on a retry, and part of the signed string.
Twitch-Eventsub-Message-Timestamp2026-01-01T12:00:00.123456789ZWhen the message was sent, RFC 3339. Part of the signed string, used exactly as given.
Twitch-Eventsub-Message-Signaturesha256=b0b3c4d5e6f708192a3b4c5d6e7f8091a2b3…HMAC-SHA256, hex, prefixed sha256=.
Twitch-Eventsub-Message-Retry0How many times this message has been re-sent.
Twitch-Eventsub-Subscription-Typestream.onlineThe event type of the subscription.
Twitch-Eventsub-Subscription-Version1The version of that event type.
Content-Typeapplication/jsonThe 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

  1. Press Create webhook URL at the top of this page and copy the address.
  2. Quickest check, with the Twitch CLI: twitch event verify-subscription subscribe -F <the address> -s test-secret-1234 sends a challenge and reports whether the reply was accepted. The same command with trigger in place of verify-subscription sends a mock notification.
  3. For a real subscription, get an app access token for a test application and send POST https://api.twitch.tv/helix/eventsub/subscriptions with Client-Id and Authorization: Bearer headers.
  4. In the JSON body set type, version and condition, and a transport of {"method":"webhook","callback":"<the address>","secret":"<10 to 100 characters>"}.
  5. 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 cost counted 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

Other webhooks with a timestamp in the signature

References

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.