Automate Follow-Up Notifications

Build an SNS notification pipeline that emails every lead and texts high-value ones.

Introduction

30 Second Summary

Think about how the fastest businesses respond to new inquiries. The moment someone fills out a form, the sales team gets an instant alert on their phone. That's not magic. It's a notification pipeline running behind the scenes.

In this project, you will build an automated notification system that sends real-time email and SMS alerts whenever a new lead submits through your API. Every lead triggers an email. High-value leads also trigger an immediate text message so your team can follow up fast.

What You'll Build

An event-driven notification pipeline that integrates Amazon SNS into your existing LeadIntakeAPI Lambda function.

By the end of this project, you'll have:

  • An SNS topic with an email subscription that delivers real-time lead notifications to your inbox.
  • A verified phone number for SMS alerts in the SNS sandbox, proving direct text message delivery from your code.
  • Updated Lambda code with conditional routing logic that evaluates each lead and sends instant text alerts only for high-value service inquiries.
  • Secret Mission: Build a priority lead scoring system with a dedicated topic that routes hot leads to SMS automatically based on a 3-tier scoring model.

Are there any prerequisites?

This project builds on your existing LeadIntakeAPI from Projects 1-3. You should already have a working LeadIntakeFunction Lambda, a DynamoDB Leads table, an API Gateway with API key authentication, and a Postman collection for testing.

Create an SNS Topic and Subscribe via Email

Right now, your LeadIntakeAPI saves leads to DynamoDB and forwards them to a webhook. But nobody on your team gets notified when a new lead arrives.

Amazon SNS (Simple Notification Service) solves this. You publish a single message to an SNS topic, and every subscriber receives it automatically. In this step, you'll create your first topic and wire up email delivery.

In this step, get ready to:
  • Create an SNS Standard topic called NewLeadNotifications.
  • Subscribe your email address and confirm the subscription.
  • Publish a test message and verify it arrives in your inbox.
Create the NewLeadNotifications topic

An SNS topic is a communication channel. When you publish a message to a topic, every subscriber receives a copy of that message.

  • In the AWS Management Console, type SNS in the top search bar and select Simple Notification Service.
  • In the left navigation panel, select Topics.
  • Select Create topic.
  • For Type, choose Standard.
  • For Name, enter NewLeadNotifications.
  • Leave all other settings as default.
  • Select Create topic.

Why Standard and not FIFO?

Standard topics support at-least-once delivery and are the right choice for notification use cases. FIFO topics add strict ordering and deduplication, which adds complexity you don't need for simple alerts.

Your topic is now created. You'll see the topic details page with the Topic ARN displayed near the top.

  • Copy the ARN value (it looks like arn:aws:sns:us-east-1:123456789012:NewLeadNotifications).
  • Save this ARN somewhere accessible, like a sticky note or text file. You will need it in a later step when configuring your Lambda function.

What is a Topic ARN?

An ARN (Amazon Resource Name) is a unique identifier for any resource in AWS. Your topic's ARN is how your Lambda code will know exactly which topic to publish notifications to.

Add an email subscription and confirm it

A topic without subscribers is like a megaphone in an empty room. Let's add your email as the first subscriber so messages actually reach someone.

  • On the topic details page, select Create subscription.
  • For Protocol, choose Email.
  • For Endpoint, enter your personal email address.
  • Select Create subscription.

AWS sends a confirmation email to verify you own the address. You must confirm before the subscription can deliver messages.

  • Check your email inbox for a confirmation message from Amazon SNS.
  • Open the email and click the Confirm subscription link.

Don't see the confirmation email?

  • Check your spam or junk folder. The email comes from an AWS address like no-reply@sns.amazonaws.com.
  • If it still hasn't arrived after a couple of minutes, return to the subscription page in the SNS console and select Request confirmation to resend it.
  • Help me troubleshoot my missing SNS confirmation email.
  • Return to the SNS console and navigate back to your NewLeadNotifications topic.
  • Under Subscriptions, confirm the Status column shows Confirmed.
Test with a manual publish

Before wiring this into your Lambda function, let's confirm the topic can actually deliver messages end to end.

  • On the topic details page, select Publish message.
  • In the Subject field, enter Test Notification.
  • In the Message body field, enter This is a test from SNS.
  • Select Publish message.
  • Check your email inbox. You should receive a message with the subject "Test Notification" and the body "This is a test from SNS" within a few seconds.

Didn't receive the test email?

  • Make sure the subscription status shows Confirmed before publishing. Messages are not delivered to unconfirmed subscriptions.
  • Check your spam or junk folder again.
  • Help me figure out why my SNS test message didn't arrive.

Your notification channel is live and delivering emails. Next up, you'll add SMS capability so high-value leads can trigger an instant text message alert.

Add SMS and Verify Your Phone Number

Your email notifications are live. Every message published to your topic lands in your inbox within seconds.

Email is great for awareness, but high-value leads need faster follow-up. In this step, you'll add SMS delivery so your team gets a text message alongside the email. First, you need to verify a phone number in the SNS SMS sandbox before AWS will let you send text messages.

In this step, get ready to:
  • Verify your phone number in the SNS SMS sandbox.
  • Add an SMS subscription to your NewLeadNotifications topic.
  • Test that a single publish delivers to both email and SMS.

Cost warning

SMS messages are not included in the SNS free tier. Each SMS to a US number costs approximately $0.00645. Testing a few messages in this step costs under $0.05 total.

The SMS sandbox restricts sending to verified phone numbers only (up to 10 numbers). You will not accidentally send messages to unverified numbers.

Navigate to the SMS sandbox and add your phone number

New AWS accounts start in the SMS sandbox. This means you can only send text messages to phone numbers you explicitly verify first.

  • In the SNS console left navigation, select Text messaging (SMS).
  • In the Sandbox destination phone numbers section, select Add phone number.
  • Enter your country code and phone number.
  • Select a verification message language. The default is English (United States).
  • Select Add phone number.

Amazon SNS sends a one-time password (OTP) to your phone. Check your text messages for the verification code.

What is the SMS sandbox?

The SMS sandbox is a safety net that prevents accidental mass texting from new accounts. You can only send messages to phone numbers you have explicitly verified.

You can verify up to 10 destination phone numbers while in the sandbox. This is plenty for testing your notification system.

Verify the phone number with the OTP
  • Enter the verification code (5-8 digits) you received via text into the Verification code box.
  • Select Verify phone number.
  • Confirm the status shows as Verified in the Sandbox destination phone numbers list.

Didn't receive the verification code?

Make sure you entered the correct country code (e.g., +1 for the United States). Check that your phone has cellular signal and is not blocking messages from unknown senders.

If the code still does not arrive, select Add phone number again to request a new OTP.

Help me troubleshoot my SNS SMS verification code not arriving.

Add an SMS subscription to the topic and test both channels

Now that your phone number is verified, you can subscribe it to your topic. This means every message published to the topic goes to both your email and your phone.

  • Navigate back to your NewLeadNotifications topic. Select Topics in the left navigation, then click your topic name.
  • Select Create subscription.
  • For Protocol, choose SMS.
  • For Endpoint, enter your verified phone number in E.164 format (e.g., +12065550100).
  • Select Create subscription.

What is E.164 format?

E.164 is the international phone number format that starts with a plus sign, then the country code, then the number with no spaces or dashes. For US numbers, that looks like +1 followed by the 10-digit number.

Now test that both channels deliver from a single publish.

  • On the topic details page, select Publish message.
  • Enter a Subject: Dual Channel Test.
  • Enter a Message body: Testing email and SMS delivery together.
  • Select Publish message.
  • Confirm the message arrives in both your email inbox AND as a text message on your phone.

Why does the Subject only appear in email?

The SNS Subject parameter is used as the email subject line. SMS messages only deliver the message body because text messages do not have a subject field.

Got the email but not the SMS?

Check that your SMS subscription's endpoint uses E.164 format (starts with + and your country code). Also confirm the phone number in the subscription matches the one you verified in the sandbox.

Navigate to Text messaging (SMS) in the left navigation and confirm your number shows Verified in the sandbox list.

Help me figure out why my SNS SMS subscription is not delivering messages.

Your notification topic now reaches your team on two channels with a single publish. Next, you'll wire this directly into your Lambda function so notifications fire automatically on every lead submission.

Modify Lambda to Publish Notifications

Your SNS topic is live and delivering messages to both email and SMS. Now it is time to connect it to your Lambda function so notifications fire automatically on every lead submission.

Right now, your LeadIntakeFunction saves leads to DynamoDB and forwards them to a webhook. In this step, you will wire up Amazon SNS so the function also publishes a notification every time a new lead arrives.

In this step, get ready to:
  • Add the SNS topic ARN as an environment variable and attach the SNS permissions policy.
  • Write a send_notification function and integrate it into your handler.
  • Deploy and test the full notification flow with Postman.
Add the SNS_TOPIC_ARN environment variable and attach the SNS policy

Your Lambda function needs two things to publish to SNS: the topic's ARN (so it knows where to send messages) and permission to call the SNS API.

  • Switch back to the Lambda console and select your LeadIntakeFunction.
  • Select the Configuration tab, then choose Environment variables in the left sidebar.
  • Click Edit.
  • Click Add environment variable.
  • For Key, enter SNS_TOPIC_ARN.
  • For Value, paste the Topic ARN you copied from your NewLeadNotifications topic.
  • Click Save.

Where do I find my Topic ARN?

Open the SNS console, select Topics, then click NewLeadNotifications. The ARN is displayed at the top of the topic details page. It looks like arn:aws:sns:us-east-1:123456789012:NewLeadNotifications.

Now attach the SNS permissions policy to your function's execution role.

  • Still in the Configuration tab, select Permissions in the left sidebar.
  • Under Role name, click the execution role name to open it in the IAM console.
  • In the Permissions policies section, click Add permissions.
  • In the dropdown, choose Attach policies.
  • In the search box, enter AmazonSNSFullAccess.
  • Select the checkbox next to AmazonSNSFullAccess.
  • Click Add permissions.

What does AmazonSNSFullAccess do?

This AWS managed policy grants your Lambda function permission to call all SNS API actions. It includes the sns:Publish action that your code needs to send messages to your topic.

In a production environment, you would create a scoped policy allowing only sns:Publish on your specific topic ARN. For learning purposes, the managed policy keeps things simple.

You should now see AmazonSNSFullAccess listed alongside your existing policies on the role's Permissions tab.

Don't see the policy listed?

Make sure you searched for the exact name AmazonSNSFullAccess (no spaces). If the search returns no results, confirm you are in the IAM console (not the Lambda console) and that you clicked Add permissions then Attach policies.

Help me attach the AmazonSNSFullAccess policy to my Lambda execution role

Write the send_notification function and integrate it into the handler

With the environment variable and permissions in place, your function's code can now use boto3 to publish messages to SNS. You will add the SNS client, write a notification function, and call it from the handler.

  • Return to the Lambda console and select the Code tab for your LeadIntakeFunction.
  • In lambda_function.py, find this line near the top of the file:
table = dynamodb.Table("Leads")
  • Add the SNS client initialization directly below it, so the two lines together look like this:
table = dynamodb.Table("Leads")
sns_client = boto3.client("sns")
  • Next, find this line:
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
  • Add the SNS topic ARN read directly below it:
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN", "")

Why initialize these outside the handler?

Code outside the handler runs once when Lambda starts a new execution environment. By creating the SNS client and reading environment variables here, Lambda reuses them across multiple invocations without re-initializing each time.

Now add the send_notification function. This goes after the send_to_webhook function and before lambda_handler.

  • Add the following send_notification function below send_to_webhook:
def send_notification(lead_data):
    if not SNS_TOPIC_ARN:
        return {"success": False, "error": "No SNS topic ARN configured"}

    message = (
        f"New Lead Received!\n\n"
        f"Name: {lead_data['name']}\n"
        f"Email: {lead_data['email']}\n"
        f"Phone: {lead_data['phone']}\n"
        f"Service Interest: {lead_data['service_interest']}\n"
        f"Submitted: {lead_data['submitted_at']}\n"
    )

    try:
        sns_client.publish(
            TopicArn=SNS_TOPIC_ARN,
            Message=message,
            Subject="New Lead: " + lead_data["name"],
        )
        return {"success": True}
    except Exception as e:
        return {"success": False, "error": str(e)}

What does this code do?

  • The guard clause checks that SNS_TOPIC_ARN is configured. If it is missing, the function returns an error dict instead of crashing.
  • The message variable builds a formatted string with all the lead details.
  • The sns_client.publish() call sends the message to your topic. The Subject parameter is used as the email subject line. It is ignored for SMS subscribers.
  • The try/except block catches any SNS errors and returns them gracefully instead of crashing the whole function.

Finally, update the handler to call send_notification and include its result in the response.

  • Inside lambda_handler, find this line near the bottom:
    webhook_result = send_to_webhook(lead_data)
  • Add the notification call directly below it:
    webhook_result = send_to_webhook(lead_data)
    notification_result = send_notification(lead_data)
  • Now find the return statement's json.dumps block and add the notification result. The full return should look like this:
    return {
        "statusCode": 201,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "message": "Lead received successfully",
            "lead": lead_data,
            "webhook": webhook_result,
            "notification": notification_result,
        }),
    }

What changed in the response?

You added a "notification" key to the response body. When Postman receives the API response, you will see the notification status alongside the lead data and webhook result.

✔️ Awesome, I've got everything!

Great. Double check you have no syntax errors, then move on to deploy and test.

ⓧ I'd like to double check the full code

Here is the complete lambda_function.py at this point. Compare yours against it:

import json
import boto3
import urllib.request
import os
from datetime import datetime, timezone

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Leads")
sns_client = boto3.client("sns")

WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN", "")


def send_to_webhook(lead_data):
    if not WEBHOOK_URL:
        return {"success": False, "error": "No webhook URL configured"}

    payload = json.dumps({
        "firstName": lead_data["name"].split()[0] if " " in lead_data["name"] else lead_data["name"],
        "lastName": lead_data["name"].split()[-1] if " " in lead_data["name"] else "",
        "email": lead_data["email"],
        "phone": lead_data["phone"],
        "serviceInterest": lead_data["service_interest"],
        "submittedAt": lead_data["submitted_at"],
        "source": "lead-intake-api",
    }).encode("utf-8")

    req = urllib.request.Request(
        WEBHOOK_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=5) as response:
            return {"success": True, "status_code": response.status}
    except Exception as e:
        return {"success": False, "error": str(e)}


def send_notification(lead_data):
    if not SNS_TOPIC_ARN:
        return {"success": False, "error": "No SNS topic ARN configured"}

    message = (
        f"New Lead Received!\n\n"
        f"Name: {lead_data['name']}\n"
        f"Email: {lead_data['email']}\n"
        f"Phone: {lead_data['phone']}\n"
        f"Service Interest: {lead_data['service_interest']}\n"
        f"Submitted: {lead_data['submitted_at']}\n"
    )

    try:
        sns_client.publish(
            TopicArn=SNS_TOPIC_ARN,
            Message=message,
            Subject="New Lead: " + lead_data["name"],
        )
        return {"success": True}
    except Exception as e:
        return {"success": False, "error": str(e)}


def lambda_handler(event, context):
    try:
        body = json.loads(event.get("body") or "{}")
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"error": "Invalid JSON in request body"}),
        }

    errors = []
    required_fields = ["name", "email", "phone", "service_interest"]

    for field in required_fields:
        if not body.get(field):
            errors.append(f"Missing required field: {field}")

    if errors:
        return {
            "statusCode": 400,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"errors": errors}),
        }

    lead_data = {
        "name": body["name"],
        "email": body["email"],
        "phone": body["phone"],
        "service_interest": body["service_interest"],
        "submitted_at": datetime.now(timezone.utc).isoformat(),
    }

    item = {
        "email": lead_data["email"],
        "submitted_at": lead_data["submitted_at"],
        "name": lead_data["name"],
        "phone": lead_data["phone"],
        "service_interest": lead_data["service_interest"],
    }

    table.put_item(Item=item)

    webhook_result = send_to_webhook(lead_data)
    notification_result = send_notification(lead_data)

    return {
        "statusCode": 201,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "message": "Lead received successfully",
            "lead": lead_data,
            "webhook": webhook_result,
            "notification": notification_result,
        }),
    }
Deploy and test with Postman

Your code is ready. Time to deploy it and confirm that submitting a lead triggers an email notification automatically.

  • In the Lambda code editor, click Deploy.
  • Switch to Postman and open your POST /leads request.
  • Send a test lead with a body like this:
{
    "name": "Test User",
    "email": "test@example.com",
    "phone": "+12065550100",
    "service_interest": "teeth whitening"
}
  • Confirm the API response returns a 201 status code.
  • In the response body, look for the "notification" field showing {"success": true}.
  • Check your email inbox. You should receive a message with the subject line "New Lead: Test User" containing the lead details.
  • Check your phone. You should also receive an SMS with the same lead details (since your SMS subscription is still on the topic).

Response shows notification success but no email arrived?

  • Check your spam or junk folder. AWS notification emails sometimes land there.
  • Verify your email subscription status is Confirmed in the SNS console under your topic's Subscriptions tab.
  • Confirm the SNS_TOPIC_ARN environment variable value matches the ARN shown on your topic page exactly (no trailing spaces).

Help me debug why my SNS email notification is not arriving

Seeing an Access Denied error in the notification field?

  • Go back to IAM and confirm AmazonSNSFullAccess appears in the Permissions policies list for your execution role.
  • If you just attached the policy, click Deploy again in the Lambda console to force credential refresh on the next invocation.

Help me fix the Access Denied error when my Lambda publishes to SNS

Every lead submission now triggers a real-time email notification to your team. Next up, you will add conditional logic so only high-value leads get an additional SMS alert.

Add Conditional SMS Routing

Your notification system works. Every lead that comes in triggers an email to your inbox. But here's the problem: not every lead deserves the same urgency.

A patient asking about teeth whitening can wait. A patient asking about botox or implants represents thousands in potential revenue. Your team needs to know about those leads instantly, on their phone, not buried in an inbox. In this step, you'll add conditional logic that evaluates each lead's service interest and sends an SMS alert only for high-value opportunities.

In this step, get ready to:
  • Remove the SMS subscription from your topic and add a direct phone number environment variable.
  • Write conditional routing logic that evaluates leads against high-value service keywords.
  • Test both notification paths in Postman to confirm the routing works.
Remove SMS subscription from topic and add the ALERT_PHONE_NUMBER environment variable

Right now, your topic sends every message to both email and SMS subscribers. You want SMS to fire only for high-value leads, so you need to remove the blanket SMS subscription and switch to direct phone-number publishing from your Lambda code.

  • In the SNS console left navigation, select Subscriptions.
  • Find the SMS subscription (the one with Protocol SMS and your phone number as the Endpoint).
  • Select the checkbox next to it, then click Delete.
  • In the Delete subscription dialog, click Delete to confirm.

Why remove the SMS subscription?

Publishing to a topic delivers to ALL subscribers. If SMS stays on the topic, every single lead triggers a text message. By removing the SMS subscription and publishing directly to a phone number with sns_client.publish(PhoneNumber=...), your Lambda controls exactly when a text goes out.

This is the difference between fan-out (topic sends to everyone) and targeted delivery (code decides who gets what).

  • Switch to the Lambda console and open your LeadIntakeFunction.
  • Select the Configuration tab, then Environment variables, then Edit.
  • Click Add environment variable.
  • Set Key to ALERT_PHONE_NUMBER and Value to your verified phone number in E.164 format (e.g., +12065550100).
  • Click Save.

What is E.164 format?

E.164 is the international phone number format that starts with a + followed by the country code and number with no spaces or dashes. For US numbers, that looks like +1 followed by your 10-digit number.

Confirm your topic's Subscriptions tab now shows only the email subscription. The SMS subscription should be gone.

Still see the SMS subscription?

Make sure you deleted the SMS subscription from the Subscriptions page in the left navigation, not from the topic's detail page. Refresh the page if the status hasn't updated.

Help me figure out why my SNS SMS subscription is still showing after deletion.

Add the is_high_value check and send_sms_alert function

Now you'll write the conditional logic. Your Lambda will check each lead's service interest against a list of high-value keywords. If there's a match, it sends a direct SMS to your phone.

  • In the Lambda Code tab, find the line that reads SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN", "") and add the following code below it:
ALERT_PHONE_NUMBER = os.environ.get("ALERT_PHONE_NUMBER", "")

HIGH_VALUE_SERVICES = ["botox", "implants", "veneers", "invisalign", "all-on-4", "full mouth reconstruction"]


def is_high_value(service_interest):
    service_lower = service_interest.lower()
    for keyword in HIGH_VALUE_SERVICES:
        if keyword in service_lower:
            return True
    return False

What does this code do?

ALERT_PHONE_NUMBER reads the phone number you just saved in the environment variable. HIGH_VALUE_SERVICES is a list of keywords representing the most profitable dental services.

The is_high_value function lowercases the lead's service interest, then checks whether any high-value keyword appears in it. This means "Botox consultation" matches because "botox" is found inside the lowercased string.

You won't see a change yet. The next chunk adds the function that actually sends the SMS.

  • Below the send_notification function, add the send_sms_alert function by copying the following code:
def send_sms_alert(lead_data):
    if not ALERT_PHONE_NUMBER:
        return {"success": False, "error": "No alert phone number configured"}

    sms_message = f"HOT LEAD: {lead_data['name']} wants {lead_data['service_interest']}. Call {lead_data['phone']} NOW."

    try:
        sns_client.publish(
            PhoneNumber=ALERT_PHONE_NUMBER,
            Message=sms_message,
        )
        return {"success": True, "priority": True}
    except Exception as e:
        return {"success": False, "error": str(e)}

What does this code do?

Unlike send_notification which publishes to a topic (fan-out to all subscribers), send_sms_alert publishes directly to a phone number. This is a targeted one-to-one delivery.

The message is deliberately urgent and concise. It includes the lead's name, what they want, and their phone number so your team can call back immediately.

You won't see a change yet. The next chunk wires this into the handler.

  • In the lambda_handler function, find the line that reads notification_result = send_notification(lead_data) and replace everything from there to the end of the function with the following code:
    notification_result = send_notification(lead_data)

    sms_result = None
    if is_high_value(lead_data["service_interest"]):
        sms_result = send_sms_alert(lead_data)

    return {
        "statusCode": 201,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "message": "Lead received successfully",
            "lead": lead_data,
            "webhook": webhook_result,
            "notification": notification_result,
            "sms_alert": sms_result,
        }),
    }

What does this code do?

sms_result starts as None. The if is_high_value(...) check only triggers send_sms_alert when the lead's service interest contains a high-value keyword. If it doesn't match, sms_result stays None and the API response shows "sms_alert": null.

This is the conditional routing pattern. Every lead gets email, but only high-value leads get the SMS escalation.

  • Click Deploy in the Lambda console to save and deploy your updated function.

Seeing a syntax error on deploy?

Check that the indentation inside lambda_handler is consistent. Every line inside the function should be indented with 4 spaces. Also confirm you have a comma after each key-value pair in the json.dumps dictionary.

Help me fix the syntax error in my Lambda function after adding conditional SMS routing.

✔️ Awesome, I've got everything!

Great. Double check you've clicked Deploy and see the "Changes deployed" banner before moving on.

ⓧ I'd like to double check the full code

Here is the complete lambda_function.py at this point:

import json
import boto3
import urllib.request
import os
from datetime import datetime, timezone

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Leads")
sns_client = boto3.client("sns")

WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN", "")
ALERT_PHONE_NUMBER = os.environ.get("ALERT_PHONE_NUMBER", "")

HIGH_VALUE_SERVICES = ["botox", "implants", "veneers", "invisalign", "all-on-4", "full mouth reconstruction"]


def is_high_value(service_interest):
    service_lower = service_interest.lower()
    for keyword in HIGH_VALUE_SERVICES:
        if keyword in service_lower:
            return True
    return False


def send_to_webhook(lead_data):
    if not WEBHOOK_URL:
        return {"success": False, "error": "No webhook URL configured"}

    payload = json.dumps({
        "firstName": lead_data["name"].split()[0] if " " in lead_data["name"] else lead_data["name"],
        "lastName": lead_data["name"].split()[-1] if " " in lead_data["name"] else "",
        "email": lead_data["email"],
        "phone": lead_data["phone"],
        "serviceInterest": lead_data["service_interest"],
        "submittedAt": lead_data["submitted_at"],
        "source": "lead-intake-api",
    }).encode("utf-8")

    req = urllib.request.Request(
        WEBHOOK_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=5) as response:
            return {"success": True, "status_code": response.status}
    except Exception as e:
        return {"success": False, "error": str(e)}


def send_notification(lead_data):
    if not SNS_TOPIC_ARN:
        return {"success": False, "error": "No SNS topic ARN configured"}

    message = (
        f"New Lead Received!\n\n"
        f"Name: {lead_data['name']}\n"
        f"Email: {lead_data['email']}\n"
        f"Phone: {lead_data['phone']}\n"
        f"Service Interest: {lead_data['service_interest']}\n"
        f"Submitted: {lead_data['submitted_at']}\n"
    )

    try:
        sns_client.publish(
            TopicArn=SNS_TOPIC_ARN,
            Message=message,
            Subject="New Lead: " + lead_data["name"],
        )
        return {"success": True}
    except Exception as e:
        return {"success": False, "error": str(e)}


def send_sms_alert(lead_data):
    if not ALERT_PHONE_NUMBER:
        return {"success": False, "error": "No alert phone number configured"}

    sms_message = f"HOT LEAD: {lead_data['name']} wants {lead_data['service_interest']}. Call {lead_data['phone']} NOW."

    try:
        sns_client.publish(
            PhoneNumber=ALERT_PHONE_NUMBER,
            Message=sms_message,
        )
        return {"success": True, "priority": True}
    except Exception as e:
        return {"success": False, "error": str(e)}


def lambda_handler(event, context):
    try:
        body = json.loads(event.get("body") or "{}")
    except json.JSONDecodeError:
        return {
            "statusCode": 400,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"error": "Invalid JSON in request body"}),
        }

    errors = []
    required_fields = ["name", "email", "phone", "service_interest"]

    for field in required_fields:
        if not body.get(field):
            errors.append(f"Missing required field: {field}")

    if errors:
        return {
            "statusCode": 400,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"errors": errors}),
        }

    lead_data = {
        "name": body["name"],
        "email": body["email"],
        "phone": body["phone"],
        "service_interest": body["service_interest"],
        "submitted_at": datetime.now(timezone.utc).isoformat(),
    }

    item = {
        "email": lead_data["email"],
        "submitted_at": lead_data["submitted_at"],
        "name": lead_data["name"],
        "phone": lead_data["phone"],
        "service_interest": lead_data["service_interest"],
    }

    table.put_item(Item=item)

    webhook_result = send_to_webhook(lead_data)
    notification_result = send_notification(lead_data)

    sms_result = None
    if is_high_value(lead_data["service_interest"]):
        sms_result = send_sms_alert(lead_data)

    return {
        "statusCode": 201,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({
            "message": "Lead received successfully",
            "lead": lead_data,
            "webhook": webhook_result,
            "notification": notification_result,
            "sms_alert": sms_result,
        }),
    }
Test both paths in Postman

Your Lambda now has two distinct notification paths. Time to prove both work correctly by sending a low-value lead and a high-value lead.

  • In Postman, send a POST request to your /leads endpoint with the following body:
{
    "name": "Test Low Value",
    "email": "low@example.com",
    "phone": "+12065550101",
    "service_interest": "teeth whitening"
}
  • Check the response body. You should see "sms_alert": null because "teeth whitening" is not in the high-value list.
  • Confirm you received an email notification but no SMS on your phone.
  • Send a second POST request with a high-value service interest:
{
    "name": "Test High Value",
    "email": "high@example.com",
    "phone": "+12065550102",
    "service_interest": "botox consultation"
}
  • Check the response body. You should see "sms_alert": {"success": true, "priority": true} because "botox" matches a high-value keyword.
  • Confirm you received both an email notification AND an SMS on your phone with the "HOT LEAD" message.

Email works but no SMS arrives?

Double check that the ALERT_PHONE_NUMBER environment variable matches your verified sandbox phone number exactly in E.164 format. The number must be verified in the SNS SMS sandbox to receive messages.

Also confirm your Lambda execution role still has the AmazonSNSFullAccess policy attached. Direct phone-number publishing requires the same SNS permissions as topic publishing.

Help me troubleshoot why my Lambda is not sending SMS alerts for high-value leads.

Your notification system now makes smart decisions. Every lead gets an email, but only the high-value ones trigger an urgent SMS. Next up, you'll take on the Secret Mission and build a full scoring system with tiered routing.

Secret mission

Priority Lead Routing with Scoring

Your current system treats all high-value leads the same. What if you could score leads on a 3-tier scale and route only the hottest opportunities to a dedicated priority channel? Build a lead scoring system with a second SNS topic that sends SMS alerts exclusively for top-tier leads.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. SNS topics cost nothing at rest. You only pay when messages are published.

Cost warning

SNS email notifications are free (up to 1,000 per month). SMS messages have no free tier and cost approximately $0.006 per message to US numbers. If you leave the notification system active and continue sending leads, each SMS alert incurs a small charge.

Resources you used:

  • SNS Standard topic: NewLeadNotifications (with email subscription).
  • SNS Standard topic: PriorityLeadAlerts (with SMS subscription, if you completed the Secret Mission).
  • Verified phone number in the SNS SMS sandbox.
  • AmazonSNSFullAccess policy attached to the LeadIntakeFunction execution role.
  • Lambda environment variables: SNS_TOPIC_ARN and SNS_PRIORITY_TOPIC_ARN.

Keep everything running

No action needed. Choose this if you plan to keep building on this notification system or want to continue receiving lead alerts.

  • SNS topics and subscriptions cost nothing at rest. You only pay per message published.
  • Your Lambda, DynamoDB table, and API Gateway remain free-tier eligible.
  • The verified phone number in the SMS sandbox stays active indefinitely at no cost.

Pause - I'll come back to this later

There is no pause option for SNS topics. To avoid any SMS charges, simply stop sending leads to your API endpoint.

  • SNS topics with no messages published incur zero cost. You do not need to delete them to stop charges.
  • If you want to prevent accidental SMS charges, remove the SNS_PRIORITY_TOPIC_ARN environment variable from your Lambda to disable priority SMS routing.

Delete - I don't want to use this again

Remove all SNS resources and revert your Lambda to its pre-notification state.

  • Delete SNS topics: In the Amazon SNS console, select Topics in the left navigation. Select the NewLeadNotifications topic, then choose Delete. Enter delete me in the confirmation dialog and choose Delete. Repeat for PriorityLeadAlerts if you created it. Deleting a topic also deletes all its subscriptions.
  • Remove the verified phone number: In the SNS console, select Text messaging (SMS) in the left navigation. In the Sandbox destination phone numbers section, select your phone number and choose Delete phone number. Enter delete me to confirm and choose Delete. You can only delete a number 24 hours after verification.
  • Detach the IAM policy: Open the IAM console. Select Roles in the left navigation. Click the LeadIntakeFunction execution role name. In the Permissions tab, find AmazonSNSFullAccess and select Remove. Confirm the removal.
  • Remove Lambda environment variables: In the Lambda console, open your LeadIntakeFunction. Go to Configuration > Environment variables > Edit. Remove SNS_TOPIC_ARN and SNS_PRIORITY_TOPIC_ARN. Select Save.

Nice Work!

Nice Work!

Nice work! You just built a fully automated notification pipeline that alerts your team the moment a new lead comes in.

You've learned how to:

  • Built an SNS topic with an email subscription that delivers real-time lead notifications on every new submission.
  • Integrated boto3 SNS publish into your Lambda function with proper IAM permissions to send messages programmatically.
  • Implemented conditional routing logic that evaluates leads against high-value service keywords and sends immediate SMS alerts for priority opportunities.
  • Secret Mission: Built a 3-tier lead scoring system with a dedicated priority topic that routes hot leads to SMS automatically.

Ready to quiz yourself?