Webhooks Overview

You can listen to events using webhooks, SQS or SNS. When setting up a webhook you can specify the exact events you want to receive, or select to receive all events.

To ensure that a webhook is triggered by Stream you can verify it’s signature. Webhook retries are in place. If you want to ensure an outage in your API never loses an event, it’s better to use SQS or SNS for reliability.

Quick Start

Here’s how to quickly set up webhooks using the event_hooks configuration:

Subscribe to Specific Events

// Subscribe to message.new and message.updated events only
var webhookHook = new EventHook
{
    HookType = HookType.Webhook,
    Enabled = true,
    EventTypes = new List<string> { "message.new", "message.updated" },
    WebhookUrl = "https://example.com/webhooks/stream/messages"
};

await client.UpdateAppSettingsAsync(new AppSettingsRequest
{
    EventHooks = new List<EventHook> { webhookHook }
});

Subscribe to All Events

Use an empty event_types array to receive all existing and future events:

// Subscribe to all events (empty list = all events)
var webhookHook = new EventHook
{
    HookType = HookType.Webhook,
    Enabled = true,
    EventTypes = new List<string>(), // empty list = all events
    WebhookUrl = "https://example.com/webhooks/stream/all"
};

await client.UpdateAppSettingsAsync(new AppSettingsRequest
{
    EventHooks = new List<EventHook> { webhookHook }
});

For reliable event delivery, you can also configure SQS or SNS instead of webhooks.

Debugging webhook requests with NGROK

The easiest way to debug webhooks is with NGROK.

  1. Start NGROK
brew install ngrok
ngrok http 8000
  1. Update your webhook URL to the NGROK url

  2. Trigger a webhook

  3. Open up the ngrok inspector

http://127.0.0.1:4040/inspect/http

Handling the webhook

A few guidelines for the webhook handling

  • Webhooks should accept HTTP POST requests with JSON payloads
  • Response code should be 2xx
  • Webhook should be ready to accept the same call multiple times: in case of network or remote server failure Stream Chat could retry the request
  • It’s important to validate the signature, so you know the request originated from Stream
  • Support HTTP Keep-Alive
  • Use HTTPS

The example below shows how to log the message new and verify the request

// signature comes from the HTTP header x-signature
appClient.VerifyWebhook(requestBody, signature);

All webhook requests contain these headers:

NameDescriptionExample
X-Webhook-IdUnique ID of the webhook call. This value is consistent between retries and could be used to deduplicate retry calls123e4567-e89b-12d3-a456-426614174000
X-Webhook-AttemptNumber of webhook request attempt starting from 11
X-Api-KeyYour application’s API key. Should be used to validate request signaturea1b23cdefgh4
X-SignatureHMAC signature of the request body. See Signature sectionca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb

Webhook types

In addition to the above there are 3 special webhooks.

TypeDescription
PushPush webhook is useful for triggering push notifications on your end
Before Message SendAllows you to modify or moderate message content before sending it to the chat for everyone to see
Custom CommandsReacts to custom /slash commands

Configuration

Before Message Send and Custom Commands

These webhooks continue to use the original configuration method and are NOT part of the multi-event hooks system:

  • Before Message Send: before_message_send_hook_url
  • Custom Commands: custom_action_handler_url
await appClient.UpdateAppSettingsAsync(new AppSettingsRequest
{
  BeforeMessageSendHookUrl = "https://example.com/webhooks/stream/before-message-send", // sets Before Message Send webhook address
  CustomActionHandlerUrl = "https://example.com/webhooks/stream/custom-commands?type={type}", // sets Custom Commands webhook address
});

Push webhook

The example below shows how to use the push webhooks

// Note: Any previously existing hooks not included in event_hooks array will be deleted.
// Get current settings first to preserve your existing configuration.

// STEP 1: Get current app settings to preserve existing hooks
var settings = await client.GetAppSettingsAsync();
var existingHooks = settings.App.EventHooks ?? new List<EventHook>();
Console.WriteLine($"Current event hooks: {existingHooks}");

// STEP 2: Add webhook hook while preserving existing hooks
var newWebhookHook = new EventHook
{
    HookType = HookType.Webhook,
    Enabled = true,
    EventTypes = new List<string>(), // empty list = all events
    WebhookUrl = "https://example.com/webhooks/stream/push"
};

// STEP 3: Update with complete array including existing hooks
var allHooks = new List<EventHook>(existingHooks) { newWebhookHook };
await client.UpdateAppSettingsAsync(new AppSettingsRequest
{
    EventHooks = allHooks
});

// Test the webhook connection
await appClient.CheckPushAsync(new AppCheckPushRequest
{
    WebhookUrl = "https://example.com/webhooks/stream/push",
});

You can also configure specific event types by providing an array of event names instead of an empty array:

// Configure webhook for specific events only
var newWebhookHook = new EventHook
{
    HookType = HookType.Webhook,
    Enabled = true,
    EventTypes = new List<string> { "message.new", "message.updated", "message.deleted" }, // specific events
    WebhookUrl = "https://example.com/webhooks/stream/messages"
};

Request info

Some webhooks contain a field request_info, which holds information about the client that issued the request. This info is intended as an additional signal that you can use for moderation, fraud detection, or other similar purposes.

When configuring the SDK, you may also set an additional x-stream-ext header to be sent with each request. The value of this header is passed along as an ext field in the request_info. You can use this to pass along information that may be useful, such as device information. Refer to the SDK-specific docs on how to set this header.

"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-chat-react-10.11.0-stream-chat-javascript-client-browser-8.12.1",
 "ext": "device-id=123"
}

For example, in Javascript, you can set the value like this:

client = new StreamChat(apiKey, {
  axiosRequestConfig: {
    headers: {
      "x-stream-ext": "device-id=123",
    },
  },
});

The format of the ext header is up to you and you may leave it blank if you don’t need it. The value is passed as-is, so you can use a simple value, comma-separated key-values, or more structured data, such as JSON. Binary data must be encoded as a string, for example using base64 or hex encoding.

Pending Message Options

You can configure pending message hooks to handle messages that require approval before being sent. The following options are available:

OptionTypeDescriptionRequired
webhook_urlstringThe URL where pending message events will be sentYes, except for CALLBACK_MODE_NONE
timeout_msnumberHow long messages should stay pending before being deleted in millisecondsYes
callback.modestringCallback mode (“CALLBACK_MODE_NONE”, “CALLBACK_MODE_REST”)Yes

You may set up to two pending message hooks per application. Only the first commit to a pending message will succeed; any subsequent commit attempts will return an error, as the message is no longer pending. If multiple hooks specify a timeout_ms, the system will use the longest timeout value.

For more information on configuring pending messages, please refer to the Pending Messages documentation.

Restricting access to webhook

If necessary, you can only expose your webhook service to Stream. This is possible by configuring your network (eg. iptables rules) to drop all incoming traffic that is not coming from our API infrastructure.

Below you can find the complete list of egress IP addresses that our webhook infrastructure uses. Such list is static and is not changing over time.

US-EastZONE IDeip
Primaryuse1-az234.225.10.29/32
Secondaryuse1-az434.198.125.61/32
Tertiaryuse1-az352.22.78.160/32
Quaternaryuse1-az63.215.161.238/32
EU-westZONE IDeip
Primaryeuw1-az352.212.14.212/32
Secondaryeuw1-az152.17.43.232/32
Tertiaryeuw1-az234.241.110.177/32
SydneyZONE IDeip
Primaryapse2-az354.252.193.245/32
Secondaryapse2-az213.55.254.141/32
Tertiaryapse2-az13.24.48.104/32
mumbaiZONE IDeip
Primaryaps1-az165.1.48.87/32
Secondaryaps1-az315.206.221.25/32
Tertiaryaps1-az213.233.48.78/32
SingaporeZONE IDeip
Primaryapse1-az213.229.11.158/32
Secondaryapse1-az152.74.225.150/32
Tertiaryapse1-az352.76.180.70/32
OHIOZONE IDEIP
Primaryuse2-az13.14.163.216/32
Secondaryuse2-az23.15.245.3/32
Tertiaryuse2-az33.141.116.179/32
CANADAZONE IDEIP
Primarycac1-az135.183.141.98/32
Secondarycac1-az252.60.71.231/32
Tertiarycac1-az43.97.253.35/32
OREGONZONE IDEIP
Primaryusw2-az152.25.165.25/32
Secondaryusw2-az244.237.58.11/32
Tertiaryusw2-az352.10.213.81/32