Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Receive and Download Images on Incoming Media Messages with Node.js


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.


Create MMS processing project

create-mms-processing-project page anchor

Create an Express application

create-an-express-application page anchor

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:


_10
npm install twilio

Add a new router called MessagingRouter that handles an incoming SMS request.

Express Router

express-router page anchor

_116
const express = require('express');
_116
const Twilio = require('twilio');
_116
const extName = require('ext-name');
_116
const urlUtil = require('url');
_116
const path = require('path');
_116
const fs = require('fs');
_116
const fetch = require('node-fetch');
_116
const config = require('../config');
_116
_116
const PUBLIC_DIR = './public/mms_images';
_116
const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;
_116
const { MessagingResponse } = Twilio.twiml;
_116
const { NODE_ENV } = process.env;
_116
_116
function 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
_116
module.exports = {
_116
MessagingRouter,
_116
};


Receive MMS message and images

receive-mms-message-and-images page anchor

Get Incoming Message Details

get-incoming-message-details page anchor

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:

Extract Media Urls From Request Body

extract-media-urls-from-request-body page anchor

_116
const express = require('express');
_116
const Twilio = require('twilio');
_116
const extName = require('ext-name');
_116
const urlUtil = require('url');
_116
const path = require('path');
_116
const fs = require('fs');
_116
const fetch = require('node-fetch');
_116
const config = require('../config');
_116
_116
const PUBLIC_DIR = './public/mms_images';
_116
const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;
_116
const { MessagingResponse } = Twilio.twiml;
_116
const { NODE_ENV } = process.env;
_116
_116
function 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
_116
module.exports = {
_116
MessagingRouter,
_116
};

Determine content type of media

determine-content-type-of-media page anchor

Attachments to MMS messages can be of many different file types. JPG(link takes you to an external page) and GIF(link takes you to an external page) images, as well as MP4(link takes you to an external page) and 3GP(link takes you to an external page) 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.

Map MIME Type To File Extension

map-mime-type-to-file-extension page anchor

_116
const express = require('express');
_116
const Twilio = require('twilio');
_116
const extName = require('ext-name');
_116
const urlUtil = require('url');
_116
const path = require('path');
_116
const fs = require('fs');
_116
const fetch = require('node-fetch');
_116
const config = require('../config');
_116
_116
const PUBLIC_DIR = './public/mms_images';
_116
const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;
_116
const { MessagingResponse } = Twilio.twiml;
_116
const { NODE_ENV } = process.env;
_116
_116
function 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
_116
module.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:

  1. They are publicly accessible without any need for authentication to make sharing easy.
  2. They are permanent (unless you explicitly delete the media).

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.

Save Media to Local File System

save-media-to-local-file-system page anchor

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:


_10
https://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.


_116
const express = require('express');
_116
const Twilio = require('twilio');
_116
const extName = require('ext-name');
_116
const urlUtil = require('url');
_116
const path = require('path');
_116
const fs = require('fs');
_116
const fetch = require('node-fetch');
_116
const config = require('../config');
_116
_116
const PUBLIC_DIR = './public/mms_images';
_116
const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;
_116
const { MessagingResponse } = Twilio.twiml;
_116
const { NODE_ENV } = process.env;
_116
_116
function 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
_116
module.exports = {
_116
MessagingRouter,
_116
};

Another idea for these image files could be uploading them to a cloud storage service like Azure Blob Storage(link takes you to an external page) or Amazon S3(link takes you to an external page). 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.

Delete media from Twilio

delete-media-from-twilio page anchor

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(link takes you to an external page). As shown here:

Delete Media From Twilio Servers

delete-media-from-twilio-servers page anchor

_116
const express = require('express');
_116
const Twilio = require('twilio');
_116
const extName = require('ext-name');
_116
const urlUtil = require('url');
_116
const path = require('path');
_116
const fs = require('fs');
_116
const fetch = require('node-fetch');
_116
const config = require('../config');
_116
_116
const PUBLIC_DIR = './public/mms_images';
_116
const { twilioPhoneNumber, twilioAccountSid, twilioAuthToken } = config;
_116
const { MessagingResponse } = Twilio.twiml;
_116
const { NODE_ENV } = process.env;
_116
_116
function 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
_116
module.exports = {
_116
MessagingRouter,
_116
};

(warning)

Warning

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(link takes you to an external page). 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(link takes you to an external page) for storage of all the media files that you keep on Twilio's servers.

We'd love to hear what you build with this.


Rate this page: