# Guide to AWS SNS Webhooks: Features and Best Practices

Amazon SNS can deliver messages to an HTTP/S endpoint, which makes it a webhook source for anything subscribed to an SNS topic: S3 events, CloudWatch alarms, application notifications, fan-out from other AWS services. If you're receiving SNS over HTTP rather than SQS or Lambda, you're handling webhooks, with SNS's particular handshake and signature model.

This guide covers how SNS HTTP/S delivery works, the subscription confirmation handshake, how to verify the RSA signature (and when there isn't one), the retry behavior, and the best practices for running an SNS endpoint in production.

## What are AWS SNS webhooks?

An SNS HTTP/S subscription delivers each published message as an HTTP POST with a JSON envelope. The `x-amz-sns-message-type` header tells you the message type: `SubscriptionConfirmation` (sent once when you subscribe), `Notification` (the actual messages), or `UnsubscribeConfirmation`. The envelope carries fields like `Type`, `MessageId`, `TopicArn`, `Message`, `Timestamp`, `Signature`, `SignatureVersion`, and `SigningCertURL`.

Two things make SNS distinctive: you must confirm the subscription with a handshake before any notifications flow, and messages are signed with an RSA signature you verify against an AWS-hosted certificate, not an HMAC.

## AWS SNS webhook features

| Feature | Details |
| --- | --- |
| Delivery | HTTP POST, JSON envelope; `x-amz-sns-message-type` header |
| Message types | `SubscriptionConfirmation`, `Notification`, `UnsubscribeConfirmation` |
| Subscription | Confirm by fetching `SubscribeURL` (or `ConfirmSubscription` with `Token`) |
| Signature | RSA over a canonical string; cert fetched from `SigningCertURL` |
| SignatureVersion | 1 = SHA1 (default), 2 = SHA256 (opt-in) |
| Cert restriction | `SigningCertURL` must be an `sns.*.amazonaws.com` URL |
| Raw delivery | `RawMessageDelivery=true` strips the envelope; no `Signature` to verify |
| Acknowledgement | Respond 2xx within ~15 seconds |
| Retries | Configurable HTTP/S delivery policy (default ~50 retries across 4 phases); DLQ via `RedrivePolicy` |
| Content-Type | `text/plain` by default |

## The subscription confirmation handshake

When you subscribe an HTTP/S endpoint to a topic, SNS first POSTs a `SubscriptionConfirmation` message. It contains a `SubscribeURL` (and a `Token`). To activate the subscription you must confirm it, either by making a GET request to the `SubscribeURL`, or by calling `ConfirmSubscription` with the `Token`. Until you do, no notifications are delivered.

```javascript
app.post("/sns", express.text({ type: "*/*" }), async (req, res) => {
  const msg = JSON.parse(req.body);
  const type = req.headers["x-amz-sns-message-type"];

  if (type === "SubscriptionConfirmation") {
    await fetch(msg.SubscribeURL); // confirm the subscription
    return res.sendStatus(200);
  }

  // ... verify signature, then handle Notification (below)
});

```

## Securing AWS SNS webhooks

### Verify the RSA signature

Every `Notification` and confirmation message (unless raw delivery is on) is RSA-signed. Verification means: build a canonical string from specific fields in byte-sorted order as `Key\nValue\n` (no trailing newline), fetch the X.509 certificate from `SigningCertURL`, extract its public key, and RSA-verify the base64-decoded `Signature`. The fields differ by message type:

* Notification: `Message`, `MessageId`, `Subject` (only if present), `Timestamp`, `TopicArn`, `Type`
* SubscriptionConfirmation / UnsubscribeConfirmation: `Message`, `MessageId`, `SubscribeURL`, `Timestamp`, `Token`, `TopicArn`, `Type`

Use `SignatureVersion` to pick the hash: `1` means SHA1, `2` means SHA256. Two security musts: reject any `SigningCertURL` that isn't an `sns.*.amazonaws.com` URL (otherwise an attacker points you at their own cert), and cache the certificate rather than fetching it per message.

This is fiddly, error-prone crypto, so prefer a maintained validator over hand-rolling it. AWS publishes `sns-validator` (npm) and a PHP validator; the AWS SDKs also provide message-validation helpers. One caveat: confirm your validator supports SignatureVersion 2 (SHA256) before relying on it for SigV2 topics, since older versions predate it.

```javascript
const MessageValidator = require("sns-validator");
const validator = new MessageValidator(); // verify SigV2/SHA256 support for your topics

app.post("/sns", express.text({ type: "*/*" }), (req, res) => {
  const msg = JSON.parse(req.body);

  validator.validate(msg, async (err, message) => {
    if (err) return res.sendStatus(403); // signature or cert check failed

    if (message.Type === "SubscriptionConfirmation") {
      await fetch(message.SubscribeURL);
      return res.sendStatus(200);
    }

    res.sendStatus(200); // acknowledge within ~15 seconds
    processQueue.add(message); // process the Notification asynchronously
  });
});

```

### Raw message delivery has no signature

If the subscription has `RawMessageDelivery=true` (indicated by `x-amz-sns-rawdelivery: true`), SNS strips the envelope entirely and delivers just the message body. There's no `Signature` field to verify. If you rely on raw delivery, authenticate the endpoint another way, such as restricting it to SNS's IP ranges or requiring a secret path.

## AWS SNS webhook limitations and pain points

### Signature verification is easy to get wrong

The Problem: The canonical string has strict rules (exact fields, byte-sorted, `Key\nValue\n`, no trailing newline, `Subject` only if present), the hash algorithm depends on `SignatureVersion`, and the cert must be validated as AWS-hosted. Any deviation fails verification or, worse, accepts a forged message.

Why It Happens: SNS uses asymmetric RSA signing with a fetched certificate rather than a single HMAC header, which is more moving parts.

Workarounds:

* Use a maintained validator and confirm SigV2/SHA256 support.
* Enforce the `sns.*.amazonaws.com` restriction on `SigningCertURL` and cache certs.

How Hookdeck Can Help: Hookdeck verifies SNS signatures at the edge, including the cert fetch and the version-specific hashing, so your application receives pre-verified messages without hand-rolled RSA code.

### The confirmation handshake is a prerequisite

The Problem: No notifications arrive until you confirm the subscription by fetching `SubscribeURL`. An endpoint that ignores the `SubscriptionConfirmation` message looks broken because it silently receives nothing.

Why It Happens: SNS requires proof you control the endpoint before it starts delivering.

Workarounds:

* Handle `x-amz-sns-message-type: SubscriptionConfirmation` explicitly and fetch `SubscribeURL`.
* Log confirmations so a failed handshake is visible.

How Hookdeck Can Help: Hookdeck can answer the SNS handshake for you and then forward confirmed notifications to your service, so subscription setup isn't a manual step you can forget.

### Raw delivery removes the signature

The Problem: With raw message delivery, there's no signature to verify, so any POST to your URL looks legitimate.

Why It Happens: Raw delivery is designed to pass the message body straight through, which excludes the signing envelope.

Workarounds:

* Prefer standard (enveloped) delivery when you need signature verification.
* If you need raw delivery, restrict the endpoint by IP or a secret path.

How Hookdeck Can Help: Hookdeck provides a dedicated, authenticated ingestion URL and can enforce its own verification in front of your endpoint, closing the gap raw delivery leaves.

### Retries and duplicates

The Problem: SNS retries failed HTTP/S deliveries per the topic's delivery policy (by default around 50 retries across four backoff phases), which means the same message can arrive more than once.

Why It Happens: At-least-once delivery favors not losing messages over exactly-once semantics.

Workarounds:

* Dedupe on `MessageId`.
* Configure a dead-letter queue via `RedrivePolicy` so undeliverable messages aren't lost.
* Make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries and durably queues them, so retries don't double-process and a downstream outage doesn't rely solely on SNS's retry window. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Confirm the subscription, then verify every message

Handle `SubscriptionConfirmation` by fetching `SubscribeURL`, and verify the RSA signature on every `Notification` with a validator that supports your `SignatureVersion`.

### Enforce the cert domain and cache certs

Reject any `SigningCertURL` that isn't `sns.*.amazonaws.com`, and cache the fetched certificate so you're not downloading it per message.

### Acknowledge fast, process asynchronously

Return 2xx within the delivery window and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Dedupe on `MessageId` and set a DLQ

Use `MessageId` as an idempotency key, and configure `RedrivePolicy` so undeliverable messages land in a dead-letter queue rather than being dropped.

### Know your delivery mode

If raw delivery is enabled there's no signature; authenticate the endpoint another way and document which mode each subscription uses.

## Conclusion

Consuming AWS SNS over HTTP/S means handling two things most webhook sources don't: a subscription confirmation handshake before any messages flow, and RSA signature verification against an AWS-hosted certificate, with a canonical string and a hash algorithm that depend on the message type and `SignatureVersion`. Raw delivery removes the signature entirely, and at-least-once delivery brings duplicates.

That's real operational surface: handshake handling, careful signature verification, cert-domain enforcement, deduplication, and a dead-letter strategy. [Hookdeck](https://hookdeck.com) verifies SNS signatures, answers the handshake, deduplicates, and durably queues every message at the edge, so your application only ever processes verified, unique notifications.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle AWS SNS webhooks reliably in minutes.