I love mashups. It’s fun to combine technologies in unexpected ways. So when Wade Wegner of Salesforce asked me to participate in a webinar about the new Salesforce Toolkit for .NET, I decided to think of something unique to demonstrate. So, I showed off how to link Twilio – which in an API-driven service for telephony and SMS – with Salesforce.com data. In this scenario, job applicants can call a phone number, enter their tracking ID and hear the current status of their application. The rest of this blog post walks through what I built.
The Salesforce.com application
In my developer sandbox, I added a new custom object called Job Application that holds data about applicants, which job they applied to, and the status of the application (e.g. Submitted, In Review, Rejected).
I then created a bunch of records for job applicants. Here’s an example of one applicant in my system.
I want to expose a programmatic interface to retrieve “Application Status” that’s an aggregation of multiple objects. To make that happen, I created a custom Apex controller that exposes a REST endpoint. You can see below that I defined a custom class called ApplicationStatus and then a GetStatus operation that inflates and returns that custom object. The RESTful attributes (@RestResource, @HttpGet) make this a service accessible via REST query.
@RestResource(urlMapping='/ApplicationStatus/*') global class CandidateRestService { global class ApplicationStatus { String ApplicationId {get; set; } String JobName {get; set; } String ApplicantName {get; set; } String Status {get; set; } } @HttpGet global static ApplicationStatus GetStatus(){ //get the context of the request RestRequest req = RestContext.request; //extract the job application value from the URL String appId = req.requestURI.substring(req.requestURI.lastIndexOf('/')+1); //retrieve the job application seroter__Job_Application__c application = [SELECT Id, seroter__Application_Status__c, seroter__Applicant__r.Name, seroter__Job_Opening__r.seroter__Job_Title__c FROM seroter__Job_Application__c WHERE seroter__Application_ID__c = :appId]; //create the application status object using relationship (__r) values ApplicationStatus status = new ApplicationStatus(); status.ApplicationId = appId; status.Status = application.seroter__Application_Status__c; status.ApplicantName = application.seroter__Applicant__r.Name; status.JobName = application.seroter__Job_Opening__r.seroter__Job_Title__c; return status; } }
With this in place – and creating an “application” that gave me a consumer key and secret for remote access – I had everything I needed to consume Salesforce.com data.
The ASP.NET Web API project
How does Twilio know what to say when you call one of their phone numbers? They have a markup language called TwiML that includes the constructs for handling incoming calls. What I needed was a web service that Twilio could reach and return instructions for what to say to the caller.
I created an ASP.NET Web API project for this service. I added NuGet packages for DeveloperForce.Force (to get the Force.com Toolkit for .NET) and Twilio.Mvc, Twilio.TwiML, and Twilio. Before slinging the Web API Controller, I added a custom class that helps the Force Toolkit talk to custom REST APIs. This class, CustomServiceHttpClient, copies the base ServiceHttpClient class and changes a single line.
public async Task<T> HttpGetAsync<T>(string urlSuffix) { var url = string.Format("{0}/{1}", _instanceUrl, urlSuffix); var request = new HttpRequestMessage() { RequestUri = new Uri(url), Method = HttpMethod.Get };
Why did I do this? The class that comes with the Toolkit builds up a particular URL that maps to the standard Salesforce.com REST API. However, custom REST services use a different URL pattern. This custom class just takes in the base URL (returned by the authentication query) and appends a suffix that includes the path to my Apex controller operation.
I slightly changed the WebApiConfig.cs to add a “type” to the route template. I’ll use this to create a pair of different URIs for Twilio to use. I want one operation that it calls to get initial instructions (/api/status/init) and another to get the actual status resource (/api/status).
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{type}", defaults: new { type = RouteParameter.Optional } ); } }
Now comes the new StatusController.cs that handles the REST input. The first operation takes in VoiceRequest object that comes from Twilio and I build up a TwiML response. What’s cool is that Twilio can collect data from the caller. See the “Gather” operation where I instruct Twilio to get 6 digits from the caller, and post to another URI. In this case, it’s a version of this endpoint hosted in Windows Azure. Finally, I forced the Web API to return an XML document instead of sending back JSON (regardless of what comes in the inbound Accept header).
The second operation retrieves the Salesforce credentials from my configuration file, gets a token from Salesforce (via the Toolkit), issues the query to the custom REST endpoint, and takes the resulting job application detail and injects it into the TwiML response.
public class StatusController : ApiController { // GET api/<controller>/init public HttpResponseMessage Get(string type, [FromUri]VoiceRequest req) { //build Twilio response using TwiML generator TwilioResponse resp = new TwilioResponse(); resp.Say("Thanks for calling the status hotline.", new { voice = "woman" }); //Gather 6 digits and send GET request to endpoint specified in the action resp.BeginGather(new { action = "http://twilioforcetoolkit.azurewebsites.net/api/status", method = "GET", numDigits = "6" }) .Say("Please enter the job application ID", new { voice = "woman" }); resp.EndGather(); //be sure to force XML in the response return Request.CreateResponse(HttpStatusCode.OK, resp.Element, "text/xml"); } // GET api/<controller> public async Task<HttpResponseMessage> Get([FromUri]VoiceRequest req) { var from = req.From; //get the digits the user typed in var nums = req.Digits; //SFDC lookup //grab credentials from configuration file string consumerkey = ConfigurationManager.AppSettings["consumerkey"]; string consumersecret = ConfigurationManager.AppSettings["consumersecret"]; string username = ConfigurationManager.AppSettings["username"]; string password = ConfigurationManager.AppSettings["password"]; //create variables for our auth-returned values string url, token, version; //authenticate the user using Toolkit operations var auth = new AuthenticationClient(); //authenticate await auth.UsernamePasswordAsync(consumerkey, consumersecret, username, password); url = auth.InstanceUrl; token = auth.AccessToken; version = auth.ApiVersion; //create custom client that takes custom REST path var client = new CustomServiceHttpClient(url, token, new HttpClient()); //reference the numbers provided by the caller string jobId = nums; //send GET request to endpoint var status = await client.HttpGetAsync<dynamic>("services/apexrest/seroter/ApplicationStatus/" + jobId); //get status result JObject statusResult = JObject.Parse(System.Convert.ToString(status)); //create Twilio response TwilioResponse resp = new TwilioResponse(); //tell Twilio what to say to the caller resp.Say(string.Format("For job {0}, job status is {1}", statusResult["JobName"], statusResult["Status"]), new { voice = "woman" }); //be sure to force XML in the response return Request.CreateResponse(HttpStatusCode.OK, resp.Element, "text/xml"); } }
My Web API service was now ready to go.
Running the ASP.NET Web API in Windows Azure
As you can imagine, Twilio can only talk to services exposed to the public internet. For simplicity sake, I jammed this into Windows Azure Web Sites from Visual Studio.
Once this service was deployed, I hit the two URLs to make sure that it was returning TwiML that Twilio could use. The first request to /api/status/init returned:
Cool! Let’s see what happens when I call the subsequent service endpoint and provide the application ID in the URL. Notice that the application ID provided returns the corresponding job status.
So far so good. Last step? Add Twilio to the mix.
Setup Twilio Phone Number
First off, I bought a new Twilio number. They make it so damn easy to do!
With the number in place, I just had to tell Twilio what to do when the phone number is called. On the phone number’s settings page, I can set how Twilio should respond to Voice or Messaging input. In both cases, I point to a location that returns a static or dynamic TwiML doc. For this scenario, I pointed to the ASP.NET Web API service and chose the “GET” operation.
So what happens when I call? Hear the audio below:
[audio https://seroter.files.wordpress.com/2014/07/twiliosalesforce.mp3 |titles=Calling Twilio| initialvolume=30|animation=no]One of the other great Twilio features is the analytics. After calling the number, I can instantly see usage trends …
… and a log of the call itself. Notice that I see the actual TwiML payload processed for the request. That’s pretty awesome for troubleshooting and auditing.
Summary
In the cloud, it’s often about combining best-of-breed capabilities to deliver innovative solutions that no one technology has. It’s a lot easier to do this when working with such API-friendly systems as Salesforce and Twilio. I’m sure you can imagine all sorts of valuable cases where an SMS or voice call could retrieve (or create) data in a system. Imagine walking a sales rep through a call and collecting all the data from the customer visit and creating an Opportunity record! In this scenario, we saw how to query Salesforce.com (using the Force Toolkit for .NET) from a phone call and return a small bit of data. I hope you enjoyed the walkthrough, and keep an eye out for the recorded webcast where Wade and I explain a host of different scenarios for this Force Toolkit.
2 thoughts