Receive and Reply to SMS and MMS Messages in Python with Amazon Lambda

Today we're going to get to "Hello, World!" on a serverless Twilio application using Amazon API Gateway, Amazon Lambda, and Python.  By the end of this guide, you'll have the roots of your next world-changing Twilio Application.  By the end of this paragraph you'll have a promise - you won't need to spin up a single VPS.

Sound exciting?  Indubitably.  Let's get started!

Incoming SMS Diagram

Oh, and if you haven't yet, please log into or create an AWS Account, and log into or create a Twilio Account.  Don't worry, we'll be here when you're ready.

What is a Webhook?

Webhooks are user-defined HTTP callbacks. They are usually triggered by some event, such as receiving an SMS message or an incoming phone call. When that event occurs, Twilio makes an HTTP request (usually a POST or a GET) to the URL configured for the webhook.

To handle a webhook, you only need to build a small web application that can accept the HTTP requests. Almost all server-side programming languages offer some framework for you to do this. Examples across languages include ASP.NET MVC for C#, Servlets and Spark for Java, Express for Node.js, Django and Flask for Python, and Rails and Sinatra for Ruby. PHP has its own web app framework built in, although frameworks like Laravel, Symfony and Yii are also popular.

Whichever framework and language you choose, webhooks function the same for every Twilio application. They will make an HTTP request to a URI that you provide to Twilio. Your application performs whatever logic you feel necessary - read/write from a database, integrate with another API or perform some computation - then replies to Twilio with a TwiML response with the instructions you want Twilio to perform.

Using Amazon Lambda with Twilio and Amazon API Gateway

Although the piece that will eventually be exposed to the world will belong to API Gateway, it's conceptually easier to start with code on Lambda.  Since this may feel backwards to you, be prepared for some forward references in this section.  This will all become clear when we expose everything to the world from API Gateway.

To get started, from your choice of Amazon region, create a new Lambda function from your Lambda Console:

Create Lambda Function

If you do not yet have a function in a region, you should use the 'Get Started Now' button:

Lambda Get Started Now

By paging or searching, choose the blueprint 'microservice-http-endpoint-python'.  Next remove the 'API Gateway' trigger by hitting the red remove button.  We will be adding our own trigger when we are done with the code.

'microservice-http-endpoint-python' gets us to a good starting position with lambda_function set up with the proper function signature to take input from API Gateway (eventually).

Editing Code in Lambda's Inline Editor

Our entire "Hello, World!" Application fits in 6 lines of code.  Simply: we're going to import Python 3's print, print out all of the headers and HTTP parameters we've received, then return TwiML.

Enter this code in its entirety into the inline editor:

Loading Code Samples...
from __future__ import print_function

def lambda_handler(event, context):
    print("Received event: " + str(event))
    return '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\
           '<Response><Message>Hello world! -Lambda</Message></Response>'
Responding with "Hello, World!" on Amazon Lambda with Python 2.7 in 6 lines of code.
Responding to SMS or MMS Messages in Lambda

Responding with "Hello, World!" on Amazon Lambda with Python 2.7 in 6 lines of code.

Accessing Request Headers and Body Parameters

All of the POSTted HTTP parameters and any Headers we choose to include will be inside the event dictionary.  Although it appears to be magic at this point, there is a two step process in API Gateway to prepare event for our consumption.  As API Gateway expects JSON input, we'll be using a Body Mapping and splitting on ampersand characters to prepare the dictionary for Python.

print("Received event: " + str(event))

There is some more magic in this line - print is redirected automatically to Amazon Cloudwatch. When our role and policies are properly setup, invocations of this function and any output will be found in CloudWatch.

Generating TwiML Manually with Lambda and Python

TwiML, Twilio's XML Markup Language, is incredibly powerful and lets you easily instruct Twilio on how to handle incoming actions.  We're only using a tiny subset of TwiML today in order to respond to a message with a message of our own.

return '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\
       '<Response><Message>Hello world! -Lambda</Message></Response>'

With this string, we are first informing Twilio that we are responding with XML encoded in UTF-8.  We then set up a Message tag nested inside a Response tag; the Message tag informs Twilio that we would like to reply with the contents of the tag (in this case by SMS).

Responding with Media (MMS Messages)

Responding with an MMS is also very easy with the flexibility TwiML allows.  Simply add a <Media> tag to the <Response> and you'll be peppering your message with pngs in no time (yes, other image formats work as well).

Is your picture worth 1,000 words but you need 5,000?  No problem - Twilio allows up to 10 media items per response.

Creating a Role and Policy for Our Lambda Function

Since we used the microservice-http-endpoint-python blueprint, Amazon makes it easy to set up a proper role and policy.  Use the Policy Template 'Simple Microservice permissions' which should be pre-populated, and pick a name for your new role.  We've chosen the name 'myTwilioRole':

Lambda Policy and Role Definition

Advanced Lambda Settings

Under 'Advanced settings', feel free to set your Memory (MB) to the minimum of 128 MB.  We've personally used a timeout of 10 seconds without issue.

In production, you'll want to revisit these limits; note how Lambda's pricing changes based upon your selections.

Create Our Lambda Function

We're now ready to create the function!  Simply hit 'Next' to get to a review screen, and when satisfied create it.

At this point feel free to 'Test' the function using the blue button at top; regardless of input you should see this result (including double quotes):

"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Message>Hello world! -Lambda</Message></Response>"

Setting Up Amazon API Gateway

Your lambda function now has resources allocated, policies and roles defined, and responds beautifully - our next step is to find a way to let the world speak with it. That's where API Gateway comes into play.  We're going to use the Lambda/API Gateway integration to set up a path from Twilio to the Lambda function we just defined.

From API Gateway's Console and your choice of region (it doesn't need to match the Lambda function's region), select 'Create API':

Create API in Amazon API Gateway

As before, if you have no APIs in the current region, you schould click on 'Get Started' to begin:

API Gateway Get Started

In the 'Create new API' screen, fill out an API name and Description.  We're naming our API 'twilio_simple_responses' with a Description of 'Simply respond with Twilio.'.

After accepting the new name and definition, in the 'Actions' pull-down, you'll want to select 'Create Resource':

Create New Resource API Gateway

Name your resource something like 'Message Responder', and use a Resource Path of '/message'.  Then 'Create Resource':

Create Resource in API Gateway

Now select 'Create Method', select 'POST', and hit the check mark (✓).  You'll then be taken to the setup screen.  Here select 'Lambda Function' integration without Proxy integration, and select the region and name of your Lambda Function.  Note that the 'Lambda Function' field will automatically search as you type - you likely only need to enter the first few characters to find your function:

Setup a Lambda Function Integration in API Gateway

After saving, click 'Okay' on the granting permissions pop-up and you'll be ready to integrate Lambda!

We're going to take the setup one step at a time to properly integrate our function with API Gateway.  Note that the majority of changes we're going to make in the console are because API Gateway is designed for JSON input and output.  We need to work around that in order for Twilio, which posts a form and expects XML, to understand our response.

Integrating Our Request with our Lambda Function

Although our function is bare-bones now, most applications will eventually need some logic based upon incoming messages.  We need to map a content type of application/x-www-form-urlencoded into JSON to work properly with API Gateway and Lambda.  Click on 'Integration Request', then expand 'Body Mapping Techniques'.

Next, click 'Add mapping template', inserting 'application/x-www-form-urlencoded' and hitting the check mark (✓).  There will be a pop-up asking you to secure this mapping; confirm you would like to secure it.  API Gateway will change your 'Request Body Passthrough' to 'When there are no templates defined (recommended)' (if it does not or you cancel the request move it now).

We will use code first suggested by avilewin in the AWS Developer Forums to split our HTTP parameters into JSON key/value pairs.  Enter this code into the editor which appears under the Content-Type box:

#set($httpPost = $input.path('$').split("&"))
#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

Next, go back to 'Method Execution' by following the link at the top of the frame.  You may need to scroll up.

Integrating Our Lambda Response with API Gateway

Next, we need to do some plumbing from the output of our Lambda function.  While in a traditional API you'd want to properly use HTTP status codes, you'll generally only want to respond with a 200 and sometimes a 401 to Twilio's request.  Additionally, API Gateway is set up to respond with a 'Content-Type' of application/json, while Twilio expects application/xml.  Let's fix that now for the 200 case.

Click on the 'Integration Response' link.  Expand the '200' Response, then expand the 'Body Mapping Templates'.  If there is an 'application/json' entry, remove that now.

Now click 'Add mapping template' and add application/xml.  You'll want to enter this very simple mapping:

#set($inputRoot = $input.path('$')) 

Essentially, we are only echoing the return value of the Lambda function.  This will also take care of the surrounding double quotes (") in the return string.  'Save' the mapping, and return to Method Execution with the link at the top of the screen.

Our API Gateway Response

Click the 'Method Response' link.  Under 'Response Body for 200', if application/json is defined, remove that now.  Then simply add application/xml with an 'Empty' model and click the check mark (✓).

Return to Method Execution with the link at the top of the screen.

Deploying Our API and Choosing a Stage

We're almost there now - from the Actions menu, select 'Deploy API':

Deploy an API in API Gateway

You'll then be asked which stage to deploy to - if you don't yet have one created, you can create a new stage from the menu.  Name your stage 'prod':

Create a New Stage in API Gateway

Expand the 'prod' stage which appears (or surf to 'Stages' in the sidebar), and click on 'POST' under '/' and '/message'.  At the top, you'll see an 'Invoke URL':

Invoke URL in API Gateway

Copy that URL... with a full clipboard we're 2/3 of the way to a serverless Twilio application!  Let's push on.

Configure Your Webhook URL

From the Twilio Console, navigate to the Numbers Section in the sidebar (#).  Next, select the number you'd like to route to our new Lambda function.

Under 'Messaging' and in 'A Message Comes In', select 'Webhook' and paste the API Gateway URL into the text box (highlighted below).  Ensure 'HTTP POST' is selected.

SMS Webhook

Backup Webhook URL

You'll also notice the 'Primary Handler Fails' box.  In production, you may want to have a secondary handler for incoming messages.  Twilio will automatically fail-over to the secondary handler if it can't reach the primary handler in 15 seconds or if there is some other problem.  See our Availability and Reliability guide for more details.

Text Your Twilio Number

And with that, all of the plumbing is complete!  You now have Twilio watching for incoming messages, API Gateway listening for requests from Twilio, and Python logic in Lambda listening for API Gateway.  Try texting your Twilio number to verify that the world still wants to say hello to you.

What's Next?  Making a Large Delta with Twilio and Lambda

We highly suggest that you next visit our guide on validating incoming Twilio requests with Python on Lambda.  In that article we'll explore some more advanced features of Lambda such as loading our Python Helper library and defining Environmental Variables, along with demonstrating some simple phone number checks and validating Twilio requests.

If you're ready to press on, Add-Ons will often simplify your next steps.  Add-ons make the Twilio platform even more powerful by allowing you to integrate some amazing tools and services from our partners into your application.

Whatever you build, we're here to help - and to celebrate when you deploy.  Let us know when you hit the deploy button by messaging us on Twitter!


Paul Kamp
Kat King
David Prothero

Need some help?

We all do sometimes; code is hard. Get help now from our support team, or lean on the wisdom of the crowd browsing the Twilio tag on Stack Overflow.

1 / 1
Loading Code Samples...
from __future__ import print_function

def lambda_handler(event, context):
    print("Received event: " + str(event))
    return '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\
           '<Response><Message>Hello world! -Lambda</Message></Response>'