var webhookHooks = new List<EventHook>
{
new EventHook
{
HookType = HookType.Webhook,
Enabled = true,
EventTypes = new List<string>(), // empty list = all events
WebhookUrl = "<webhook url>",
},
new EventHook
{
HookType = HookType.Webhook,
Enabled = true,
EventTypes = new List<string> { "feeds.activity.added" }, // specific events
WebhookUrl = "<webhook url>",
},
};
await client.UpdateAppAsync(new UpdateAppRequest
{
EventHooks = webhookHooks,
});Event Handling
You can configure your Stream app to receive webhook events as well as AWS SNS and AWS SQS. Webhooks are usually the simplest way to receive events from your app and to perform additional action based on what happens to your application.
The configuration can be done using the API or from the Dashboard. By default, all events are sent to your webhook/sqs/sns endpoint, you can also configure the events you want to receive in the dashboard. Signature verification and the SDK parsing helpers are covered in the webhooks guide; retries, event failover and the IP allowlist are summarised under Delivery and reliability below.
Some important points to consider:
- The selection of events you want to receive applies to all the endpoints you have configured.
- You can configure multiple endpoints for the same app (eg. AWS SNS and HTTP Webhook).
- If your app is configured to receive all events, you can still filter the events you want to receive in your webhook handler.
- If your app is configured to receive all events, newly introduced event types will be sent to your webhook handler by default.
- If you pick specific events, newly introduced event types will not be sent to your webhook handler by default (you can still manually add them later on).
How to implement a webhook handler
Your webhook handler needs to follow these rules:
- accept HTTP POST requests with JSON payload
- be reachable from the public internet. Tunneling services like Ngrok are supported
- respond with response codes from 200 to 299 as fast as possible
Your webhook handler can use the type field to handle events based correctly based on their type and payload.
All webhook requests contain these headers:
| Name | Description | Example |
|---|---|---|
| X-Webhook-Id | Unique ID of the webhook call. This value is consistent between retries, so use it to deduplicate retried deliveries | 123e4567-e89b-12d3-a456-426614174000 |
| X-Webhook-Attempt | Number of webhook request attempt starting from 1 | 1 |
| X-Api-Key | Your application's API key. Should be used to validate request signature | a1b23cdefgh4 |
| X-Signature | HMAC signature of the request body. See Signature section | ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb |
Request info
Most feeds events also carry a request_info object describing the client whose request caused the event. Use it as an extra signal for moderation, fraud detection or auditing.
{
"type": "feeds.activity.added",
"request_info": {
"type": "client",
"ip": "86.84.2.2",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0",
"sdk": "stream-feeds-js-v1.2.3",
"ext": "device-id=abc123"
}
}| Field | Description |
|---|---|
type | client when the request was authenticated with a user token, server when it used your API key and secret |
ip | The originating client IP, taken from X-Forwarded-For when the request carries it |
user_agent | The User-Agent header of the request |
sdk | The Stream SDK and version that made the request |
ext | Free-form passthrough from the X-Stream-Ext request header, omitted when that header is absent or empty |
Treat the whole object as optional. It is present whenever the event can be traced back to an API request, including work that finishes asynchronously after the response, such as URL enrichment on a new activity or a feed deletion that runs in the background. It is absent when there is no originating request at all: events produced by an activity processor, or by a feed visibility change, ship without the field rather than with an empty object. Failover copies never include it either, because they store the bare event JSON.
Adding your own data to ext
Four of the five fields are filled in for you. ext is the one you control: whatever your app sends in the x-stream-ext request header arrives on the event unchanged. Use it for context the payload does not otherwise carry, such as a device identifier or an app build number.
It is set on the client, when the client is constructed. The JavaScript, React and React Native SDKs support this today: see Adding your own data to ext in the client docs. On the other client SDKs ext arrives empty, because they do not yet accept custom request headers.
ext is a client-side affordance. The server-side SDKs do not expose custom request headers, so an event caused by a server-side call arrives with type set to server and no ext. See Request info for the cross-product reference.
Best Practices
We highly recommend following common security guidelines to make your webhook integration safe and fast:
- Use HTTPS with a certificate from a trusted authority
- Verify the "X-Signature" header to ensure the request is coming from Stream
- Support HTTP Keep-Alive
- Use a highly available infrastructure such as AWS Elastic Load Balancer, Google Cloud Load Balancer, or similar
- Offload the processing of the message if possible (read, store, and forget)
- When decoding JSON into objects, ensure that your webhook can handle new fields being added to the JSON payload as well as new event types (eg. log unknown fields and event types instead of failing)
Delivery and reliability
Feeds hooks run on the same delivery pipeline as every other Stream product, so the platform webhooks guide is the full reference. The parts that matter most for feeds traffic are below.
Retries
A failed delivery is retried immediately, with no backoff. Retries stop as soon as any one of these limits is reached: 5 attempts, 6 seconds per attempt, 15 seconds total for the event including retries, 3 retryable response errors (408, 429 or any 5xx), or 2 network errors.
Any other non-2xx response is treated as final and is not retried. Retry-After is not honoured. Retries reuse the same X-Webhook-Id, so deduplicate on that header rather than on event contents. The full table is under Retries.
Event failover
If every attempt fails, Stream can write the event to a Google Cloud Storage bucket you own instead of dropping it, so an outage on your side costs you a replay rather than the data. Add a failover_config to the webhook hook you configured above:
{
"hook_type": "webhook",
"enabled": true,
"event_types": ["feeds.activity.added"],
"webhook_url": "<webhook url>",
"failover_config": {
"type": "gcs",
"gcs_bucket": "my-failover-bucket",
"gcs_path": "feeds/failed-events",
"gcs_credentials": "<service account JSON key as a string>"
}
}Two things to know before you build the consumer:
- A 403 from your endpoint is read as a deliberate rejection, not an outage, so that event is dropped and never reaches the bucket. Return a 5xx if you want it kept for replay.
- The stored
payloadis the bare event JSON, without the envelope fields that live HTTP delivery adds.
Failover is webhook-only today: app settings reject a failover_config on an SQS or SNS hook. Bucket layout, credentials, permissions and the failure envelope are documented under Event failover.
Payload compression
Apps created after May 7, 2026 have enable_hook_payload_compression on by default, so your handler receives gzipped feeds payloads out of the box. Payloads under 256 bytes stay uncompressed, and the X-Signature HMAC is always computed over the uncompressed body. Call verifyAndParseWebhook from your server SDK and both cases are handled for you. See Payload compression.
Restricting access to your endpoint
Stream delivers from a fixed set of egress IP addresses, so you can drop all other incoming traffic to your webhook service. Take the current list from Restricting access to webhook rather than copying it into your configuration, so you are never filtering against a stale range.
Available Event Types
Below is a comprehensive table of all available event types and their descriptions. WebhookEvent model definition can be found in Open API specification.
Poll events (feeds.poll.*) are the one exception: they are broadcast to connected clients over the WebSocket only, and are not delivered to webhooks, SQS or SNS.
| Event Name | Description |
|---|---|
| Activity Events | |
feeds.activity.added | Fired when a new activity is added to a feed |
feeds.activity.updated | Fired when an activity is modified |
feeds.activity.deleted | Fired when an activity is removed |
feeds.activity.restored | Fired when an activity is restored |
feeds.activity.removed_from_feed | Fired when an activity is removed from a specific feed |
feeds.activity.pinned | Fired when an activity is pinned to the top |
feeds.activity.unpinned | Fired when an activity is unpinned |
feeds.activity.feedback | Fired when activity feedback is provided |
feeds.activity.marked | Fired when an activity is marked |
| Notification Events | |
feeds.notification_feed.updated | Fired when the notification status, or notification groups (aggregated activities) are updated |
| Comment Events | |
feeds.comment.added | Fired when a new comment is added to an activity |
feeds.comment.updated | Fired when a comment is modified |
feeds.comment.deleted | Fired when a comment is removed |
| Reaction Events | |
feeds.activity.reaction.added | Fired when a reaction is added to an activity |
feeds.activity.reaction.deleted | Fired when a reaction is removed from an activity |
feeds.activity.reaction.updated | Fired when a reaction on an activity is updated |
feeds.comment.reaction.added | Fired when a reaction is added to a comment |
feeds.comment.reaction.deleted | Fired when a reaction is removed from a comment |
feeds.comment.reaction.updated | Fired when a reaction on a comment is updated |
| Poll Events | |
feeds.poll.closed | Fired when a poll is closed |
feeds.poll.deleted | Fired when a poll is deleted |
feeds.poll.updated | Fired when a poll is modified |
feeds.poll.vote_casted | Fired when a vote is cast |
feeds.poll.vote_changed | Fired when a vote is changed |
feeds.poll.vote_removed | Fired when a vote is removed |
| Feed Events | |
feeds.feed.created | Fired when a new feed is created |
feeds.feed.updated | Fired when a feed is modified |
feeds.feed.deleted | Fired when a feed is deleted |
| Feed Group Events | |
feeds.feed_group.changed | Fired when a feed group is changed |
feeds.feed_group.deleted | Fired when a feed group is deleted |
| Member Events | |
feeds.feed_member.added | Fired when a member is added to a feed |
feeds.feed_member.removed | Fired when a member is removed from a feed |
feeds.feed_member.updated | Fired when a member's role/permissions change |
| Follow Events | |
feeds.follow.created | Fired when a follow relationship is created |
feeds.follow.deleted | Fired when a follow relationship is removed |
feeds.follow.updated | Fired when follow settings are modified |
| Bookmark Events | |
feeds.bookmark.added | Fired when an activity is bookmarked |
feeds.bookmark.deleted | Fired when a bookmark is removed |
feeds.bookmark.updated | Fired when bookmark metadata is modified |
feeds.bookmark_folder.deleted | Fired when bookmark folder is deleted |
feeds.bookmark_folder.updated | Fired when bookmark folder is updated |
| Stories Events | |
feeds.stories_feed.updated | Fired when a stories feed is updated |
| Moderation Events | |
moderation.custom_action | Fired when a custom moderation action is performed |
moderation.flagged | Fired when content is flagged for moderation |
moderation.mark_reviewed | Fired when content is marked as reviewed |
| User Events | |
user.banned | Fired when a user is banned |
user.deactivated | Fired when a user is deactivated |
user.muted | Fired when a user is muted |
user.reactivated | Fired when a user is reactivated |
user.updated | Fired when a user is updated |