Test Amazon SNS HTTPS subscriptions locally
Receive Amazon SNS messages on localhost with a Horizon tunnel, confirm the subscription, and verify the message signature in Next.js.
Receive Amazon SNS messages on your laptop while you build, with a public HTTPS URL SNS can reach.
Horizon has no AWS integration. SNS sends messages to an HTTPS endpoint, and Horizon provides that URL.
SNS signs differently from most webhook providers. There is no shared secret. SNS signs each message with an RSA key (SHA256 for signature version 2, SHA1 for version 1) and sends the signature and a certificate URL in the JSON body. Use a validator that fetches and checks the certificate for you.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - An AWS account with an Amazon SNS topic
- A Next.js app that uses the App Router and runs on port 3000
Start your app
Install sns-validator, the validator from the AWS JavaScript team, and its types:
npm install sns-validatornpm install -D @types/sns-validatorSNS sends three message types, named in the x-amz-sns-message-type header and in the Type field of the body: SubscriptionConfirmation, Notification and UnsubscribeConfirmation. The body is JSON, but the Content-Type is text/plain, so read it with request.text() and parse it yourself.
The handler validates the signature, rejects any other topic, and confirms the subscription by sending a GET request to SubscribeURL. AWS says to reject any message with an unexpected TopicArn.
import { promisify } from "node:util";
import MessageValidator from "sns-validator";
const validator = new MessageValidator();
const validateMessage = promisify(validator.validate.bind(validator));
export async function POST(request: Request) {
const expectedTopicArn = process.env.SNS_TOPIC_ARN;
if (!expectedTopicArn) {
return new Response("Missing SNS_TOPIC_ARN", { status: 500 });
}
let message: Record<string, unknown> | undefined;
try {
message = await validateMessage(JSON.parse(await request.text()));
} catch {
return new Response("Invalid signature", { status: 403 });
}
if (message?.TopicArn !== expectedTopicArn) {
return new Response("Unexpected topic", { status: 403 });
}
if (message.Type === "SubscriptionConfirmation") {
await fetch(String(message.SubscribeURL));
console.log("Confirmed SNS subscription");
return new Response("ok", { status: 200 });
}
if (message.Type === "Notification") {
console.log(`Received SNS message ${message.MessageId}: ${message.Message}`);
}
return new Response("ok", { status: 200 });
}Add your topic's ARN (Amazon Resource Name) to .env.local:
SNS_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:my-topicStart the app:
npm run devStart a tunnel
Start the tunnel before you subscribe, because SNS sends the confirmation message when you create the subscription. Use -s with a subdomain you reserved:
hrzn tunnel http://localhost:3000 -s my-sns-appHORIZON: Tunnel connected
URL https://my-sns-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-sns-appYour public URL is https://my-sns-app.hrzn.run. Without -s the subdomain is random and changes on every run, so you would subscribe a new URL after each restart. Reserved subdomains are a paid feature, see Pricing.
Subscribe the endpoint
Your endpoint URL is the tunnel URL plus the route path: https://my-sns-app.hrzn.run/api/webhooks/sns.
- Sign in to the Amazon SNS console.
- In the navigation pane, choose Subscriptions.
- Choose Create subscription.
- For Topic ARN, choose your topic.
- For Protocol, select HTTPS.
- For Endpoint, paste the endpoint URL.
- Choose Create subscription.
The new subscription shows PendingConfirmation. SNS has sent a SubscriptionConfirmation message to your handler, which visits SubscribeURL. Refresh the Subscriptions page. The subscription now shows its subscription ARN.
SNS doesn't send notifications until the subscription is confirmed.
Publish a message
SNS has no resend button for notifications. Publish a new message to the topic:
- In the SNS console, choose Topics, then choose your topic.
- Choose Publish message.
- Enter a Subject and a Message, then choose Publish message.
Or use the AWS CLI:
aws sns publish --topic-arn arn:aws:sns:us-east-1:123456789012:my-topic --message "Hello from SNS"Check it works
During the subscribe step, the Horizon terminal prints one line for the confirmation:
POST 200 /api/webhooks/snsYour app terminal prints Confirmed SNS subscription. After you publish a message, Horizon prints another POST 200 line, and your app terminal prints:
Received SNS message 22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324: Hello from SNSTroubleshooting
The subscription stays in PendingConfirmation
SNS sent the confirmation before your app and tunnel were ready, or the handler rejected it.
- Check that
npm run devand the Horizon terminal are running. - Check that
SNS_TOPIC_ARNmatches your topic exactly. The handler returns403for any other topic. - Subscribe again. SNS retries a failed delivery, so you may see the confirmation arrive more than once.
The handler returns 403 for a real message
- Check the Horizon line. A
403means the signature check failed or theTopicArndiffers fromSNS_TOPIC_ARN. - Don't change the body before you validate it. The signature covers the original
MessageandSubjectvalues. - If a message contains multibyte characters, set
validator.encoding = "utf8", as the validator's README describes.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. The subscription still points at the old URL. Restart with -s my-sns-app, or subscribe the new URL.
Messages stop arriving
SNS treats 5xx and 429 responses as retryable. Any other error status counts as a permanent failure and SNS doesn't retry. Check that the Horizon terminal and npm run dev are running, then publish a new message.
Next steps
- Read AWS's guide to verifying the signatures of Amazon SNS messages.
- Read how to prepare your endpoint for SNS messages.
- Reserve a subdomain so your URL never changes: see pricing.
Test Alchemy webhooks locally
Receive Alchemy Notify webhook events on localhost with a Horizon tunnel, and verify the X-Alchemy-Signature header.
Test HostedHooks webhooks locally
Receive HostedHooks webhook events on localhost with a Horizon tunnel, and verify the HostedHooks signature in a Next.js route.