Build a stock quote SMS bot with Twilio and TypeScript

March 29, 2021
Written by
Phil Nash
Twilion
Reviewed by

Build a stock quote SMS bot with Twilio and TypeScript

We've seen how to send an SMS with TypeScript and how to receive and reply to SMS messages with TypeScript. Now let's create something we could use! If you're into investing, you can never have too many ways to check in on the stock market, so let's build an application you can send a stock symbol to and get back a stock quote.

In this post we will build a Node.js application with TypeScript, using Express, the Twilio Node package, and the Finnhub API to reply to incoming SMS messages with stock quotes.

What you will need

To build the application in this post you will need:

With that prepared, let's get building.

A running start

In the blog post on receiving and responding to SMS messages in TypeScript we built a good base for this application which we will work from. If you haven't followed that post, you can get up to speed quickly. First, download or clone this repo of Twilio TypeScript examples and change into the receive-sms directory:

git clone https://github.com/philnash/twilio-typescript-examples.git
cd twilio-typescript-examples/receive-sms

Install the dependencies:

npm install

Compile the TypeScript:

npm run build

Start the server:

npm start

You now have your application running locally on port 3000, to connect it to a Twilio number you will need to create a publicly available URL that can tunnel through to your local machine. I like to do this with ngrok. Open a second terminal and run ngrok with the command:

ngrok http 3000

Once the tunnel has connected, ngrok will display a URL with a random subdomain that now points at your application. It should look like https://RANDOM_STRING.ngrok.io. Open your Twilio console and navigate to your incoming numbers, and choose the number you want to use for this app, orbuy a new one. Edit the number and add your ngrok URL, plus the /messages path, as the webhook for when a message comes in.

A screen shot from the Twilio console. When editing a number, enter your ngrok webhook URL in the box marked "A message comes in".

If you have the Twilio CLI installed you can do this on the command line with the command:

twilio phone-numbers:update PHONE_NUMBER --sms-url https://RANDOM_STRING.ngrok.io/messages

With all that set up, send an SMS message to your Twilio number. You will get a response echoing back what you said.

A view of an iPhone's messaging app, with a reply from our TypeScript application.

This is all handled in the routes/messages.js file; incoming messages are routed to the /messages endpoint, the body of the message is extracted from the request body and a response is built up using messaging TwiML. The response is then returned as XML.

router.post("/", (req: MessagingRequest, res: Response<string>) => {
  const message = req.body.Body;

  const response = new MessagingResponse();
  response.message(`Hello from TypeScript! You said "${message}"`);

  res.set("Content-Type", "application/xml");
  res.send(response.toString());
});

We did some other work to set the type of the incoming request and response. Check out the rest of the blog post to learn more about that.

Let's get on with turning our incoming SMS messages into stock quotes.

Making API requests in TypeScript

In this application we are going to turn our incoming message body into a request to the Finnhub API for a quote.

Loading config into your app

To use the API we will need to load the Finnhub API key into the application. We recommend doing this via environment variables so that you don't inadvertently commit API credentials to your project. In this application we will manage that with the dotenv package.

Install dotenv to your development dependencies:

npm install dotenv -D

Open package.json and change the "start" script to:

"start": "nodemon --require dotenv/config ."

This will require and run dotenv when you start the application.

Find your Finnhub API key on the dashboard.

The Finnhub dashboard. The API Key is displayed in a box under the email address you used to sign up.

Create a file called .env and add your Finnhub API key to it like so:

FINNHUB_API_KEY=YOUR_API_KEY

Open config.ts and add a line that reads the Finnhub API key and exports it again:

 
export const port = process.env.PORT || 3000;
export const finnhubApiKey = process.env.FINNHUB_API_KEY;

With that done, we can move on to making requests to the API.

Making API requests in TypeScript

There are many ways to make HTTP requests in Node.js. We will use the got package to make our requests to the Finnhub API. got is fully written in TypeScript, so it provides us with types. Install got to the project:

npm install got

Create a new directory called src and a file within that directory called quotes.ts. This is where we will write a function to make requests to the Finnhub API. Start by importing got and your Finnhub API key from config.

import got from "got";
import { finnhubApiKey } from "../config";

Now, let's define the function we'll use to make requests to the API. It will take the symbol we want to look up as a string and, since it will make an asynchronous HTTP request, return a Promise that resolves to an object of type Quote.

export const getQuote = async (symbol: string): Promise<Quote> => {
}

There's a problem here though, we don't have a Quote type. If we take a look at the Finnhub documentation we can see a sample response from the API:

{
  "c": 261.74,
  "h": 263.31,
  "l": 260.68,
  "o": 261.07,
  "pc": 259.45,
  "t": 1582641000
}

The properties refer to the different prices the API returns; the current price, the day's high, the day's low, the opening price and the price at the previous close. The final property, t, is a timestamp for the prices. Using this, we can define our Quote type inside quotes.ts:

type Quote = {
  o: number;
  h: number;
  l: number;
  c: number;
  pc: number;
  t: number;
};

Let's make the request to the API using got. We make a GET request to the API URL https://finnhub.io/api/v1/quote and pass the symbol we are looking up as a URL parameter. We can pass the API key as a URL parameter or in the headers as the X-Finnhub-Token header. With got we can set the expectation up front that the response will be JSON and the package will parse the response for us, so we can then return the body of the response.

export const getQuote = async (symbol: string): Promise<Quote> => {
  const response = await got("https://finnhub.io/api/v1/quote", {
    searchParams: { symbol },
    responseType: "json",
    headers: {
      "X-Finnhub-Token": finnhubApiKey,
    },
  });
  return response.body;
}

If you write out the code above, you will find that TypeScript is not happy about compiling. response.body is of type unknown. We know it is a JSON response that has been parsed into an object, but TypeScript doesn't know the shape of that object. In this case, we can assert, since we read the documentation, that it is of the type Quote that we defined.

We assert that the body is of type Quote using the syntax as Quote, like so:

export const getQuote = async (symbol: string): Promise<Quote> => {
  const response = await got("https://finnhub.io/api/v1/quote", {
    searchParams: { symbol },
    responseType: "json",
    headers: {
      "X-Finnhub-Token": finnhubApiKey,
    },
  });
  return response.body as Quote;
}

Now TypeScript is happy and we can use this function in the rest of our application.

Building an SMS response

Open routes/messages.ts, we're now going to alter the response from echoing the body of the message to making a call to the Finnhub API and sending back data about the stock prices.

At the top of the file import the getQuote function we just defined:

import { twiml } from "twilio";
import { urlencoded, Router, Response } from "express";
import { MessagingRequest } from "../types/request";
import { getQuote } from "../src/quotes";

Remove the code from within the route definition and start building our new response. First we make the route handler an async function. Then in the handler itself we get the body of the incoming message and uppercase it. This is the symbol we will look up with the API. We also create a new TwiML messaging response.

router.post("/", async (req: MessagingRequest, res: Response<string>) => {
  const symbol = req.body.Body.toUpperCase();
  const response = new MessagingResponse();
  // ...
});

Next we want to make the request to the API. This could fail, so we wrap it in a try/catch block. When we get the response we send the data back however we like. I'm going to compare the current price to the previous close and show an emoji based on whether it is higher or lower. We set the message to send back by calling message on the TwiML response object.

If there is an error, we'll return an error to the sender instead. Finally, whether the API request was a success or not, we set the content type of the response to "application/xml" and send the TwiML response as a string.

router.post("/", async (req: MessagingRequest, res: Response<string>) => {
  const symbol = req.body.Body.toUpperCase();
  const response = new MessagingResponse();
  try {
    const quote = await getQuote(symbol);
    const emojiChart = quote.c > quote.pc ? "📈" : "📉";

    response.message(
      `Current price for ${symbol}: ${formatter.format(quote.c)}
Previous close: ${formatter.format(quote.pc)} ${emojiChart}`
    );
  } catch (error) {
    console.error(error);
    response.message(`Could not find a stock price for ${symbol}.`);
  }
  res.contentType("application/xml");
  res.send(response.toString());
});

Last thing to do is compile the code and start the server.

npm run build
npm start

If you still have ngrok running from earlier, then you are good to go. If you need to start ngrok again you will likely need to update your number configuration in the Twilio console to use a new ngrok subdomain.

Send a text to your number with a stock symbol you want to look up. If all goes well, you will get a response with the current price.

Success! Sending a stock symbol to the phone number returns the current price and a comparison to the previous day&#x27;s closing price. Apparently I picked a bad day on the stock market to try this out. GameStop was down almost $60 and Apple was down $2.45.

If it doesn't work, check the application log and there should be an error message telling you what went wrong.

Now you can make API requests in TypeScript

In this post you have seen how to take an incoming SMS message, turn the body into an API request and format and return the results as a reply. We used the Finnhub API and stock quotes as the example here and you could do a lot more with this data. You could use more of the data returned from the quote API, or pick a different endpoint, like foreign exchange rates or even cryptocurrencies. You could also pick any other HTTP API, there are a few good examples of fun APIs in this blog post.

The code for this post, as well as the others in this series of posts about building Twilio apps with TypeScript, is available on GitHub.

Let me know what you are building with Twilio and keeping type-safe with TypeScript. Drop me a line at philnash@twilio.com or send me a tweet at @philnash.