├── .gitignore ├── README.md ├── handler.js ├── package.json └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .serverless 3 | .npmignore 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Using AWS Lambda & API Gateway to Send Shipment Tracking Updates via SMS with Shippo & Twilio 2 | 3 | In this project, we’re going to receive a notification from webhook about an a physical shipment in transit and trigger an SMS with the updated tracking information. We'll build an AWS Lambda function that will trigger whenever Shippo pushes an update about a shipment to our AWS API Gateway Endpoint. Inside of the Lambda function, we’re going to call Twilio to send an SMS update with our tracking info provided by Shippo’s webhook. 4 | 5 | Now, I know what you’re thinking, this sounds pretty complicated and requires a lot of manual set up and repeated uploading of JavaScript files to AWS, but you’d be wrong. We’re going to use Serverless to do a lot of the heavy lifting on this for us, because I’m all about writing less code to do more. 6 | 7 | Things you'll want before getting started with this tutorial: 8 | 9 | * [Twilio Account](https://www.twilio.com/try-twilio) 10 | 11 | > You'll need your Account SID and Auth Token from this (you can find these both in your dash after signing up) 12 | 13 | * [Shippo Account](https://goshippo.com/register) 14 | 15 | > You just need to plug in your API endpoint URL to the [webhooks](https://goshippo.com/docs/webhooks) area to have it work. 16 | 17 | You can get Serverless by installing it globally on your machine using: 18 | 19 | `npm install -g serverless` 20 | 21 | Serverless provides a way to easily create a new service by just using their CLI as follows (you can omit the path if you don't want it to create a directory for you): 22 | 23 | `serverless create --template aws-nodejs --path twilio-shippo` 24 | 25 | Before you dig into creating your Lambda function, you'll want to setup a User in your AWS account for Serverless to have access for creating everything. They have a useful guide [here](https://serverless.com/framework/docs/providers/aws/guide/credentials/) that can walk you through getting your credentials setup 26 | 27 | *** 28 | >**WARNING**: *`In a production environment, we recommend limiting the permissions of the Serverless IAM user to the AWS services required for the project. Consider using a separate AWS account in the interim, if you cannot get permission to your organization's primary AWS accounts.`* 29 | 30 | *** 31 | 32 | Setting up a user *can* be as simple as adding a user `serverless-admin` with `AdministratorAccess` and using the credentials with the following command: 33 | 34 | `serverless config credentials --provider aws --key ACCESS_KEY_ID --secret SECRET_ACCESS_KEY` 35 | 36 | Once you have the credentials setup you can start adding function dependencies at the top of the `handler.js` file that Serverless created: 37 | 38 | ```javascript 39 | const twilio = require('twilio')('TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'); 40 | ``` 41 | 42 | From here, we want to create the endpoint that we'll be putting into [Shippo's webhook](https://goshippo.com/docs/webhooks) interface for capturing all of our tracking updates. Every time Shippo detects a new update to the status of a tracking number that we have POSTed to them, Shippo will send out updates to our API endpoint that we give to them. 43 | 44 | By default, Serverless will create an exported function named `hello`, but we're going replace that with our own called `smsUpdates` (its also probably good to add a log so we can see this show up in CloudWatch): 45 | ```javascript 46 | // Reminder: This should be appended below the code found above 47 | module.exports.smsUpdates = (event, context, callback) => { 48 | console.log(event); 49 | // Don't worry, we'll be adding more here down below 50 | } 51 | ``` 52 | 53 | We are creating a POST endpoint, since Shippo will be POSTing the tracking updates to us. We'll then parse the data to relay over to Twilio to send out our SMS messages. 54 | 55 | First, let's parse the body of the message that Shippo has sent to us. We'll set up a few variable to prevent repeating ourselves, and we'll add some logic in there to handle if there is no location provided with our tracking update. 56 | 57 | ```javascript 58 | module.exports.smsUpdates = (event, context, callback) => { 59 | console.log(event); 60 | var body = event.body, 61 | trackingStatus = body.tracking_status, 62 | trackingLocation = ''; 63 | 64 | if (trackingStatus.location && trackingStatus.location.city) { 65 | trackingLocation = trackingStatus.location.city + ', ' 66 | + trackingStatus.location.state 67 | } else { 68 | trackingLocation = 'UNKNOWN'; 69 | }; 70 | } 71 | ``` 72 | Now that we have our logic built for handling the body of the response and safely handle when we don't get a location with our tracking status, we can dig into sending a formatted SMS using Twilio. 73 | 74 | The basic format for sending Twilio messages requires that we have a destination number (for sending our SMS to), our Twilio number that we're sending from, and a message to send (duh!). 75 | 76 | Here is what it looks like once we add sending our message: 77 | ```javascript 78 | module.exports.smsUpdates = (event, context, callback) => { 79 | console.log(event); 80 | var body = event.body, 81 | trackingStatus = body.tracking_status, 82 | trackingLocation = ''; 83 | 84 | if (trackingStatus.location && trackingStatus.location.city) { 85 | trackingLocation = trackingStatus.location.city + ', ' 86 | + trackingStatus.location.state 87 | } else { 88 | trackingLocation = 'UNKNOWN'; 89 | }; 90 | 91 | const response = { 92 | statusCode: 200, 93 | body: JSON.stringify({ 94 | input: event, 95 | }), 96 | }; 97 | 98 | const destinationNumber = '+12025550119'; // Replace with your own number 99 | const twilioNumber = '+12025550118'; // Replace with your Twilio number 100 | 101 | twilio.sendMessage({ 102 | to: destinationNumber, 103 | from: twilioNumber, 104 | body: 'Tracking #: ' + body.tracking_number + 105 | '\nStatus: ' + trackingStatus.status + 106 | '\nLocation: ' + trackingLocation 107 | }) 108 | .then(function(success) { 109 | console.log(success); 110 | callback(null, response); 111 | }) 112 | .catch(function(error) { 113 | console.log(error); 114 | callback(null, response); 115 | }) 116 | }; 117 | ``` 118 | 119 | You'll also notice that we create a `response` object for sending a 200 response back, since Shippo expects a 200 response when there is a successful receipt of a webhook post. 120 | 121 | We're also using `console.log()` to log all messages to CloudWatch, which is really helpful in debugging or seeing the history of webhook events. 122 | 123 | Now is a good time for us to tackle fixing up the serverless.yml file that will tell serverless how we want our lambda function configured and what AWS services it would use. 124 | 125 | ```yml 126 | service: serverless-tracking 127 | 128 | provider: 129 | name: aws 130 | runtime: nodejs4.3 131 | stage: dev 132 | region: us-west-2 133 | memorySize: 128 134 | 135 | functions: 136 | smsUpdates: 137 | handler: handler.smsUpdates 138 | events: 139 | - http: 140 | path: smsupdates 141 | method: post 142 | integration: lambda 143 | cors: true 144 | ``` 145 | 146 | The above should get our function linked up to trigger when an AWS API Gateway endpoint `smsupdates` receives a POST. That should send off our tracking update to Twilio. If you want more details on configuring your Serverless service, checkout their docs [here](https://serverless.com/framework/docs/providers/aws/guide/services/). 147 | 148 | Once you have your `serverless.yml` file setup, you can just use 149 | 150 | `serverless deploy` 151 | 152 | And your function should be uploaded to AWS with details about it logged out to the console. 153 | 154 | Next, navigate to [https://app.goshippo.com/api](https://app.goshippo.com/api) and scroll down to Webhooks to click **+ Add Webhook**. Since we had our route go to `smsupdates` we'll want to append that to our url so that the updates post to the right place. 155 | 156 | Look for these lines: 157 | 158 | ``` 159 | endpoints: 160 | POST - https://YOUR_UNIQUE_ID.execute-api.us-west-2.amazonaws.com/dev/smsupdates 161 | ``` 162 | 163 | After pasting this into the URL field in Shippo, make sure that the dropdown under Event Type is set to tracking and click the green checkbox to save it. Now we can test the function by clicking on test on the far right. If everything goes well, you should receive an SMS with tracking information at the number you had in the `to` field of your Twilio sendMessage object. 164 | 165 | Now you can get SMS updates for all numbers that you post to Shippo automatically without having to provision any servers, and you only pay when you are receiving updates using Lambda and API Gateway with AWS. You could even take it a step further and include phone numbers for SMS updates in the `metadata` field when POSTing to Shippo and parse that out to dynamically send SMS updates to customers. 166 | 167 | You can find more information about Shippo and how to use their [shipping API](https://goshippo.com/docs) to improve your shipping experience at [goshippo.com](https://goshippo.com). 168 | -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | const twilio = require('twilio')('TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN'); 2 | 3 | module.exports.smsUpdates = (event, context, callback) => { 4 | console.log(event); 5 | var body = event.body, 6 | trackingStatus = body.tracking_status, 7 | trackingLocation = ''; 8 | 9 | if (trackingStatus.location && trackingStatus.location.city) { 10 | trackingLocation = trackingStatus.location.city + ', ' 11 | + trackingStatus.location.state 12 | } else { 13 | trackingLocation = 'UNKNOWN'; 14 | }; 15 | 16 | const response = { 17 | statusCode: 200, 18 | body: JSON.stringify({ 19 | input: event, 20 | }), 21 | }; 22 | 23 | const destinationNumber = '+12025550119'; // Replace with your own number 24 | const twilioNumber = '+12025550118'; // Replace with your Twilio number 25 | 26 | twilio.sendMessage({ 27 | to: destinationNumber, 28 | from: twilioNumber, 29 | body: 'Tracking #: ' + body.tracking_number + 30 | '\nStatus: ' + trackingStatus.status + 31 | '\nLocation: ' + trackingLocation 32 | }) 33 | .then(function(success) { 34 | console.log(success); 35 | callback(null, response); 36 | }) 37 | .catch(function(error) { 38 | console.log(error); 39 | callback(null, response); 40 | }) 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-twilio-shippo", 3 | "version": "1.0.0", 4 | "description": "Guide for using Serverless with Twilio and Shippo for SMS updates on tracking", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mootrichard/serverless-twilio-shippo.git" 12 | }, 13 | "keywords": [ 14 | "Shippo", 15 | "Serverless", 16 | "Twilio", 17 | "Webhook" 18 | ], 19 | "author": "Richard Moot", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/mootrichard/serverless-twilio-shippo/issues" 23 | }, 24 | "homepage": "https://github.com/mootrichard/serverless-twilio-shippo#readme", 25 | "dependencies": { 26 | "twilio": "^2.11.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-tracking 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs4.3 6 | stage: dev 7 | region: us-west-2 8 | memorySize: 128 9 | 10 | functions: 11 | smsUpdates: 12 | handler: handler.smsUpdates 13 | events: 14 | - http: 15 | path: smsupdates 16 | method: post 17 | integration: lambda 18 | cors: true 19 | --------------------------------------------------------------------------------