|
| 1 | +# Introduction |
| 2 | + |
| 3 | +This repository implements an Infrastructure as Code (IaC), serverless stack that exposes a REST API for sending |
| 4 | +SMS & WhatsApp messages to your customers while handling conversation windows with WhatsApp destinations |
| 5 | +(more on this below). |
| 6 | + |
| 7 | +It uses AWS End User Messaging as its communications platform and logs message history and handles |
| 8 | +WhatsApp user consent automatically in Amazon DynamoDB. |
| 9 | + |
| 10 | +[TOC] |
| 11 | + |
| 12 | +# Requirements |
| 13 | + |
| 14 | +The code has been tested with Python 3.12 in macOS. You will also need: |
| 15 | +* [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) |
| 16 | +* Python 3.12 |
| 17 | +* Docker or Podman for compiling the Lambda images |
| 18 | +* The requirements defined in [`requirements.txt`](requirements.txt) |
| 19 | +* SMS-related requirements in AWS End User Messaging SMS: |
| 20 | + - [Configuration set](https://docs.aws.amazon.com/sms-voice/latest/userguide/configuration-sets.html) |
| 21 | + - [Phone number or sender ID](https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-number-types.html). This is |
| 22 | + referred to as the "originating entity" later in this document. |
| 23 | +* WhatsApp-related requirements: |
| 24 | + - A [WhatsApp Business Account](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-waba.html) |
| 25 | + [configured in AWS End User Messaging Social](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-phone-numbers-add.html). |
| 26 | + + If you only have configured a single WhatsApp phone number, the solution will use that for sending messages. |
| 27 | + For other use cases and for efficiency purposes, you can specify the phone number to use when deploying the |
| 28 | + solution. |
| 29 | + + The Business Account in End User Messaging Social must be configured with a |
| 30 | + [message and event destination](https://docs.aws.amazon.com/social-messaging/latest/userguide/managing-event-destinations.html) |
| 31 | + pointing to an SNS topic that this solution will use for tracking SMS message delivery. |
| 32 | + - A default WhatsApp template in English requesting that your users to connect with you. When you try to send a |
| 33 | + WhatsApp message to a number that has not communicated with you in the last 24h, the solution will send this |
| 34 | + template to the user and keep the message in a queue for 2h. If the customer replies to your template in this 2h |
| 35 | + window your original message will be automatically sent to the user automatically. |
| 36 | + |
| 37 | +# Architecture |
| 38 | + |
| 39 | +The diagram below ilustrates the main components of the architecture of the solution and their dependencies. |
| 40 | +There are two main flows of information: |
| 41 | + |
| 42 | +* The flow which sends the SMS/WhatsApp messages to the end users, automatically tracking user consent in a |
| 43 | + specific DynamoDB table. |
| 44 | +* The flow which receives the message notifications from either SNS or EventBridge and tracks their status in a |
| 45 | + separate DynamoDB table. This flow also handles the case where the end user writes a WhatsApp message to the |
| 46 | + phone associated with the WhatsApp Business Account and registers their consent receive free-text messages. |
| 47 | + |
| 48 | + |
| 49 | + |
| 50 | +# Sending messages |
| 51 | + |
| 52 | +## SMS |
| 53 | + |
| 54 | +Sending SMS messages is handled with AWS End User Messaging SMS and in order to be able to send SMS you will need to |
| 55 | +register a Configuration set and a phone number or sender ID, as described above. |
| 56 | + |
| 57 | +The tracking of SMS delivery is performed by monitoring |
| 58 | +[EventBridge events](https://docs.aws.amazon.com/sms-voice/latest/userguide/monitor-event-bridge.html) and follows |
| 59 | +the principles described [below](#observability). |
| 60 | + |
| 61 | +## WhatsApp |
| 62 | + |
| 63 | +In WhatsApp you generally cannot send free-form messages to users unless they have contacted you in the previous 24 |
| 64 | +hours. In order to contact new users, you must either have them send you a message or send them |
| 65 | +[a Meta-approved template](https://developers.facebook.com/docs/whatsapp/message-templates/guidelines/) asking the |
| 66 | +destination user to write back to you. When they do, you are allowed to send free-text messages to your users for the |
| 67 | +next 24 hours. |
| 68 | + |
| 69 | +The communication flow for talking to your clients in WhatsApp is as follows: |
| 70 | + |
| 71 | +1. You send a pre-approved message template to your customer asking them to write back to you. This message should |
| 72 | + explicitly ask the user to not answer anything if they do not want to be contacted. |
| 73 | +2. If the user answers with any text you can start sending free-form messages for the next 24 hours. |
| 74 | +3. After 24 hours the communication window closes and you have to send a new template to the customer in order to be |
| 75 | + able to send free-form messages. |
| 76 | + |
| 77 | +This solution tracks user communications with your number by automatically sending a template as needed and only trying |
| 78 | +to send free-form messages if the user has responded to the template. If the user does not respond 2 hours after the |
| 79 | +template is sent to them, the initiating message is discarded. |
| 80 | + |
| 81 | +The following flow is executed when you send a request to the REST API endpoint to send a free-form message: |
| 82 | + |
| 83 | +```mermaid |
| 84 | +flowchart TD |
| 85 | + Client([Original free-form message]) -->|POST /v1/sendWhatsApp| API[API Gateway] |
| 86 | + API -->|Validates & authorizes| Q["`WhatsApp Queue |
| 87 | + (2h TTL)`"] |
| 88 | + Q --> E{{User wrote to us in the last 24h?}} |
| 89 | + E -->|Yes|L([Send free-form message]) |
| 90 | + E -->|No|T{{Template sent?}} |
| 91 | + T -->|Yes|W[Return message to queue] |
| 92 | + T -->|No|ST[Send template] |
| 93 | + ST -->W |
| 94 | + W -->Q |
| 95 | + |
| 96 | + style E fill:#009,color:#ddd |
| 97 | + style T fill:#009,color:#ddd |
| 98 | +``` |
| 99 | + |
| 100 | +This handling is transparent to you, and you are only responsible for sending the initial request to send a free-form |
| 101 | +message. |
| 102 | + |
| 103 | +Once you make the initial request to the API to send the message, you can track its status as described |
| 104 | +[below](#observability). |
| 105 | + |
| 106 | +# Observability |
| 107 | + |
| 108 | +Metadata about the messages and whether they were delivered or not (but not the messages themselves) is stored in |
| 109 | +a DynamoDB table for observability purposes. This data does not, however, contain destination phone numbers and has a |
| 110 | +TTL of 1 year. These messages are identified by their AWS End User Messaging message ID and (sometimes) by their |
| 111 | +WahtsApp message ID, but contain no other perrsonally identifiable information. |
| 112 | + |
| 113 | +An entry in the message tracking table will typically contain the following fields: |
| 114 | + |
| 115 | +* `type`: Message type (either `sms` or `whatsapp`) |
| 116 | +* `eum_msg_id`: AWS End User Messaging message ID. This is a random unique id. |
| 117 | +* `wa_msg_id`: Meta-provided WhatsApp Message ID. Only available for WhatsApp messages and only once |
| 118 | + Meta server have processed the message send request. Contains `__UNKOWN__` for SMS messages or WhatsApp messages |
| 119 | + that have not yet been processed by Meta. |
| 120 | +* `delivery_history`: Map with the history of the ISO-formatted instants when the message transitioned states. |
| 121 | +* `expiration_date`: The UTC timestamp when the memssage will expire. |
| 122 | +* `latest_status`: The most recent delivery status for the message. |
| 123 | +* `latest_update`: The UTC timestamp when the message delivery information was last updated. |
| 124 | +* `registration_date`: The ISO-formatted instant when the message was registered. |
| 125 | + |
| 126 | +The status a message transverses through its lifecyle are: |
| 127 | + |
| 128 | +* `unknown`: Message status is unknown. Unused. |
| 129 | +* `failed`: Message delivery has failed. Unused. |
| 130 | +* `sent_for_delivery`: Message has been processed by this stack and sent to AWS End User Messaging for delivery. |
| 131 | +* `sent`: Message has been sent to the user. Does not gguarantee that the user has received it. |
| 132 | +* `delivered`: Message has been delivered to the user's terminal. Does not guarantee that the user has read it. Also, |
| 133 | + SMS carriers might not provide us with this information so correctly delivered SMS messages might not be marked as |
| 134 | + `delivered` in the table. |
| 135 | +* `read`: [WhatsApp specific] The message has been shown to the user in the WhatsApp application. |
| 136 | + |
| 137 | +# Deployment sample |
| 138 | + |
| 139 | +```bash |
| 140 | +# Run this only if using Podman instead of Docker |
| 141 | +export CDK_DOCKER=podman |
| 142 | +# Deploy the solution |
| 143 | +cdk deploy \ |
| 144 | + --parameters ConfigurationSetArn='${CONFIGURATION_SET_ARN}' \ |
| 145 | + --parameters OriginatingEntity='${ORIGINATING_PHONE_ARN}' \ |
| 146 | + --parameters WhatsAppNotificationTopicARN='${SNS_TOPIC_ARN}' \ |
| 147 | + --parameters MessageType='TRANSACTIONAL' \ |
| 148 | + --parameters WATemplate='${WHATSAPP_TEMPLATE_NAME}' \ |
| 149 | + --parameters WAPhoneNumberARN='${WHATSAPP_PHONE_NUMBER_ARN}' |
| 150 | +``` |
| 151 | + |
| 152 | +You will get several outputs if everything is correct, they're referenced in the step below as the following fields: |
| 153 | +* `RestAPIAPIKey`: the ID of the Rest API key |
| 154 | +* `RestAPISMSApiGateway`: the URL of the SMS send endpoint in API Gateway |
| 155 | +* `RestAPIWhatsAppApiGateway`: the URL of the WhatsApp send endpoint in API Gateway |
| 156 | + |
| 157 | +# Message sending sample |
| 158 | + |
| 159 | +```bash |
| 160 | +# Send a SMS message |
| 161 | +curl -X POST -H "x-api-key: $(aws apigateway get-api-key --api-key ${RestAPIAPIKey} --include-value | jq -r .value)" -H "Content-Type: application/json" -d '{"destination_number": "${DESTINATION_NUMBER}", "message_body": "${MESSAGE_BODY}"}' ${RestAPISMSApiGateway} |
| 162 | +# Send a WhatsApp message |
| 163 | +curl -X POST -H "x-api-key: $(aws apigateway get-api-key --api-key ${RestAPIAPIKey} --include-value | jq -r .value)" -H "Content-Type: application/json" -d '{"destination_number": "${DESTINATION_NUMBER}", "message_body": "${MESSAGE_BODY}"}' ${RestAPIWhatsAppApiGateway} |
| 164 | +``` |
| 165 | + |
| 166 | +# Future work |
| 167 | + |
| 168 | +More work is required to turn this code into a production sample. Some ideas for future improvement: |
| 169 | + |
| 170 | +* WhatsApp delivery error handling in particular should be improved. While the solution should handle 24h |
| 171 | + WhatsApp communication windows automatically and re-sends the default template if needed, it does not |
| 172 | + handle the case where delivery to WhatsApp phone numbers fails for whatever reason. |
| 173 | + The logic for handling these failures can be found in the [`wa_status_handler`](lambda/wa_status_handler/main.py) |
| 174 | + lambda code. |
| 175 | +* Also, the WhatsApp sending logic only sends English templates. WhatsApp templates can be configured per-language, so |
| 176 | + you will most likely want to make the template sending logic configurable per-language. |
| 177 | + The handling code is located in the [`send_whatsapp`](lambda/send_whatsapp/main.py) lambda. |
| 178 | +* In the WhatsApp flow, if the user answers to the template message more than 2h after the template has been sent (and |
| 179 | + therefore the initiating free-form message has already been automatically discarded) no extra communication is sent, |
| 180 | + which can be confusing for users. Extra work should be done to improve the UX for these cases (maybe by sending a |
| 181 | + specific message explaining that the original message has expired?). |
| 182 | +* The solution only supports sending basic message types. WhatsApp supports a |
| 183 | + [wide variety of rich messages](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/send-messages). The |
| 184 | + solution could be extended to support these different message types. |
0 commit comments