# Stream Webhooks

Stream here means **Stream (getstream.io)**: Chat, Activity Feeds, Video and Moderation. It is not Bunny Stream (see [bunny-stream-webhooks](/webhooks/skills/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.

```javascript
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
  }
}

```

```python
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/stream-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/stream-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/stream-webhooks/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:

```javascript
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

| Event | Description |
| --- | --- |
| `message.new` | A new message is added |
| `message.updated` | A message is updated |
| `message.deleted` | A message is deleted |
| `reaction.new` | A reaction is added to a message |
| `channel.created` | A channel is created |
| `user.updated` | A user is updated |
| `member.added` | A user is added to a channel |

> **For the full event list**, see [Stream webhook events](https://getstream.io/chat/docs/node/webhook_events/).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Signature` | Hex HMAC-SHA256 of the raw body, keyed with the app API secret |
| `X-Webhook-Id` | Unique ID of the call; identical across retries, use it to dedupe |
| `X-Api-Key` | Your app's API key (not the secret); check it matches your app |
| `X-Webhook-Attempt` | Attempt 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

```bash
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

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stream-webhooks/references/overview.md) - Stream webhook concepts and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stream-webhooks/references/setup.md) - Configure the webhook URL and event types
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/stream-webhooks/references/verification.md) - Signature verification details and gotchas