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.
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.comrestriction onSigningCertURLand 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: SubscriptionConfirmationexplicitly and fetchSubscribeURL. - 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
RedrivePolicyso 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.