Building An Answering Machine

Banner for a MediaJam post

Eugene Musebe

During non-working hours, it makes sense to route phone calls to an answering machine as opposed to leaving the phone unattended ringing at all times. Let us see how we can create a phone answering machine that records the phone calls and allows easy access.


The completed project is available on Codesandbox.

You can find the full codebase on my Github

To be able to follow along, knowledge of HTML, CSS, and JavaScript is required. Knowledge of Vue.JS is a plus but not required

Project setup

We will be using Nuxt. JS to build this project. Nuxt.Js is a performance and convenient Vue.Js framework.

To get started, ensure you have yarn or npm v5.2+/v6.1+ installed. Open your terminal in your preferred working directory and run the following command.

1yarn create nuxt-app nuxtjs-phone-answering-machine
2# OR
3npx create-nuxt-app nuxtjs-phone-answering-machine
4# OR
5npm init nuxt-app nuxtjs-phone-answering-machine

Once you run the above command, you will receive a set of setup questions. Here are our recommended defaults:

Project name: nuxtjs-phone-answering-machine Programming language: JavaScript Package manager: Yarn UI framework: Tailwind CSS Nuxt.js modules: N/A Linting tools: N/A Testing framework: None Rendering mode: Universal (SSR/SSG) Deployment target: Server (Node.js hosting) Development tools: None

Once the setup process is complete, you may now enter the directory and run the project. It will be accessible on http://localhost:3000.

1cd nuxtjs-phone-answering-machine
3yarn dev
4# OR
5npm run dev

Cloudinary registration

We need to store our recordings somewhere easily accessible. For this, we will use Cloudinary, a media management platform with a set of rich APIs and SDKs. If you do not have an account create one here. Let us link our project to our account by setting up our credentials in our .env file. This is the file that contains our environmental variables. These are values we do not want to save in our code and code repository. Let's create our .env file

1touch .env

We can now add our credentials. If you do not have yours you can refer to the Account Details section of the dashboard.


Phone number registration

We will be using Twilio to get a phone number. If you don't have an account, feel free to set up one here. You should get a trial account which allows you to get a trial phone number. Once you register a trial phone number, upgrade the account to use Twilio voice.

Receiving and recording phone calls

Middleware setup

Let us prepare our Twilio voice callback. First, we need to enable server middleware in our Nuxt.Js project. Let's create api.js in the server-middleware folder.

1touch server-middleware\api.js

Let us register this middleware in the nuxt.config.js file.

1// nuxt.config.js
2export default {
3 ...
4 serverMiddleware: [
5 { path: "/api", handler: "~/server-middleware/api.js" },
6 ],

Requests to the /api route will not be sent to our middleware.

Dependency installation

Let us now install all the dependencies we need to create the callback.

1yarn add body-parser cloudinary express Twilio

Callback implementation

Now let us prepare a \api\callback endpoint to receive our Twilio phone number voice callback. If the call is not complete, we will play a message and start recording. Once the call is complete, we will receive the recording, upload it to Cloudinary and add some context metadata.

1// server-middleware/api.js
5const app = require('express')();
7const bodyParser = require('body-parser');
11app.use(bodyParser.urlencoded({ extended: true }));
13const VoiceResponse = require('twilio').twiml.VoiceResponse;
15const cloudinary = require('cloudinary');
18 cloud_name: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,
19 api_key: process.env.CLOUDINARY_API_KEY,
20 api_secret: process.env.CLOUDINARY_API_SECRET,
21 secure: true
24const tag = "nuxtjs-phone-answering-machine";
26app.all('/callback', async (request, response) => {
28 const body = request.body;
30 if (body.CallStatus !== "completed") {
31 const twiml = new VoiceResponse();
32 twiml.say('Hello. Please leave a message after the beep.');
33 twiml.record();
34 twiml.hangup();
35 response.type('text/xml');
36 return response.send(twiml.toString());
37 }
39 const maskedPhoneNumber = body.From.slice(0, 3) + body.From.slice(3, -2).replace(/[0-9]/g, "*") + body.From.slice(-2);
41 const uploaded = await cloudinary.v2.uploader.upload(
42 body.RecordingUrl,
43 {
44 resource_type: "video",
45 folder: "nuxtjs-phone-answering-machine",
46 tags: [tag],
47 context: `From=${maskedPhoneNumber}|FromCountry=${body.FromCountry}|FromCity=${body.FromCity}`
48 },
49 function (error, result) { console.log(result, error) });
51 return response.json({ uploaded });
55module.exports = app

To ensure that we safeguard our user's privacy, we mask most digits of the phone numbers.

Configuring the callback

Let us first enable our endpoint and our app to be accessible online. We are going to use ngrok for this. Follow the setup instructions here to download and authenticate your ngrok instance. Finally, instead of starting a tunnel to port 80 as instructed in the setup instructions, we tunnel to port 3000

1ngrok http 3000

Proceed to the Manage > Active phone numbers section to configure our callback. Open your test phone number and input the ngrok URL you received after tunneling. Add it to the A call comes in input:

Now when you place a phone call, it will be received, recorded, and uploaded to your Cloudinary account.

Listing recordings

We need to enable our Nuxt.Js app to receive a list of all recordings. Let us create an api/list endpoint for this purpose. We will use the Cloudinary Admin API > Get resources by tag for this.

1// server-middleware/api.js
5app.all('/list', async (request, response) => {
7 return await cloudinary.v2.api.resources_by_tag(
8 tag,
9 { resource_type: 'video', context: true },
10 function (error, result) {
11 return response.json(result);
12 }
13 );
17module.exports = app

Showing recordings

Let us fetch the recordings in the asyncData hook of the script section. This will load all the recordings into our page state.

1// pages/index.vue
4export default {
5 async asyncData () {
6 const url = `${process.env.NUXT_ENV_BASE_URL}/api/list`;
8 const recordings = await fetch(url)
9 .then(response => response.json())
10 .then(data => data.resources);
12 return {recordings};
13 }

Now we can iterate the recordings variable and display the recordings in our template section.

1<!-- pages/index.vue -->
4 <div>
5 <ul role="list">
6 <li v-for="recording in recordings" :key="recording.public_id">
7 <div>
8 <div>
9 <div>
10 <div>
11 <p>
12 Recording from {{recording.context.custom.From}}
13 ({{recording.context.custom.FromCountry}} {{recording.context.custom.FromCity}})
14 </p>
15 <p>
16 <span>
17 {{recording.created_at}}
18 </span>
19 </p>
20 </div>
21 <div>
22 <div>
23 <audio controls :src="recording.secure_url" >
24 Your browser does not support the <code>audio</code> element.
25 </audio>
26 </div>
27 </div>
28 </div>
29 </div>
30 </div>
31 </li>
32 </ul>
33 </div>

With the above code, our project can now receive, record incoming calls, and display recordings for access. To learn more about Twilio Voice API feel free to read the documentation. Feel free to also read the Cloudinary documentation to see what is possible with this powerful platform

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.