You know how to receive and reply to incoming SMS messages. What if you receive an MMS message containing an image you'd like to download? Let's learn how we can grab that image and any other incoming MMS media using Node.js.
When Twilio receives a message for your phone number, it can make an HTTP call to a webhook that you create. The easiest way to handle HTTP requests with Node is to use Express.
Twilio expects, at the very least, for your webhook to return a 200 OK
response if everything is peachy. Often, however, you will return some TwiML in your response as well. TwiML is just a set of XML commands telling Twilio how you'd like it to respond to your message. Rather than manually generating the XML, we'll use the Twilio.twiml.MessagingResponse
module in the helper library that can make generating the TwiML and the rest of the webhook plumbing easy peasy.
To install the library, run:
_10npm install twilio
Add a new router called MessagingRouter that handles an incoming SMS request.
_116const express = require('express');_116const Twilio = require('twilio');_116const extName = require('ext-name');_116const urlUtil = require('url');_116const path = require('path');_116const fs = require('fs');_116const fetch = require('node-fetch');_116const config = require('../config');_116_116const PUBLIC_DIR = './public/mms_images';_116const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;_116const { MessagingResponse } = Twilio.twiml;_116const { NODE_ENV } = process.env;_116_116function MessagingRouter() {_116 let twilioClient;_116 let images = [];_116_116 if (!fs.existsSync(PUBLIC_DIR)) {_116 fs.mkdirSync(path.resolve(PUBLIC_DIR));_116 }_116_116 function getTwilioClient() {_116 return twilioClient || new Twilio(twilioAccountSid, twilioAuthToken);_116 }_116_116 function deleteMediaItem(mediaItem) {_116 const client = getTwilioClient();_116_116 return client_116 .api.accounts(twilioAccountSid)_116 .messages(mediaItem.MessageSid)_116 .media(mediaItem.mediaSid).remove();_116 }_116_116 async function SaveMedia(mediaItem) {_116 const { mediaUrl, filename } = mediaItem;_116 if (NODE_ENV !== 'test') {_116 const fullPath = path.resolve(`${PUBLIC_DIR}/${filename}`);_116_116 if (!fs.existsSync(fullPath)) {_116 const response = await fetch(mediaUrl);_116 const fileStream = fs.createWriteStream(fullPath);_116_116 response.body.pipe(fileStream);_116_116 deleteMediaItem(mediaItem);_116 }_116_116 images.push(filename);_116 }_116 }_116_116_116 async function handleIncomingSMS(req, res) {_116 const { body } = req;_116 const { NumMedia, From: SenderNumber, MessageSid } = body;_116 let saveOperations = [];_116 const mediaItems = [];_116_116 for (var i = 0; i < NumMedia; i++) { // eslint-disable-line_116 const mediaUrl = body[`MediaUrl${i}`];_116 const contentType = body[`MediaContentType${i}`];_116 const extension = extName.mime(contentType)[0].ext;_116 const mediaSid = path.basename(urlUtil.parse(mediaUrl).pathname);_116 const filename = `${mediaSid}.${extension}`;_116_116 mediaItems.push({ mediaSid, MessageSid, mediaUrl, filename });_116 saveOperations = mediaItems.map(mediaItem => SaveMedia(mediaItem));_116 }_116_116 await Promise.all(saveOperations);_116_116 const messageBody = NumMedia === 0 ?_116 'Send us an image!' :_116 `Thanks for sending us ${NumMedia} file(s)`;_116_116 const response = new MessagingResponse();_116 response.message({_116 from: twilioPhoneNumber,_116 to: SenderNumber,_116 }, messageBody);_116_116 return res.send(response.toString()).status(200);_116 }_116_116_116 function getRecentImages() {_116 return images;_116 }_116_116 function clearRecentImages() {_116 images = [];_116 }_116_116 function fetchRecentImages(req, res) {_116 res.status(200).send(getRecentImages());_116 clearRecentImages();_116 }_116_116 /**_116 * Initialize router and define routes._116 */_116 const router = express.Router();_116 router.post('/incoming', handleIncomingSMS);_116 router.get('/config', (req, res) => {_116 res.status(200).send({ twilioPhoneNumber });_116 });_116 router.get('/images', fetchRecentImages);_116_116 return router;_116}_116_116module.exports = {_116 MessagingRouter,_116};
When Twilio calls your webhook, it sends a number of parameters about the message you just received. Most of these, such as the To
phone number, the From
phone number, and the Body
of the message are available as properties of the request body.
Since an MMS message can have multiple attachments, Twilio will send us form variables named MediaUrlX
, where X is a zero-based index. So, for example, the URL for the first media attachment will be in the MediaUrl0
parameter, the second in MediaUrl1
, and so on.
In order to handle a dynamic number of attachments, we pull the URLs out of the body request like this:
_116const express = require('express');_116const Twilio = require('twilio');_116const extName = require('ext-name');_116const urlUtil = require('url');_116const path = require('path');_116const fs = require('fs');_116const fetch = require('node-fetch');_116const config = require('../config');_116_116const PUBLIC_DIR = './public/mms_images';_116const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;_116const { MessagingResponse } = Twilio.twiml;_116const { NODE_ENV } = process.env;_116_116function MessagingRouter() {_116 let twilioClient;_116 let images = [];_116_116 if (!fs.existsSync(PUBLIC_DIR)) {_116 fs.mkdirSync(path.resolve(PUBLIC_DIR));_116 }_116_116 function getTwilioClient() {_116 return twilioClient || new Twilio(twilioAccountSid, twilioAuthToken);_116 }_116_116 function deleteMediaItem(mediaItem) {_116 const client = getTwilioClient();_116_116 return client_116 .api.accounts(twilioAccountSid)_116 .messages(mediaItem.MessageSid)_116 .media(mediaItem.mediaSid).remove();_116 }_116_116 async function SaveMedia(mediaItem) {_116 const { mediaUrl, filename } = mediaItem;_116 if (NODE_ENV !== 'test') {_116 const fullPath = path.resolve(`${PUBLIC_DIR}/${filename}`);_116_116 if (!fs.existsSync(fullPath)) {_116 const response = await fetch(mediaUrl);_116 const fileStream = fs.createWriteStream(fullPath);_116_116 response.body.pipe(fileStream);_116_116 deleteMediaItem(mediaItem);_116 }_116_116 images.push(filename);_116 }_116 }_116_116_116 async function handleIncomingSMS(req, res) {_116 const { body } = req;_116 const { NumMedia, From: SenderNumber, MessageSid } = body;_116 let saveOperations = [];_116 const mediaItems = [];_116_116 for (var i = 0; i < NumMedia; i++) { // eslint-disable-line_116 const mediaUrl = body[`MediaUrl${i}`];_116 const contentType = body[`MediaContentType${i}`];_116 const extension = extName.mime(contentType)[0].ext;_116 const mediaSid = path.basename(urlUtil.parse(mediaUrl).pathname);_116 const filename = `${mediaSid}.${extension}`;_116_116 mediaItems.push({ mediaSid, MessageSid, mediaUrl, filename });_116 saveOperations = mediaItems.map(mediaItem => SaveMedia(mediaItem));_116 }_116_116 await Promise.all(saveOperations);_116_116 const messageBody = NumMedia === 0 ?_116 'Send us an image!' :_116 `Thanks for sending us ${NumMedia} file(s)`;_116_116 const response = new MessagingResponse();_116 response.message({_116 from: twilioPhoneNumber,_116 to: SenderNumber,_116 }, messageBody);_116_116 return res.send(response.toString()).status(200);_116 }_116_116_116 function getRecentImages() {_116 return images;_116 }_116_116 function clearRecentImages() {_116 images = [];_116 }_116_116 function fetchRecentImages(req, res) {_116 res.status(200).send(getRecentImages());_116 clearRecentImages();_116 }_116_116 /**_116 * Initialize router and define routes._116 */_116 const router = express.Router();_116 router.post('/incoming', handleIncomingSMS);_116 router.get('/config', (req, res) => {_116 res.status(200).send({ twilioPhoneNumber });_116 });_116 router.get('/images', fetchRecentImages);_116_116 return router;_116}_116_116module.exports = {_116 MessagingRouter,_116};
Attachments to MMS messages can be of many different file types. JPG and GIF images, as well as MP4 and 3GP files, are all common. Twilio handles the determination of the file type for you and you can get the standard mime type from the MediaContentTypeX
parameter. If you are expecting photos, then you will likely see a lot of attachments with the mime type image/jpeg
.
_116const express = require('express');_116const Twilio = require('twilio');_116const extName = require('ext-name');_116const urlUtil = require('url');_116const path = require('path');_116const fs = require('fs');_116const fetch = require('node-fetch');_116const config = require('../config');_116_116const PUBLIC_DIR = './public/mms_images';_116const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;_116const { MessagingResponse } = Twilio.twiml;_116const { NODE_ENV } = process.env;_116_116function MessagingRouter() {_116 let twilioClient;_116 let images = [];_116_116 if (!fs.existsSync(PUBLIC_DIR)) {_116 fs.mkdirSync(path.resolve(PUBLIC_DIR));_116 }_116_116 function getTwilioClient() {_116 return twilioClient || new Twilio(twilioAccountSid, twilioAuthToken);_116 }_116_116 function deleteMediaItem(mediaItem) {_116 const client = getTwilioClient();_116_116 return client_116 .api.accounts(twilioAccountSid)_116 .messages(mediaItem.MessageSid)_116 .media(mediaItem.mediaSid).remove();_116 }_116_116 async function SaveMedia(mediaItem) {_116 const { mediaUrl, filename } = mediaItem;_116 if (NODE_ENV !== 'test') {_116 const fullPath = path.resolve(`${PUBLIC_DIR}/${filename}`);_116_116 if (!fs.existsSync(fullPath)) {_116 const response = await fetch(mediaUrl);_116 const fileStream = fs.createWriteStream(fullPath);_116_116 response.body.pipe(fileStream);_116_116 deleteMediaItem(mediaItem);_116 }_116_116 images.push(filename);_116 }_116 }_116_116_116 async function handleIncomingSMS(req, res) {_116 const { body } = req;_116 const { NumMedia, From: SenderNumber, MessageSid } = body;_116 let saveOperations = [];_116 const mediaItems = [];_116_116 for (var i = 0; i < NumMedia; i++) { // eslint-disable-line_116 const mediaUrl = body[`MediaUrl${i}`];_116 const contentType = body[`MediaContentType${i}`];_116 const extension = extName.mime(contentType)[0].ext;_116 const mediaSid = path.basename(urlUtil.parse(mediaUrl).pathname);_116 const filename = `${mediaSid}.${extension}`;_116_116 mediaItems.push({ mediaSid, MessageSid, mediaUrl, filename });_116 saveOperations = mediaItems.map(mediaItem => SaveMedia(mediaItem));_116 }_116_116 await Promise.all(saveOperations);_116_116 const messageBody = NumMedia === 0 ?_116 'Send us an image!' :_116 `Thanks for sending us ${NumMedia} file(s)`;_116_116 const response = new MessagingResponse();_116 response.message({_116 from: twilioPhoneNumber,_116 to: SenderNumber,_116 }, messageBody);_116_116 return res.send(response.toString()).status(200);_116 }_116_116_116 function getRecentImages() {_116 return images;_116 }_116_116 function clearRecentImages() {_116 images = [];_116 }_116_116 function fetchRecentImages(req, res) {_116 res.status(200).send(getRecentImages());_116 clearRecentImages();_116 }_116_116 /**_116 * Initialize router and define routes._116 */_116 const router = express.Router();_116 router.post('/incoming', handleIncomingSMS);_116 router.get('/config', (req, res) => {_116 res.status(200).send({ twilioPhoneNumber });_116 });_116 router.get('/images', fetchRecentImages);_116_116 return router;_116}_116_116module.exports = {_116 MessagingRouter,_116};
Depending on your use case, storing the URLs of the images (or videos or whatever) may be all you need. There are two key features to these URLs that make them very pliable for your use in your apps:
For example, if you are building a browser-based app that needs to display the images, all you need to do is drop an <img src="twilio url to your image">
tag into the page. If this works for you, then perhaps all you need is to store the URL in a database character field.
If you want to save the media attachments to a file, then you will need to make an HTTP request to the media URL and write the response stream to a file. If you need a unique filename, you can use the last part of the media URL. For example, suppose your media URL is the following:
_10https://api.twilio.com/2010-04-01/Accounts/ACxxxx/Messages/MMxxxx/Media/ME27be8a708784242c0daee207ff73db67
You can use that last part of the URL as a unique filename and look up the corresponding file extension for the mime type.
_116const express = require('express');_116const Twilio = require('twilio');_116const extName = require('ext-name');_116const urlUtil = require('url');_116const path = require('path');_116const fs = require('fs');_116const fetch = require('node-fetch');_116const config = require('../config');_116_116const PUBLIC_DIR = './public/mms_images';_116const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;_116const { MessagingResponse } = Twilio.twiml;_116const { NODE_ENV } = process.env;_116_116function MessagingRouter() {_116 let twilioClient;_116 let images = [];_116_116 if (!fs.existsSync(PUBLIC_DIR)) {_116 fs.mkdirSync(path.resolve(PUBLIC_DIR));_116 }_116_116 function getTwilioClient() {_116 return twilioClient || new Twilio(twilioAccountSid, twilioAuthToken);_116 }_116_116 function deleteMediaItem(mediaItem) {_116 const client = getTwilioClient();_116_116 return client_116 .api.accounts(twilioAccountSid)_116 .messages(mediaItem.MessageSid)_116 .media(mediaItem.mediaSid).remove();_116 }_116_116 async function SaveMedia(mediaItem) {_116 const { mediaUrl, filename } = mediaItem;_116 if (NODE_ENV !== 'test') {_116 const fullPath = path.resolve(`${PUBLIC_DIR}/${filename}`);_116_116 if (!fs.existsSync(fullPath)) {_116 const response = await fetch(mediaUrl);_116 const fileStream = fs.createWriteStream(fullPath);_116_116 response.body.pipe(fileStream);_116_116 deleteMediaItem(mediaItem);_116 }_116_116 images.push(filename);_116 }_116 }_116_116_116 async function handleIncomingSMS(req, res) {_116 const { body } = req;_116 const { NumMedia, From: SenderNumber, MessageSid } = body;_116 let saveOperations = [];_116 const mediaItems = [];_116_116 for (var i = 0; i < NumMedia; i++) { // eslint-disable-line_116 const mediaUrl = body[`MediaUrl${i}`];_116 const contentType = body[`MediaContentType${i}`];_116 const extension = extName.mime(contentType)[0].ext;_116 const mediaSid = path.basename(urlUtil.parse(mediaUrl).pathname);_116 const filename = `${mediaSid}.${extension}`;_116_116 mediaItems.push({ mediaSid, MessageSid, mediaUrl, filename });_116 saveOperations = mediaItems.map(mediaItem => SaveMedia(mediaItem));_116 }_116_116 await Promise.all(saveOperations);_116_116 const messageBody = NumMedia === 0 ?_116 'Send us an image!' :_116 `Thanks for sending us ${NumMedia} file(s)`;_116_116 const response = new MessagingResponse();_116 response.message({_116 from: twilioPhoneNumber,_116 to: SenderNumber,_116 }, messageBody);_116_116 return res.send(response.toString()).status(200);_116 }_116_116_116 function getRecentImages() {_116 return images;_116 }_116_116 function clearRecentImages() {_116 images = [];_116 }_116_116 function fetchRecentImages(req, res) {_116 res.status(200).send(getRecentImages());_116 clearRecentImages();_116 }_116_116 /**_116 * Initialize router and define routes._116 */_116 const router = express.Router();_116 router.post('/incoming', handleIncomingSMS);_116 router.get('/config', (req, res) => {_116 res.status(200).send({ twilioPhoneNumber });_116 });_116 router.get('/images', fetchRecentImages);_116_116 return router;_116}_116_116module.exports = {_116 MessagingRouter,_116};
Another idea for these image files could be uploading them to a cloud storage service like Azure Blob Storage or Amazon S3. You could also save them to a database, if necessary. They're just regular files at this point. Go crazy. In this case, we are saving them to the public directory in order to serve them later.
If you are downloading the attachments and no longer need them to be stored by Twilio, you can easily delete them. You can send an HTTP DELETE
request to the media URL and it will be deleted, but you will need to be authenticated to do this. To make this easy, you can use the Twilio Node Helper Library. As shown here:
_116const express = require('express');_116const Twilio = require('twilio');_116const extName = require('ext-name');_116const urlUtil = require('url');_116const path = require('path');_116const fs = require('fs');_116const fetch = require('node-fetch');_116const config = require('../config');_116_116const PUBLIC_DIR = './public/mms_images';_116const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;_116const { MessagingResponse } = Twilio.twiml;_116const { NODE_ENV } = process.env;_116_116function MessagingRouter() {_116 let twilioClient;_116 let images = [];_116_116 if (!fs.existsSync(PUBLIC_DIR)) {_116 fs.mkdirSync(path.resolve(PUBLIC_DIR));_116 }_116_116 function getTwilioClient() {_116 return twilioClient || new Twilio(twilioAccountSid, twilioAuthToken);_116 }_116_116 function deleteMediaItem(mediaItem) {_116 const client = getTwilioClient();_116_116 return client_116 .api.accounts(twilioAccountSid)_116 .messages(mediaItem.MessageSid)_116 .media(mediaItem.mediaSid).remove();_116 }_116_116 async function SaveMedia(mediaItem) {_116 const { mediaUrl, filename } = mediaItem;_116 if (NODE_ENV !== 'test') {_116 const fullPath = path.resolve(`${PUBLIC_DIR}/${filename}`);_116_116 if (!fs.existsSync(fullPath)) {_116 const response = await fetch(mediaUrl);_116 const fileStream = fs.createWriteStream(fullPath);_116_116 response.body.pipe(fileStream);_116_116 deleteMediaItem(mediaItem);_116 }_116_116 images.push(filename);_116 }_116 }_116_116_116 async function handleIncomingSMS(req, res) {_116 const { body } = req;_116 const { NumMedia, From: SenderNumber, MessageSid } = body;_116 let saveOperations = [];_116 const mediaItems = [];_116_116 for (var i = 0; i < NumMedia; i++) { // eslint-disable-line_116 const mediaUrl = body[`MediaUrl${i}`];_116 const contentType = body[`MediaContentType${i}`];_116 const extension = extName.mime(contentType)[0].ext;_116 const mediaSid = path.basename(urlUtil.parse(mediaUrl).pathname);_116 const filename = `${mediaSid}.${extension}`;_116_116 mediaItems.push({ mediaSid, MessageSid, mediaUrl, filename });_116 saveOperations = mediaItems.map(mediaItem => SaveMedia(mediaItem));_116 }_116_116 await Promise.all(saveOperations);_116_116 const messageBody = NumMedia === 0 ?_116 'Send us an image!' :_116 `Thanks for sending us ${NumMedia} file(s)`;_116_116 const response = new MessagingResponse();_116 response.message({_116 from: twilioPhoneNumber,_116 to: SenderNumber,_116 }, messageBody);_116_116 return res.send(response.toString()).status(200);_116 }_116_116_116 function getRecentImages() {_116 return images;_116 }_116_116 function clearRecentImages() {_116 images = [];_116 }_116_116 function fetchRecentImages(req, res) {_116 res.status(200).send(getRecentImages());_116 clearRecentImages();_116 }_116_116 /**_116 * Initialize router and define routes._116 */_116 const router = express.Router();_116 router.post('/incoming', handleIncomingSMS);_116 router.get('/config', (req, res) => {_116 res.status(200).send({ twilioPhoneNumber });_116 });_116 router.get('/images', fetchRecentImages);_116_116 return router;_116}_116_116module.exports = {_116 MessagingRouter,_116};
Twilio supports HTTP Basic and Digest Authentication. Authentication allows you to password protect your TwiML URLs on your web server so that only you and Twilio can access them. Learn more about HTTP authentication and validating incoming requests here.
All the code, in a complete working project, is available on GitHub. If you need to dig a bit deeper, you can head over to our API Reference and learn more about the Twilio webhook request and the REST API Media resource. Also, you will want to be aware of the pricing for storage of all the media files that you keep on Twilio's servers.
We'd love to hear what you build with this.