A simple Twilio SMS frontend using Nodejs

I really like Twilio as a service for programmatically sending and receiving SMS messages. The API has solid documentation for a number of programming languages, which makes it easy to develop with.

We’re starting to use Twilio at work for communicating with customers, and there was a need for a tool that would allow us to send SMS messages and check the history of calls and messages to the phone number we’re sending from. With that in mind, I made a simple frontend to the Twilio API.

I decided to use Express for Node.js.

The end result is a page with 4 navigation tabs that allow sending a single SMS, sending SMS in bulk, seeing the status of sent SMS messages, and fetching the inbound call/SMS log.

A simple Twilio SMS frontend using Node.js

First I installed express and express-generator and used it to generate an app skeleton.

npm install express --save
npm install -g express-generator
express

Next, I modified app.js in the root of the app folder to add my Twilio API credentials, and routes to the API client. I created routes for sending an SMS message, and for fetching inbound/outbound communications.


#[...]

const accountSid = 'MY-TWILIO-ACCOUNT-SID';
const authToken = 'MY-TWILIO-AUTH-TOKEN';
const client = require('twilio')(accountSid, authToken);
const my_number = 'MY-TWILIO-PHONE-NUMBER';

var app = express();

#[...]

// routes
app.use('/', indexRouter);
app.post('/submit', (req, res) => {
	client.messages
	  .create({
	     body: req.body.message,
	     from: my_number,
	     to: req.body.number
	   })
	  .then(function(message){
			res.send(JSON.stringify(message, undefined, 2));
			res.end();
		  })
	  .catch(function(error) {
			res.send(JSON.stringify(error, undefined, 2));
			res.end();
		});
})
app.post('/fetch-inbound', (req, res) => {
	if(req.body.number == ""){
		data = { limit: 100 };
	} else {
		data = { limit: 100, from: req.body.number };
	}
	client.messages.list(data)
		.then(function(messages){
			res.send(JSON.stringify(messages));
			res.end();
		});		
})
app.post('/fetch-outbound', (req, res) => {
	if(req.body.number == ""){
		data = { limit: 100 };
	} else {
		data = { limit: 100, to: req.body.number };
	}
	client.messages.list(data)
		.then(function(messages){
			res.send(JSON.stringify(messages));
			res.end();
		});	
})
app.post('/fetch-calls', (req, res) => {
	if(req.body.number == ""){
		data = { limit: 100 };
	} else {
		data = { limit: 100, from: req.body.number };
	}
	client.calls.list(data)
		.then(function(calls){
			res.send(JSON.stringify(calls));
			res.end();
		});		
})

#[...]

Then I wrote the index.ejs page. I used Bootstrap to make it less ugly, I used Datatables to add functionality to the tables that displayed SMS and calls, and I used jQuery to make some of the frontend stuff like aJax and DOM-manipulation easier.

<!DOCTYPE html>
<html>
  <head>
    <title>Twilio SMS Frontend</title>

    <link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet'  type='text/css'>
    <link rel='stylesheet' href='/stylesheets/style.css' />  
    <link rel='stylesheet' href='https://cdn.datatables.net/1.10.20/css/jquery.dataTables.min.css' />  

    <link rel="stylesheet" 
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" 
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
      crossorigin="anonymous">
    <script
	  src="https://code.jquery.com/jquery-3.4.1.min.js"
	  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
	  crossorigin="anonymous"></script>
	<script 
	  src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" 
	  integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" 
	  crossorigin="anonymous"></script>

	<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
	<script src="js/sms_forms.js" ></script>
	<script src="js/fetch_messages.js" ></script>
	<script src="js/fetch_call_log.js" ></script>

  </head>
  <body>

	<nav>
	  <div class="nav nav-tabs" id="nav-tab" role="tablist">
	    <a class="nav-item nav-link active" id="nav-single-sms-tab" 
	    	data-toggle="tab" href="#single-sms" role="tab" aria-controls="nav-single-sms" aria-selected="true">
	    	Single SMS
		</a>
	    <a class="nav-item nav-link" id="nav-bulk-sms-tab" 
	    	data-toggle="tab" href="#bulk-sms" role="tab" aria-controls="nav-bulk-sms" aria-selected="true">
	    	Bulk Send SMS
	    </a>
	    <a class="nav-item nav-link" id="nav-send-status-tab" 
	    	data-toggle="tab" href="#nav-send-status" role="tab" aria-controls="nav-send-status" aria-selected="false">
	    	Send Status
	    </a>
	    <a class="nav-item nav-link" id="nav-read-messages-tab" 
	    	data-toggle="tab" href="#nav-read-messages" role="tab" aria-controls="nav-read-messages" aria-selected="false">
	    	Read Messages
		</a>
	  </div>
	</nav>
	<div class="tab-content" id="nav-tabContent">

	  <div class="tab-pane fade show active" id="single-sms" role="tabpanel" aria-labelledby="nav-single-sms-tab">	  	
		<h1>Single SMS</h1>
		<p>Use this to send an SMS to a single number.</p>
		<form id="form_single" method="post" action="/submit">
			Phone number:<br>
		    <input id="number_single" type="number" name="number" style="width:200px"><br><br>
		    Message:<br>
		    <textarea id="message_single" type="message" name="message" rows=5 style="width:80%"></textarea><br><br>
		    <input id="submit_single" type="submit" value="Submit">
		</form>
	  </div>

	  <div class="tab-pane fade" id="bulk-sms" role="tabpanel" aria-labelledby="nav-bulk-sms-tab">	  	
		<h1>Bulk Send SMS</h1>
		<p>
			Use this to send SMS in bulk.<br>
			One message, multiple numbers.<br>
			Each phone number should be entered on a new line.
		</p>
		<form id="form_bulk" method="post" action="/submit">
			Phone numbers:<br>
		    <textarea id="number_bulk" type="message" name="message" rows=3 style="width:200px"></textarea><br><br>
		    Message:<br>
		    <textarea id="message_bulk" type="message" name="message" rows=5 style="width:80%"></textarea><br><br>
		    <input id="submit_bulk" type="submit" value="Submit">
		</form>
	  </div>

	  <div class="tab-pane fade" id="nav-send-status" role="tabpanel" aria-labelledby="nav-send-status-tab">
		    <h1>Send Status</h1>
		    <p>
		    	Use this to view the status of the message(s) that you just sent.<br>
		    	This page displays the response from Twilio.
		    </p>
	      	<table class="table" id="twilio_response_table">
			  <thead>
			    <tr>
			      <th scope="col">Number</th>
			      <th scope="col">Message</th>
			      <th scope="col">Timestamp</th>
			      <th scope="col">Twilio Response</th>
			    </tr>
			  </thead>
			  <tbody>
			  </tbody>
			</table>
	  </div>

	  <div class="tab-pane fade" id="nav-read-messages" role="tabpanel" aria-labelledby="nav-read-messages-tab">
		    <h1>Read Messages</h1>
		    <p>
		    	Use this to read the last 100 inbound and outbound messages.<br>
		    	Optionally, specify a number to see messages to/from.
		    </p>
			Filter by number (optional):<br> <input id="fetch_number" type="number" name="number" style="width:200px"><br><br>
		    <button type="button" id="fetch_messages" class="btn btn-primary">Refresh</button><br><br>
	      	<table class="table" id="twilio_messages_table">
			  <thead>
			    <tr>
			      <th scope="col">Type</th>
			      <th scope="col">Direction</th>
			      <th scope="col">Number</th>
			      <th scope="col">Message</th>
			      <th scope="col">Timestamp</th>
			    </tr>
			  </thead>
			  <tbody>
			  </tbody>
			</table>
	  </div>

	</div>

  </body>
</html>

Finally, I wrote the jQuery scripts in /public/js/fetch_call_log.js, /public/js/fetch_messages.js and /public/js/sms_forms.js:


// fetch_call_log.js
$( document ).ready(function() {

	calls_table = $('#twilio_calls_table').DataTable( {
          "order": [[ 1, "desc" ]]
	});


	$("#fetch_calls").click(function(e){

		calls_table.clear().draw();
		results = [];

    	var number = $("#fetch_call_log_number").val();
		$.ajax({
	        url: "./fetch-calls",
	        type: "POST",
	       	data: JSON.stringify({number: number}),
	        dataType: "json",
	        contentType: "application/json",
	        complete: function(data) {
	        	calls = JSON.parse(data.responseText);    
				for (i = 0; i < calls.length; i++) {
					date = new Date(calls[i].dateCreated);
					date = moment(date).format('YYYY-MM-DD HH:mm:ss')
					calls_table
					    .row.add( [ 
					    	calls[i].from, 
					    	date
					    	] )
					    .draw();
				}
	        }
	    });

	});

});



// fetch_messages.js

$( document ).ready(function() {

	messages_table = $('#twilio_messages_table').DataTable( {
	  "columnDefs": [
	    { "width": "5%", "targets": [0] },
	    { "width": "15%", "targets": [1] },
	    { "width": "20%", "targets": [2] },
	    { "width": "37%", "targets": [3] },
	    { "width": "23%", "targets": [4] }
	  ], 
	  autoWidth: false,
      "order": [[ 3, "desc" ]]
	});


	$("#fetch_messages").click(function(e){

		messages_table.clear().draw();
		results = [];

		fetch_inbound = function() {
	    	var number = $("#fetch_number").val();
			$.ajax({
		        url: "./fetch-inbound",
		        type: "POST",
	       		data: JSON.stringify({number: number}),
		        dataType: "json",
		        contentType: "application/json",
		        complete: function(data) {    
		        	messages = JSON.parse(data.responseText);	
		        	console.log("inbound: " + messages.length);   	        
					for (i = 0; i < messages.length; i++) {
						message = messages[i];
						results.push(message);
					}
					fetch_outbound();
		        }
		    });
		}

		fetch_outbound = function() {
	    	var number = $("#fetch_number").val();
			$.ajax({
		        url: "./fetch-outbound",
		        type: "POST",
		        data: JSON.stringify({number: number}),
		        dataType: "json",
		        contentType: "application/json",
		        complete: function(data) {
		        	messages = JSON.parse(data.responseText);     
					for (i = 0; i < messages.length; i++) {
						message = messages[i];
						results.push(message);
					}

					var temp = [];
					for (var i = 0; i < results.length; i++) {
					    if (temp.indexOf(results[i].sid) === -1) {
					        temp.push(results[i].sid);
					        results.splice(i + 1, 1)
					    };
					};

					for (i = 0; i < results.length; i++) {
						date = new Date(results[i].dateCreated);
						date = moment(date).format('YYYY-MM-DD HH:mm:ss')
						direction = results[i].direction.replace("-api", "").replace("-reply", "")
						messages_table
						    .row.add( [ 
				    			"sms",
						    	direction, 
						    	results[i].from, 
						    	results[i].body,
						    	date
						    	] )
						    .draw();
					}
					fetch_calls();
		        }
		    });
		}

		fetch_calls = function() {
	    	var number = $("#fetch_number").val();
			$.ajax({
		        url: "./fetch-calls",
		        type: "POST",
		       	data: JSON.stringify({number: number}),
		        dataType: "json",
		        contentType: "application/json",
		        complete: function(data) {
		        	calls = JSON.parse(data.responseText);    
					for (i = 0; i < calls.length; i++) {
						date = new Date(calls[i].dateCreated);
						date = moment(date).format('YYYY-MM-DD HH:mm:ss')
						messages_table
						    .row.add( [ 
					    		"call",
					    		"inbound",
						    	calls[i].from, 
					    		"N/A",
						    	date
						    	] )
						    .draw();
					}
		        }
		    });
		}

		fetch_inbound();

	});

});


// sms_forms.js

$( document ).ready(function() {

	var response_table = $('#twilio_response_table').DataTable( {
		  "columnDefs": [
		    { "width": "13%", "targets": [0] },
		    { "width": "23%", "targets": [2] },
		    { "width": "32%", "targets": [1, 3] }
		  ], 
		  autoWidth: false,
          "order": [[ 3, "asc" ]]
		});

	var enableSubmit = function(e) {
	    $(e).removeAttr("disabled");
	}

	$("#form_single, #form_bulk").submit(function(e){
	    e.preventDefault();
	});

	$("#submit_single").click(function(e){
		e.preventDefault();
	    var number = $("#number_single").val();
	    var message = $("#message_single").val();

	    $.ajax({
	        url: "./submit",
	        type: "POST",
	        data: JSON.stringify({number: number, message: message}),
	        dataType: "json",
	        contentType: "application/json",
	        complete: function(data) {
				date = new Date();
				date = moment(date).format('YYYY-MM-DD HH:mm:ss')
		        data = JSON.parse(data.responseText);
				if(data.status == 400){
					status = "Failure: " + data.message;
				} else {
					status = data.status;
				}
				response_table
				    .row.add( [ 
				    	number, 
				    	message, 
				    	date,
					    status
				    	] )
				    .draw();
	        }
	    })

	    var that = this;
	    $(this).attr("disabled", true);
	    setTimeout(function() { enableSubmit(that) }, 1000);
	});



	$("#submit_bulk").click(function(e){
		e.preventDefault();
	    var numbers = $("#number_bulk").val().split('\n');
	    var message = $("#message_bulk").val();

	    var that = this;
	    $(this).attr("disabled", true);

	    // Run ajax requests sequentially
		var sendSMS = function(userIndex) {
			if (numbers.length == userIndex) {
				console.log("sendSMS succeeded", numbers);
				setTimeout(function() { enableSubmit(that) }, 1000);
				return;
			}

			var this_number = numbers[userIndex];

		    $.ajax({
		        url: "./submit",
		        type: "POST",
		        data: JSON.stringify({number: this_number, message: message}),
		        dataType: "json",
		        contentType: "application/json",
		        complete: function(data) {

					date = new Date();
					date = moment(date).format('YYYY-MM-DD HH:mm:ss')
			        data = JSON.parse(data.responseText);
					if(data.status == 400){
						status = "Failure: " + data.message;
					} else {
						status = data.status;
					}
					response_table
					    .row.add( [ 
					    	this_number, 
					    	message, 
					    	date,
						    status
					    	] )
					    .draw();
				  	sendSMS(++userIndex);
		        }
		    })

		};

		sendSMS(0);

	});


});


I wrote the SMS form processor to allow sending SMS messages one-at-a-time or in bulk using newline delimited phone numbers.