// first argument is the request body as a string, second the signature header
const valid = client.verifyWebhook(req.rawBody, req.headers["x-signature"]);
Webhooks Overview
By using webhooks, you can tightly integrate your server application with Stream Chat. The platform supports three kinds of webhooks: Push, Before-Message-Send, and Custom Commands. All of them follow the common set of rules:
Webhook should be reachable from the public internet. Tunneling services like Ngrok are supported
Webhook should accept HTTP POST requests with JSON payload
Webhook should respond with response codes from 200 to 299
Webhook should respond as fast as possible. The exact time given for the response varies between webhook types
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 (behavior varies between webhook types)
All webhook requests contain these headers:
Name | Description | Example |
---|---|---|
X-Webhook-Id | Unique ID of the webhook call. This value is consistent between retries and could be used to deduplicate retry calls | 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 |
Security and Performance
We highly recommend following common security guidelines to make your webhook integration safe and fast:
Use HTTPS with a certificate from a trusted authority (eg.Let’s Encrypt)
Verify the ” X-Signature ” header
Support HTTP Keep-Alive
Be highly available
Offload the processing of the message if possible (read, store, and forget)
Webhook types
Stream Chat supports several webhook types:
Type | Description |
---|---|
Push | Push webhook is useful when you want your server application to receive all important events happening in the Stream Chat |
Before Message Send | This webhook allows you to modify or moderate message content before sending it to the chat for everyone to see |
Custom Commands | This webhook reacts to custom /slash commands |
Please consult with corresponding pages to choose the webhook type that best fits your needs
Signature
All HTTP requests can be verified as coming from Stream (and not tampered by a 3rd party) by analyzing the signature attached to the request. Every request includes an HTTP header called “X-Signature” containing a cryptographic signature of the message. Your webhook endpoint can validate that payload and signature match:
import stream_chat
client = StreamChat(api_key="STREAM_KEY", api_secret="STREAM_SECRET")
# Django request
valid = client.verify_webhook(request.body, request.META['HTTP_X_SIGNATURE'])
# Flask request
valid = client.verify_webhook(request.data, request.headers['X-SIGNATURE'])
require 'stream-chat'
client = StreamChat::Client.new(api_key='STREAM_KEY', api_secret='STREAM_SECRET')
# signature comes from the HTTP header x-signature
valid = client.verify_webhook(request_body, signature)
$client = new GetStream\StreamChat\Client("STREAM_API_KEY", "STREAM_API_SECRET");
// signature comes from the HTTP header x-signature
$valid = $client->verifyWebhook($requestBody, $signature);
client, _ := stream.NewClient(APIKey, []byte(APISecret))
// signature comes from the HTTP header x-signature
isValid := client.VerifyWebhook(body, signature)
// signature comes from the HTTP header x-signature
appClient.VerifyWebhook(requestBody, signature);
// signature comes from the HTTP header x-signature
var valid = App.verifyWebhook(body, signature);
If your application uses more than one API Key, you can use the HTTP Header X-Api-Key to match the signature to the correct secret. Stream will always use the oldest key.
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 client.updateAppSettings({
before_message_send_hook_url:
"https://example.com/webhooks/stream/before-message-send", // sets Before Message Send webhook address
custom_action_handler_url:
"https://example.com/webhooks/stream/custom-commands?type={type}", // sets Custom Commands webhook address
});
client.update_app_settings(
before_message_send_hook_url="https://example.com/webhooks/stream/before-message-send", # sets Before Message Send webhook address
custom_action_handler_url="https://example.com/webhooks/stream/custom-commands?type={type}", # sets Custom Commands webhook address
)
client.update_app_settings(
before_message_send_hook_url: "https://example.com/webhooks/stream/before-message-send", # sets Before Message Send webhook address
custom_action_handler_url: "https://example.com/webhooks/stream/custom-commands?type={type}", # sets Custom Commands webhook address
)
$settings = [
"before_message_send_hook_url" => "https://example.com/webhooks/stream/before-message-send", // sets Before Message Send webhook address
"custom_action_handler_url" => "https://example.com/webhooks/stream/custom-commands?type={type}", // sets Custom Commands webhook address
];
$client->updateAppSettings($settings);
App
.update()
.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
.request();
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
});
settings := &AppSettings{
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
}
client.UpdateAppSettings(ctx, settings)
Push Webhooks
You can configure your webhook endpoints in the Stream Chat Dashboard or by using server-side SDK client:
Using the Dashboard
- Go to the Stream Dashboard
- Select your app
- Navigate to your app’s settings until “Webhook & Event Configuration” section
- Add and configure webhook, SQS, SNS, and pending message
Using Server-Side SDKs
For push webhooks (all product events), use the new multi-event hooks system with the event_hooks
array:
// 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
const response = await client.getAppSettings();
console.log("Current event hooks:", response.event_hooks);
// STEP 2: Add webhook hook while preserving existing hooks
const existingHooks = response.event_hooks || [];
const newWebhookHook = {
enabled: true,
hook_type: "webhook",
webhook_url: "https://example.com/webhooks/stream/push",
event_types: [], // empty array = all events
};
// STEP 3: Update with complete array including existing hooks
await client.updateAppSettings({
event_hooks: [...existingHooks, newWebhookHook],
});
// Test the webhook connection
await client.testWebhookSettings({
webhook_url: "https://example.com/webhooks/stream/push",
});
# 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
response = client.get_app_settings()
existing_hooks = response.get("event_hooks", [])
print("Current event hooks:", existing_hooks)
# STEP 2: Add webhook hook while preserving existing hooks
new_webhook_hook = {
"enabled": True,
"hook_type": "webhook",
"webhook_url": "https://example.com/webhooks/stream/push",
"event_types": [] # empty array = all events
}
# STEP 3: Update with complete array including existing hooks
client.update_app_settings(
event_hooks=existing_hooks + [new_webhook_hook]
)
# Test the webhook connection
client.check_push("https://example.com/webhooks/stream/push")
# 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
response = client.get_app_settings
existing_hooks = response["event_hooks"] || []
puts "Current event hooks:", existing_hooks
# STEP 2: Add webhook hook while preserving existing hooks
new_webhook_hook = {
"enabled" => true,
"hook_type" => "webhook",
"webhook_url" => "https://example.com/webhooks/stream/push",
"event_types" => [] # empty array = all events
}
# STEP 3: Update with complete array including existing hooks
client.update_app_settings(
event_hooks: existing_hooks + [new_webhook_hook]
)
# Test the webhook connection
client.check_push('https://example.com/webhooks/stream/push')
// 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
$response = $client->getAppSettings();
$existingHooks = $response['event_hooks'] ?? [];
echo "Current event hooks: " . json_encode($existingHooks);
// STEP 2: Add webhook hook while preserving existing hooks
$newWebhookHook = [
'enabled' => true,
'hook_type' => 'webhook',
'webhook_url' => 'https://example.com/webhooks/stream/push',
'event_types' => [] // empty array = all events
];
// STEP 3: Update with complete array including existing hooks
$client->updateAppSettings([
'event_hooks' => array_merge($existingHooks, [$newWebhookHook])
]);
// Test the webhook connection
$client->checkPush([
'webhook_url' => 'https://example.com/webhooks/stream/push'
]);
// 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
settings, err := client.GetAppSettings(ctx)
if err != nil {
log.Fatal(err)
}
existingHooks := settings.App.EventHooks
fmt.Printf("Current event hooks: %+v\n", existingHooks)
// STEP 2: Add webhook hook while preserving existing hooks
newWebhookHook := EventHook{
HookType: WebhookHook,
Enabled: true,
EventTypes: []string{}, // empty slice = all events
WebhookURL: "https://example.com/webhooks/stream/push",
}
// STEP 3: Update with complete array including existing hooks
allHooks := append(existingHooks, newWebhookHook)
_, err = client.UpdateAppSettings(ctx, NewAppSettings().SetEventHooks(allHooks))
if err != nil {
log.Fatal(err)
}
// Test the webhook connection
req := &CheckPushRequest{
WebhookURL: "https://example.com/webhooks/stream/push",
}
client.CheckPush(ctx, req)
// 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
App.AppGetResponse response = App.get().request();
List<App.EventHook> existingHooks = response.getApp().getEventHooks();
System.out.println("Current event hooks: " + existingHooks);
// STEP 2: Add webhook hook while preserving existing hooks
App.EventHook newWebhookHook = new App.EventHook();
newWebhookHook.setHookType(App.HookType.WEBHOOK);
newWebhookHook.setEnabled(true);
newWebhookHook.setEventTypes(Collections.emptyList()); // empty list = all events
newWebhookHook.setWebhookURL("https://example.com/webhooks/stream/push");
// STEP 3: Update with complete array including existing hooks
List<App.EventHook> allHooks = new ArrayList<>(existingHooks);
allHooks.add(newWebhookHook);
App.update().eventHooks(allHooks).request();
// Test the webhook connection
App.checkPush()
.webhookUrl("https://example.com/webhooks/stream/push")
.request();
// 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
const newWebhookHook = {
enabled: true,
hook_type: "webhook",
webhook_url: "https://example.com/webhooks/stream/messages",
event_types: ["message.new", "message.updated", "message.deleted"], // specific events
};
# Configure webhook for specific events only
new_webhook_hook = {
"enabled": True,
"hook_type": "webhook",
"webhook_url": "https://example.com/webhooks/stream/messages",
"event_types": ["message.new", "message.updated", "message.deleted"] # specific events
}
# Configure webhook for specific events only
new_webhook_hook = {
"enabled" => true,
"hook_type" => "webhook",
"webhook_url" => "https://example.com/webhooks/stream/messages",
"event_types" => ["message.new", "message.updated", "message.deleted"] # specific events
}
// Configure webhook for specific events only
$newWebhookHook = [
'enabled' => true,
'hook_type' => 'webhook',
'webhook_url' => 'https://example.com/webhooks/stream/messages',
'event_types' => ['message.new', 'message.updated', 'message.deleted'] // specific events
];
// Configure webhook for specific events only
newWebhookHook := EventHook{
HookType: WebhookHook,
Enabled: true,
EventTypes: []string{"message.new", "message.updated", "message.deleted"}, // specific events
WebhookURL: "https://example.com/webhooks/stream/messages",
}
// Configure webhook for specific events only
App.EventHook newWebhookHook = new App.EventHook();
newWebhookHook.setHookType(App.HookType.WEBHOOK);
newWebhookHook.setEnabled(true);
newWebhookHook.setEventTypes(Arrays.asList("message.new", "message.updated", "message.deleted")); // specific events
newWebhookHook.setWebhookURL("https://example.com/webhooks/stream/messages");
// 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"
};
See the Multi-Event Hooks documentation for complete details.
Also, webhooks could be configured using our CLI client:
stream chat:webhook:before-message-send --url 'https://example.com/webhooks/stream/before-message-send'
stream chat:webhook:custom-commands --url 'https://example.com/webhooks/stream/custom-commands?type={type}'
See the CLI introduction for more information on the Stream CLI.
Migration to Multi-Event Hooks
All Stream applications now use the new multi-event hooks system for push webhooks, SQS, and SNS integrations. If you receive an error message like:
Cannot set webhook URL, webhook events, SQS URL, SQS key, SQS secret, SNS topic ARN, SNS key, SNS secret, or async moderation config in new hook v2 system. Use the event_hooks field to configure webhooks.
This means you need to update your server-side SDK implementation:
- For push webhooks: Stop using
webhook_url
and switch to theevent_hooks
array - For SQS/SNS: Stop using legacy settings (
sqs_url
,sns_topic_arn
, etc.) and useevent_hooks
array - Legacy webhooks remain unchanged:
before_message_send_hook_url
andcustom_action_handler_url
still work as before
See the Multi-Event Hooks documentation for complete migration examples.
The new system offers several advantages:
- Configure multiple webhook endpoints for different event types
- Support for URL templating
- More flexible event routing options
- Support for various hook types (webhook, SQS, SNS, pending message)
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.
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-East | ZONE ID | eip |
---|---|---|
Primary | use1-az2 | 34.225.10.29/32 |
Secondary | use1-az4 | 34.198.125.61/32 |
Tertiary | use1-az3 | 52.22.78.160/32 |
Quaternary | use1-az6 | 3.215.161.238/32 |
EU-west | ZONE ID | eip |
---|---|---|
Primary | euw1-az3 | 52.212.14.212/32 |
Secondary | euw1-az1 | 52.17.43.232/32 |
Tertiary | euw1-az2 | 34.241.110.177/32 |
Sydney | ZONE ID | eip |
---|---|---|
Primary | apse2-az3 | 54.252.193.245/32 |
Secondary | apse2-az2 | 13.55.254.141/32 |
Tertiary | apse2-az1 | 3.24.48.104/32 |
mumbai | ZONE ID | eip |
---|---|---|
Primary | aps1-az1 | 65.1.48.87/32 |
Secondary | aps1-az3 | 15.206.221.25/32 |
Tertiary | aps1-az2 | 13.233.48.78/32 |
Singapore | ZONE ID | eip |
---|---|---|
Primary | apse1-az2 | 13.229.11.158/32 |
Secondary | apse1-az1 | 52.74.225.150/32 |
Tertiary | apse1-az3 | 52.76.180.70/32 |
OHIO | ZONE ID | EIP |
---|---|---|
Primary | use2-az1 | 3.14.163.216/32 |
Secondary | use2-az2 | 3.15.245.3/32 |
Tertiary | use2-az3 | 3.141.116.179/32 |
CANADA | ZONE ID | EIP |
---|---|---|
Primary | cac1-az1 | 35.183.141.98/32 |
Secondary | cac1-az2 | 52.60.71.231/32 |
Tertiary | cac1-az4 | 3.97.253.35/32 |
OREGON | ZONE ID | EIP |
---|---|---|
Primary | usw2-az1 | 52.25.165.25/32 |
Secondary | usw2-az2 | 44.237.58.11/32 |
Tertiary | usw2-az3 | 52.10.213.81/32 |