Build a Legislation Tracking Bot with Node.js, Express, and Twilio Programmable Messaging

June 14, 2022
Written by
Dainyl Cua
Twilion
Reviewed by

Header image

It’s important to keep up to date with high-impact bills in your state and across the nation. However, tracking bills and monitoring their status can be difficult if you don’t know where to look.

By utilizing the LegiScan API and Twilio’s Programmable Messaging API, you can create an SMS bill-tracking bot that will search for bills relevant to keywords that you designate within a few guided messages.

LegiScan is a United States legislation tracking and information service with an API that can be used to serve JSON data of a specific or multiple legislation. The free API can search for relevant legislation on both a state and federal level with a generous monthly query limit of 30,000 requests.

Check out the bot in action!

Full Bot Response

Prerequisites

To follow along with this tutorial, you will need the following:

  • A free or paid Twilio account. If you are new to Twilio, click here to sign up for a free Twilio account and get a $10 credit when you upgrade!
  • A Twilio phone number
  • Node.js and npm
  • Ngrok: this is a tool to speed up application testing by connecting your local server to the internet
  • A personal phone number

Setting up your developer environment

To get started, open your terminal window and navigate to the directory where you will be creating this application. Copy and paste the commands below into your terminal and hit enter:

mkdir bill-bot
cd bill-bot
npm init -y
npm install twilio dotenv express express-session nodemon node-fetch@2.6.7
touch index.js .env

These commands create a new directory (named bill-bot), navigate into it, initialize a new Node.js project, install the required dependencies, and create the files index.js and .env.

The six dependencies installed were:

  • twilio, to make use of Twilio’s Messaging API
  • dotenv, to store your important credentials and access them as environment variables
  • express, to build your server
  • express-session, to keep track of which step of the bot a user is currently at
  • nodemon, to run your server locally and automatically restart it when changed
  • node-fetch, to utilize the fetch API in your Node.js environment

If you have Node.js 18 or above, you do not need to install node-fetch as it is already included.

You will be coding in two files for this tutorial: index.js and .env. In index.js, you will write the code for your backend that interprets all messages and responds accordingly. In .env, you will be storing your access credentials for the LegiScan API.

Getting your LegiScan credentials

To use the LegiScan API, you must first create an account to get your API key. Be sure to fill out all the required information and sign up for the free account type.

LegiScan Registration Page

After signing up for an account, you will need to go to the email address you entered and verify your account by clicking a verification link in LegiScan’s welcome email.

Back on the LegiScan homepage, click on LegiScan API in the navigation bar on the top of the page. If your account was verified successfully, then you will need to fill out a form to register for an API key. After submitting that, the next page should include the API key that you will use in this tutorial.

LegiScan API Dashboard

Keep the API key accessible and open the .env file you created earlier in the code editor of your choice. Inside .env, copy and paste the line below:

LEGISCAN_KEY=XXXXXX

Replace XXXXXX with your LegiScan API key and switch to the index.js file.

Handling incoming text messages

The index.js file will be responsible for handling all incoming messages and providing the proper responses for the bot. To get started writing your backend, copy and paste the code below into index.js:

require('dotenv').config();
const legiscanKey = process.env.LEGISCAN_KEY;

const fetch = require('node-fetch');
const express = require('express');
const session = require('express-session');
const MessagingResponse = require('twilio').twiml.MessagingResponse;

const app = express();

The code above initializes the fetch, dotenv, express, and express-session packages installed earlier. It also initializes the legiscanKey environment variable and imports the MessagingResponse object from the twilio package.

Now that you’ve initialized the necessary packages, you need to also include middleware to parse incoming text messages and JSON data. You also need to enable sessions for users, which will be important for performing searches later. To do so, copy and paste the highlighted lines of code below your existing code in index.js:

. . . 

app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use(session({
  secret: "billbot",
  saveUninitialized: true,
  cookie: { maxAge: 5 * 1000 * 60 },
  resave: true
}));

Be sure to change your secret string (line 5) to a unique phrase or set of characters in your own code.

Finally, you need to set up routes that will process all incoming messages. Again, copy and paste the highlighted lines of code below your existing code in index.js:

. . . 

app.post('/webhook', async (req, res) => {

});

app.listen(3000, () => {
  console.log('Bill-Bot listening on port 3000.')
});

You now have the basic layout of the backend for your bot complete. This code creates a /webhook route that listens at port 3000 on your local server with a POST endpoint which currently does nothing. Now, you need to write code to get the bot to do something.

Developing your routes

The functionality of the bot can be broken down into the following steps:

  1. Introduce the bot
  2. Designate a location
  3. Choose the bill category
  4. Search for relevant legislation information

Introduce the bot

You will want to give users a brief description of the bot that they will be using and guide them to the next step. Modify your /webhook route like so:

app.post('/webhook', async (req, res) => {
  const response = new MessagingResponse();
  response.message("Welcome to Bill-Bot! Bill-Bot gives you updates on hot-topic issues in the United States in the past week.\n\nPlease enter the state you would like to search bills in (abbreviations OK). Enter 'all' to search for bills across every state and on a federal level.");

  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(response.toString());
});

This newly added code will let the bot send a response back when anyone sends a text message to it. This is done by creating a new MessagingResponse object and utilizing the message property to send a message.

Next, the bot will also prompt the user to designate the location they would like to look up bills in. If you were able to text the number right now (you will set up a phone number for your bot after completing the code for the backend), you would get this response:

Bot Response - Step 1 (Introduction)

The bot works! Now, how would you read an incoming text message and get the correct state?

Designate a location

First, you will need to define every state name and relate them to their proper abbreviation in order to fit within the LegiScan API parameters. One way to do this is by creating a new variable called states which is an object with 50 properties; one for each state.

Every property has a state abbreviation as its key, and the corresponding state name for its value. Copy and paste the highlighted text below the line you initialized the MessagingResponse variable:

. . .
const MessagingResponse = require('twilio').twiml.MessagingResponse;

// Copy this entire states object into your code
const states = {
  "AL": "alabama",
  "AK": "alaska",
  "AZ": "arizona",
  "AR": "arkansas",
  "CA": "california",
  "CO": "colorado",
  "CT": "connecticut",
  "DE": "delaware",
  "FL": "florida",
  "GA": "georgia",
  "HI": "hawaii",
  "ID": "idaho",
  "IL": "illinois",
  "IN": "indiana",
  "IA": "iowa",
  "KS": "kansas",
  "KY": "kentucky",
  "LA": "louisiana",
  "ME": "maine",
  "MD": "maryland",
  "MA": "massachusetts",
  "MI": "michigan",
  "MN": "minnesota",
  "MS": "mississippi",
  "MO": "missouri",
  "MT": "montana",
  "NE": "nebraska",
  "NV": "nevada",
  "NH": "new hampshire",
  "NJ": "new jersey",
  "NM": "new mexico",
  "NY": "new york",
  "NC": "north carolina",
  "ND": "north dakota",
  "OH": "ohio",
  "OK": "oklahoma",
  "OR": "oregon",
  "PA": "pennsylvania",
  "RI": "rhode island",
  "SC": "south carolina",
  "SD": "south dakota",
  "TN": "tennessee",
  "TX": "texas",
  "UT": "utah",
  "VT": "vermont",
  "VA": "virginia",
  "WA": "washington",
  "WV": "west virginia",
  "WI": "wisconsin",
  "WY": "wyoming"
};

const app = express();

. . .

With your states object set up, create a new route to handle the state selection portion of your code by copying and pasting the highlighted lines of code between your /webhook route and listener:

app.post('/webhook', async (req, res) => {
  …
});

app.post('/state', async (req, res) => {
  const response = new MessagingResponse();
  const state = req.body.Body.toLowerCase();
  req.session["stateAbbreviation"] = "";

  if(state === "all") {
    req.session.stateAbbreviation = "ALL";
  } else if(Object.keys(states).includes(state.toUpperCase())) {
    req.session.stateAbbreviation = state.toUpperCase();
  } else if(Object.values(states).includes(state)) {
    req.session.stateAbbreviation = Object.keys(states)[Object.values(states).indexOf(state)];
  };

  response.message("Welcome to the new route! This should only send after a second message.");

  res.writeHead(200, {'Content-Type': 'text/xml'})
  res.end(response.toString())
});

app.listen(3000, () => {
  …
});

Now the bot can read the incoming text message. By using express-session, the bot can also store the state abbreviation as a variable for later once the bot needs to perform a search. Using the states object from earlier, the bot can correctly interpret either a state name or abbreviation if given. If a user enters “all”, then it can set the proper query parameter to search across every state legislation as well as federal legislation.

However, if you were to text the bot now, it would only send the first message again and nothing more. To fix this issue, the redirect property of a MessagingResponse can be used.

It would make sense at first glance to add response.redirect(‘/state’) to the /webhook route, but only doing that would provide this response:

Incorrect Bot Response - Step 2 (Location Designation)

As you can see, the bot correctly redirects to the /state route but reads the input immediately. One way this can be circumvented is by introducing yet another variable to the user session called menuState. This new variable will track which step of the bot a user is in and then redirect them accordingly.

First, replace the /webhook route with the code below:

app.post('/webhook', async (req, res) => {
  const response = new MessagingResponse();

  if(req.session.menuState) {
    // If there is a menu state, then the user has logged in at least once
    // Redirect the user based on the menuState
    switch(req.session.menuState) {
      // Two steps after getting introduced to the bot:
      // 1: designating a location
      // 2: choosing a category
      // 3: search legislation
      case 1:
        response.redirect('/state')
        break;
      case 2:
        response.redirect('/category')
        break;
      case 3:
        response.redirect('/search')
        break;
    };
  } else {
    // If there is no menu state, then this is a new user
    // Initialize menuState and wait for the next message                
    req.session["menuState"] = 1
    response.message("Welcome to Bill-Bot! Bill-Bot gives you updates on hot-topic issues in the United States in the past week.\n\nPlease enter the state you would like to search bills in (abbreviations OK). Enter 'all' to search for bills across every state and on a federal level.");
  };

  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(response.toString());
});

When a message gets sent to your server, it will always go through the /webhook route first.

Depending on the value of menuState and whether it exists or not, the user is then redirected to the appropriate route. Line 25 of this code block is an example of how the server will allow the user to progress through the menu.

Additionally, this code references a new /category route that will allow users to pick the legislation category and a new /search route that will search for and display the relevant legislation. You’ll build these two routes after the /state route is finished.

Next, modify the /state route by including the highlighted lines in your code:

app.post('/state', async (req, res) => {
  const response = new MessagingResponse();
  const state = req.body.Body.toLowerCase();
  req.session["stateAbbreviation"] = "";

  if(state === "all") {
    req.session.stateAbbreviation = "ALL";
  } else if(Object.keys(states).includes(state.toUpperCase())) {
    req.session.stateAbbreviation = state.toUpperCase();
  } else if(Object.values(states).includes(state)) {
    req.session.stateAbbreviation = Object.keys(states)[Object.values(states).indexOf(state)];
  }

    if(!req.session.stateAbbreviation) {
      // If stateAbbreviation is not set, then an invalid input was given
      // Do not let the user progress until a valid input is given
      response.message("Error: please input a valid state name or abbreviation. Enter 'all' to search for bills across every state and on a federal level.");
    } else {
      // Let the user progress if a valid input is given
      response.message("Please enter one of the valid keywords below to search for relevant bills.\n\n\nprivacy: legislation related to personal information privacy.\n\ntrans: legislation related to transgender individuals and gender-affirming care.");
      req.session.menuState = 2;
  };

  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(response.toString());
});

The /state route will now allow users to progress to the next route, /category. Additionally, if the user provides an invalid state name or abbreviation, then they will not be able to progress past this step until they do so. If you were able to text the bot now, this is what it would look like:

Correct Bot Response - Steps 1-3

Choose the bill category

Now, below the /state route and above the listener, add the highlighted lines of code:

app.post('/state', async (req, res) => {
  …
});

app.post('/category', async (req, res) => {

});

app.listen(3000, () => {
  …
});

For this tutorial, you will be adding two keywords: privacy, which will search for legislation related to personal information privacy, and trans, which will search for legislation related to transgender individuals and gender-affirming care. After you complete this tutorial, you will be able to add your own keywords and legislation!

In the newly created /category route, add the following code:

app.post('/category', async (req, res) => {
  const response = new MessagingResponse();
  const input = req.body.Body.toLowerCase();
  req.session["query"] = "";

  switch(input) {
    case "trans":
      req.session.query = 'action:week AND ("transgender" OR ("gender" AND "affirming" AND "care") OR ("biological" AND "sex") OR ("gender-affirming" AND "care"))';
      req.session.menuState = 3;
      response.message("Thank you for confirming the parameters.\n\nPlease send another message to commence the search.");
      break;
    case "privacy":
      req.session.query = 'action:week AND ("personal information" OR "personal data" OR consumer OR privacy)';
      req.session.menuState = 3;
      response.message("Thank you for confirming the parameters.\n\nPlease send another message to commence the search.");
      break;
    default:
      response.message("Please input a valid keyword. Valid keywords are 'privacy', 'trans'.");
  }

  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(response.toString());
});

This block of code adds a query variable to a user’s session, then changes it based on the keyword sent to the bot. Information on how to structure your LegiScan API queries can be found here in case you would like help creating your own query.

All that’s left now is to perform the search.

Search for relevant legislation information

Now, for your app, you need to figure out how you want to parse the JSON data that returns from the query. You can either play around with the API yourself to get an idea of what you want returned to the user, or utilize the LegiScan API user manual.

For the purposes of this tutorial, you will be searching for information on the 5 most relevant bills in the past week and sending a detailed message to the user. Below the /category route and above the listener, add the final route (/search).

app.post('/category', async (req, res) => {
  …
});

// Copy the entire '/search' route into your code
app.post('/search', async (req, res) => {
  const response = new MessagingResponse();
  const data = await fetch(`https://api.legiscan.com/?key=${legiscanKey}&op=getSearch&state=${req.session?.stateAbbreviation}&query=${req.session?.query}`);
  const results = await data.json();
  const summary = results.searchresult.summary;

  if(summary.count) {
    const bills = Object.values(results.searchresult);
    response.message(`Bill-Bot has found ${summary.count} relevant bills which have been introduced or acted upon in the past week. To save data, only the 5 most relevant bills will be shown below.`);
    bills.pop() // Gets rid of the summary page
    const focusBills = bills.slice(0, 5);

    for(const bill of focusBills) {
      const billId = bill.bill_id;
      const actionDate = bill.last_action_date;
      const action = bill.last_action;
      const state = bill.state;
      const data = await fetch(`https://api.legiscan.com/?key=${legiscanKey}&op=getBill&id=${billId}`);
      const results = await data.json();
      const billData = results.bill;
      const status = billData.status;
      const statusArray = ['N/A', 'introduced', 'engrossed', 'enrolled', 'passed', 'vetoed', 'failed'];
      const statusDate = billData.status_date;
      const title = billData.title;
      const number = billData.bill_number;
      const description = billData.description;
      const titleDescriptionMatch = title == description;

      if(titleDescriptionMatch) {
        await response.message(`In ${state}, bill ${number} has been acted upon in the last week. See details below.\n\nAction: ${action}\nAction Date: ${actionDate}\n\nStatus: ${statusArray[status]}\nStatus Date: ${statusDate}\n\nTitle and Description: ${title}`);
      } else {
        await response.message(`In ${state}, bill ${number} has been acted upon in the last week. See details below.\n\nAction: ${action}\nAction Date: ${actionDate}\n\nStatus: ${statusArray[status]}\nStatus Date: ${statusDate}\n\nTitle: ${title}\n\nDescription: ${description}`);
      };
    };
  } else {
    await response.message("No new bills introduced or acted upon in the past week surrounding query in the specified location.");
  };

  response.message("Thank you for using Bill-Bot! Please send another message if you would like to perform another search.");
  req.session.destroy();

  res.writeHead(200, {'Content-Type': 'text/xml'});
  res.end(response.toString());
});


app.listen(3000, () => {
  …
});

The lines of code above will perform a search using the fetch API and return a message containing the bill parameters you specified for this tutorial. Additionally, req.session.destroy() will remove the user’s session from the server and allow them to perform another search with the bot.

All that’s left is to connect your local server to the internet using ngrok, and then forward all incoming text messages to your Twilio phone number towards your server by configuring the phone number’s webhook.

Server tunneling with ngrok

The first step to connecting your local server endpoints to your Twilio phone number is by connecting it to the internet. In a production environment, you would have your bot deployed on the cloud or through a physical server.

Head back to your terminal, make sure you’re still in your project’s working directory, and run the following command:

npx nodemon index.js

If the nodemon package was installed correctly, your screen should look like this:

Correct nodemon display

Now, open a new terminal window and navigate to the same working directory of your project. Run the following command:

ngrok http 3000

If ngrok was installed correctly, the terminal should now look like this:

ngrok display with forwarding URL emphasized

Until ngrok is terminated, all requests to the forwarding URL (boxed in red) will be forwarded to your local server at port 3000. This forwarding URL is randomly generated, so if you need to restart ngrok you will need to connect the newly created URL to your Twilio phone number again.

Configuring your Twilio phone number webhook

Now, you need to connect your local server via the forwarding URL to your Twilio phone number.

In the Active numbers section of your Twilio console (assuming you created a free account and bought a phone number), click the phone number you will use for Bill-Bot.

Scroll down to the Messaging section and put your forwarding URL in the textbox under the label “A MESSAGE COMES IN”. You will also need to add /webhook to the end of your URL to ensure the message goes to the correct route. The URL you enter should look something like this: https://xxxx.ngrok.io/webhook.

Adding /webhook to the end of your URL is very important. Without it, your messages will be forwarded to the localhost:3000/endpoint and not to the localhost:3000/webhook endpoint you made.

Twilio console messaging webhook URL location

Be sure that the left dropdown menu is on Webhook and the right dropdown menu is on HTTP POST. Finally, click the blue button labeled Save.

You can now send a message to your Twilio phone number and interact with the bot. Try it out now by texting “Ahoy!” to your Twilio number.

What’s next?

Congratulations! You just finished creating a bill-tracking bot through Twilio’s SMS API, now try customizing it or adding some new features. Maybe you want to add message scheduling to your bot? Perhaps you want to try creating a chatbot with a no-code solution using Twilio Studio? Try seeing what you can add to your bot or create a brand new one while utilizing one of Twilio’s many APIs.

If you decided you’ve had enough coding for today, consider being an activist! Use your voice to educate others, advocate for legislation, and stand up for things you believe in. You can also engage in community service, volunteer for a good cause, or create public awareness for political issues in any way you can.

Ruth Bader Ginsberg Gif

I can’t wait to see what you build next!

Dainyl Cua is a Developer Voices Intern on Twilio’s Developer Network. They would love to talk at any time and help you out. They can be reached through email via dcua[at]twilio.com or through LinkedIn.