Create Custom Voicemail with Node.js, Express and Socket.io
Published on May 14, 2024

Introduction

In this tutorial, you'll learn how to build your voicemail. We will use Express.js and Socket.io.

You can find the complete code on GitHub.

Outline and Prerequisites

  • Node.js installed

  • Ngrok is installed to expose your local development server to the internet.

  • A basic understanding of JavaScript and Node.js

  • A Vonage API account to access our Voice API

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Project Structure

For your reference, this is how the project directory will look like:

[node_modules] [public] [voicemails] // Voicemail mp3 files here index.html .env index.js package-lock.json package.json private.key

Initialize the Project

  1. Create a directory for your application and change into it.

  2. Run npm init -y to initialize your project.

Set Up Ngrok

To make our local server's endpoints accessible from the internet without deploying our application to a production environment, we'll expose our local machine using ngrok.

Open a new terminal or command prompt window and initiate ngrok on port 3000 with the following command: ngrok http 3000.

Create a Vonage Application and Add the Private Key

  1. Log in/ Sign Up to your Vonage API dashboard and navigate to "Your applications" under Build & Manage.

  2. Click "+ Create a new application". Give your application a name and select "Generate public and private key." Then, download the generated private key.

  3. In the "Capabilities" section, enable "Voice" and provide the URLs for the event, and answer webhook endpoints. Use your Ngrok URL: ${NGROK_URL}/event for Event URL and ${NGROK_URL}/answer for Answer URL.

  4. Save your application. Note the Application ID and API Key are provided.

  5. Add the downloaded private key to your project directory.

Check out our ngrok guide for help with the webhook endpoints

Purchase a Virtual Number

Next, we'll secure a virtual number to serve as the point of contact for incoming calls. This number will be associated with our Vonage application. Here's the process for obtaining one:

Install the Vonage CLI: npm install @vonage/cli -g Search for available numbers in your desired region: vonage numbers:search US (replace US with your country code if needed). Buy a number: vonage numbers:buy 12079460000 US.

Remember that some numbers cannot be purchased via the Command Line, so you must purchase them via the dashboard. You can do so by going to the Vonage Dashboard and the Buy Numbers page. Tick 'Voice' in the search filter and select the country where you want to buy a number.

Once we've selected our number, we must link it to our Vonage application. This can be done directly through the Vonage Dashboard or using the CLI. By linking the number, we ensure calls are properly directed to our application.

vonage apps:link --number=12079460000 VONAGE_APPLICATION_ID

You can also do it from the dashboard. Go to the Application page and click on the application you created earlier. Click the 'Link' button next to the number you want to link in the Voice section.

Create Environment Variables

Create a .env file in your project root with the following, filling in your data from the Vonage Dashboard: Check this blog post if you're not familiar with How to Use Environment Variables in Node.js.

URL=YOUR_NGROK_URL API_KEY=Your Vonage API key is used to authenticate API requests. API_SECRET=Your Vonage API secret VONAGE_APPLICATION_ID= The ID of your Vonage application. Vonage applications allow you to manage your communication services and define how they interact with your web application. PRIVATE_KEY=private.key

Install Dependencies

We are going to use the following dependencies in our project: Express Socket.io @vonage/server-client) body-parser dotenv

You can install them by running the following command:

npm install express socket.io @vonage/server-sdk body-parser dotenv

Server Setup

Create the index.js file and initialize your server with the required packages. Configure a Vonage client using variables from .env and outline the /answer, /event, and /voicemail routes.

require("dotenv").config();

const express = require("express");
const app = express();
const http = require("http").Server(app);
const io = require("socket.io")(http);

const bodyParser = require("body-parser");
const { FileClient } = require("@vonage/server-client");

// Ensure these environment variables are correctly set in your .env file
const fileClient = new FileClient({
  applicationId: process.env.VONAGE_APPLICATION_ID,
  privateKey: process.env.PRIVATE_KEY,
});

app.use(express.static("public"));
app.use(bodyParser.json());

app.get('/answer', (req, res) => {});

app.post('/event', (req, res) => {});

app.post('/voicemail', (req, res) => {});

http.listen(3000, () => console.log("Server is running on port 3000"));

Define Call Handling Logic

In the /answer route, we set up a call flow using an NCCO (Call Control Object) to interact with callers. This script greets the caller, prompts them to leave a message after a beep, and ends the recording when the '#' key is pressed, followed by a farewell message. The res.send(ncco); line sends this instruction set back to Vonage, executing it during the call.

app.get("/answer", (req, res) => {
  const ncco = [
    {
      action: "talk",
      voiceName: "Ivy",
      text: "Please record your message for me after the beep. Press # to end.",
    },
    {
      action: "record",
      eventUrl: [`${process.env.URL}/voicemail`],
      endOnKey: "#",
      beepStart: true,
    },
    {
      action: "talk",
      voiceName: "Ivy",
      text: "Message recorded. Bye!"
    }
  ];
  res.send(ncco);
});

Customize the greeting to make the call more personal. Many voice options allow you to adjust the vocal tone, pitch, and style to match your preferences or brand identity. Explore different voice styles to find the perfect match for your application. Note that some voice styles may not support SSML (Speech Synthesis Markup Language), so it's worth checking the list of supported languages and styles.

Handle Events and Voicemails

The /event endpoint processes various events sent by the Vonage API. When an event occurs, such as an answered call or a recorded voicemail, the Vonage API sends data related to that event to this endpoint. Our server logs this event data to the console for visibility and returns a 204 (No Content response) to acknowledge receipt. This minimal handling is sufficient for many applications, though you could extend this to perform specific actions based on event types.

Implement logic in /event to process events.

app.post("/event", (req, res) => {
  console.log("Event received:", req.body);
  res.sendStatus(204);
});

When someone leaves a voicemail message, the Vonage API sends a POST request to our /voicemail endpoint with details about the recording, including a URL where the recorded audio file can be accessed.

Our server simulates saving this recording by creating a filename based on the current timestamp and pretending to save the file at a specified path. This simplification is for demonstration purposes; you would likely download the recording from the provided URL and store it more securely in an actual application.

After "saving" the file, the server emits a voicemail event to all connected clients through Socket.io, passing along the filename and the current date and time.

In /voicemail, simulate saving voicemails and emit a voicemail event to the client.

app.post("/voicemail", (req, res) => {
  const filename = `voicemail-${Date.now()}.mp3`;
  const path = `${__dirname}/public/voicemails/${filename}`;

  fileClient
    .downloadFile(req.body.recording_url, path)
    .then(() => {
      io.emit("voicemail", {
        date: new Date().toISOString(),
        file: filename,
      });
      res.sendStatus(200);
    })
    .catch((err) => {
      console.error("Error saving file:", err);
      res.status(500).send(err);
    });
});

http.listen(3000, () => console.log("Server is running on port 3000"));

The User Interface

In public/index.html, we will add an <audio> element for playing voicemail messages and a container for listing voicemails, allowing the voicemail receipt to play or delete messages received.

<meta charset="utf-8">
    <title>Voicemail</title>


    <audio>Sorry, your browser does not support this audio.</audio>
    <div id="voicemails" class="voicemails">
        <h2>New Voicemails</h2>
        <!-- Voicemails will be dynamically added here -->
    </div>

Display the Received Voice Mail Messages

Our JavaScript enriches the user interface by connecting with the server via Socket.io for real-time updates, like when new voicemails arrive. It automatically adds these voicemails to a list, showing their date and providing buttons to play or delete them. While the play button uses an <audio> element for playback, the delete button only removes the voicemail from the display, not the server. Enhancing the application to delete voicemails from the server would also be a valuable improvement, requiring extra server-side logic to handle file deletions securely.

Add the following code to your index.html file:

<script src="/socket.io/socket.io.js"></script>
  <script>
    const voicemailsDiv = document.getElementById("voicemails");
    const audio = document.querySelector("audio");

    document.addEventListener("click", function (e) {
      let el = e.target;
      if (el.classList.contains("play_btn")) {
        audio.src = el.getAttribute("data-src");
        audio.play();
        console.log("Playing audio from:", audio.src);
      }
      if (el.classList.contains("del_btn")) {
        el.closest(".voicemail-item").remove();
        console.log("Voicemail deleted");
      }
    });

    const socket = io();
    socket.on("connect", () => {
      console.log("Connected to server via Socket.io");
    });
    socket.on("voicemail", (vm) => {
      console.log("Received voicemail event:", vm);
      const voicemailElement = document.createElement("div");
      voicemailElement.classList.add("voicemail-item");
      voicemailElement.innerHTML = `
            <div>Date: ${vm.date}</div>
            <button class="play_btn" data-src="/${vm.file}">Play</button>
            <button class="del_btn">Delete</button>
        `;
      voicemailsDiv.appendChild(voicemailElement);
    });
  </script>

Test It Out

To get your server running, run node index.js in your terminal from the root of your directory, and make sure ngrok is running.

If you call the phone number associated with your Vonage application, you'll hear the greeting you provided with the NCCOs. Once you record your message, it should appear on your HTML page on localhost:3000.

Conclusion

You've now created a voicemail system, enabling callers to leave messages displayed on a webpage. While current functionality does not extend to server-side file deletion, addressing this would be a valuable enhancement.

Chat with us on our Vonage Developer Community Slack or send us a message on X.

Amanda CavallaroDeveloper Advocate

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.

Subscribe to Our Developer Newsletter

Subscribe to our monthly newsletter to receive our latest updates on tutorials, releases, and events. No spam.