Hands On

Integrating Your Call Center with Location Services

By Raymond Camden | 10 April 2020

Try HERE Maps

Create a free API key to build location-aware apps and services.

Get Started

Over the past few weeks I've been doing some digging into the RingCentral suite of APIs. RingCentral provides APIs focused on messaging, everything from phone and SMS handling to group chat. You can explore their APIs more in depth on their developer site. When I first started playing around with their APIs, I focused more on the SMS aspect as that's something (texting I mean) that I tend to use every day. But then I started looking more into their call handling (you know, voice calls) features and I saw some interesting possibilities. 

One API in particular seemed interesting to me - the Call Log. As you can imagine, this API provides the ability to search for calls made to and from a phone number. It provides an incredible amount of detail on each call, including information about who the call was made to or who it was from. This information includes the geographic location for the call which immediately made me think of how I could integrate it with our own APIs.

I thought it would be fun to build a demo that combined both the RingCentral API suite and our tools. In order to replicate what I've built, you will want to sign up for a free RingCentral developer account. Once you've done that, you can spend some time in their Getting Started guide to familiarize yourself with their developer portal. You will need to have at least one developer application using the "Read Call Log" permission as well. The RingCentral folks did a great job with their documentation so I won't repeat it here, but know that while I'll be demonstrating their API with JavaScript, you can use any language you want for your development. Finally, in order to get some data, I actually made calls both to and from the developer number you get as part of their system. I'll warn you now my data is going to be a bit boring, but obviously in real world conditions you'll have a lot more interesting data to play with.

To work with our APIs, you'll want to sign up for a free developer account here as well. You'll then need to add an API key to use with our REST-based APIs. Both we and RingCentral provide a very generous free tier so you can just keep your credit card put away!

Alright, so once you've got your developer accounts set up, how can we integrate the two services? First, let's demonstrate what we're going to build. Given that RingCentral's Call Log API tells you from where a call came or to where a call was placed, our demo is simply going to map all of these locations on a map. 

Remembering that I said my data is a bit boring and limited, here's how it looks when rendered on a map:

Building this required two main steps. First, I built a serverless endpoint to load the call log for an account. This was done with RingCentral's API. Then I built a serverless endpoint to translate each call record's location into a geographic location. This was done with our Geocoding API.

As for how I used the serverless aspect, I used Netlify Functions and a simple static web site. I discussed this in depth a few days ago in this blog post: Create Your Own Geocode Serverless Application - on the JAMStack. Definitely take a minute or two to look over that post if you're unfamiliar with integrating APIs and the JAMStack. 

Now that we know what we are building and how we're building it, let's look at the components. The first thing I'll share is the serverless endpoint to retrieve a call log. The API reference provides information about arguments and result types. For my call, I specified that I wanted "Detailed" information (which as you can guess will return as much information as possible) and that I wanted all calls placed since January 1, 2020. This was somewhat arbitrary. In a real production environment you would probably have a different date set. So for example, I'd imagine limiting your data to one week or one month. I also used their pagination support to return as many calls as possible. I had a bit of difficulty with their docs in this one particular area so I'll warn folks now that the "fetch data and keep fetching while there's more" aspect may not be perfect. With that out of the way, here's the code I built.


const fetch = require('node-fetch');

const SDK = require('@ringcentral/sdk').SDK;

RINGCENTRAL_CLIENTID = 'your client id';
RINGCENTRAL_CLIENTSECRET = 'your client secret';
RINGCENTRAL_SERVER = 'https://platform.devtest.ringcentral.com';
RINGCENTRAL_USERNAME = 'a ring central phone number';
RINGCENTRAL_PASSWORD = 'my password brings all the boys to the yard';
RINGCENTRAL_EXTENSION = '101';
var rcsdk = new SDK({
    server: RINGCENTRAL_SERVER,
    clientId: RINGCENTRAL_CLIENTID,
    clientSecret: RINGCENTRAL_CLIENTSECRET
});
const platform = rcsdk.platform();

exports.handler = async function(event, context) {

  await rcLogin();
  let callLog = await rcCallLog();

  return {
    headers: {
      "Content-Type":"application/json"
    },
    statusCode: 200,
    body: JSON.stringify(callLog)
  }

}

async function rcLogin() {
  return platform.login({
    username: RINGCENTRAL_USERNAME,
    password: RINGCENTRAL_PASSWORD,
    extension: RINGCENTRAL_EXTENSION
  });
}

async function rcCallLog() {
 
  let result = [];
  let hasMore = true;
  let page = 1;
  let perPage = 100;
  let url = '/restapi/v1.0/account/~/extension/~/call-log';
  while(hasMore) {
    let resp = await platform.get(url, {
      view: 'Detailed',
      dateFrom:'2020-01-1',
      perPage:perPage,
      page:page
    });
    let data = await resp.json();
    result = result.concat(data.records);
    if(data.navigation.firstPage.uri !== data.navigation.lastPage.uri) {
      page++;
    } else hasMore = false;
  }
  return result;

}

This function makes use of the RingCentral JavaScript SDK to simplify calls to the API. It's a good API wrapper and I definitely suggest using it. With the SDK I make a call to log the account in and then fetch the call log. This particular demo is tied to one phone number and password but could be expanded to be more dynamic. The end result is an array of call data. Here's what one call looks like, and as I said, it's quite detailed.


{
	"uri": "https://platform.devtest.ringcentral.com/restapi/v1.0/account/272299004/extension/272299004/call-log/AMc4eBTIRS_3DUA?view=Detailed",
	"id": "AMc4eBTIRS_3DUA",
	"sessionId": "9407926005",
	"startTime": "2020-03-10T19:10:38.547Z",
	"duration": 161,
	"type": "Voice",
	"direction": "Outbound",
	"action": "RingOut PC",
	"result": "Call connected",
	"to": {
		"phoneNumber": "+15044446512",
		"location": "New Orleans, LA"
	},
	"from": {
		"name": "Raymond Camden",
		"phoneNumber": "+14703177709",
		"extensionId": "272299004"
	},
	"extension": {
		"uri": "https://platform.devtest.ringcentral.com/restapi/v1.0/account/272299004/extension/272299004",
		"id": 272299004
	},
	"telephonySessionId": "s-661473cc51ba4c33ac7c88faf3774676",
	"transport": "PSTN",
	"lastModifiedTime": "2020-03-10T19:13:40.033Z",
	"billing": {
		"costIncluded": 0,
		"costPurchased": 0
	},
	"legs": [
		{
			"startTime": "2020-03-10T19:10:17.260Z",
			"duration": 183,
			"type": "Voice",
			"direction": "Outbound",
			"action": "RingOut PC",
			"result": "Accepted",
			"to": {
				"phoneNumber": "+13374128987",
				"location": "Lafayette, LA"
			},
			"from": {
				"name": "Raymond Camden",
				"phoneNumber": "+15044446512",
				"extensionId": "272299004"
			},
			"extension": {
				"uri": "https://platform.devtest.ringcentral.com/restapi/v1.0/account/272299004/extension/272299004",
				"id": 272299004
			},
			"reason": "Accepted",
			"reasonDescription": "The call connected to and was accepted by this number.",
			"telephonySessionId": "s-661473cc51ba4c33ac7c88faf3774676",
			"transport": "PSTN",
			"billing": {
				"costIncluded": 0,
				"costPurchased": 0
			},
			"legType": "RingOutClientToSubscriber"
		},
		{
			"startTime": "2020-03-10T19:10:38.547Z",
			"duration": 161,
			"type": "Voice",
			"direction": "Outbound",
			"action": "RingOut PC",
			"result": "Call connected",
			"to": {
				"phoneNumber": "+15044446512",
				"location": "New Orleans, LA"
			},
			"from": {
				"name": "Raymond Camden",
				"phoneNumber": "+14703177709",
				"extensionId": "272299004"
			},
			"extension": {
				"uri": "https://platform.devtest.ringcentral.com/restapi/v1.0/account/272299004/extension/272299004",
				"id": 272299004
			},
			"telephonySessionId": "s-661473cc51ba4c33ac7c88faf3774676",
			"transport": "PSTN",
			"legType": "RingOutClientToCaller",
			"master": true
		}
	]
}

That's a whole heaping mess of data! What we care about though falls into three sections. First is "direction" - this will be either "Outbound" or "Inbound". Once we know the direction, we either look at "to" for "Outbound" calls or "from" for "Inbound" calls. We will always (as far as my testing shows) have a "location" field we can use. In the example above you can see it's "New Orleans, LA". 

This brings us to our next part - our Geocoding API. This API lets us pass a value like the location above and get back a longitude and latitude value. In my previous blog post I built this endpoint already, but here it is again:


/* eslint-disable */
const fetch = require('node-fetch')

const HERE_API_KEY = process.env.HERE_API_KEY;

exports.handler = async function(event, context) {

  let location = event.queryStringParameters.location;
  if(!location) {
    return {
      statusCode:500,
      body: 'Must pass location parameters.'
    }
  }

  let locationPosition = await getLocation(location);

  return {
    headers: {
      'Content-Type':'application/json'
    },
    statusCode: 200,
    body: JSON.stringify(locationPosition)
  }
}

async function getLocation(x) {
   let url = `https://geocode.search.hereapi.com/v1/geocode?q=${encodeURIComponent(x)}&apikey=${HERE_API_KEY}`;
   let response = await fetch(url);
   let data = await response.json();
   // assume first result
   return data.items[0].position;
}

This endpoint takes in one argument via the query string, "location", and then uses the Geocoding API to return the position. When run with the location "Lafayette, LA", here's the result:


{"lat":30.22032,"lng":-92.01705}

Now that we've got the serverless functions up and running. The last thing we need to do is visualize it. For that we'll use our Maps JavaScript library.

I set up a quick HTML page with just a div for the map. I then used this JavaScript to setup and initialize the map.


const KEY = 'my key';
var platform = new H.service.Platform({
	'apikey': KEY
});

var defaultLayers = platform.createDefaultLayers();

var map = new H.Map(
	document.getElementById('mapContainer'),
	defaultLayers.vector.normal.map,
	{
		zoom: 5,
		center: { lat: 30.22, lng: -92.02 },
		pixelRatio: window.devicePixelRatio || 1
	}
);

var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
var ui = H.ui.UI.createDefault(map, defaultLayers);

This map is centered on Louisiana to make it a bit nicer for my particular needs, but you could center it in America, or show the entire globe. Next it's time to load data and render the markers on the map:

 


loadData(map);

async function loadData(map) {
	// first get my call log
	let callLogResp = await fetch('/.netlify/functions/callLog');
	let callLog = await callLogResp.json();
	// Ok, now to geocode
	// credit for this - https://medium.com/@antonioval/making-array-iteration-easy-when-using-async-await-6315c3225838
	const pArray = callLog.map(async l => {
		let loc = '';
		if(l.direction === 'Outbound') {
			loc = l.to.location;
		} else {
			loc = l.from.location;
		}
		const geocodeRequest = await fetch('/.netlify/functions/getGeocode?location='+loc);
		return geocodeRequest.json();
	});
	const geoData = await Promise.all(pArray);

    /*
	Ok, at this point, callLog is an array of calls and geoData is corresponding array of locations.
	Let's add our markers
	*/
	let group = new H.map.Group();
	map.addObject(group);
	group.addEventListener('tap', evt => {
		var bubble =  new H.ui.InfoBubble(evt.target.getGeometry(), {
			content: evt.target.getData()
		});
		ui.addBubble(bubble);
	}, false);

	for(let i=0; i<callLog.length; i++) {
		let marker = new H.map.Marker({lat:geoData[i].lat, lng:geoData[i].lng});
		marker.setData(`
${callLog[i].direction} Call
		`);
		group.addObject(marker);
	}
}

Alright, lets break this down. The loadData function begins by calling the endpoint that gets the call log. Next we need to iterate over the results and geocode each one. Notice how we check the direction of the call to determine where the address is. 

Once we have all our data, we can then iterate and generate markers for each. For my markers I simply added the direction. You can see the marker in action here:

 

Remember that the call log contains a lot of information. We could definitely add more to the marker here to make it more interesting. In fact, there's a whole slew of things that would be easy to do to make this more useful:

  • Instead of showing every call with the same marker, we could use different markers for in versus out-bound calls.
  • We could differentiate based on time. For example, I assume that a call center would get more calls from the east coast early in the morning since they wake up first. As the day goes on, I'd expect call volumes to gradually move west. But I may be wrong. Our Maps library would definitely make it possible to show and hide markers based on a time filter.
  • We could differentiate locations that get larger volumes of calls. Right now it's just one marker per location, but if we use different icons to represent the level of calls, it would better highlight what geographic regions are more important to our business.
I hope this example has been useful and I absolutely encourage you to look out for other ways to integrate our APIs with other services!