• Choose:

How to Build a Call Tracking App with the Telnyx Voice API

⏱ 60 minutes build time.

🧰 Clone the sample application from our GitHub repo

🎬 Check out our on-demand webinar walking through this tutorial.


In this tutorial, you'll learn how to build a Call Tracking application using the Telnyx API, and our Node SDK.

Call Control (the Telnyx Voice API), combined with our Numbers API, provides everything you need to build a robust call tracking application:

  • The Numbers API enables you to search the Telnyx phone number inventory in real time; filtering by Area Code, City/State, and more to find the perfect local number for your use-case.
  • Call Control enables you to quickly setup dynamic forwarding numbers, toggle dual-channel recording, join/leave dynamic conferences, and pull post-call analytics.

By following this tutorial, you'll build an app that can:

  1. Search and order a phone number by area code.
  2. Store a 'binding' of Telnyx phone numbers to a forwarding number (to which incoming calls to the Telnyx phone numbers will be forwarded).
  3. Receive inbound calls to the Telnyx phone number.
  4. Transfer calls using Call Control.
  5. Store webhook events associated with calls to a datastore.

Create a Telnyx Mission Control Portal account

To get started, you'll need to create an account. Once you've verified your email address and logged into the Mission Control Portal, you'll get $10 of credit to test out our platform.

Set up your local machine to receive webhooks from Telnyx

Next, you'll need a means of receiving webhooks sent by Telnyx to notify your application of call events. One of the easiest ways to accomplish this is to use a tool like ngrok to generate a tunnelling URL, which connects to a locally running application via a port on your machine.

In this example, port 8000 is used. After downloading and installing ngrok, run ./ngrok http 8000 and make note of the resultant HTTPS Forwarding URL.

Create a Telnyx Call Control Application

From the Portal, create a new Call Control Application, and paste the HTTPS Forwarding URL from the previous steps to send webhooks from this application to your local machine via ngrok.

Ensure API v2 is selected, and save your application. We don't need to worry about any other appliction settings for now.

Select your application again to edit it, and make a note of the ID. This is how you'll identify your Call Control Application in your code.

Create an Outbound Voice Profile

From the Portal, create a new Outbound Voice Profile. Click Add connections/apps to profile and select the Call Control Application you created in the previous step.

In the International Allowed Destinations section, ensure you have selected the region(s) in which you want your application to work.

Initialize and Install packages via npm

Initialize your call tracking application with the defaults presented to you.

mkdir call-tracking
cd call-tracking
npm init

Then install the necessary packages for the call tracking application

npm i dotenv
npm i express
npm i telnyx

This will create a package.json file with the packages needed to run the application.

Set up environment variables

The following environment variables need to be set for your call tracking application to work:

VariableDescription
TELNYX_API_KEYYour Telnyx API Key, which can be created in the portal.
TELNYX_PUBLIC_KEYYour Telnyx Public Key, which is accessible via the portal.
TELNYX_CONNECTION_IDThe ID from your Call Control Application
PORTThe port through which the app will be served. This variable defaults to 8000

This app uses the excellent dotenv package to manage environment variables.

Make a copy of the file below, add your credentials, and save as .env in the root directory.

TELNYX_PUBLIC_KEY=
TELNYX_API_KEY=
TELNYX_CONNECTION_ID=
PORT=8000

Create JavaScript files to build a Call Tracking Application

We'll use a few .js files to build the call tracking application.

  • index.js as our entry point to the application
  • db.js for our database controller (in-memory DB for sample)
  • callControl.js to manage call-control webhooks
  • bindings.js to manage call-tracking bindings and post-call metadata
touch index.js
touch db.js
touch callControl.js
touch bindings.js

Setup Express Server for Call Tracking

The index.js file sets up 2 express routes:

  • /call-control : To handle call-control webhooks
  • /bindings : To manage phone number bindings and call information
// In index.js
require('dotenv').config()

const express = require('express');
const app = express();

app.use(express.json());
app.use(express.urlencoded({extended: true}));

const callControlPath = '/call-control';
const callControl = require('./callControl');
app.use(callControlPath, callControl);

const bindingsPath = '/bindings'
const bindings = require('./bindings');
app.use(bindingsPath, bindings);

app.listen(process.env.TELNYX_APP_PORT);
console.log(`Server listening on port ${process.env.TELNYX_APP_PORT}`);

Setup database for Call Tracking information

The db.js file contains the in-memory database to manage our phone numbers and call information. It exports 1 array and 3 functions:

  • bindings = [] : Our in-memory database
  • addPhoneNumberBinding : accepts a Telnyx phone number and a destination number to save to the database.
    • Called when ordering / creating a new call-tracking number
  • getDestinationPhoneNumber : accepts a Telnyx phone number and searches the database for a match, then returns the destination phone number.
    • Called when receiving an inbound call to look up transfer destination.
  • saveCall : accepts a Telnyx event and saves the call to the database based on the payload.
    • Called when the call.hangup event is received to save post-call information
  • getBinding: accepts a Telnyx phone number and returns the matching binding information from the database.
    • Called when GET bindings has a telnyxPhoneNumber query parameter
// in db.js
const bindings = [];
module.exports.bindings = bindings;

module.exports.addPhoneNumberBinding = (telnyxPhoneNumber, destinationPhoneNumber) => {
  const index = bindings.findIndex(binding => binding.telnyxPhoneNumber === telnyxPhoneNumber);
  if (index > 0) {
    return {
      ok: false,
      message: `Binding of Telnyx: ${telnyxPhoneNumber} already exists`,
      binding: bindings[index]
    }
  }
  const binding = {
    telnyxPhoneNumber,
    destinationPhoneNumber,
    calls: []
  }
  bindings.push(binding);
  return { ok: true }
};

module.exports.getDestinationPhoneNumber = telnyxPhoneNumber => {
  const destinationPhoneNumber = bindings
    .filter(binding => binding.telnyxPhoneNumber === telnyxPhoneNumber)
    .reduce((a, binding) => binding.destinationPhoneNumber, '');
  return destinationPhoneNumber;
};

module.exports.saveCall = callWebhook => {
  const telnyxPhoneNumber = callWebhook.payload.to;
  const index = bindings.findIndex(
      binding => binding.telnyxPhoneNumber === telnyxPhoneNumber);
  bindings[index].calls.push(callWebhook);
};

module.exports.getBinding = telnyxPhoneNumber => {
  return bindings.filter(
      binding => binding.telnyxPhoneNumber === telnyxPhoneNumber);
};

Managing phone number bindings for Call Tracking

The bindings.js file contains all the logic for:

// in bindings.js
const express = require('express');
const telnyx = require('telnyx')(process.env.TELNYX_API_KEY);
const router = module.exports = express.Router();
const db = require('./db');
const CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;

const searchNumbers = async (req, res, next) => {
  const isInvalidRequest = (!req.body.areaCode || !req.body.destinationPhoneNumber || req.body.areaCode.length !== 3)
  if (isInvalidRequest) {
    res.send({
      message: 'Invalid search criteria, please send 3 digit areaCode',
      example: '{ "areaCode": "919", "destinationPhoneNumber": "+19198675309" }'
    });
    return;
  }
  try {
    const areaCode = req.body.areaCode;
    const availableNumbers = await telnyx.availablePhoneNumbers.list({
      filter: {
        national_destination_code: areaCode,
        features: ["sms", "voice", "mms"],
        limit: 1
      }
    });
    const phoneNumber = availableNumbers.data.reduce((a, e) => e.phone_number, '');
    if (!phoneNumber) {
      res.send({message: 'No available phone numbers'}).status(200);
    } else {
      res.locals.phoneNumber = phoneNumber;
      next();
    }
  } catch (e) {
    const message = ''
    console.log(message);
    console.log(e);
    res.send({message}, ...e).status(400);
  }
}

const orderNumber = async (req, res, next) => {
  try {
    const phoneNumber = res.locals.phoneNumber;
    const result = await telnyx.numberOrders.create({
      connection_id: CONNECTION_ID,
      phone_numbers: [{
        phone_number: phoneNumber
      }]
    });
    res.locals.phoneNumberOrder = result.data;
    next();
  } catch (e) {
    const message = `Error ordering number: ${res.locals.phoneNumber}`
    console.log(message);
    console.log(e);
    res.send({message}, ...e).status(400);
  }
}

const saveBinding = async (req, res) => {
  try {
    const telnyxPhoneNumber = res.locals.phoneNumber;
    const destinationPhoneNumber = req.body.destinationPhoneNumber;
    db.addPhoneNumberBinding(telnyxPhoneNumber, destinationPhoneNumber);
    res.send(res.locals.phoneNumberOrder);
  } catch (e) {
    res.send(e).status(409);
  }
}

const getBindings = async (req, res) => {
  if (req.query.telnyxPhoneNumber) {
    const telnyxPhoneNumber = req.query.telnyxPhoneNumber;
    const binding = db.getBinding(telnyxPhoneNumber);
    res.send(binding).status(200);
  } else {
    res.send(db.bindings);
  }
}

router.route('/')
.post(searchNumbers, orderNumber, saveBinding)
.get(getBindings);

Managing call flows for Call Tracking

The callControl.js file contains the routes and functions for:

// in callControl.js
const express = require('express');
const telnyx = require('telnyx')(process.env.TELNYX_API_KEY);
const router = module.exports = express.Router();
const db = require('./db');

const outboundCallController = async (req, res) => {
  res.sendStatus(200); // Play nice and respond to webhook
  const event = req.body.data;
  const callIds = {
    call_control_id: event.payload.call_control_id,
    call_session_id: event.payload.call_session_id,
    call_leg_id: event.payload.call_leg_id
  }
  console.log(`Received Call-Control event: ${event.event_type} DLR with call_session_id: ${callIds.call_session_id}`);
}

const handleInboundAnswer = async (call, event, req) => {
  console.log(`call_session_id: ${call.call_session_id}; event_type: ${event.event_type}`);
  try {
    const webhook_url = (new URL('/call-control/outbound', `${req.protocol}://${req.hostname}`)).href;
    const destinationPhoneNumber = db.getDestinationPhoneNumber(event.payload.to);
    await call.transfer({
      to: destinationPhoneNumber,
      webhook_url
    })
  } catch (e) {
    console.log(`Error transferring on call_session_id: ${call.call_session_id}`);
    console.log(e);
  }
}

const handleInboundHangup = (call, event) => {
  console.log(`call_session_id: ${call.call_session_id}; event_type: ${event.event_type}`);
  db.saveCall(event);
}

const inboundCallController = async (req, res) => {
  res.sendStatus(200); // Play nice and respond to webhook
  const event = req.body.data;
  const callIds = {
    call_control_id: event.payload.call_control_id,
    call_session_id: event.payload.call_session_id,
    call_leg_id: event.payload.call_leg_id
  }
  const call = new telnyx.Call(callIds);
  switch (event.event_type) {
    case 'call.initiated':
      await call.answer();
      break;
    case 'call.answered':
      await handleInboundAnswer(call, event, req);
      break;
    case 'call.hangup':
      handleInboundHangup(call, event);
      break;
    default:
      console.log(`Received Call-Control event: ${event.event_type} DLR with call_session_id: ${call.call_session_id}`);
  }
}

router.route('/outbound')
.post(outboundCallController);

router.route('/inbound')
.post(inboundCallController);

Running the Call Tracking application

Now that you've saved all the examples and built your routes, it's time to run the application.

Launch ngrok and update your Call Control Application

We need to be able to receive webhooks from Telnyx, sent over the public Internet. We'll use ngrok for this tutorial.

Launch ngrok on the PORT specified in your .env file. If you're using port 8000 (the default for this app), you can simply run ./ngrok http 8000

$ ./ngrok http 8000

ngrok by @inconshreveable

Session Status                online
Account                       Little Bobby Tables (Plan: Free)
Version                       2.x.x
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://ead8b6b4.ngrok.io -> localhost:8000
Forwarding                    https://ead8b6b4.ngrok.io -> localhost:8000

Connections                   ttl     opn     rt1.   rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

Once you've set up ngrok (or another tunneling service of your choice) you can add the public proxy URL to your Inbound Settings in the Mission Control Portal.

To do this, click the edit symbol [] next to your Call Control Application

In the Send a webhook to the URL: field, paste the forwarding address from ngrok into the Webhook URL field. Add /call-control/inbound to the end of the URL to direct the request to the webhook endpoint in your server.

If we were using the example URL from the code sample above, the URL would be http://ead8b6b4.ngrok.io/call-control/inbound.

Run the Node.JS Call Tracking Application

Start the server by running node index.js.

Once everything is setup, you should now be able to:

  • Allocate a new call tracking number and bind it to a forwarding number
  • Call the allocated number and get connected to the destination.

Create a Binding for Call Tracking

The bindings interface is managed through a RESTful API.

To create a new binding create a POST request to your ngrok URL (in this example: http://ead8b6b4.ngrok.io/bindings)

The POST request accepts a JSON object with the following fields:

  • areaCode: Desired area code for the new call tracking phone number
  • destinationPhoneNumber : Number which we'll forward all incoming calls to the call-tracking phone number
POST http://ead8b6b4.ngrok.io/bindings HTTP/1.1
Content-Type: application/json; charset=utf-8

{
  "areaCode" : "919",
  "destinationPhoneNumber": "+19198675309"
}

The application will search the Telnyx number inventory for a phone number matching the areaCode passed, and will order the first result returned from the API. It then creates a binding so that any inbound call to the Telnyx phone number is forwarded to the destination phone number.

List Call Tracking bindings and call information

The bindings endpoint supports a GET request to pull call information and existing bindings.

The bindings object returns a calls array with the hangup webhooks saved. The length of the array equals the number of calls the call tracking number received. The duration for each call can be calculated as the difference between the start_time and end_time values.

GET http://ead8b6b4.ngrok.io/bindings HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "telnyxPhoneNumber": "+19193234088",
    "destinationPhoneNumber": "+19198675309",
    "calls": [
      {
        "event_type": "call.hangup",
        "id": "cddecb2a-bb3c-4e90-8e85-e1b6d51a901b",
        "occurred_at": "2021-01-26T16:00:55.413407Z",
        "payload": {
            "call_control_id": "v2:GegDKN9TMwSPYwUALiLrqNd-TpfER6QgvvNg49reRPtz6mhrhBiTTg",
            "call_leg_id": "a704d6e6-5fef-11eb-9e5f-02420a0f7568",
            "call_session_id": "a704df56-5fef-11eb-9718-02420a0f7568",
            "client_state": null,
            "connection_id": "1557657082730120568",
            "end_time": "2021-01-26T16:00:55.413407Z",
            "from": "+14154886792",
            "hangup_cause": "normal_clearing",
            "hangup_source": "caller",
            "sip_hangup_cause": "200",
            "start_time": "2021-01-26T16:00:46.873401Z",
            "to": "+19193234088"
          },
          "record_type": "event"
      }
    ]
  }
]

Call Tracking Follow-Ons

Now that you've successfully built a call tracking application, check out our on-demand webinar to learn more about the app and discover some ideas to build new features.

Our developer Slack community is full of Node developers like you - be sure to join to see what your fellow developers are building!

Was this page helpful?