Gareth Wilson Gareth Wilson

Guide to AWS SNS Webhooks: Features and Best Practices

Published


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

FeatureDetails
DeliveryHTTP POST, JSON envelope; x-amz-sns-message-type header
Message typesSubscriptionConfirmation, Notification, UnsubscribeConfirmation
SubscriptionConfirm by fetching SubscribeURL (or ConfirmSubscription with Token)
SignatureRSA over a canonical string; cert fetched from SigningCertURL
SignatureVersion1 = SHA1 (default), 2 = SHA256 (opt-in)
Cert restrictionSigningCertURL must be an sns.*.amazonaws.com URL
Raw deliveryRawMessageDelivery=true strips the envelope; no Signature to verify
AcknowledgementRespond 2xx within ~15 seconds
RetriesConfigurable HTTP/S delivery policy (default ~50 retries across 4 phases); DLQ via RedrivePolicy
Content-Typetext/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.

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.

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.

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.

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.

Make AWS SNS webhooks production-ready

Hookdeck verifies SNS signatures, answers the handshake, deduplicates, and durably queues every message

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 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 for free and handle AWS SNS webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.