# SNS

Stream can send payloads of all events from your application to an [Amazon SNS](https://aws.amazon.com/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](/chat/docs/php/webhook-events/) page.

## Configuration

You can configure your SNS topic through the [Stream Dashboard](https://getstream.io/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:

1. By providing a key and secret

2. 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):

<Tabs>

```json label="JSON"
{
  "Sid": "AllowStreamProdAccount",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::185583345998:root"
  },
  "Action": "SNS:Publish",
  "Resource": "arn:aws:sns:us-west-2:1111111111:customer-sns-topic"
}
```

</Tabs>

To configure an SNS topic, use the `event_hooks` array and Update App Settings method:

<Tabs>

```js label="JavaScript"
// 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 SNS hook while preserving existing hooks
const 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 hooks
await client.updateAppSettings({
  event_hooks: [...existingHooks, newSNSHook],
});

// Test the SNS connection
await client.testSNSSettings({
  sns_topic_arn: "arn:aws:sns:us-east-1:123456789012:sns-topic",
  sns_key: "yourkey",
  sns_secret: "yoursecret",
});
```

```python label="Python"
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 hooks
response = 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 hooks
new_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 hooks
client.update_app(
    event_hooks=existing_hooks + [new_sns_hook]
)

# Test the SNS connection
client.check_sns(sns_key="yourkey", sns_secret="yoursecret", sns_topic_arn="arn:aws:sns:us-east-1:123456789012:sns-topic")
```

```ruby label="Ruby"
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 hooks
response = client.common.get_app
existing_hooks = response.app.event_hooks || []
puts "Current event hooks:", existing_hooks

# STEP 2: Add SNS hook while preserving existing hooks
new_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 hooks
client.common.update_app(Models::UpdateAppRequest.new(
  event_hooks: existing_hooks + [new_sns_hook]
))

# Test the SNS connection
client.common.check_sns(Models::CheckSNSRequest.new(
  sns_key: 'yourkey',
  sns_secret: 'yoursecret',
  sns_topic_arn: 'arn:aws:sns:us-east-1:123456789012:sns-topic'
))
```

```php label="PHP"
// 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",
));
```

```go label="Go"
// 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.GetApp(ctx, &getstream.GetAppRequest{})
if err != nil {
    log.Fatal(err)
}
existingHooks := settings.Data.App.EventHooks
fmt.Printf("Current event hooks: %+v\n", existingHooks)

// STEP 2: Add SNS hook while preserving existing hooks
newSNSHook := 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 hooks
allHooks := append(existingHooks, newSNSHook)
_, err = client.UpdateApp(ctx, &getstream.UpdateAppRequest{
    EventHooks: allHooks,
})
if err != nil {
    log.Fatal(err)
}

// Test the SNS connection
client.CheckSNS(ctx, &getstream.CheckSNSRequest{
    SnsTopicArn: getstream.PtrTo("arn:aws:sns:us-east-1:123456789012:sns-topic"),
    SnsKey:      getstream.PtrTo("yourkey"),
    SnsSecret:   getstream.PtrTo("yoursecret"),
})
```

```java label="Java"
// 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 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 hooks
var 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 hooks
var allHooks = new ArrayList<>(existingHooks);
allHooks.add(newSNSHook);
client.updateApp(UpdateAppRequest.builder()
    .eventHooks(allHooks)
    .build()).execute();

// Test the SNS connection
client.checkSNS(CheckSNSRequest.builder()
    .snsKey("yourkey")
    .snsSecret("yoursecret")
    .snsTopicArn("arn:aws:sns:us-east-1:123456789012:sns-topic")
    .build()).execute();
```

```csharp label="C#"
// 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.GetAppAsync();
var existingHooks = settings.Data.App.EventHooks ?? new List<EventHook>();
Console.WriteLine($"Current event hooks: {existingHooks}");

// STEP 2: Add SNS hook while preserving existing hooks
var 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 hooks
var allHooks = new List<EventHook>(existingHooks) { newSNSHook };
await client.UpdateAppAsync(new UpdateAppRequest
{
    EventHooks = allHooks,
});

// Test the SNS connection
await client.CheckSNSAsync(new CheckSNSRequest
{
    SnsKey = "yourkey",
    SnsSecret = "yoursecret",
    SnsTopicArn = "arn:aws:sns:us-east-1:123456789012:sns-topic",
});
```

</Tabs>

## Configuration Options

The following options are available when configuring an SNS event hook:

| Option        | Type    | Description                                                                             | Required                                                                           |
| ------------- | ------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| id            | string  | Unique identifier for the event hook                                                    | No. If empty, it will generate an ID.                                              |
| enabled       | boolean | Boolean flag to enable/disable the hook                                                 | Yes                                                                                |
| hook_type     | string  | Must be set to `"sns"`                                                                  | Yes                                                                                |
| sns_topic_arn | string  | The AWS SNS topic ARN                                                                   | Yes                                                                                |
| sns_region    | string  | The AWS region where the SNS topic is located (e.g., "us-east-1")                       | Yes                                                                                |
| sns_auth_type | string  | Authentication type: `"keys"` for access key/secret or `"resource"` for role-based auth | Yes                                                                                |
| sns_key       | string  | AWS access key ID (required if auth_type is "keys")                                     | Yes if using key auth                                                              |
| sns_secret    | string  | AWS secret access key (required if auth_type is "keys")                                 | Yes if using key auth                                                              |
| event_types   | array   | Array of event types this hook should handle                                            | No. Not provided or empty array means subscribe to all existing and future events. |

## Payload Compression

SNS honours the same `enable_hook_payload_compression` flag — see the [webhooks overview](/chat/docs/php/webhooks-overview/#payload-compression) 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:

```json
{
  "content_encoding": "gzip",
  "content_type": "application/json",
  "payload_encoding": "base64"
}
```

### Reading notifications from SNS

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](/chat/docs/php/webhooks-overview/#handling-the-webhook) 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.

<admonition type="note">

**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.

</admonition>

<admonition type="info">

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.

</admonition>

<Tabs>

```js label="JavaScript"
const event = client.parseSns(notification.Message);
// event.type, event.message, event.user, ...
```

```python label="Python"
event = client.parse_sns(notification["Message"])
# event.type, event.message, event.user, ...
```

```ruby label="Ruby"
event = client.parse_sns(notification['Message'])
```

```php label="PHP"
$event = $client->parseSns($notification['Message']);
```

```go label="Go"
event, err := client.ParseSns(notification.Message)
```

```csharp label="C#"
var ev = client.ParseSns(notification.Message);
```

```java label="Java"
var event = client.parseSns(notification.getMessage());
```

</Tabs>

#### 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.

<admonition type="note">

**Need a stateless helper?** The same unwrap + decode + parse logic is exposed as a static / module-level `parse_sns` (see the [webhooks overview](/chat/docs/php/webhooks-overview/#handling-the-webhook) for per-language imports). Use it in workers that don't keep a chat client around.

</admonition>

Building this without a Stream SDK? Expand the per-language reference implementation below.

<disclosure label="Reference implementation (no SDK)">

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](/chat/docs/php/webhooks-overview/#handling-the-webhook); 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](https://datatracker.ietf.org/doc/html/rfc1952) gzip magic (`1f 8b`) |
| `parse_sns(notification_body)`         | Unwrap SNS JSON envelope when needed, then `parse_event(decode_sns_payload(inner_message))`                                                                                     |

<Tabs>

```js label="JavaScript"
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);
```

```python label="Python"
import json


def unwrap_sns_notification_body(notification_body: str) -> str:
    s = notification_body.strip()
    if not s.startswith("{"):
        return notification_body
    try:
        env = json.loads(notification_body)
    except json.JSONDecodeError:
        return notification_body
    msg = env.get("Message")
    return msg if isinstance(msg, str) else notification_body


def parse_sns(notification_body: str):
    inner = unwrap_sns_notification_body(notification_body)
    return parse_event(decode_sqs_payload(inner))


event = parse_sns(notification["Message"])
```

```ruby label="Ruby"
require 'json'

def unwrap_sns_notification_body(notification_body)
  s = notification_body.strip
  return notification_body unless s.start_with?('{')

  env = JSON.parse(notification_body)
  msg = env['Message']
  msg.is_a?(String) ? msg : notification_body
rescue JSON::ParserError
  notification_body
end

def parse_sns(notification_body)
  inner = unwrap_sns_notification_body(notification_body)
  parse_event(decode_sqs_payload(inner))
end

event = parse_sns(notification['Message'])
```

```php label="PHP"
function unwrapSnsNotificationBody(string $notificationBody): string {
    $s = ltrim($notificationBody);
    if ($s === '' || $s[0] !== '{') {
        return $notificationBody;
    }
    try {
        $env = json_decode($notificationBody, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        return $notificationBody;
    }
    return is_string($env['Message'] ?? null) ? $env['Message'] : $notificationBody;
}

function parseSns(string $notificationBody): array {
    return parseEvent(decodeSqsPayload(unwrapSnsNotificationBody($notificationBody)));
}

$event = parseSns($notification['Message']);
```

```go label="Go"
package webhook

import (
	"bytes"
	"compress/gzip"
	"encoding/base64"
	"encoding/json"
	"io"
	"strings"
)

var gzipMagic = []byte{0x1f, 0x8b}

func UnwrapSnsNotificationBody(notificationBody string) string {
	s := strings.TrimSpace(notificationBody)
	if len(s) == 0 || s[0] != '{' {
		return notificationBody
	}
	var env struct {
		Message string `json:"Message"`
	}
	if json.Unmarshal([]byte(notificationBody), &env) != nil {
		return notificationBody
	}
	if env.Message != "" {
		return env.Message
	}
	return notificationBody
}

func DecodeSqsPayload(body string) ([]byte, error) {
	buf, err := base64.StdEncoding.DecodeString(body)
	if err != nil {
		buf = []byte(body)
	}
	if len(buf) >= 2 && bytes.Equal(buf[:2], gzipMagic) {
		gz, err := gzip.NewReader(bytes.NewReader(buf))
		if err != nil {
			return nil, err
		}
		defer gz.Close()
		return io.ReadAll(gz)
	}
	return buf, nil
}

func ParseSns(notificationBody string) (map[string]any, error) {
	payload, err := DecodeSqsPayload(UnwrapSnsNotificationBody(notificationBody))
	if err != nil {
		return nil, err
	}
	return ParseEvent(payload)
}

// event, err := ParseSns(notification.Message)
```

```java label="Java"
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());
```

```csharp label="C#"
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);
```

</Tabs>

</disclosure>

Enabling compression and the production checklist are documented on the [webhooks overview](/chat/docs/php/webhooks-overview/#enabling-compression) — the same `enable_hook_payload_compression` flag covers SNS.

## SNS Best practices and Assumptions

- Set the maximum message size set to 256 KB.

Messages bigger than the maximum message size will be dropped.


---

This page was last updated at 2026-06-12T11:05:34.468Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/php/sns/](https://getstream.io/chat/docs/php/sns/).