Reply to SMS Messages with an ESP8266 with Amazon AWS IoT, Lambda, and API Gateway

February 23, 2017
Written by
Paul Kamp
Reviewed by
Kat King


Ever wish you could send a text to your toaster?  After today you'll be well on your way - we're going to show how to receive and reply to SMS or MMS messages with an Espressif ESP8266, Amazon AWS IoT, Lambda, and API Gateway.  We'll also look at TwiML, Twilio's markup language, and how Webhooks work with Twilio.

Let's carry on and look at a serverless solution to receiving SMS and MMS messages on a $2 piece of silicon.  (The toast part?  You'll have to figure that out.)

Going Serverless With Amazon AWS

This is our fourth guide exploring working with Twilio and Amazon's AWS services.  On some of the integration steps, we'll point you to a previous post where we detail the setup.  If you just want to skip ahead, you can clone the code behind this article from GitHub.

Here's a preview of this guide's architecture:

  • An ESP8266 communicating with Amazon's AWS IoT
  • Amazon's API Gateway to handle incoming Twilio Webhook requests, passing them to Lambda
  • Lambda will forward legal messages to the ESP8266 on the MQTT topic 'twilio'  
  • Responses from the ESP8266 will use the passthrough setup we detailed in our guide on sending MMS and SMS messages from an ESP8266 via AWS IoT and Amazon Lambda

Sending Messages:

Sending Messages with AWS IoT, Lambda, and the ESP8266

Receiving Messages:

Receiving SMS and MMS Messages from Twilio with AWS IoT and Lambda

Activating Amazon AWS IoT

In our article on sending messages with the ESP8266 and Lambda, we covered the AWS IoT setup in greater detail.  If you haven't yet gone through that article, we'd highly suggest you do so first - but we'll give a summary of the steps you'll need to complete.

  1. Add a new ESP8266 'Thing' and 'Type' in AWS IoT
  2. Create a new policy allowing access to the 'twilio' topic in MQTT (we used "iot:*").
  3. Create a new IAM user with permission to do everything on IoT.  Store the credentials to use on the ESP8266.
  4. From the 'Interact' tab of your created thing, retrieve the "Update to this thing shadow" MQTT topic and HTTPS Endpoint to use on the ESP8266.

Also highly suggested: keep a tab open with the Test MQTT Client subscribed to both the shadow update topic and the 'twilio' channel.

Setting Up a Mock API with Amazon API Gateway

We're going to use API Gateway to expose a route for Twilio to access.  We first went over this process in our receiving and replying to SMS and MMS messages using Amazon Lambda guide.

From the API Gateway console, create a new API with the 'Create API' button.  Give it a friendly name and description, then create the API.  Follow that up with a new resource 'message' (at '/message'), and create a 'POST' method on message.

For now, select 'Mock' as an 'Integration type' - we'll eventually add a Lambda function, but for now this'll suffice. Leave the API as is (don't deploy it yet) and let's start carving out our Lambda functions.

Exercising the Espressif ESP8266

While you'll be able to follow our MQTT posts without hardware, to complete this guide you'll need an ESP8266.  To maximize accessibility, we've targeted the Arduino IDE with this tutorial.  While we can't support other setups, we'd love to hear about your successes outside.

ESP8266 Board Selection

No board yet?  The Arduino on ESP8266 repository's tested board list is the best place to get started.  A tested development board variant is the quickest way to get up and running on the hardware.  You can always switch later when you have the MVP working.

We were using the ESP8266 Thing from Sparkfun while crafting this guide.  The Sparkfun Thing overloads the DTR pin and may cause trouble with your serial monitor.  We've added an option for SoftwareSerial debugging on top of HardwareSerial (or none).  Set your preference at the top of the .ino file.

Setting Up the ESP8266 For Your Accounts

You'll need to edit a number of variables at the top of the .ino file before getting started.  We covered exactly where to find each of the variables in our article on sending SMS or MMS messages from an ESP8266 with Lambda, but we'll briefly summarize now.

  • Gather credentials for your WiFi ssid and password.
  • Get an AWS key (for an IAM user with IoT permissions), AWS secret (for the same user), AWS IoT region, and the HTTPS endpoint from the 'Interact' section of the ESP8266 Thing's entry in AWS IoT.  
  • Change the your_device_number and number_to_text variables to an owned Twilio number and a phone that can receive texts, respectively.  Optionally change your_sms_message and optional_image_path to change the message you'll receive at power-on.

Adding Libraries to Arduino

We're relying on a few libraries for the guide today:

Using Arduino's Library Manager is possible for two of the libraries, but the others must be added manually. For a complete overview of library management on Arduino, see the official documentation.

Add Through Library Manager (Search)

  • ArduinoJSON
  • WebSockets

Add Manually to Arduino

The easiest way to get these libraries into Arduino is to install directly from the zip file once you download.

This can be done directly from the ZIP Library Installer in the Arduino IDE:

'Sketch' Menu -> 'Add .ZIP Library' -> select downloaded .zip file

Add ZIP Library to Arduino

Download links:

Building the Client Code and Programming the Board

If you've followed all the steps, programming the board should be as easy as setting the board and serial port of your ESP8266 in the Arduino IDE and clicking 'Upload'.  If you have the Test MQTT Client open, watch for a message on the 'twilio' topic to come from your ESP8266 as soon as you turn it on.

Receiving Incoming Messages on the ESP8266

The way the code handles incoming messages is to take a pointer to a function that you define that can handle a MQTT::MessageData reference.  MQTT::MessageData is a structure which contains a few things you'll need, including a reference to another struct 'Message'.

struct Message
    enum QoS qos;
    bool retained;
    bool dup;
    unsigned short id;
    void *payload;
    size_t payloadlen;

struct MessageData
    MessageData(MQTTString &aTopicName, struct Message &aMessage)  : message(aMessage), topicName(aTopicName)
    { }

    struct Message &message;
    MQTTString &topicName;

We've split our listeners into two functions for the example application, one on the Device Shadow update channel, and the other on the twilio topic.  In the twilio callback function, you can see how we shuttle the message body into a C-Style string, and use ArduinoJSON to parse some needed fields.  Finally, we reverse the string and send it back out to the twilio MQTT topic, where it'll eventually make its way to your phone.

Editor: we moved the files over from the repo when we migrated this post to the Twilio blog. You'll need:

  • TwilioLambdaHelper.cpp
  • TwilioLambdaHelper.hpp
  • Twilio_ESP8266_AWS_IoT_Example.ino

... in the same directory to compile the IoT side.

You'll need forwarding scripts to send and receive messages with Lambda, as well:

  • (Receive SMS)
  • (Send SMS)

The files are pasted throughout this post.

 * Twilio send and receive SMS/MMS messages through AWS IoT, Lambda, and API 
 * Gateway.
 * This application demonstrates sending out an SMS or MMS from an ESP8266 via
 * MQTT over Websocket to AWS IoT, which forwards it through AWS Lambda on to 
 * Twilio.  No local Twilio keys need be stored on the ESP8266.
 * It also demonstrates receiving an SMS or MMS via AWS API Gateway, Lambda, 
 * and AWS IoT.  An empty response is returned at the Lambda level and the 
 * ESP8266 uses the same path as the sending route to deliver the message.
 * This code owes much thanks to Fábio Toledo, odelot on Github.  It is based 
 * on his example of connecting to AWS over MQTT over Websockets:
#include <ESP8266WiFi.h>
#include <WebSocketsClient.h>

// AWS WebSocket Client 
#include "AWSWebSocketClient.h"

// Embedded Paho WebSocket Client
#include <MQTTClient.h>
#include <IPStack.h>
#include <Countdown.h>

// Handle incoming messages
#include <ArduinoJson.h>

// Local Includes
#include "TwilioLambdaHelper.hpp"

 * Start here for configuration variables.

 * IoT/Network Configuration.  Fill these with the values from WiFi and AWS.
 * You can use your AWS Master key/secret, but it's better to create a 
 * new user with all IoT roles. 
char wifi_ssid[]                = "YOUR NETWORK";
char wifi_password[]            = "NETWORK PASSWORD";
char aws_key[]                  = "IAM USER ACCESS KEY";
char aws_secret[]               = "IAM USER SECRET KEY";
char aws_region[]               = "IoT REGION";
char* aws_endpoint              = "ENDPOINT FROM AWS IoT";
const char* shadow_topic        = "$aws/things/YOUR_THING/shadow/update";

/* Twilio Settings - First is a Twilio number, second your number. */
char* your_device_number        = "+18005551212";
char* number_to_text            = "+18005551212";
char* your_sms_message          = "'RacecaR is a palindrome!";
//char* optional_image_path       = "";
char* optional_image_path       = "";

/* Optional Settings.  You probably do not need to change these. */
const char* twilio_topic        = "twilio";
int ssl_port = 443;

/* You can use either software, hardware, or no serial port for debugging. */

/* Pointer to the serial object, currently for a Sparkfun ESP8266 Thing */
#include <SoftwareSerial.h>
extern SoftwareSerial swSer(13, 4, false, 256);
Stream* serial_ptr = &swSer;
Stream* serial_ptr = &Serial;
Stream* serial_ptr = NULL;

/* Global TwilioLambdaHelper  */
TwilioLambdaHelper lambdaHelper(

 * Our Twilio message handling callback.  This is passed as a callback function
 * when we subscribe to the Twilio topic, and will handle any incoming messages
 * on that topic.
 * You'll want to add your own application logic inside of here.  For this 
 * demo, we'll take the first 160 characters of the message body and send it 
 * back in reverse and optionally write to a serial connection.
 * No, this doesn't handle unicode - prepare for weird results if you send
 * any non-ASCII characters!
void handle_incoming_message_twilio(MQTT::MessageData& md)
        MQTT::Message &message = md.message;
        std::unique_ptr<char []> msg(new char[message.payloadlen+1]());
        memcpy (msg.get(),message.payload,message.payloadlen);
        StaticJsonBuffer<maxMQTTpackageSize> jsonBuffer;
        JsonObject& root = jsonBuffer.parseObject(msg.get());
        String to_number           = root["To"];
        String from_number         = root["From"];
        String message_body        = root["Body"];
        String message_type        = root["Type"];

        // Only handle messages to the ESP's number
        if (strcmp(to_number.c_str(), your_device_number) != 0) {
        // Only handle incoming messages
        if (!message_type.equals("Incoming")) {

        // Basic demonstration of rejecting a message based on which 'device'
        // it is sent to, if devices get one Twilio number each.
        lambdaHelper.print_to_serial("\n\rNew Message from Twilio!");
        lambdaHelper.print_to_serial("\r\nTo: ");
        lambdaHelper.print_to_serial("\n\rFrom: ");

        // Now reverse the body and send it back.
        std::unique_ptr<char []> return_body(new char[161]());
        int16_t r = message_body.length()-1, i = 0;

        // Lambda will limit body size, but we should be defensive anyway.
        // uint16_t is fine because 'maxMQTTpackageSize' limits the total 
        // incoming message size.
        // 160 characters is _index_ 159.
        r = (r < 160) ? r : 159; 
        return_body.get()[r+1] = '\0';
        while (r >= 0) {
                return_body.get()[i++] = message_body[r--];
        // Send a message, reversing the to and from number

 * Our device shadow update handler.  When AWS has a Shadow update, you should
 * do whatever you need to do (flip pins, light LEDs, etc.) in this function.
 * (By default we'll just dump everything to serial if it's enabled.)
void handle_incoming_message_shadow(MQTT::MessageData& md)
        MQTT::Message &message = md.message;
        lambdaHelper.print_to_serial("Current Remaining Heap Size: ");

        std::unique_ptr<char []> msg(new char[message.payloadlen+1]());
        memcpy (msg.get(), message.payload, message.payloadlen);


/* Setup function for the ESP8266 Amazon Lambda Twilio Example */
void setup() {
        WiFi.begin(wifi_ssid, wifi_password);
        #if USE_SOFTWARE_SERIAL == 1
        #elif USE_HARDWARE_SERIAL == 1
        while (WiFi.status() != WL_CONNECTED) {
        lambdaHelper.print_to_serial("Connected to WiFi, IP address: ");

        if (lambdaHelper.connectAWS()){


 * Our loop checks that the AWS Client is still connected, and if so calls its
 * yield() function encapsulated in lambdaHelper.  If it isn't connected, the 
 * ESP8266 will attempt to reconnect. 
void loop() {
        if (lambdaHelper.AWSConnected()) {
        } else {
                // Handle reconnection if necessary.
                if (lambdaHelper.connectAWS()){

Simple, right?  That's really all it takes to handle incoming messages on the ESP8266.

Letting Lambda Handle Lots of Little Things

We're going to have two Lambda functions in the backend of our application: one for sending messages and one for receiving messages.

Sending Messages With Amazon Lambda

The first function we've already described in detail in the sending guide.  If you haven't yet done that exercise, we'd suggest visiting now before continuing this guide.  It details how to set up AWS IoT to talk to the ESP8266 and how to forward messages from the 'twilio' MQTT topic to Lambda.  We'll be using the exact same function to reply to messages, so that will need to be added to Lambda.

Briefly, here's what you need to do:

  1. Load the code from the 'Lambda Function Send SMS' directory in the GitHub repo into a new Lambda function
  2. Change the 'Handler' in the Configuration tab in Lambda to point to 'twilio_functions.iot_handler'
  3. Set up a trigger with the SQL Statement of SELECT * FROM 'twilio' WHERE Type='Outgoing', and in AWS IoT change the SQL version to '2015-10-08'
  4. Enter the environment variables AUTH_TOKEN and ACCOUNT_SID

Now, JSON messages published to the 'twilio' MQTT topic in AWS IoT will be forwarded to Lambda.  In Lambda we do a tiny bit of processing and then use the Twilio Python Helper Library to send SMS and MMS messages.

Receiving Messages with Lambda and Returning a Blank Reply

The second aspect is new for this article - we need a Lambda function to receive incoming requests from Twilio.

On your computer, start a new directory and include the content from the GitHub repository's 'Lambda Function Receive Message' directory inside.  Install the Twilio Python Helper Library inside the directory (personally, we used pip install with a target), and zip up the contents of the directory (but not the directory itself!).

Create a new Lambda function in the same region as the 'Mock' API and choose that API as the trigger to the new function.  Under role, select 'Create a New Role From Template', selecting 'AWS IoT Button permissions'.  Give that role a descriptive name - we'll need to visit it again after - and create your function.

Now in Lambda, change to the 'Code' tab, and select 'Upload a .ZIP File' from the 'Code entry type' menu.  Upload the zip file you created on your computer, and make sure you are using the Python 2.7 runtime.

Now in the 'Configuration' tab, change the handler to 'twilio_functions.twilio_webhook_handler'.  This is pointing to the file you just opened, and telling AWS to call the twilio_webhook_handler() function when triggered.

Very simple example of how to forward incoming Twilio webhooks to your IoT
devices using Lambda to publish to MQTT topics.

As a webhook comes in, we'll verify the webhook is from Twilio then extract the
key information and send to our IoT device(s) subscribed to our
topic.  Those devices will then presumably handle the information and react,
perhaps by sending a message back.

from __future__ import print_function

import json
import os
import boto3
import urllib
from twilio.request_validator import RequestValidator

def twilio_webhook_handler(event, context):
    """Receive request from Twilio and passes through to a topic."""
    print("Received event: " + str(event))
    null_response = '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' + \

    # Trap no X-Twilio-Signature Header
    if u'twilioSignature' not in event:
        return null_response

    form_parameters = {
        k: urllib.unquote_plus(v) for k, v in event.items()
        if k != u'twilioSignature'

    validator = RequestValidator(os.environ['AUTH_TOKEN'])
    request_valid = validator.validate(

    # Trap invalid requests not from Twilio
    if not request_valid:
        return null_response

    # Trap fields missing
    if u'Body' not in form_parameters or u'To' not in form_parameters \
            or u'From' not in form_parameters:
        return null_response

    # Don't let through messages with > 160 characters
    if len(form_parameters['Body']) > 160:
        return '<?xml version=\"1.0\" encoding=\"UTF-8\"?>' + \
               '<Response><Message>Keep it under 160!</Message></Response>'

    # Now we package up the From, To, and Body field and publish it to the
    # 'twilio' topic (or whatever you have set).  Boto3 lets us easily publish
    # to any topic in a particular region, but ensure you have correctly set
    # permissions for the role.  'iot:Publish' needs to be included for these
    # next lines to work.

    aws_region = os.environ['AWS_IOT_REGION']
    aws_topic = os.environ['AWS_TOPIC']
    client = boto3.client('iot-data', region_name=aws_region)

            "To": form_parameters[u'To'],
            "From": form_parameters[u'From'],
            "Body": form_parameters[u'Body'],
            "Type": "Incoming"

    # A blank response informs Twilio not to take any actions.
    # Since we are reacting asynchronously, if we are to respond
    # it will come through a different channel.
    # Even though we aren't responding to the webhook directly, this will all
    # happen very quickly.
    return null_response

Next, set four environment variables asked for in the code (also in the 'Code' tab of Lambda):

  • AWS_TOPIC ('twilio', with no quotes)
  • REQUEST_URL (For now, '' or blank - we'll come back to this)

Examining the Lambda Receive-and-Forward Python Code

You'll notice a few interesting details in this Lambda function.  

First, we validate messages from Twilio in the same manner as described in our validating incoming Twilio Webhooks with Python in Lambda article.  

Next, we check the message body is under 160 characters before we burden the ESP8266 with a new message.  (There are a few orders of magnitude more RAM on Lambda than on the ESP8266).

Finally, we use the in-built boto3 library to post to the 'twilio' MQTT topic in your AWS region.

Allowing Lambda to Post to MQTT Topics

Remember that role you created in Lambda?  If you deployed the API and let Twilio use it as of now, you'd find that Twilio can access your route just fine... but publishing to the 'twilio' MQTT topic is unauthorized.  Let's rectify that by applying the proper permissions to your Lambda function's role.

Go to IAM by using the 'My Security Credentials' in your name pulldown:

Go to Amazon IAM With The Menu Near Your Name

In there, click on the 'Roles' link in the left sidebar.  Find the role you assigned to this Lambda function and click on it.  Next, click on the 'Create Role Policy' button under 'Inline Policies':

Create Inline Role Policy in Amazon IAM

Now, you need to add an inline policy which allows Lambda to post to all topics:

    "Version": "2012-10-17",
    "Statement": [
            "Effect": "Allow",
            "Action": [
            "Resource": [

With that policy, your Lambda function is authorized to publish to MQTT topics.  Unless you turn off the validation steps you won't be able to test it yet - you'll have to wait until our final step, hooking up Twilio, for that.

Triggering Lambda with API Gateway

You will want to modify the 'Mock' API we built out earlier.  When you linked to it from the Lambda trigger, it should have already added the function as the integration to your API.  If it instead created a new resource and method, delete it - under the /message POST resource and method you can add the function directly from inside API Gateway.

Now we'll need to configure API Gateway now exactly as we did in our receiving and replying to SMS and MMS messages from Lambda article.  The full steps are detailed in that post, but here's the summary of what you'll need to do:

  1. In 'Method Request', add the X-Twilio-Signature HTTP Request Header to pass through to Lambda.
  2. In 'Integration Request', remove all Body Mapping Templates and add a new one for application/x-www-form-urlencoded with a template of:
    #set($httpPost = $input.path('$').split("&"))
    "twilioSignature": "$input.params('X-Twilio-Signature')",
    #foreach( $kvPair in $httpPost )
     #set($kvTokenised = $kvPair.split("="))
     #if( $kvTokenised.size() > 1 )
       "$kvTokenised[0]" : "$kvTokenised[1]"#if( $foreach.hasNext ),#end
       "$kvTokenised[0]" : ""#if( $foreach.hasNext ),#end
  3. In 'Integration Response', remove existing Body Mapping Templates and add one in HTTP status code 200 for application/xml.  Use this two line template:
    #set($inputRoot = $input.path('$')) 
  4. In 'Method Response', for HTTP Status 200 remove any existing response bodies and add one for application/xml.

If you did all that, you're ready to deploy!  From the 'Action' pulldown menu, 'Deploy API':

Deploy an API in API Gateway

You will be asked to choose an existing (or create a new) stage.  We called our stage 'prod'... feel free to name it whatever you'd like - just remember what it's called.  

After deploying, you'll get a URL to your new /message route.  Copy that exact URL, '/message' and all, into the REQUEST_URL environment variable of your Lambda function.  This is also the time to make sure the trigger is enabled on the Lambda side - make sure that the 'prod' stage of the API will trigger your function.

Keep that URL on your clipboard - you're going to use it one more time.

Configure Your Twilio Webhook URL

It's now time to plug Twilio into your Amazon-and-Thing setup. Log into your Twilio Account and go to the console.  Click the 'pound sign'/'hashtag' ('#') in the sidebar and navigate to 'Active Numbers'.

Existing numbers you have will show an icon if SMS and MMS are enabled.  You'll need one with both capabilities for the initial ESP8266 application's startup (it sends an MMS).  If you'd only likle to exercise SMS sending capabilities, set optional_image_path to an empty string before uploading to your board.


If you don't yet have a number, Twilio makes your search easy.  Select SMS and MMS before you start your search and we'll only return numbers available to purchase that feature both capabilities:

Buy a SMS-Capable Twilio Phone Number

Whether you bought your number new or you're recycling, click on the number to use.  (Note: this can be different than the number programmed into the ESP8266, you can use any Twilio number you own.)

In the screen that appears, under 'Messaging' and in 'A Message Comes In', select 'Webhook' and paste the URL from your new API in API Gateway into the text box (highlighted below).  Ensure 'HTTP POST' is selected.

SMS Webhook

Backup Webhook URLs

You'll also notice the 'Primary Handler Fails' box.  When you go to production, you'll want to add a failover webhook here to catch any issues with your primary webhook  Twilio will automatically use the secondary handler if there is a problem with your webhook or we can't reach the primary handler in 15 seconds.  See our Availability and Reliability guide for details.

SMS Your Twilio Number Under 160 Characters!

It's time to test the whole setup - send a SMS to the Twilio number assigned to the ESP8266.  If all goes well, you should see your message sent back to you from the ESP... esrever ni!

Try texting it "Oozy rat in a sanitary zoo.".  Or really, any line from this Weird Al song.

Make the Thing Your Own

With that under your belt, you've now got an end to end solution for receiving and replying to SMS and MMS messages with some Amazon serverless AWS solutions.  If you combine the sending SMS and MMS messages from the ESP8266 and Lambda post, you'll also have a handle on outgoing messages.  Call it "Incoming and outgoing text messages in 64 Kb of RAM".  (640Kb really was enough for anyone).

If you're ready to press on, Add-Ons will often simplify the next steps on your journey to production.  Quickly add our partners' convenient powerful services and tools to your application- try it, you'll love the ease of use.

Please keep us posted on your production journey.  We'd love to hear from you on Twitter - let us know what you're building, or what you've built!