Stream can send payloads of all events from your application to an Amazon SNS topic you own.
A chat application with a lot of users generates a lots of events. With a standard Webhook configuration, events are posted to your server and can overwhelm unprepared servers during high-use periods. While the server is out, it will not be able to receive Webhooks and will fail to process them. One way to avoid this issue is to use Stream Chat's support for sending webhooks to Amazon SNS.
SNS removes the chance of losing data for Chat events by providing a large, scalable message exchange that delivers events generated by Stream Chat to as many consumers as you like.
The complete list of supported events is identical to those sent through webhooks and can be found on the Events page.
You can configure your SNS topic through the Stream Dashboard or programmatically using the REST API or an SDK with Server Side Authorization.
There are 2 ways to configure authentication on your SNS topic:
By providing a key and secret
Or by having Stream's AWS account assume a role on your SNS topic. With this option you omit the key and secret, but instead you set up a resource-based policy to grant Stream Publish permission on your SNS topic. The following policy needs to be attached to your topic (replace the value of Resource with the fully qualified ARN of your topic):
To configure an SNS topic, use the event_hooks array and Update App Settings method:
// 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 hooksconst response = await client.getAppSettings();console.log("Current event hooks:", response.event_hooks);// STEP 2: Add SNS hook while preserving existing hooksconst existingHooks = response.event_hooks || [];const newSNSHook = { enabled: true, hook_type: "sns", sns_topic_arn: "arn:aws:sns:us-east-1:123456789012:sns-topic", sns_region: "us-east-1", sns_auth_type: "keys", // or "resource" for role-based auth sns_key: "yourkey", sns_secret: "yoursecret", event_types: [], // empty array = all events};// STEP 3: Update with complete array including existing hooksawait client.updateAppSettings({ event_hooks: [...existingHooks, newSNSHook],});// Test the SNS connectionawait client.testSNSSettings({ sns_topic_arn: "arn:aws:sns:us-east-1:123456789012:sns-topic", sns_key: "yourkey", sns_secret: "yoursecret",});
from getstream.models import EventHook# 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 hooksresponse = client.get_app()existing_hooks = response.data.app.event_hooks or []print("Current event hooks:", existing_hooks)# STEP 2: Add SNS hook while preserving existing hooksnew_sns_hook = EventHook( enabled=True, hook_type="sns", sns_topic_arn="arn:aws:sns:us-east-1:123456789012:sns-topic", sns_region="us-east-1", sns_auth_type="keys", # or "resource" for role-based auth sns_key="yourkey", sns_secret="yoursecret", event_types=[], # empty array = all events)# STEP 3: Update with complete array including existing hooksclient.update_app( event_hooks=existing_hooks + [new_sns_hook])# Test the SNS connectionclient.check_sns(sns_key="yourkey", sns_secret="yoursecret", sns_topic_arn="arn:aws:sns:us-east-1:123456789012:sns-topic")
require 'getstream_ruby'Models = GetStream::Generated::Models# 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 hooksresponse = client.common.get_appexisting_hooks = response.app.event_hooks || []puts "Current event hooks:", existing_hooks# STEP 2: Add SNS hook while preserving existing hooksnew_sns_hook = { 'enabled' => true, 'hook_type' => 'sns', 'sns_topic_arn' => 'arn:aws:sns:us-east-1:123456789012:sns-topic', 'sns_region' => 'us-east-1', 'sns_auth_type' => 'keys', # or "resource" for role-based auth 'sns_key' => 'yourkey', 'sns_secret' => 'yoursecret', 'event_types' => [] # empty array = all events}# STEP 3: Update with complete array including existing hooksclient.common.update_app(Models::UpdateAppRequest.new( event_hooks: existing_hooks + [new_sns_hook]))# Test the SNS connectionclient.common.check_sns(Models::CheckSNSRequest.new( sns_key: 'yourkey', sns_secret: 'yoursecret', sns_topic_arn: 'arn:aws:sns:us-east-1:123456789012:sns-topic'))
// 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->getApp();$existingHooks = $response->getData()->app->eventHooks ?? [];// STEP 2: Add SNS hook while preserving existing hooks$newSNSHook = new Models\EventHook( enabled: true, hookType: "sns", snsTopicArn: "arn:aws:sns:us-east-1:123456789012:sns-topic", snsRegion: "us-east-1", snsAuthType: "keys", // or "resource" for role-based auth snsKey: "yourkey", snsSecret: "yoursecret", eventTypes: [], // empty array = all events);// STEP 3: Update with complete array including existing hooks$client->updateApp(new Models\UpdateAppRequest( eventHooks: array_merge($existingHooks, [$newSNSHook]),));// Test the SNS connection$client->checkSNS(new Models\CheckSNSRequest( snsTopicArn: "arn:aws:sns:us-east-1:123456789012:sns-topic", snsKey: "yourkey", snsSecret: "yoursecret",));
// 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 hookssettings, err := client.GetApp(ctx, &getstream.GetAppRequest{})if err != nil { log.Fatal(err)}existingHooks := settings.Data.App.EventHooksfmt.Printf("Current event hooks: %+v\n", existingHooks)// STEP 2: Add SNS hook while preserving existing hooksnewSNSHook := getstream.EventHook{ HookType: getstream.PtrTo("sns"), Enabled: getstream.PtrTo(true), EventTypes: []string{}, // empty slice = all events SnsTopicArn: getstream.PtrTo("arn:aws:sns:us-east-1:123456789012:sns-topic"), SnsRegion: getstream.PtrTo("us-east-1"), SnsAuthType: getstream.PtrTo("keys"), // or "resource" for role-based auth SnsKey: getstream.PtrTo("yourkey"), SnsSecret: getstream.PtrTo("yoursecret"),}// STEP 3: Update with complete array including existing hooksallHooks := append(existingHooks, newSNSHook)_, err = client.UpdateApp(ctx, &getstream.UpdateAppRequest{ EventHooks: allHooks,})if err != nil { log.Fatal(err)}// Test the SNS connectionclient.CheckSNS(ctx, &getstream.CheckSNSRequest{ SnsTopicArn: getstream.PtrTo("arn:aws:sns:us-east-1:123456789012:sns-topic"), SnsKey: getstream.PtrTo("yourkey"), SnsSecret: getstream.PtrTo("yoursecret"),})
// 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 hooksvar response = client.getApp(GetAppRequest.builder().build()).execute().getData();var existingHooks = response.getApp().getEventHooks();System.out.println("Current event hooks: " + existingHooks);// STEP 2: Add SNS hook while preserving existing hooksvar newSNSHook = EventHook.builder() .hookType("sns") .enabled(true) .eventTypes(Collections.emptyList()) // empty list = all events .snsTopicArn("arn:aws:sns:us-east-1:123456789012:sns-topic") .snsRegion("us-east-1") .snsAuthType("keys") // or "resource" for role-based auth .snsKey("yourkey") .snsSecret("yoursecret") .build();// STEP 3: Update with complete array including existing hooksvar allHooks = new ArrayList<>(existingHooks);allHooks.add(newSNSHook);client.updateApp(UpdateAppRequest.builder() .eventHooks(allHooks) .build()).execute();// Test the SNS connectionclient.checkSNS(CheckSNSRequest.builder() .snsKey("yourkey") .snsSecret("yoursecret") .snsTopicArn("arn:aws:sns:us-east-1:123456789012:sns-topic") .build()).execute();
// 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 hooksvar settings = await client.GetAppAsync();var existingHooks = settings.Data.App.EventHooks ?? new List<EventHook>();Console.WriteLine($"Current event hooks: {existingHooks}");// STEP 2: Add SNS hook while preserving existing hooksvar newSNSHook = new EventHook{ HookType = "sns", Enabled = true, EventTypes = new List<string>(), // empty list = all events SnsTopicArn = "arn:aws:sns:us-east-1:123456789012:sns-topic", SnsRegion = "us-east-1", SnsAuthType = "keys", // or "resource" for role-based auth SnsKey = "yourkey", SnsSecret = "yoursecret",};// STEP 3: Update with complete array including existing hooksvar allHooks = new List<EventHook>(existingHooks) { newSNSHook };await client.UpdateAppAsync(new UpdateAppRequest{ EventHooks = allHooks,});// Test the SNS connectionawait client.CheckSNSAsync(new CheckSNSRequest{ SnsKey = "yourkey", SnsSecret = "yoursecret", SnsTopicArn = "arn:aws:sns:us-east-1:123456789012:sns-topic",});
SNS honours the same enable_hook_payload_compression flag — see the webhooks overview for enablement and the production checklist. When compression is on, the notification Message is gzipped + base64-encoded (SNS only accepts UTF-8) and the producer sets these message attributes:
Call parseSns on your chat client with the notification body (either the raw envelope JSON string or the pre-extracted Message field) — it unwraps the SNS envelope when needed, reverses the base64 + gzip payload, and returns a typed event. The return shape matches verifyAndParseWebhook sans verification (see the overview for the per-language note on typed vs parsed-JSON output). The same call works whether or not compression is on, and whether the topic is delivered over HTTP or via an SQS subscription.
No Stream X-Signature on SNS. Stream does not ship an app-level HMAC on SNS payloads. SNS itself is authenticated with AWS signatures (SigningCertURL / Signature) when you subscribe over HTTPS.
HTTP subscribers receive one JSON envelope per POST; forwarded-through-SQS workers usually parse JSON and pass Message or the full inner document. Either form works — parse_sns detects and unwraps the envelope when needed.
var event = client.parseSns(notification.getMessage());
Where each argument comes from
Argument
Source
Example
notificationBody
The raw SNS HTTP envelope JSON, or the pre-extracted Message field (UTF-8 string)
requestBody / notification.Message
parse_sns takes only the notification body string. No HMAC is involved; use your API secret only for other chat API calls.
Need a stateless helper? The same unwrap + decode + parse logic is exposed as a static / module-level parse_sns (see the webhooks overview for per-language imports). Use it in workers that don't keep a chat client around.
Building this without a Stream SDK? Expand the per-language reference implementation below.
SNS uses the same wire format as SQS (base64 + gzip inside Message), so the payload decoder matches decode_sqs_payload; the composite name stays decode_sns_payload in docs for clarity. Reuse parse_event from the webhooks overview; the SNS-specific composite is unwrap (when needed) + decode + parse — no verify_signature step:
Helper
Purpose
decode_sns_payload(message) -> bytes
Same as decode_sqs_payload: base64-decode then gzip-decompress when the result starts with the RFC 1952 gzip magic (1f 8b)
parse_sns(notification_body)
Unwrap SNS JSON envelope when needed, then parse_event(decode_sns_payload(inner_message))
function unwrapSnsNotificationBody(notificationBody) { const s = notificationBody.trim(); if (!s.startsWith("{")) return notificationBody; try { const env = JSON.parse(notificationBody); if (env && typeof env.Message === "string") return env.Message; } catch { /* not JSON envelope */ } return notificationBody;}function parseSns(notificationBody) { const message = unwrapSnsNotificationBody(notificationBody); return parseEvent(decodeSqsPayload(message));}const event = parseSns(notificationEnvelopeJsonOrMessage);
public static String unwrapSnsNotificationBody(String notificationBody) { String s = notificationBody.strip(); if (!s.startsWith("{")) return notificationBody; try { var env = new ObjectMapper().readTree(notificationBody); if (env.hasNonNull("Message") && env.get("Message").isTextual()) { return env.get("Message").asText(); } } catch (Exception ignored) { } return notificationBody;}public static JsonNode parseSns(String notificationBody) throws Exception { String inner = unwrapSnsNotificationBody(notificationBody); byte[] payload = decodeSqsPayload(inner); return parseEvent(payload);}// var event = parseSns(notification.getMessage());
public static string UnwrapSnsNotificationBody(string notificationBody){ var s = notificationBody.TrimStart(); if (s.Length == 0 || s[0] != '{') return notificationBody; try { using var doc = JsonDocument.Parse(notificationBody); if (doc.RootElement.TryGetProperty("Message", out var m) && m.ValueKind == JsonValueKind.String) return m.GetString() ?? notificationBody; } catch (JsonException) { } return notificationBody;}public static JsonElement ParseSns(string notificationBody){ var inner = UnwrapSnsNotificationBody(notificationBody); var payload = DecodeSqsPayload(inner); return ParseEvent(payload);}// var ev = ParseSns(notification.Message);
Enabling compression and the production checklist are documented on the webhooks overview — the same enable_hook_payload_compression flag covers SNS.