All posts by Rommel Sunga

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 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

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

Implement Tenants in your Amazon SES environment, Part 3: Implementation guide

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/implement-tenants-in-your-amazon-ses-environment-part-3-implementation-guide/

This is part 3 in a series covering the new tenants feature in Amazon Simple Email Service (SES). The first post in this series discussed how users can improve email deliverability with tenant management in Amazon SES. Part 2 covered key aspects involved in planning the migration of existing Amazon SES infrastructure to use tenant-based reputation isolation.

With Amazon SES tenants, users can:

  • Manage individual tenant onboardings and their reputations in isolation
  • Provision isolated tenants within a single SES account
  • Apply automated reputation policies to manage email sending
  • Detect and isolate deliverability issues within isolated email streams
  • Preserve sender reputation and improve inbox placement with mailbox providers

This post provides a step-by-step migration guide to Tenants for key AWS components like AWS Identity and Access Management (IAM) permissions, Amazon CloudWatch logging and Amazon EventBridge monitoring. Additionally, we provide several code examples that show how to programmatically provision tenants in real time as customers are onboarded. The goal is to demonstrate how to use the Tenants feature to achieve reputation isolation between customers or business units (BUs), get more control over sending policies, and enable the automatic pause mechanism to limit the damage from problematic senders.

Step-by-step migration guide

Having completed the inventory and configuration planning prescribed in part 2 of this series, we are ready to start the 4-step migration (or implementation) of the tenants feature in the AWS SES account.

The Amazon SES Tenants Migration process is as follows:

  1. Preparation
    1. Verify SES V2 API usage
    2. Update SDK versions
  2. Create Your First Tenant
    1. Create Tenant API calls
    2. Implement in onboarding workflow
    3. Verify tenant creation
  3. Associating Resources
    1. Link verified domains to tenants
    2. Associate configuration sets
    3. Connect IP pools
  4. Updating Your Sending Code
    1. Add Tenant/Name parameter to API calls
    2. Add X-SES-TENANT header for SMTP
    3. Update application sending code
    4. Test email sending functionality

Preparation

When sending email through a tenant, be sure to specify the tenant in the API calls or SMTP headers and ensure that all resources used are associated with that tenant. Before getting started, keep in mind that there’s an additional charge per tenant per month based on the number of emails. For more detailed information, see the SES Pricing page.

For applications that use SMTP, see the SMTP Implementation section later in this document.

For applications that use the SES API, confirm that the latest Amazon SES V2 API is being used, as tenant management capabilities are only available in this version (see SES V2 API migration guide). We also recommend verifying that the AWS SDK version being used supports these operations, otherwise you may need to update to a version that supports them.

The SES V2 API includes the seven essential operations for managing the tenant architecture that the application will need to leverage throughout the migration (or implementation) and tenant lifecycle:

  1. CreateTenant for establishing new tenant containers
  2. CreateTenantResourceAssociation for linking resources like domains and configuration sets to tenants
  3. DeleteTenant for removing tenants when workloads offboard
  4. DeleteTenantResourceAssociation to unlink resources from tenants
  5. GetTenant retrieves specific tenant details
  6. ListTenants provides an overview of all tenants in an account
  7. ListTenantResources shows which resources are associated with each tenant

Implementation Steps

Creating the tenant(s)

The following Python code example uses the AWS SDK for Python (Boto3) and CreateTenant to demonstrate tenant creation. We’ve added optional tags to better organize the tenant resource for billing or logging purposes.

import boto3
from botocore.exceptions import ClientError

def setup_ses_tenant():
    ses_client = boto3.client('sesv2')
    
    # Create a new tenant with descriptive tags
    tenant_response = ses_client.create_tenant(
        TenantName='MyTenant',
        Tags=[
            {
                'Key': 'Environment',
                'Value': 'Production'
            },
            {
                'Key': 'CustomerID',
                'Value': '[customer_id]'
            }
        ]
    )
    
    # Verify tenant creation
    tenants = ses_client.list_tenants()
    print(f"Total tenants in account: {len(tenants['Tenants'])}")
    
    return tenant_response['TenantName']


if __name__ == "__main__":
    try:
        tenant_name = setup_ses_tenant()
        print(f"Successfully created tenant: {tenant_name}")
    except ClientError as e:
        print(f"AWS error: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")

This example code can be used when a customer first onboards onto the platform to send email. By creating a tenant for this customer, resources under that tenant will be associated together whenever the tenant is used as explained in the next steps.

Associating resources with the tenant

Each tenant needs appropriate resources—configuration sets, sending identities, and potentially dedicated IP pools—to begin sending email. The association process should align with the resource sharing strategy as established during the migration planning phase.

The following Python code example uses the AWS SDK for Python (Boto3) and CreateTenantResourceAssociation to associate resources to the tenant that was created in the previous step.

import boto3
from botocore.exceptions import ClientError

def associate_resources_with_tenant():
    ses_client = boto3.client('sesv2')
    
    try:
        # Associate verified domain identity
        ses_client.create_tenant_resource_association(
            TenantName='MyTenant',
            ResourceArn='arn:aws:ses:[aws_region]:[account_id]:identity/[domain_name]'
        )
        print("Successfully associated email identity with tenant")

        # Associate configuration set for tracking
        ses_client.create_tenant_resource_association(
            TenantName='MyTenant',
            ResourceArn='arn:aws:ses:[aws_region]:[account_id]:configuration-set/MyTenantConfigurationSet'
        )
        print("Successfully associated configuration set with tenant")

    except ClientError as e:
        print(f"Error: {e.response['Error']['Message']}")
        raise

if __name__ == "__main__":
    associate_resources_with_tenant()

Consider implementing batch association for email streams with multiple domains or configuration sets. This approach reduces API calls and improves provisioning efficiency. Remember that resources can be shared across multiple tenants if the architecture requires it, allowing flexible resource allocation strategies.

Update the applications

The transition to tenant-based sending requires minimal code changes in the apps, namely adding the TenantName and ConfigurationSetName to the sending process.

API Implementations

For applications that use the SES V2 API, add the tenant and configuration set parameters to the send calls as demonstrated below using the AWS SDK for Python (Boto3) :

import boto3

def send_email_from_tenant():
    ses_client = boto3.client('sesv2')
    
    response = ses_client.send_email(
        FromEmailAddress='sender@[domain_name]',
        Destination={
            'ToAddresses': ['[recipient_email]']
        },
        Content={
            'Simple': {
                'Subject': {
                    'Data': 'Test email from SES tenant'
                },
                'Body': {
                    'Text': {
                        'Data': 'This is a test email sent from an SES tenant'
                    }
                }
            }
        },
        ConfigurationSetName='MyTenantConfigurationSet',
        TenantName='MyTenant'  # Critical addition for tenant routing
    )
    
    print(f"Message sent! Message ID: {response['MessageId']}")
    return response

if __name__ == "__main__":
    send_email_from_tenant()

SMTP Implementations

For sending applications that use SMTP, add the X-SES-TENANT and the ConfigurationSetName header parameters to every message. In the code block that follows, we demonstrate the proper header configuration for SMTP sending using Python:

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib

def send_smtp_email_with_tenant():
    sender = 'smtp-sender@[domain_name]'
    sender_name = 'Sender Name'
    recipient = '[recipient_email]'
    username_smtp = '[smtp_username]'
    configuration_set = 'MyTenantConfigurationSet'
    host = 'email-smtp.[aws_region].amazonaws.com'
    port = 587
    
    subject = 'Amazon SES test (SMTP interface accessed using Python)'
    body_text = """Email Test
    This email was sent through the Amazon SES SMTP interface using Python."""
    body_html = """<h1>Email Test</h1>
        <p>This email was sent through the 
        <a href="https://aws.amazon.com/ses">Amazon SES</a> SMTP
        interface using Python."</p>"""
    
    msg = MIMEMultipart('alternative')
    msg['Subject'] = subject
    msg['From'] = f"{sender_name} <{sender}>"
    msg['To'] = recipient
    msg['X-SES-CONFIGURATION-SET'] = configuration_set
    
    # Critical: Specify tenant for SMTP sending
    msg['X-SES-TENANT'] = 'MyTenant'
    
    part1 = MIMEText(body_text, 'plain')
    part2 = MIMEText(body_html, 'html')
    msg.attach(part1)
    msg.attach(part2)
    
    try:
        server = smtplib.SMTP(host, port)
        server.ehlo()
        server.starttls()
        server.ehlo()
        server.login(username_smtp, fetch_smtp_password_from_secure_storage())
        server.sendmail(sender, recipient, msg.as_string())
        print("Email sent successfully!")
        
    except Exception as e:
        print(f"Error: {str(e)}")
    
    finally:
        server.quit()

def fetch_smtp_password_from_secure_storage():
    # Implement secure password retrieval from AWS Secrets Manager
    # or your preferred secret storage solution
    return '[smtp_password]'


if __name__ == "__main__":
    send_smtp_email_with_tenant()

Configuring IAM policies and permissions for tenants

This section explains how to configure IAM permissions for SES tenants, including how to set up different permission levels for tenant management, email sending, and monitoring while following security best practices to control access based on organizational roles. Remember that IAM policies for tenants follow the principle of least privilege. Start with minimal permissions and expand as needed, regularly reviewing and removing unused permissions to maintain security.

Tenant management permissions

SES Tenants can be controlled through specific IAM permissions that determine who can create, modify, and use specific tenant(s) in the organization. The tenant management system’s core API actions discussed previously can be granted with granular access through IAM policies for administrative operations. The Service Authorization Reference for Amazon Simple Email Service v2 page contains the latest documentation for the service-specific actions used below for Amazon Simple Email Service.

We recommend limiting each IAM role’s permission based on the minimum capabilities required by that role. This helps mitigate the potential for negative effects if an SMTP credential is misused. What follows is a basic IAM policy that demonstrates full tenant management capabilities; this is NOT demonstrating the principle of least privilege (yet):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:CreateTenant",
                "ses:DeleteTenant",
                "ses:GetTenant",
                "ses:ListTenants",
                "ses:CreateTenantResourceAssociation",
                "ses:DeleteTenantResourceAssociation",
                "ses:ListTenantResources",
                "ses:ListResourceTenants"
            ],
            "Resource": "*"
        }
    ]
}

Configuring sending permissions with tenants

Applications that send emails through tenants need different permissions than those managing tenants. The key distinction is using the ses:TenantName condition to restrict which tenants an application can use for sending.

The IAM policy below allows sending emails only through the specified CustomerA-Tenant tenant, ensuring applications can’t accidentally or maliciously send through other customers’ tenants.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendEmail",
                "ses:SendBulkEmail"
            ],
            "Resource": [
                "arn:aws:ses:[aws_region]:[account_id]:identity/*",
                "arn:aws:ses:[aws_region]:[account_id]:configuration-set/*"
            ],
            "Condition": {
                "StringEquals": {
                    "ses:TenantName": "CustomerA-Tenant"
                }
            }
        }
    ]
}

Separating administrative and operational access

Production environments implement role separation between tenant management and email sending operations. Administrative roles handle tenant creation and resource association during customer onboarding, while application roles can only send emails through assigned tenants. The IAM policy below is an example of an administrative role; it allows creating and configuring tenants for use during customer onboarding but does not allow the tenant deletion action to prevent accidental deletions.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:CreateTenant",
                "ses:ListTenants"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ses:CreateTenantResourceAssociation",
                "ses:GetTenant"
            ],
            "Resource": "arn:aws:ses:[aws_region]:[account_id]:tenant/*/tn-*"
        },
        {
            "Effect": "Deny",
            "Action": "ses:DeleteTenant",
            "Resource": "*"
        }
    ]
}

Resource-level permissions

Tenants support resource-level permissions using Amazon Resource Names (ARNs), enabling fine-grained access control. Grant access to specific tenants by specifying the tenant name and tenant id (ex. CustomerA-Tenant/tn-1a2b3c4d5e6f7890abcdef1234567890) rather than granting blanket permissions using the “*/tn-*” wildcard as above. To obtain the tenant id you can use the list-tenants command of the AWS SESv2 CLI.

The IAM policy below grants access only to tenants CustomerA-Tenant and CustomerB-Tenant where the following tenant id is a placeholder that should be replaced by the correct tenant id that you obtained from list-tenants.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ses:GetTenant",
            "Resource": [
                "arn:aws:ses:[aws_region]:[account_id]:tenant/CustomerA-Tenant/tn-1a2b3c4d5e6f7890abcdef1234567890",
                "arn:aws:ses:[aws_region]:[account_id]:tenant/CustomerB-Tenant/tn-9876543210fedcba0987654321abcdef"
            ]
        }
    ]
}

Monitoring and compliance access

Security and compliance teams often need read-only access to monitor tenant usage. The following IAM policy grants read-only access to all tenants, but does not permit modifications or deletions of any tenants:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ses:GetTenant",
                "ses:ListTenants",
                "ses:ListTenantResources"
            ],
            "Resource": "*"
        }
    ]
}

Monitoring Tenants with EventBridge and CloudWatch

Amazon SES integrates with Amazon EventBridge to deliver comprehensive monitoring capabilities for tenant management, providing real-time visibility into reputation changes and enabling automated response workflows. EventBridge is a serverless service that uses JSON-formatted events to connect application components, making it straightforward to build scalable event-driven applications. Amazon SES’s tenant feature’s integration with EventBridge enables organizations to track tenant-specific metrics and other metrics, such as reputation findings and reputation status changes, and tenant status changes.

Understanding EventBridge Integration with SES

EventBridge operates as a router that receives events from SES and delivers them to one or many destinations (aka targets). When SES features experience state changes or status updates, they automatically send events to the EventBridge default event bus. Rules associated with the event bus evaluate events as they arrive, checking whether each event matches the rule’s pattern before routing to specified targets. For SES tenant management, organizations can receive real-time alerts through Amazon EventBridge when tenant reputation findings are detected or when tenant status changes occur. These events are delivered on a best-effort basis; they might be delivered out of order, requiring users to deploy processing logic to handle such scenarios gracefully.

The code block that follows can guide users through the basic EventBridge integration with the SES tenant feature. For more extensive documentation on integrating with EventBridge please consult AWS documentation.

Setting up EventBridge integration

Create an EventBridge rule using the AWS SDK for Python (Boto3) to capture tenant status changes and reputation findings:

# Create rule for monitoring tenant status changes
aws events put-rule \
    --name "SESTenantStatusMonitor" \
    --description "Monitor SES tenant status changes" \
    --event-pattern '{
        "source": ["aws.ses"],
        "detail-type": [
            "Sending Status Enabled",
            "Sending Status Disabled",
            "Advisor Recommendation Status Open",
            "Advisor Recommendation Status Closed"
        ]
    }'

Routing events from EventBridge to CloudWatch Logs

CloudWatch provides the capability to collect raw data and process it into readable, near real-time metrics. Follow these steps to set up a CloudWatch log group for SES tenant events and configure the appropriate IAM permissions:

  1. Create a CloudWatch log group for tenant events:

aws logs create-log-group --log-group-name "/aws/ses/tenants"

  1. Add resource-based policy to CloudWatch Logs:
aws logs put-resource-policy \
    --policy-name EventBridgeToCloudWatchLogsPolicy \
    --policy-document '{
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "TrustEventsToStoreLogEvent",
            "Effect": "Allow",
            "Principal": {
                "Service": ["events.amazonaws.com", "delivery.logs.amazonaws.com"]
            },
            "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
            "Resource": "arn:aws:logs:[aws_region]: [account_id]:log-group:/aws/ses/tenants:*"
        }]
    }'
  1. Add the EventBridge target:

aws events put-targets \
    --rule "SESTenantStatusMonitor" \
    --targets '[{
        "Id": "SendToCloudWatchLogs",
        "Arn": "arn:aws:logs:[aws_region]:[account_id]:log-group:/aws/ses/tenants"
    }]'

An example event of a paused (“disabled”) tenant is shown below for reference:

{
    "version": "0",
    "id": "3cc76530-9842-03a9-fdef-e4e4f667cf4e",
    "detail-type": "Sending Status Disabled",
    "source": "aws.ses",
    "account": "[account_id]",
    "time": "2025-10-01T03:37:17Z",
    "region": "[aws_region]",
    "resources": [
        "arn:aws:ses:[aws_region]::tenant/CustomerA-Tenant/tn-2a8c678ec0000fdaf76cc1f127b40"
    ],
    "detail": {
        "version": "1.0.0",
        "data": {
            "origin": "CUSTOMER_MANAGED",
            "record": {
                "status": "DISABLED",
                "cause": "Status manually updated.",
                "lastUpdatedTimestamp": [
                    2025,
                    10,
                    1,
                    3,
                    37,
                    17,
                    671000000
                ]
            }
        }
    }
}

Managing tenant reputation with key Tenants features

Reputation management for individual tenants is one of the core benefits of using Amazon SES’s tenant feature, providing automated protection against deliverability issues that could damage overall account reputation. This section demonstrates how to configure reputation policies that can automatically pause tenants when they experience high bounce rates or reputation issues, as well as how to manually control tenant sending status for custom workflows.

Setting reputation policies

Amazon SES provides three reputation policy enforcement levels that determine how the system responds to reputation findings.

  • The Standard policy (recommended) pauses sending only for high-impact findings, providing a balance between protection and operational flexibility.
  • The Strict policy pauses sending for any reputation finding, offering maximum protection for sensitive environments.
  • The None option disables automated pausing while continuing to track findings, useful for manual monitoring scenarios.

Reputation findings are generated in two severity levels—low and high—based on metrics like bounce rates and complaint rates. When these metrics indicate a potential deliverability issue, SES creates findings that can trigger automatic pausing based on a chosen policy.

In the following code block, we demonstrate how to set a reputation policy on the CustomerA-Tenant

import boto3
from botocore.exceptions import ClientError

def update_tenant_reputation_policy(tenant_arn, policy_arn):
    ses_client = boto3.client('sesv2')
    
    ses_client.update_reputation_entity_policy(
        ReputationEntityType='RESOURCE',
        ReputationEntityReference=tenant_arn,
        ReputationEntityPolicy=policy_arn
    )
    print(f"Tenant policy_arn updated to: {policy_arn}")

# Example usage
if __name__ == "__main__":
    tenant_arn = "arn:aws:ses:[aws_region]:[account_id]:tenant/CustomerA-Tenant/tn-2a8c678ec0000fdaf76cc1f127b40"
    
    # Enable normal sending
    update_tenant_reputation_policy(tenant_arn, 'arn:aws:ses:[aws_region]:aws:reputation-policy/standard')
    
    # Disable/pause sending
    update_tenant_reputation_policy(tenant_arn, 'arn:aws:ses:[aws_region]:aws:reputation-policy/strict')
    
    # Reinstate with monitoring
    update_tenant_reputation_policy(tenant_arn, 'arn:aws:ses:[aws_region]:aws:reputation-policy/none')

Our recommendation is to choose the Standard policy for most production tenants, as this policy provides automatic protection against severe reputation issues while avoiding unnecessary disruptions. Reserve the Strict policy for new or untrusted tenants where maximum caution is warranted. Use the None option during initial monitoring periods or when implementing custom reputation management logic.

Handling paused tenants

When a tenant is paused, either automatically through reputation policies or manually, the sending status prevents any emails from being sent through that tenant. The system derives this aggregate status from both customer-managed and AWS-managed statuses; if either is set to DISABLED, the tenant cannot send emails.

Amazon SES publishes notifications to EventBridge when tenant status changes occur or new reputation findings are detected, enabling real-time response to reputation events. After investigating and resolving the underlying issues, the tenant’s sending capabilities can be reinstated. During reinstatement (REINSTATED status), the tenant can continue sending while metrics are monitored to verify improvement.

def update_tenant_status(tenant_arn, status):
    ses_client = boto3.client('sesv2')
    
    ses_client.update_reputation_entity_customer_managed_status(
        ReputationEntityType='RESOURCE',
        ReputationEntityReference=tenant_arn,
        SendingStatus=status
    )
    print(f"Tenant status updated to: {status}")

# Example usage
if __name__ == "__main__":
    tenant_arn = "arn:aws:ses:[aws_region]:[account_id]:tenant/CustomerA-Tenant/tn-2a8c678ec0000fdaf76cc1f127b40"
    
    # Enable normal sending
    update_tenant_status(tenant_arn, 'ENABLED')
    
    # Disable/pause sending
    update_tenant_status(tenant_arn, 'DISABLED')
    
    # Reinstate with monitoring
    update_tenant_status(tenant_arn, 'REINSTATED')

The REINSTATED status allows the tenant to continue sending even with active reputation findings. Once metrics return to healthy levels, the tenant automatically transitions back to ENABLED status. This approach ensures minimal disruption while protecting the overall account reputation from problematic email streams.

Managing tenant lifecycle

When customers modify a service tier or leave the platform, proper cleanup ensures resource efficiency and maintains account organization.

Removing resource associations

Before deleting a tenant, remove all resource associations to prevent orphaned configurations:

import boto3
from botocore.exceptions import ClientError

def remove_resource_from_tenant():
    ses_client = boto3.client('sesv2')
    
    try:
        # Remove identity association
        ses_client.delete_tenant_resource_association(
            TenantName='MyTenant',
            ResourceArn='arn:aws:ses:[aws_region]:[account_id]:identity/[domain_name]'
        )
        print("Successfully removed identity association")
        
        # Remove configuration set association
        ses_client.delete_tenant_resource_association(
            TenantName='MyTenant',
            ResourceArn='arn:aws:ses:[aws_region]:[account_id]:configuration-set/MyTenantConfigurationSet'
        )
        print("Successfully removed configuration set association")
    
    except ClientError as e:
        print(f"Error: {e.response['Error']['Message']}")
        raise


if __name__ == "__main__":
    remove_resource_from_tenant()

Deleting tenants

Once all resources are disassociated, remove the tenant entirely:

import boto3
from botocore.exceptions import ClientError

def delete_tenant(tenant_name):
    ses_client = boto3.client('sesv2')
    
    try:
        # Delete the tenant
        ses_client.delete_tenant(TenantName=tenant_name)
        print(f"Successfully deleted tenant: {tenant_name}")
        return True
        
    except ClientError as e:
        print(f"Error deleting tenant: {e.response['Error']['Message']}")
        raise

# Example usage with error handling
if __name__ == "__main__":
    try:
        delete_tenant("MyTenant")
    except ClientError as e:
        print(f"Cleanup failed: {e}")

Tenant lifecycle management ensures clean transitions when customers change service tiers or leave the platform. Implement these operations in customer offboarding workflows to maintain optimal account organization and resource utilization.

Resource Management

Resource Sharing Capabilities

Resources can be assigned to multiple tenants simultaneously. This enables sharing common resources between tenants while maintaining separate reputation tracking. For example, the reputation for marketing and transactional email could be tracked separately across independent tenants while using the same sending domain. SES validates tenant-resource associations at send time, rejecting requests if the specified tenant lacks access to the requested resources.

Resource Migration Between Tenants

Resource migration involves two API calls. First, remove the association from the current tenant using DeleteTenantResourceAssociation, then create a new association with the target tenant using CreateTenantResourceAssociation. This process can be automated for bulk migrations during reorganizations.

Reputation Management

Tenant Isolation Protection

Each tenant maintains independent reputation metrics and sending status. When one tenant experiences deliverability issues, it can be automatically paused without affecting other tenants’ ability to send, protecting both shared resources and account-level reputation.

Tenant Pausing Triggers

Tenants can either be paused manually using the UpdateReputationEntityCustomerManagedStatus API or paused automatically based on the reputation policy assigned to the tenant. Reputation policies pause tenants based on reputation findings generated from bounce rates, complaint rates, and third-party feedback reports. The Standard policy (recommended) pauses only for high-severity findings (bounce rate > 15%, complaint rate > 1%), while Strict pauses even for low severity findings (bounce rate > 10%, complaint rate > 0.5%).

Tenant Reactivation Process

For tenants paused by automated reputation policies, use the UpdateReputationEntityCustomerManagedStatus API to reinstate sending after addressing root causes. Tenants paused by AWS Trust & Safety require case resolution through AWS Support.

Migrating Existing Customers

Creating tenants for existing email streams can be completed with no disruption to email sending. Start by creating tenants for each customer or business unit, then associate existing resources like email identities, configuration sets, and templates using the tenant association APIs. Once those steps are complete, update the application, or inform the customer or BU they now need to specify the tenant name and configuration set in their SES SendEmail API calls or SMTP headers which enables SES to route emails through the appropriate tenant.

Reputation Metrics Transition

New reputation metrics will be tracked separately for each tenant from the point of creation and use. Historical metrics from before tenant implementation are not available. Tenant metrics contribute to the overall account-level reputation. For example, if tenant-a, tenant-b, and tenant-c each send 1,000 emails and:

  • Tenant-a receives 150 bounce notifications over 1,000 emails (for a bounce rate of 15%), tenant reputation protection will pause this tenant before the issue escalates.
  • Tenant-b receives 0 bounces over 1,000 emails, for a bounce rate of 0%
  • Tenant-c receives 0 bounces over 1,000 emails, for a bounce rate of 0%

The SES Account Level Bounce Rate (all tenants) is 150 out of 3,000, or 5%

Account Activation Requirements

No account-level activation is required to configure and use the Tenant feature, it is immediately available through the SES V2 API or the AWS SES Console. Users can also start using the tenant management APIs (CreateTenant, CreateTenantResourceAssociation, DeleteTenant, etc.) without any account modifications or support requests.

Conclusion

This post covered detailed migration steps, monitoring setup, practical implementation examples and troubleshooting steps. From running a SaaS platform, to managing multiple brands, to operating separate business units, tenant-based reputation isolation ensures Amazon SES email infrastructure scales reliably as an organization grows.

Additional resources


About the authors

Implement Tenants in your Amazon SES environment, Part 2: Assessment and planning

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/implement-tenants-in-your-amazon-ses-environment-part-2-assessment-and-planning/

Running multiple users or business units (BUs) on a shared email infrastructure often creates deliverability and compliance risks. The tenant-based reputation isolation feature in Amazon Simple Email Service (Amazon SES) solves these challenges by separating email reputation by customer, BU, or workload. This is part 2 in a series covering the new tenants feature in Amazon Simple Email Service (SES). The first post in this series discussed how users can improve email deliverability with tenant management in Amazon SES (for more details, see the blog post).

In this post, we provide an overview of the tenants feature and guidance for planning the implementation to or migration of your existing Amazon SES infrastructure to use tenant-based reputation isolation.

Part 3 covers practical implementation steps for the tenants feature, including configuration steps for key components like Identity and Access Management (IAM) permissions, Amazon CloudWatch logging, and Amazon EventBridge monitoring. We also provide code examples using the Amazon SES v2 APIs that show how to provision tenants in real time as you onboard downstream customers or business divisions onto your Amazon SES account. Key outcomes include complete reputation isolation between customers, control over sending policies, and automated pause mechanisms for problematic senders.

Solution overview

Amazon SES now offers tenant management capabilities that enable isolated email sending environments within a single AWS account. This provides granular reputation management across different email streams. You can assign dedicated configuration sets, sending identities, and templates to each tenant. Each tenant functions as a logical container that maintains its own sending reputation independently. This tenant-based reputation isolation makes sure deliverability issues with one tenant won’t affect your other tenants. You also get real-time visibility into tenant-level metrics, including messages sent, bounce rates, and complaint rates.

The following diagram illustrates the solution architecture.

Tenants are a best practice for those who want visibility, isolation, and control over their email reputation, from large-scale platforms to smaller teams:

  • Independent software vendors (ISVs) with multiple customers, and marketing and software as a service (SaaS) solutions using a central AWS SES account – You can provision a tenant for each customer to segregate email sending and prevent one client’s sending from negatively affecting deliverability for other clients.
  • Enterprises and other large organizations with multiple BUs – You can maintain separate reputation profiles for different BUs, departments, or brands within the same AWS account.
  • Organizations with distinct mail streams – You can separate transactional and marketing mail, or keep product notifications distinct from internal communications, to make sure issues with one tenant’s email stream don’t affect the other tenants.
  • Single-stream senders – Even if you currently only operate one email stream, you can future-proof your SES account by assigning a tenant from day one. This provides visibility into reputation findings and helps you apply proactive policies to stay ahead of deliverability issues before they impact sending.

In the following sections, we outline how to plan and execute your migration to Amazon SES tenants. We show how to assess your current setup, choose the right tenant structure for your organization, and implement a gradual rollout that minimizes disruption to your existing email operations.

Pre-migration assessment

If you are already using Amazon SES, the transition to tenant-based architecture begins with understanding your current email infrastructure. Start by creating an inventory of your existing sending identities (domains) and identify what, if any, Amazon SES configuration set is assigned. This will help you understand how each customer or BU is currently distributed across your Amazon SES infrastructure. Review each configuration set to identify and track the various configuration options, such as the sending IP pool and archive option. The information you uncover in this phase will serve as the blueprint for your tenant associations and help you make sure each tenant has access to the appropriate resources, while maintaining proper isolation boundaries.

Recommended tenant structure: Individual tenants per customer

For most implementations, we recommend creating one or more tenants per customer or BU so you can achieve complete tenant-level isolation of email workloads. With this approach, you can supply dedicated SMTP or API credentials per tenant to provide secure access to allocated resources. You can apply different reputation policies per tenant as needed and automatically pause any tenant that conflicts with the reputation policy. Additionally, the shared tenant monitoring tools in Amazon SES help you automatically monitor and enforce reputation-based policies at the tenant level, so that problematic email sending behavior from one tenant doesn’t impact the deliverability of others.

The following diagram shows an example of an ISV that operates a SaaS platform on AWS that sends emails on behalf of its many customers from a single Amazon SES account. They have followed the recommended approach by implementing a 1:1 mapping between tenants and customers. This strategy provides complete reputation isolation for each customer and makes sure the ISV operating the Amazon SES account can automatically prevent deliverability issues from one customer from impacting others. This architecture minimizes the reputation damage a single tenant can cause to the ISV’s Amazon SES account’s shared resources like IP pools and domains, making it the optimal choice for maintaining high deliverability standards.

Resource sharing strategies

Your resource allocation strategy depends on your business model and customer segmentation. Consider the following recommended approaches:

  • Complete resource isolation – Each customer, brand, BU, or workload is assigned to individual, dedicated resources using the tenant configuration. This approach offers the simplest migration path: associate each customer’s dedicated resources (identities, IP addresses) with their corresponding tenant and continue operations with enhanced isolation. This model works well for enterprise customers and ISVs who deploy dedicated IPs for customers and require complete separation.
  • Tiered resource sharing – Organizations and ISVs with service tiers (such as free, pro, and VIP) can align tenants and resource sharing with these tiers. Free-tier customers might use the free-tier tenant and share basic resources, pro customers access the pro-tier tenant that maps to enhanced shared resources, and VIP customers receive dedicated resources, often using VIP customer-specific tenants. This balances cost-efficiency with appropriate isolation levels for each customer segment.
  • Extensive resource sharing – Even when email streams share most resources in the Amazon SES account, such as sending identities and IP pools, tenant isolation tenant isolation allows you to protect the sender reputation for independent email streams you send through the shared resources. Consider grouping similar types of customers or email streams into a single tenant or assigning a certain number of customers to each tenant. Although the grouping might be varied, the Amazon SES tenant feature can still minimize the effect of problematic email streams in any one tenant from affecting other tenants by pausing the offending tenant. This helps avoid Amazon SES account-level reputation damage to shared resources, protecting customers and stakeholders using those resources.

Tenant limits and quotas

Amazon SES provides tenant limits to accommodate various organizational scales. The default limit supports 10,000 tenants per account, which is sufficient for most organizations. Qualifying accounts can request increases for more tenants as needed through the Service Quotas console.

Importantly, implementing tenants doesn’t impact your account-level sending quotas or transactions per second (TPS) limits; these remain unchanged and continue to apply across the tenants within your account.

Low-friction adoption

Amazon SES tenants are designed for low-friction adoption, providing isolated containers for your existing Amazon SES email infrastructure. With tenants, you can continue sending emails using your current domains, configuration sets, and IP pools while progressively associating them with appropriate tenants. If you use them, your senders can continue targeting their assigned configuration set. After you’ve configured your Amazon SES account with tenants, instruct your customers or event buses to add a new Amazon SES API call or SMTP header specifying their unique tenant ID. If you aren’t yet using configuration sets, we’ve found that the introduction of the Amazon SES tenants feature often serves as a compelling reason for ISVs and large organizations to adopt configuration sets. If your organization has been looking for ways to offer enhanced email services to existing customers, you might want to use this opportunity to review customer accounts and offer dedicated IPs or email archiving to important email workloads. The ability to gradually roll out the Amazon SES tenants feature facilitates selective adoption, starting with a subset of customers, brands, event buses, or workloads before expanding to your entire customer base. This phased approach enables testing and refinement of your tenant strategy without disrupting existing email operations. Organizations can validate their tenant configuration with low-risk customers before rolling out to critical segments, providing a smooth transition to enhanced reputation management.

Conclusion

In this post, we discussed key aspects of the migration process to Amazon SES tenants, from initial assessment to tenant structure planning. Part 3 of this series will cover details of IAM configuration, monitoring setup, and practical implementation examples. Whether you’re running a SaaS platform, managing multiple brands, operating separate BUs, or just starting to use Amazon SES for your organization, the tenant feature provides simple reputation isolation, boosts efficiency, and helps create better experiences for users.

To learn more, refer to the following resources:


About the authors

Track OTP success with AWS End User Messaging SMS feedback

Post Syndicated from Rommel Sunga original https://aws.amazon.com/blogs/messaging-and-targeting/track-otp-success-with-aws-end-user-messaging-sms-feedback/

In this post, we show how to implement message feedback for SMS one-time passwords (OTPs) using AWS End User Messaging. OTP verification through SMS is a fundamental component of modern authentication systems. Although sending OTPs follows an established pattern, tracking their delivery and usage presents several challenges. This post shows how to implement the AWS End User Messaging Message Feedback API to monitor OTP delivery and conversion rates effectively. This post highlights the Message Feedback API in an OTP use case; for practical examples and detailed guidance on building a secure OTP architecture, see Build a Secure One-Time Password Architecture with AWS.

Challenges with OTP tracking

Organizations commonly face these key challenges with OTP tracking:

  • Relying solely on Delivery Receipt (DLR) data for confirming message delivery, which is third-party carrier data that can be subject to interpretation by carriers or message providers, whereas conversion tracking through message feedback provides first-party data that can more accurately reflect actual message delivery and usage
  • Measuring accurate user authentication success rates
  • Identifying OTP verification issues across different geographic regions, carriers and delivery paths

To address these challenges, you can use the AWS End User Messaging Message Feedback API to track delivery and conversion rates, providing first-party data for more accurate insights into message delivery and usage patterns. Although OTP use cases are the most common and serve as our example implementation of message feedback, the same tracking logic can also be applied to other types of SMS conversions, such as promotional link clicks, shopping cart additions, account activations, appointment confirmations, and delivery notifications.

Solution overview

The OTP message flow consists of two main phases. Let’s first examine how the system handles the initial OTP request.

Phase 1: OTP request flow

When a customer initiates an OTP request, your system begins a carefully orchestrated process. First, your application receives this request and generates a unique OTP. With the OTP generated, your system prepares to send it through the AWS End User Messaging API, specifically enabling message feedback tracking by setting the MessageFeedbackEnabled parameter to true when calling SendTextMessage.

Upon successful sending, it returns a unique message ID, which your system must store alongside the generated OTP. This message ID serves as a crucial tracking identifier for the entire verification process. The message is then dispatched to the customer’s device, and your system enters a waiting state, ready to process the verification attempt.

The following diagram illustrates the OTP request flow.

OTP Request Flow Diagram

Phase 2: OTP verification flow

The verification process begins when the customer receives the OTP through SMS and submits it back to your system. Upon receiving the submission, your system first validates the OTP against the stored value. This verification step is critical, because its outcome determines how you will update the message feedback status.

If the customer successfully verifies the OTP, your system calls the PutMessageFeedback API with the stored message ID and sets the status to "RECEIVED", indicating successful delivery and usage of the OTP. However, if the verification fails or the customer doesn’t respond within the timeout period, your system sets the status to "FAILED".

If your system doesn’t explicitly update the feedback status within 1 hour, AWS automatically sets it to "FAILED".

The following diagram illustrates the OTP verification flow.

Prerequisites

Before you begin implementing OTP message feedback, make sure you have the following components and permissions in place:

Send SMS with message feedback enabled

You can enable message feedback in two ways. The first method is to use the MessageFeedbackEnabled parameter when sending an SMS, the second is to send a message with a configuration set with message feedback already enabled. Using a configuration set is often more convenient for bulk implementations because you don’t need to specify message feedback settings in each API call.

To send an SMS with message feedback enabled directly, you can use the following function:

import boto3

# Initialize the End User Messaging client
client = boto3.client('pinpoint-sms-voice-v2')

def send_otp_with_feedback():
    # Generate a unique OTP
    otp = generate_otp()  
    
    # Send SMS with feedback enabled
    response = client.send_text_message(
        DestinationPhoneNumber='+15555550123',  # Replace with your destination phone number
        OriginationIdentity='+14255550120',  # Replace with your origination identity
        MessageBody=f'Your verification code is: {otp}',
        MessageFeedbackEnabled=True
    )
    
    # Store OTP details for verification
    store_otp_details(response['MessageId'], otp)
    return response['MessageId']

The function uses the following details:

  • store_otp_details() is a placeholder function where you store the OTP details in a database for later retrieval
  • generate_otp() is a placeholder function where you generate your OTPs to send using SMS

If you prefer to use a configuration set with message feedback enabled, you can use the following alternative function:

def send_otp_with_feedback_using_configuration_set():
    # Initialize the End User Messaging client
    client = boto3.client('pinpoint-sms-voice-v2')
    
    # Generate OTP
    otp = generate_otp()
    
    # Send SMS using configuration set
    response = client.send_text_message(
        DestinationPhoneNumber='+15555550123',  # Replace with your destination phone number
        OriginationIdentity='pool-201d59fffd554bdfbaf9ee8aEXAMPLE',  # Replace with your origination identity
        MessageBody=f'Your verification code is: {otp}',
        ConfigurationSetName='example-us-east-configuration-set'  # Replace with your configuration set name
    )
    
    # Store OTP details for later verification
    store_otp_details(response['MessageId'], otp)
    
    return response['MessageId']

Your configuration set must have message feedback enabled to use this option. You can enable it using the AWS Command Line Interface (AWS CLI) with the following command:

aws pinpoint-sms-voice-v2 set-default-message-feedback-enabled \
--configuration-set-name "YourConfigSetName" \
--message-feedback-enabled

Another option is to use the AWS End User Messaging console, where you can enable message feedback under Set Settings for the desired configuration set.

Update feedback

After you send a message, you can update the message status to indicate whether a user has successfully completed an action, such as entering the OTP on your application or webpage:

def update_message_feedback(message_id: str, status: str) -> dict:
    try:
        # Initialize the End User Messaging client
        client = boto3.client('pinpoint-sms-voice-v2')
        
        # Update the message feedback status
        response = client.put_message_feedback(
            MessageId=message_id,
            MessageFeedbackStatus=status
        )
        
        return response
        
    except Exception as e:
        print(f"Error updating message feedback: {str(e)}")
        raise

# Example usage
message_id = "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"  # Replace with your message ID
status = "RECEIVED"  # Use "FAILED" for unsuccessful verifications

result = update_message_feedback(message_id, status)
print(f"Feedback status updated: {result}")

Verify feedback metrics

The AWS End User Messaging dashboard provides comprehensive metrics to help you monitor your OTP performance. The following metrics are available for customizable time periods:

  • Number of messages with feedback completion
  • Percentage of messages with feedback completion
  • Number of SMS with feedback completion by country

To review your application’s overall message feedback metrics, choose Dashboard in the AWS End User Messaging console navigation pane, then choose Message Feedback Metrics.

The dashboard presents three key metrics:

  • Number of messages with feedback completion – The count of SMS and MMS messages where the message feedback record is set to RECEIVED
  • Percentage of messages with feedback completion – The percentage of SMS and MMS messages where the message feedback record is set to RECEIVED
  • Number of SMS with feedback completion by country – The count of message feedback received by country

The progression to 100% completion indicates optimal system performance, where all sent OTPs were successfully received and verified by users, and the message feedback record is set to RECEIVED within the expected timeframe. This high completion rate suggests effective message delivery and a smooth user verification experience. Variations in completion rates across countries can help identify potential regional delivery challenges or user behavior patterns.

The 30% conversion starting point shown in this example is used for illustration purposes only, demonstrating messages that were intentionally left unconverted during testing.

Best practices for OTP implementation

For a secure and reliable OTP implementation, follow these best practices to balance security with user experience:

  • Include rate limiting to prevent abuse
  • Implement proper timeout mechanisms for OTPs
  • Make sure error handling provides clear feedback to users
  • Maintain comprehensive logging for security audits

Conclusion

By implementing the Message Feedback API for OTP tracking, you can gain valuable insights into your authentication system’s effectiveness in real time. This approach helps you monitor successful OTP usage and identify potential delivery issues that might affect user authentication, with granular metrics broken down by geographic regions. The data collected through message feedback offers a more accurate picture of actual user interactions compared to carrier-provided delivery receipts, helping you make data-driven decisions about your authentication system.

To build upon this foundation, consider implementing Amazon CloudWatch alerts for your conversion metrics, and optimizing your message templates based on performance data. The combination of real-time feedback, detailed analytics, and proactive monitoring can help make sure your OTP system remains both secure and efficient.

For additional implementation guidance and best practices, refer to the following resources:


About the authors