Agent skill

Stream Webhooks Skill

Receive and verify Stream (getstream.io) webhooks for Chat, Feeds and Video. Use when setting up Stream webhook handlers, debugging X-Signature verification, or handling events like message.new, message.updated, reaction.new, channel.created, user.updated, and member.added.

Install this skill

npx skills add hookdeck/webhook-skills --skill stream-webhooks


Stream here means Stream (getstream.io): Chat, Activity Feeds, Video and Moderation. It is not Bunny Stream (see bunny-stream-webhooks) or any other "Stream" product.

When to Use This Skill

  • How do I receive Stream webhooks?
  • How do I verify the Stream X-Signature header?
  • How do I handle message.new, reaction.new or member.added events?
  • Why is my Stream webhook signature verification failing?
  • Why do Stream webhooks fail verification when Payload Compression is on?

Verification (core)

Stream signs the raw request body with HMAC-SHA256, keyed with your app's API secret (the full secret, used as a UTF-8 string), and sends the lowercase hex digest in X-Signature. There is no timestamp and no prefix in the header. Compare timing-safe.

const crypto = require('crypto');

function verifyStreamWebhook(rawBody, signature, apiSecret) {
  if (!signature) return false;
  const expected = crypto.createHmac('sha256', apiSecret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  } catch {
    return false; // different lengths
  }
}
import hmac, hashlib

def verify_stream_webhook(raw_body: bytes, signature: str, api_secret: str) -> bool:
    expected = hmac.new(api_secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature or "", expected)

SDK option: every Stream server SDK can do this for you, e.g. client.verifyAndParseWebhook(rawBody, signature) (Node stream-chat / @stream-io/node-sdk) or verify_and_parse_webhook(body, signature, secret) (Python getstream). Keep a manual check like the above when you need to verify without a client, or in an edge function.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Payload Compression must be OFF (Hookdeck)

Stream's enable_hook_payload_compression app setting gzips payloads (over 256 bytes). The signature is computed over the uncompressed bytes, so a verifier that sees the gzipped body fails, and Hookdeck's Stream source cannot verify compressed payloads. Apps created after May 7, 2026 have it on by default, so turn it off before pointing Stream at Hookdeck:

await client.updateAppSettings({ enable_hook_payload_compression: false });

If you receive webhooks directly and keep compression on, gunzip the body first (gzip magic bytes 1f 8b), then verify the HMAC on the decompressed bytes. The examples do this.

Common Event Types

EventDescription
message.newA new message is added
message.updatedA message is updated
message.deletedA message is deleted
reaction.newA reaction is added to a message
channel.createdA channel is created
user.updatedA user is updated
member.addedA user is added to a channel

For the full event list, see Stream webhook events.

Important Headers

HeaderDescription
X-SignatureHex HMAC-SHA256 of the raw body, keyed with the app API secret
X-Webhook-IdUnique ID of the call; identical across retries, use it to dedupe
X-Api-KeyYour app's API key (not the secret); check it matches your app
X-Webhook-AttemptAttempt number, starting at 1

Respond with a 2xx quickly. Stream retries immediately on network errors, 408, 429 and 5xx responses (up to 5 attempts within 15 seconds); other non-2xx responses are not retried.

Environment Variables

STREAM_API_SECRET=your_stream_app_api_secret   # Dashboard > your app > API secret
STREAM_API_KEY=your_stream_app_api_key         # Optional: checked against X-Api-Key

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 stream --path /webhooks/stream

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →