SMS and MMS Marketing Notifications with Node.js and Express

January 10, 2017
Written by
Reviewed by
Paul Kamp
Twilion
Mica Swyers
Contributor
Opinions expressed by Twilio contributors are their own
Kat King
Twilion
Jose Oliveros
Contributor
Opinions expressed by Twilio contributors are their own

sms-mms-notifications-node-express

Ready to implement SMS and MMS marketing notifications?  Today we'll look at adding them to your Node.js and Express application.

Here's how it will work at a high level:

  1. A possible customer sends an SMS to a Twilio phone number you advertise somewhere.
  2. Your application confirms that the user wants to opt into SMS and MMS notifications from your company.
  3. An administrator or marketing campaign manager crafts a message that will go out to all subscribers via SMS or MMS message.

Building Blocks

To get this done, you'll be working with the following tools:

  • TwiML and the <Message> Verb: We'll use TwiML to manage interactions initiated by the user via SMS.
  • Messages Resource: We will use the REST API to broadcast messages out to all subscribers.

Let's get started!

Subscriber Model

In order to track our subscribers, we need to start at the beginning and provide the right model.

To facilitate, we will need to implement a couple of things:

  • a model object to save information about a Subscriber
  • an Express web application that can respond to Twilio webhook requests when our number gets an incoming text.

Let's start by looking at the model for a Subscriber.

Creating Subscribers

We begin by adding a Mongoose model that will store information about a subscriber in a MongoDB database.

For our purposes we don't need to store very much information about the subscriber - just their phone number (so we can send them updates) and a boolean flag indicating whether or not they are opted-in to receive updates.

Editor: this is a migrated tutorial. Find the original code at https://github.com/TwilioDevEd/marketing-notifications-node/

var mongoose = require('mongoose');
var twilio = require('twilio');
var config = require('../config');

// create an authenticated Twilio REST API client
var client = twilio(config.accountSid, config.authToken);

var SubscriberSchema = new mongoose.Schema({
    phone: String,
    subscribed: {
        type: Boolean,
        default: false
    }
});

// Static function to send a message to all current subscribers
SubscriberSchema.statics.sendMessage = function(message, url, callback) {
    // Find all subscribed users
    Subscriber.find({
        subscribed: true
    }, function(err, docs) {
        if (err || docs.length == 0) {
            return callback.call(this, {
                message: 'Couldn\'t find any subscribers!'
            });
        }

        // Otherwise send messages to all subscribers
        sendMessages(docs);
    });

    // Send messages to all subscribers via Twilio
    function sendMessages(docs) {
        docs.forEach(function(subscriber) {
            // Create options to send the message
            var options = {
                to: subscriber.phone,
                from: config.twilioNumber,
                body: message
            };

            // Include media URL if one was given for MMS
            if (url) options.mediaUrl = url;

            // Send the message!
            client.sendMessage(options, function(err, response) {
                if (err) {
                    // Just log it for now
                    console.error(err);
                } else {
                    // Log the last few digits of a phone number
                    var masked = subscriber.phone.substr(0, 
                        subscriber.phone.length - 5);
                    masked += '*****'; 
                    console.log('Message sent to ' + masked);
                }
            });
        });

        // Don't wait on success/failure, just indicate all messages have been
        // queued for delivery
        callback.call(this);
    }
};

var Subscriber = mongoose.model('Subscriber', SubscriberSchema);
module.exports = Subscriber;

Now that we have a model object to save a subscriber, let's move up to the controller level.

Creating a Route for a Twilio Webhook

In an Express web application, we can configure JavaScript functions to handle incoming HTTP requests. When Twilio receives an incoming message, it will send an HTTP POST request to one of the routes we set up.

In this code, we configure all the routes our application will handle, and map those routes to controller functions.

Twilio can send your web application an HTTP request when certain events happen, such as an incoming text message to one of your Twilio phone numbers. These requests are called webhooks, or status callbacks. For more, check out our guide to Getting Started with Twilio Webhooks. Find other webhook pages, such as a security guide and an FAQ in the Webhooks section of the docs.

const pages = require('./pages');
const message = require('./message');

// Map routes to controller functions
module.exports = function(app) {
    // Twilio SMS webhook route
    app.post('/message', message.webhook);

    // Render a page that will allow an administrator to send out a message
    // to all subscribers
    app.get('/', pages.showForm);

    // Handle form submission and send messages to subscribers
    app.post('/message/send', message.sendMessages);
};

And how do we tell Twilio to call a route on an incoming event?  We'll look at that logic, next.

Mapping the Webhook to a Route

Click on one of your Twilio numbers on the Manage Phone Numbers screen in the account portal. For this number, you will need to configure a public server address for your application, as well as a route which Twilio will POST to when your number gets any incoming messages:

SMS Webhook Configuration URL

 

The route has to be publicly exposed.  In the above screenshot you can see a route exposed by ngrok.  Our colleague Kevin has written about testing with ngrok in the past.

const pages = require('./pages');
const message = require('./message');

// Map routes to controller functions
module.exports = function(app) {
    // Twilio SMS webhook route
    app.post('/message', message.webhook);

    // Render a page that will allow an administrator to send out a message
    // to all subscribers
    app.get('/', pages.showForm);

    // Handle form submission and send messages to subscribers
    app.post('/message/send', message.sendMessages);
};

Let's dive into the controller function that will handle incoming message logic next.

Handling An Incoming Message

Since the webhook function will be called every time our application receives a message, it will eventually need quite a bit of business logic. We'll look at how this function works piece by piece as the tutorial continues, but let's focus on the first message the user sends for now.

Creating a New Subscriber

We begin by getting the user's phone number from the incoming Twilio request. Now, we need to find a Subscriber with a matching phone number (a phone number is a unique property on the Subscriber).

If there's no subscriber with this phone number, we create one, save it, and respond with a friendly message asking them to text "subscribe".  We are very careful to have potential customers confirm that they would like to receive marketing messages from us.

const Subscriber = require('../models/subscriber');
const messageSender = require('../lib/messageSender');

// Create a function to handle Twilio SMS / MMS webhook requests
exports.webhook = function(request, response) {
  // Get the user's phone number
  const phone = request.body.From;

  // Try to find a subscriber with the given phone number
  Subscriber.findOne({
    phone: phone,
  }, function(err, sub) {
    if (err) return respond('Derp! Please text back again later.');

    if (!sub) {
      // If there's no subscriber associated with this phone number,
      // create one
      const newSubscriber = new Subscriber({
          phone: phone,
      });

      newSubscriber.save(function(err, newSub) {
        if (err || !newSub)
          return respond('We couldn\'t sign you up - try again.');

        // We're signed up but not subscribed - prompt to subscribe
        respond('Thanks for contacting us! Text "subscribe" to ' +
           'receive updates via text message.');
      });
    } else {
      // For an existing user, process any input message they sent and
      // send back an appropriate message
      processMessage(sub);
    }
  });

  // Process any message the user sent to us
  function processMessage(subscriber) {
    // get the text message command sent by the user
    let msg = request.body.Body || '';
    msg = msg.toLowerCase().trim();

    // Conditional logic to do different things based on the command from
    // the user
    if (msg === 'subscribe' || msg === 'unsubscribe') {
      // If the user has elected to subscribe for messages, flip the bit
      // and indicate that they have done so.
      subscriber.subscribed = msg === 'subscribe';
      subscriber.save(function(err) {
        if (err)
          return respond('We could not subscribe you - please try '
              + 'again.');

        // Otherwise, our subscription has been updated
        let responseMessage = 'You are now subscribed for updates.';
        if (!subscriber.subscribed)
          responseMessage = 'You have unsubscribed. Text "subscribe"'
              + ' to start receiving updates again.';

        respond(responseMessage);
      });
    } else {
      // If we don't recognize the command, text back with the list of
      // available commands
      const responseMessage = 'Sorry, we didn\'t understand that. '
        + 'available commands are: subscribe or unsubscribe';

      respond(responseMessage);
    }
  }

  // Set Content-Type response header and render XML (TwiML) response in a
  // Jade template - sends a text message back to user
  function respond(message) {
    response.type('text/xml');
    response.render('twiml', {
        message: message,
    });
  }
};

// Handle form submission
exports.sendMessages = function(request, response) {
  // Get message info from form submission
  const message = request.body.message;
  const imageUrl = request.body.imageUrl;

  // Send messages to all subscribers
  Subscriber.find({
    subscribed: true,
  }).then((subscribers) => {
    messageSender.sendMessageToSubscribers(subscribers, message, imageUrl);
  }).then(() => {
    request.flash('successes', 'Messages on their way!');
    response.redirect('/');
  }).catch((err) => {
    console.log('err ' + err.message);
    request.flash('errors', err.message);
    response.redirect('/');
  });
};

And that's all we want at this step! We've created a Subscriber model to keep track of the people that want our messages, and shown how to add people to the database when they text us for the first time.

Next, let's look at the logic we need to put in place to allow them to manage their subscription status.

Managing User Subscriptions

We want to provide the user with two SMS commands to manage their subscription status: subscribe and unsubscribe.

These commands will toggle a boolean on a Subscriber record in the database, and only opted-in customers will receive our marketing messages. Because we want to respect our users' preferences, we don't opt them in automatically - rather, we have them confirm that they want to receive messages from us first.

To make this happen, we will need to update the controller logic which handles the incoming text message to do a couple things:

  • If the user is already in the database, parse the message they sent to see if it's a command we recognize
  • If it is a subscribe or unsubscribe command, update their subscription status in the database
  • If it is a command we don't recognize, send them a message explaining available commands

Let's go back into our controller function to see how this works.

Process an Incoming Message

This internal function handles parsing the incoming message from the user and executing conditional logic to see if they have issued us a command we recognize. It's executed after we have already hit the database once to retrieve the current Subscriber model.

const Subscriber = require('../models/subscriber');
const messageSender = require('../lib/messageSender');

// Create a function to handle Twilio SMS / MMS webhook requests
exports.webhook = function(request, response) {
  // Get the user's phone number
  const phone = request.body.From;

  // Try to find a subscriber with the given phone number
  Subscriber.findOne({
    phone: phone,
  }, function(err, sub) {
    if (err) return respond('Derp! Please text back again later.');

    if (!sub) {
      // If there's no subscriber associated with this phone number,
      // create one
      const newSubscriber = new Subscriber({
          phone: phone,
      });

      newSubscriber.save(function(err, newSub) {
        if (err || !newSub)
          return respond('We couldn\'t sign you up - try again.');

        // We're signed up but not subscribed - prompt to subscribe
        respond('Thanks for contacting us! Text "subscribe" to ' +
           'receive updates via text message.');
      });
    } else {
      // For an existing user, process any input message they sent and
      // send back an appropriate message
      processMessage(sub);
    }
  });

  // Process any message the user sent to us
  function processMessage(subscriber) {
    // get the text message command sent by the user
    let msg = request.body.Body || '';
    msg = msg.toLowerCase().trim();

    // Conditional logic to do different things based on the command from
    // the user
    if (msg === 'subscribe' || msg === 'unsubscribe') {
      // If the user has elected to subscribe for messages, flip the bit
      // and indicate that they have done so.
      subscriber.subscribed = msg === 'subscribe';
      subscriber.save(function(err) {
        if (err)
          return respond('We could not subscribe you - please try '
              + 'again.');

        // Otherwise, our subscription has been updated
        let responseMessage = 'You are now subscribed for updates.';
        if (!subscriber.subscribed)
          responseMessage = 'You have unsubscribed. Text "subscribe"'
              + ' to start receiving updates again.';

        respond(responseMessage);
      });
    } else {
      // If we don't recognize the command, text back with the list of
      // available commands
      const responseMessage = 'Sorry, we didn\'t understand that. '
        + 'available commands are: subscribe or unsubscribe';

      respond(responseMessage);
    }
  }

  // Set Content-Type response header and render XML (TwiML) response in a
  // Jade template - sends a text message back to user
  function respond(message) {
    response.type('text/xml');
    response.render('twiml', {
        message: message,
    });
  }
};

// Handle form submission
exports.sendMessages = function(request, response) {
  // Get message info from form submission
  const message = request.body.message;
  const imageUrl = request.body.imageUrl;

  // Send messages to all subscribers
  Subscriber.find({
    subscribed: true,
  }).then((subscribers) => {
    messageSender.sendMessageToSubscribers(subscribers, message, imageUrl);
  }).then(() => {
    request.flash('successes', 'Messages on their way!');
    response.redirect('/');
  }).catch((err) => {
    console.log('err ' + err.message);
    request.flash('errors', err.message);
    response.redirect('/');
  });
};

Next: how to handle user commands.

Handling a Subscription Command

If the user has texted subscribe or unsubscribe, we will update their subscription status in the database. We will then respond to them via SMS with the opposite command to either opt in to updates or opt out.

const Subscriber = require('../models/subscriber');
const messageSender = require('../lib/messageSender');

// Create a function to handle Twilio SMS / MMS webhook requests
exports.webhook = function(request, response) {
  // Get the user's phone number
  const phone = request.body.From;

  // Try to find a subscriber with the given phone number
  Subscriber.findOne({
    phone: phone,
  }, function(err, sub) {
    if (err) return respond('Derp! Please text back again later.');

    if (!sub) {
      // If there's no subscriber associated with this phone number,
      // create one
      const newSubscriber = new Subscriber({
          phone: phone,
      });

      newSubscriber.save(function(err, newSub) {
        if (err || !newSub)
          return respond('We couldn\'t sign you up - try again.');

        // We're signed up but not subscribed - prompt to subscribe
        respond('Thanks for contacting us! Text "subscribe" to ' +
           'receive updates via text message.');
      });
    } else {
      // For an existing user, process any input message they sent and
      // send back an appropriate message
      processMessage(sub);
    }
  });

  // Process any message the user sent to us
  function processMessage(subscriber) {
    // get the text message command sent by the user
    let msg = request.body.Body || '';
    msg = msg.toLowerCase().trim();

    // Conditional logic to do different things based on the command from
    // the user
    if (msg === 'subscribe' || msg === 'unsubscribe') {
      // If the user has elected to subscribe for messages, flip the bit
      // and indicate that they have done so.
      subscriber.subscribed = msg === 'subscribe';
      subscriber.save(function(err) {
        if (err)
          return respond('We could not subscribe you - please try '
              + 'again.');

        // Otherwise, our subscription has been updated
        let responseMessage = 'You are now subscribed for updates.';
        if (!subscriber.subscribed)
          responseMessage = 'You have unsubscribed. Text "subscribe"'
              + ' to start receiving updates again.';

        respond(responseMessage);
      });
    } else {
      // If we don't recognize the command, text back with the list of
      // available commands
      const responseMessage = 'Sorry, we didn\'t understand that. '
        + 'available commands are: subscribe or unsubscribe';

      respond(responseMessage);
    }
  }

  // Set Content-Type response header and render XML (TwiML) response in a
  // Jade template - sends a text message back to user
  function respond(message) {
    response.type('text/xml');
    response.render('twiml', {
        message: message,
    });
  }
};

// Handle form submission
exports.sendMessages = function(request, response) {
  // Get message info from form submission
  const message = request.body.message;
  const imageUrl = request.body.imageUrl;

  // Send messages to all subscribers
  Subscriber.find({
    subscribed: true,
  }).then((subscribers) => {
    messageSender.sendMessageToSubscribers(subscribers, message, imageUrl);
  }).then(() => {
    request.flash('successes', 'Messages on their way!');
    response.redirect('/');
  }).catch((err) => {
    console.log('err ' + err.message);
    request.flash('errors', err.message);
    response.redirect('/');
  });
};

List Available Commands

If a user texted in something we don't recognize, we helpfully respond with a listing of all known commands.

You could take this further and implement "help" text for each command you offer in your application, but in this simple use case the commands should be self-explanatory.

const Subscriber = require('../models/subscriber');
const messageSender = require('../lib/messageSender');

// Create a function to handle Twilio SMS / MMS webhook requests
exports.webhook = function(request, response) {
  // Get the user's phone number
  const phone = request.body.From;

  // Try to find a subscriber with the given phone number
  Subscriber.findOne({
    phone: phone,
  }, function(err, sub) {
    if (err) return respond('Derp! Please text back again later.');

    if (!sub) {
      // If there's no subscriber associated with this phone number,
      // create one
      const newSubscriber = new Subscriber({
          phone: phone,
      });

      newSubscriber.save(function(err, newSub) {
        if (err || !newSub)
          return respond('We couldn\'t sign you up - try again.');

        // We're signed up but not subscribed - prompt to subscribe
        respond('Thanks for contacting us! Text "subscribe" to ' +
           'receive updates via text message.');
      });
    } else {
      // For an existing user, process any input message they sent and
      // send back an appropriate message
      processMessage(sub);
    }
  });

  // Process any message the user sent to us
  function processMessage(subscriber) {
    // get the text message command sent by the user
    let msg = request.body.Body || '';
    msg = msg.toLowerCase().trim();

    // Conditional logic to do different things based on the command from
    // the user
    if (msg === 'subscribe' || msg === 'unsubscribe') {
      // If the user has elected to subscribe for messages, flip the bit
      // and indicate that they have done so.
      subscriber.subscribed = msg === 'subscribe';
      subscriber.save(function(err) {
        if (err)
          return respond('We could not subscribe you - please try '
              + 'again.');

        // Otherwise, our subscription has been updated
        let responseMessage = 'You are now subscribed for updates.';
        if (!subscriber.subscribed)
          responseMessage = 'You have unsubscribed. Text "subscribe"'
              + ' to start receiving updates again.';

        respond(responseMessage);
      });
    } else {
      // If we don't recognize the command, text back with the list of
      // available commands
      const responseMessage = 'Sorry, we didn\'t understand that. '
        + 'available commands are: subscribe or unsubscribe';

      respond(responseMessage);
    }
  }

  // Set Content-Type response header and render XML (TwiML) response in a
  // Jade template - sends a text message back to user
  function respond(message) {
    response.type('text/xml');
    response.render('twiml', {
        message: message,
    });
  }
};

// Handle form submission
exports.sendMessages = function(request, response) {
  // Get message info from form submission
  const message = request.body.message;
  const imageUrl = request.body.imageUrl;

  // Send messages to all subscribers
  Subscriber.find({
    subscribed: true,
  }).then((subscribers) => {
    messageSender.sendMessageToSubscribers(subscribers, message, imageUrl);
  }).then(() => {
    request.flash('successes', 'Messages on their way!');
    response.redirect('/');
  }).catch((err) => {
    console.log('err ' + err.message);
    request.flash('errors', err.message);
    response.redirect('/');
  });
};

Now we'll show how to return a response to Twilio in a form it expects.

Responding with TwiML

This respond helper function handles generating a TwiML (XML) response using a Jade template.

const Subscriber = require('../models/subscriber');
const messageSender = require('../lib/messageSender');

// Create a function to handle Twilio SMS / MMS webhook requests
exports.webhook = function(request, response) {
  // Get the user's phone number
  const phone = request.body.From;

  // Try to find a subscriber with the given phone number
  Subscriber.findOne({
    phone: phone,
  }, function(err, sub) {
    if (err) return respond('Derp! Please text back again later.');

    if (!sub) {
      // If there's no subscriber associated with this phone number,
      // create one
      const newSubscriber = new Subscriber({
          phone: phone,
      });

      newSubscriber.save(function(err, newSub) {
        if (err || !newSub)
          return respond('We couldn\'t sign you up - try again.');

        // We're signed up but not subscribed - prompt to subscribe
        respond('Thanks for contacting us! Text "subscribe" to ' +
           'receive updates via text message.');
      });
    } else {
      // For an existing user, process any input message they sent and
      // send back an appropriate message
      processMessage(sub);
    }
  });

  // Process any message the user sent to us
  function processMessage(subscriber) {
    // get the text message command sent by the user
    let msg = request.body.Body || '';
    msg = msg.toLowerCase().trim();

    // Conditional logic to do different things based on the command from
    // the user
    if (msg === 'subscribe' || msg === 'unsubscribe') {
      // If the user has elected to subscribe for messages, flip the bit
      // and indicate that they have done so.
      subscriber.subscribed = msg === 'subscribe';
      subscriber.save(function(err) {
        if (err)
          return respond('We could not subscribe you - please try '
              + 'again.');

        // Otherwise, our subscription has been updated
        let responseMessage = 'You are now subscribed for updates.';
        if (!subscriber.subscribed)
          responseMessage = 'You have unsubscribed. Text "subscribe"'
              + ' to start receiving updates again.';

        respond(responseMessage);
      });
    } else {
      // If we don't recognize the command, text back with the list of
      // available commands
      const responseMessage = 'Sorry, we didn\'t understand that. '
        + 'available commands are: subscribe or unsubscribe';

      respond(responseMessage);
    }
  }

  // Set Content-Type response header and render XML (TwiML) response in a
  // Jade template - sends a text message back to user
  function respond(message) {
    response.type('text/xml');
    response.render('twiml', {
        message: message,
    });
  }
};

// Handle form submission
exports.sendMessages = function(request, response) {
  // Get message info from form submission
  const message = request.body.message;
  const imageUrl = request.body.imageUrl;

  // Send messages to all subscribers
  Subscriber.find({
    subscribed: true,
  }).then((subscribers) => {
    messageSender.sendMessageToSubscribers(subscribers, message, imageUrl);
  }).then(() => {
    request.flash('successes', 'Messages on their way!');
    response.redirect('/');
  }).catch((err) => {
    console.log('err ' + err.message);
    request.flash('errors', err.message);
    response.redirect('/');
  });
};

The TwiML response is reasonably simple, but for the sake of completeness let's peek in to see what it does.

Responding with TwiML: Jade Template

Jade is a popular template engine for Node.js that can be used to generate HTML or XML markup in a web application. We use Jade here to generate a TwiML Response containing a Message tag with the text we defined in the controller.

This will command Twilio to respond with a text message to the incoming message we just parsed.

doctype xml
Response
  Message= message

That's it for the user-facing commands! Now, we need to provide our marketing team with an interface to send messages out to all subscribers. Let's take a look at that next.

Sending SMS or MMS Notifications

Now that we have a list of subscribers for our awesome SMS and MMS content, we need to provide our marketing team some kind of interface to send out messages.

To make this happen, we will need to update our application to do a few things:

  • Create a route to render a web form that an administrator canuse to craft the campaign
  • Create a controller function to handle the form's submissions
  • Use the Twilio API to send out messages to all current subscribers
const pages = require('./pages');
const message = require('./message');

// Map routes to controller functions
module.exports = function(app) {
    // Twilio SMS webhook route
    app.post('/message', message.webhook);

    // Render a page that will allow an administrator to send out a message
    // to all subscribers
    app.get('/', pages.showForm);

    // Handle form submission and send messages to subscribers
    app.post('/message/send', message.sendMessages);
};

Let's begin at the front end with the web form our administrators will interact with.

Creating the Web Form

Here we use Jade to render an HTML document containing a web form to be used by our marketing campaign administrators.

It just has a couple of fields - one to specify a text message, and another to specify an optional URL to an image on the public Internet that we could send via MMS.

extends layout

block content
  h1 SMS Notifications

  p.
    Use this form to send MMS notifications to any subscribers.

  form(action='/message/send', method='POST')

    // The text of the message to send
    .form-group
      label(for='message') Enter a message
      input.form-control(type='text', name='message',
        placeholder='Hi there, gorgeous ;)')

    // An optional image URL
    .form-group
      label(for='imageUrl') (Optional) Image URL to send in an MMS
      input.form-control(type='text', name='imageUrl',
        placeholder='http://fake.twilio.com/some_image.png')

    button.btn.btn-primary(type='submit') Send Yourself a Message!

Let's go to the controller next to see what happens when the form is submitted.

Handling the Form's Submission

On the server, we grab the message text and image URL from the POST body, and use a function on our Subscriber model to send text messages to all current subscribers.

When the messages are on their way, we redirect back to the same web form with a flash message containing feedback about the messaging attempt.

const Subscriber = require('../models/subscriber');
const messageSender = require('../lib/messageSender');

// Create a function to handle Twilio SMS / MMS webhook requests
exports.webhook = function(request, response) {
  // Get the user's phone number
  const phone = request.body.From;

  // Try to find a subscriber with the given phone number
  Subscriber.findOne({
    phone: phone,
  }, function(err, sub) {
    if (err) return respond('Derp! Please text back again later.');

    if (!sub) {
      // If there's no subscriber associated with this phone number,
      // create one
      const newSubscriber = new Subscriber({
          phone: phone,
      });

      newSubscriber.save(function(err, newSub) {
        if (err || !newSub)
          return respond('We couldn\'t sign you up - try again.');

        // We're signed up but not subscribed - prompt to subscribe
        respond('Thanks for contacting us! Text "subscribe" to ' +
           'receive updates via text message.');
      });
    } else {
      // For an existing user, process any input message they sent and
      // send back an appropriate message
      processMessage(sub);
    }
  });

  // Process any message the user sent to us
  function processMessage(subscriber) {
    // get the text message command sent by the user
    let msg = request.body.Body || '';
    msg = msg.toLowerCase().trim();

    // Conditional logic to do different things based on the command from
    // the user
    if (msg === 'subscribe' || msg === 'unsubscribe') {
      // If the user has elected to subscribe for messages, flip the bit
      // and indicate that they have done so.
      subscriber.subscribed = msg === 'subscribe';
      subscriber.save(function(err) {
        if (err)
          return respond('We could not subscribe you - please try '
              + 'again.');

        // Otherwise, our subscription has been updated
        let responseMessage = 'You are now subscribed for updates.';
        if (!subscriber.subscribed)
          responseMessage = 'You have unsubscribed. Text "subscribe"'
              + ' to start receiving updates again.';

        respond(responseMessage);
      });
    } else {
      // If we don't recognize the command, text back with the list of
      // available commands
      const responseMessage = 'Sorry, we didn\'t understand that. '
        + 'available commands are: subscribe or unsubscribe';

      respond(responseMessage);
    }
  }

  // Set Content-Type response header and render XML (TwiML) response in a
  // Jade template - sends a text message back to user
  function respond(message) {
    response.type('text/xml');
    response.render('twiml', {
        message: message,
    });
  }
};

// Handle form submission
exports.sendMessages = function(request, response) {
  // Get message info from form submission
  const message = request.body.message;
  const imageUrl = request.body.imageUrl;

  // Send messages to all subscribers
  Subscriber.find({
    subscribed: true,
  }).then((subscribers) => {
    messageSender.sendMessageToSubscribers(subscribers, message, imageUrl);
  }).then(() => {
    request.flash('successes', 'Messages on their way!');
    response.redirect('/');
  }).catch((err) => {
    console.log('err ' + err.message);
    request.flash('errors', err.message);
    response.redirect('/');
  });
};

Let's jump into the model now to see how these messages are sent out.

Configuring a Twilio REST Client

When the model object is loaded, it creates a Twilio REST API client that can be used to send SMS and MMS messages. The client requires your Twilio account credentials (an account SID and auth token), which can be found in the Twilio Console:

Account Credentials

 

 

var mongoose = require('mongoose');
var twilio = require('twilio');
var config = require('../config');

// create an authenticated Twilio REST API client
var client = twilio(config.accountSid, config.authToken);

var SubscriberSchema = new mongoose.Schema({
    phone: String,
    subscribed: {
        type: Boolean,
        default: false
    }
});

// Static function to send a message to all current subscribers
SubscriberSchema.statics.sendMessage = function(message, url, callback) {
    // Find all subscribed users
    Subscriber.find({
        subscribed: true
    }, function(err, docs) {
        if (err || docs.length == 0) {
            return callback.call(this, {
                message: 'Couldn\'t find any subscribers!'
            });
        }

        // Otherwise send messages to all subscribers
        sendMessages(docs);
    });

    // Send messages to all subscribers via Twilio
    function sendMessages(docs) {
        docs.forEach(function(subscriber) {
            // Create options to send the message
            var options = {
                to: subscriber.phone,
                from: config.twilioNumber,
                body: message
            };

            // Include media URL if one was given for MMS
            if (url) options.mediaUrl = url;

            // Send the message!
            client.sendMessage(options, function(err, response) {
                if (err) {
                    // Just log it for now
                    console.error(err);
                } else {
                    // Log the last few digits of a phone number
                    var masked = subscriber.phone.substr(0, 
                        subscriber.phone.length - 5);
                    masked += '*****'; 
                    console.log('Message sent to ' + masked);
                }
            });
        });

        // Don't wait on success/failure, just indicate all messages have been
        // queued for delivery
        callback.call(this);
    }
};

var Subscriber = mongoose.model('Subscriber', SubscriberSchema);
module.exports = Subscriber;

Let's check out the static function that sends the messages next.

Sending Messages from Node.js

Here we define a static function on the model which will query that database for all Subscribers that have opted in to receive notifications.

Once the list of active subscribers has been found, we loop through and send each of them a message based on the parameters sent in from the controller. If there's no image URL associated with the message, we omit that field from the Twilio API request.

var mongoose = require('mongoose');
var twilio = require('twilio');
var config = require('../config');

// create an authenticated Twilio REST API client
var client = twilio(config.accountSid, config.authToken);

var SubscriberSchema = new mongoose.Schema({
    phone: String,
    subscribed: {
        type: Boolean,
        default: false
    }
});

// Static function to send a message to all current subscribers
SubscriberSchema.statics.sendMessage = function(message, url, callback) {
    // Find all subscribed users
    Subscriber.find({
        subscribed: true
    }, function(err, docs) {
        if (err || docs.length == 0) {
            return callback.call(this, {
                message: 'Couldn\'t find any subscribers!'
            });
        }

        // Otherwise send messages to all subscribers
        sendMessages(docs);
    });

    // Send messages to all subscribers via Twilio
    function sendMessages(docs) {
        docs.forEach(function(subscriber) {
            // Create options to send the message
            var options = {
                to: subscriber.phone,
                from: config.twilioNumber,
                body: message
            };

            // Include media URL if one was given for MMS
            if (url) options.mediaUrl = url;

            // Send the message!
            client.sendMessage(options, function(err, response) {
                if (err) {
                    // Just log it for now
                    console.error(err);
                } else {
                    // Log the last few digits of a phone number
                    var masked = subscriber.phone.substr(0, 
                        subscriber.phone.length - 5);
                    masked += '*****'; 
                    console.log('Message sent to ' + masked);
                }
            });
        });

        // Don't wait on success/failure, just indicate all messages have been
        // queued for delivery
        callback.call(this);
    }
};

var Subscriber = mongoose.model('Subscriber', SubscriberSchema);
module.exports = Subscriber;

And with that, you've seen all the wiring necessary to add marketing notifications for your own use case!  Next we'll look at some other features you might enjoy.

Where to Next?

That's it! We've just implemented a an opt-in process and an administrative interface to run an SMS and MMS marketing campaign. Now all you need is killer content to share with your users via text or MMS (you'll have to craft that on your own...).

Here's a couple of our favorite Node posts on adding features:

Appointment Reminders

Automate the process of reaching out to your customers in advance of an upcoming appointment.

 

Click to Call

Convert your company web traffic into phone calls with the click of a button.

Did this help?

Thanks for checking this tutorial out! Whatever you're building and whatever you've built - we'd love to hear about it.  Drop us a line on Twitter.