AWS Messaging & Targeting Blog

How to Implement Self-Managed Opt-Outs for SMS with Amazon Pinpoint

Amazon Pinpoint offers marketers and developers the ability to send SMS to over 240 countries and/or regions around the world; giving users the global reach, scalability, cost effective pricing, and high deliverability that is required to build a successful SMS program. SMS is a flexible communication channel that facilitates different business requirements, including One-Time Password (OTP), reminders, and bulk marketing to name a few. Regardless of the content that you are sending via SMS there is a requirement to manage your recipients’ opt-in/out status. Read on to learn your two options for managing opt-outs and how you can configure them. Amazon Pinpoint offers a fully managed opt-out capability and the ability to self-manage the process with your own tools.

NOTE: If you are sending to US numbers with a toll-free (TFN) number the carriers will automatically manage those numbers and are not eligible for either of these processes.

Managed Opt-Out Process
If you prefer to have Pinpoint manage your opt-out processes you can refer to our blog “How to Manage SMS Opt-Outs with Amazon Pinpoint” to learn how to configure keywords and opt-out lists.

Self-Managed Opt-Out Process
Many customers use Pinpoint’s Managed Opt-Out process to deliver their communications, but some scenarios require the ability to self-manage this process. Self-managing the opt-out process provides more granular control over customer communication preferences and allows customers to centralize those preferences within their own applications.
Common reasons for organizations to implement self-managed opt-out include but aren’t limited to:

  1. Have an existing self-managed opt-out capability with a standing toolset that is already integrated with other aspects of their communication stack.
  2. Need multiple options for their customers to manage the communication preferences such as a web portal, call center, and mobile application to name a few.
  3. Require full control in order to implement custom logic that caters to their business needs.
  4. Want to change their SMS provider to Pinpoint while not changing what they have already built within an existing application.

How to implement Self-Managed Opt-Outs with Pinpoint
Choosing to self-manage your opt-outs requires some configuration within Pinpoint and the use of other AWS services. The solution outlined in this blog will use Amazon Pinpoint in addition to the following services:

  1. AWS Lambda
  2. Amazon DynamoDB
  3. Amazon SNS

NOTE: If you have existing services/applications that allow you to implement similar functionality as explained in this blog, you don’t have to use these services listed above.

What’s in scope?

This blog covers the following scenario:

  1. SMS is being sent with Amazon Pinpoint SMS and Voice V2 API – SendTextMessage
  2. You specify the Origination Identity (OID) to be used (short code, long code, 10DLC, etc) as the parameter to send the SMS to the destination phone number.

While the following scenarios can be self-managed this blog does not cover the following cases:

  1. You use a phone-pool to send SMS.
  2. You do not specify an OID in your call SendTextMessage call and let Amazon Pinpoint figure out and use the appropriate OID.

Keywords in scope

  1. Opt-out keywords – All Opt-out keywords mentioned in this document are included in the code.
  2. Opt-in keywords – This blog considers ‘JOIN’ as a valid keyword that SMS recipients can respond with to opt back in for the SMS communication.

NOTE: The code examples in this blog can be modified to add any additional custom keywords for your use case.

Assumptions/Prerequisites

  1. You have the necessary permissions to configure the following services in the same AWS account and region where Amazon Pinpoint is implemented.
    a. AWS Lambda
    b. Amazon DynamoDB
    c. Amazon SNS
  2. Your instance of Amazon Pinpoint has at least one OID approved and provisioned to send SMS.
    a. If you need help to determine what OID fits your use case(s) use this guide
    b. NOTE: Sender IDs do not have an ability to receive 2-way communication. If you are using Sender IDs, you still must manage opt-outs, but must do so by offering alternative ways of opting out such as a web portal, call center, and/or mobile applications.

Solution Overview

The solution proposed in this blog is fully serverless arhitecture and uses AWS managed services to eliminate a need for you to maintain and manage any of the infrastructure components.

  1. Your application invokes AWS Lambda function ‘InvokeSendTextMessage’ which calls Amazon Pinpoint SMS and Voice V2 API – SendTextMessage.
  2. The AWS Lambda function called ‘InvokeSendTextMessage’ performs the following tasks:
    2a. Fetches the latest item based on the descending order of the timestamp with the destination phone number and OID from Amazon DynamoDB table ‘SMSOptOut‘.
    2b. If an item is found with the customer response as any valid keyword for Opt-out (Refer section Keywords in scope), the process stops and Amazon Pinpoint APIs won’t be called by InvokeSendTextMessage function as the customer chose to opt-out.
    2c. If an item is not found or is found with the customer response as any valid keyword for Opt-in (Refer section Keywords in scope), the function calls Amazon Pinpoint SMS and Voice V2 API – SendTextMessage to deliver it to the customer/destination phone number.

NOTE: Amazon DynamoDB table can also be configured to receive the Opt-out or Opt-in information through various other channels (app, website, customer care etc.) if you have multiple interfaces for customer to do so but that is not in the scope for this blog.

Refer to the section, ‘InvokeSendTextMessage function code’ to understand the sample AWS Lambda function code. The code uses Python 3.12 language.

  1. The message is successfully delivered to a destination phone number.
  2. If the customers responds to the same OID (because you have enabled 2-way SMS feature) with a keyword that is a valid value from all the keywords in scope (Refer section Keywords in scope), Amazon SNS topic is configured with Amazon Pinpoint which captures the customer response.
    Note: The other keywords are not in scope for this blog, but you can specifically add all possible keywords that customers can respond with. There can be an accidental responses by a customer which will be ignored by the AWS Lambda code.
  3. AWS Lambda function ‘AddOptOutInDynamoDB’ is a subscriber to the topic in Amazon SNS and processes customer responses.
  1. AWS Lambda function ‘AddOptOutInDynamoDB’ performs few tasks as described below
  2. 6a. If the customer response is a keyword that is a valid value from all the keywords in scope (Refer section Keywords in scope), Amazon Lambda function ‘AddOptOutInDynamoDB‘ extracts the OID and customer phone number information from the response and adds the entry in the Amazon DynamoDB table ‘SMSOptOut’. This way Amazon DynamoDB table keeps getting latest customer opt-in/opt-out status.
    6b. Once the item is successfully put in dynamodb table ‘SMSOptOut‘, if the customer response was any Opt-out keyword (Refer section Keywords in scope), the function sends a SMS to the customer who has just opted out to confirm the status. “YOU HAVE BEEN UNSUBSCRIBED. IF THIS WAS A MISTAKE PLEASE TEXT “JOIN” TO THIS NUMBER TO BE RESUBSCRIBED”.
    If the customer response was any Opt-in keyword (Refer section Keywords in scope) the function sends a SMS to the customer who has just opted back in to confirm the status. “YOU HAVE BEEN SUBSCRIBED. IF THIS WAS A MISTAKE PLEASE TEXT “STOP” OR “UNSUBSCRIBE” TO THIS NUMBER TO BE UNSUBSCRIBED”. (But SMS recipients can still respond with any valid keyword mentioned in Opt-out keyword list)

NOTE: Refer the section ‘AddOptOutInDynamoDB function code’ to understand the sample code. The code uses Python3.12 language.

  1. The Opt-in/Opt-out status confirmation SMS is successfully delivered to a customer/destination phone number.

Amazon Pinpoint setup

  1. Enable 2-way SMS messaging for the OID that you procured. Refer to the screenshot below for your reference.

2-way SMS setting:

  1. Enable the self-managed opt-out feature for the OID. Once enabled, Amazon Pinpoint does not respond to opt-out messages sent to the SNS topic by your recipients. You can collect the response from the customers in an AWS SNS topic and process it as per your business needs.

Self Managed Opt-Out feature setting:

Amazon SNS setup

  1. On Amazon SNS console, click on ‘Topics’ and then ‘create a topic’ as shown below.

  1. Click ‘Create Subscription‘ to add the Lambda function ‘AddOptOutInDynamoDB‘ as a subscriber by using the Amazon Resource Name (ARN).

Amazon DynamoDB Setup

Table Name: ‘SMSOptOut

The Customer phone number is used as the primary key(PK). The sort key(SK) contains multiple values that include OID, timestamp, and the customer response separated by #. By having generic attribute names as PK and SK, you can expand the usage of this table for accommodating any custom business needs. For example: Customer can use any of the individual phone numbers like short code, long code, or 10DLC to send the SMS and any of these values can be accomodated as a part of the sort key (SK). The sort key can be used for granular retrieval to see the latest customer status (For example: ‘STOP‘). It can then additionally have attributes like OID, Timestamp, Response and others as per your requirements. The table uses On-demand Read/Write capacity mode. Refer to this document to understand On-demand capacity mode in detail.

Sample item in DynamoDB table is below


InvokeSendTextMessage function code

This AWS Lambda function calls Amazon Pinpoint SMS and Voice V2 API – SendTextMessage. It uses Query API for DynamoDB to scan the items for SourcePhoneNumber (customer phone number) and OID (Part of SK) in descending order of the timestamp. If an item exists with customer response value is a valid keyword for Opt-out (Refer section Keywords in scope), it means the customer has opted-out and the SMS can’t be sent. If no item is found or the customer response value is a valid keyword for Opt-in (Refer section Keywords in scope), the customer can be contacted and the funtion calls SendTextMessage API with the same OID and customer phone number.

import boto3
import os
from boto3.dynamodb.conditions import Key, Attr
import json

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('SMSOptOut')
pinpoint = boto3.client('pinpoint-sms-voice-v2')
OptOutKeyword = ['ARRET','CANCEL','END','OPT-OUT','OPTOUT','QUIT','REMOVE','STOP','TD','UNSUBSCRIBE']
OptInKeyword = ['JOIN']

#adds item in the DynamoDB table
def query_table(OID, SourcePhoneNumber):
    try:
        response = table.query(
            KeyConditionExpression=Key('PK').eq(SourcePhoneNumber) & Key('SK').begins_with(OID),
            ScanIndexForward=False,
            Limit=1
        )
    except Exception as e: 
        print("Error when writing an item in DynamoDB table: ",e) 
        raise e
    return response

#send opt-in/opt-out confirmation text using send_text_message.
def send_confirmation_text(SourcePhoneNumber,OID,messageBody,messageType): 
    
    try: 
        response = pinpoint.send_text_message(DestinationPhoneNumber=SourcePhoneNumber, 
        OriginationIdentity=OID,
        MessageBody=messageBody, 
        MessageType=messageType 
        ) 
    except Exception as e: 
        print("Error in sending text message using Pinpoint send_text_message api:", e) 
        raise e

#gets the message from SNS topic
def lambda_handler(event, context):
    OID = event['OID']
    SourcePhoneNumber = event['SourcePhoneNumber']

    response=query_table(OID, SourcePhoneNumber)
    items = response['Items']
    
    #Count number of items. The value will be either 1 or 0.
    count = len(items)

    # If the latest customer response is any OptOutKeyword
    if count == 1 and items[0]['Response'] in OptOutKeyword:
        print("Exit : Customer has opted out, do not send SMS")
    # If the latest customer response is any OptOutKeyword
    elif count == 0 or (count == 1 and items[0]['Response'] in OptInKeyword):
        send_confirmation_text(SourcePhoneNumber,OID,'This is a test message from Amazon Pinpoint','TRANSACTIONAL')
    # Only allowed values for customer response are valid OptOutKeyword or OptInKeyword 
    else: 
        print("The customer response is not one of the allowed keyword")

AddOptOutInDynamoDB Lambda function code

For example: When customer responds with ‘STOP’, the response is captured in the SNS topic that you configured with Amazon Pinpoint. The response json looks as shown below –

{
"originationNumber": "+1224xxxxxxx",
"destinationNumber": "+1844xxxxxxx",
"messageKeyword": "KEYWORD_xxxxxxxxxxxx",
"messageBody": "STOP",
"previousPublishedMessageId": "xxxxxxxxxxxxxx",
"inboundMessageId": "xxxxxxxxxxxxxx"
}

This Lambda function extracts the OID (destinationNumber), Customer phone number (originationNumber), and the customer response (messageBody) from the json payload above and adds an entry in the DynamoDB table (SMSOptOut). Once the item is put successfully in DynamoDB table, the function also sends out a confirmation SMS (either Opt-in or Opt-out) to the customer phone number using SendTextMessage.
For example:

  • If the customer response value is a valid keyword for Opt-out (Refer section Keywords in scope), the confirmation SMS is ‘YOU HAVE BEEN UNSUBSCRIBED. IF THIS WAS A MISTAKE PLEASE TEXT “JOIN” TO THIS NUMBER TO BE RESUBSCRIBED’.
  • If the customer response value is a valid keyword for Opt-in (Refer section Keywords in scope), the confirmation SMS is ‘YOU HAVE BEEN SUBSCRIBED. IF THIS WAS A MISTAKE PLEASE TEXT “STOP” OR “UNSUBSCRIBE” TO THIS NUMBER TO BE UNSUBSCRIBED’.
import json
import boto3
import datetime

dynamodb = boto3.resource('dynamodb')
dynamodb_table = dynamodb.Table('SMSOptOut')
pinpoint = boto3.client('pinpoint-sms-voice-v2')
OptOutKeyword = ['ARRET','CANCEL','END','OPT-OUT','OPTOUT','QUIT','REMOVE','STOP','TD','UNSUBSCRIBE']
OptInKeyword = ['JOIN']

#adds item in the DynamoDB table
def put_item(data,current_timestamp):
    print(dynamodb_table)
    try:
        response = dynamodb_table.put_item(
            Item = {
                'PK': data['originationNumber'],
                'SK': data['destinationNumber']+'#'+current_timestamp+'#'+data['messageBody'], 
                'OID': data['destinationNumber'],
                'Timestamp': current_timestamp,
                'Response': data['messageBody']
            }
        )
    except Exception as e:
        print("Error when writing an item in DynamoDB table: ",e)
        raise e

#send opt-in/opt-out confirmation text using send_text_message.
def send_confirmation_text(data,messageBody,messageType):
    try:
        response = pinpoint.send_text_message(
            DestinationPhoneNumber=data['originationNumber'],
            OriginationIdentity=data['destinationNumber'],
            MessageBody=messageBody,
            MessageType=messageType
            )
    except Exception as e:
        print("Error in sending text message using Pinpoint send_text_message api:", e)
        raise e
        
#gets the message from SNS topic
def lambda_handler(event, context):
    message = event['Records'][0]['Sns']['Message']
    data = json.loads(message)
    current_timestamp = datetime.datetime.now().isoformat()

    if data['messageBody'] in OptOutKeyword:
        put_item(data,current_timestamp)
        send_confirmation_text(data, 'YOU HAVE BEEN UNSUBSCRIBED. IF THIS WAS A MISTAKE PLEASE TEXT "JOIN" TO THIS NUMBER TO BE RESUBSCRIBED', 'TRANSACTIONAL')
    elif data['messageBody'] in OptInKeyword:
        put_item(data,current_timestamp)
        send_confirmation_text(data, 'YOU HAVE BEEN SUBSCRIBED. IF THIS WAS A MISTAKE PLEASE TEXT "STOP" OR "UNSUBSCRIBE" TO THIS NUMBER TO BE UNSUBSCRIBED', 'TRANSACTIONAL')
    else:
        print("The customer response is not one of the allowed keyword")

Clean Up

DynamoDB storage and any Lambda invocation will incur a cost so it is important to delete these resources if you do not plan on using them as shown below.

DynamoDB:

    1. On DynamoDB table, select table ‘SMSOptOut’ and click Delete. Confirm the action.

Lambda:

  1. On Lambda console, find the 2 functions you created and click Actions → Delete. Confirm the actions.

Amazon Pinpoint

  1. After deleting the DynamoDB table and Lambda functions your self-managed Opt-out flow will not work so you will need to disable self-managed opt-outs in Pinpoint for the respective OID as shown below.

Conclusion
In this post, you learned how to implement a self-managed opt-out workflow when using Pinpoint SMS. Keep in mind that when you implement the self-managed Opt-out flow, Pinpoint will not track or maintain any opt-out status for the OID that it was enabled for.

Take the time to plan out your approach, follow the steps outlined in this blog, and take advantage of any resources available to you within your support tier.

Decide what origination IDs you will need here
Review the documentation for the V2 SMS and Voice API here
Check out the support tiers comparison here

Resources:
https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-numbers-sms-by-country.html
https://aws.amazon.com/blogs/messaging-and-targeting/how-to-utilise-amazon-pinpoint-to-retry-unsuccessful-sms-delivery/
https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-sms-limitations-opt-out.html
https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-sms-simulator.html
https://docs.aws.amazon.com/dynamodb/
https://docs.aws.amazon.com/sns/
https://docs.aws.amazon.com/lambda/