Sending SMS messages and making robocalls from Dynamics CRM

In this post I will show how to send SMS messages and make automated phone calls from Dynamics CRM using Tropo, a cloud voice and SMS messaging API. Tropo is not the only player in this space, but I think it has the best set of features, and it's completely free to use in development. For more information on what Tropo does and how it works, take a look at this overview: https://www.tropo.com/how-it-works.

There multiple ways to integrate Dynamics CRM and Tropo, but for the purpose of this introduction, there are two things we need to do:

  1. Create a Tropo application that makes outbound phone calls and sends outbound SMS messages based on parameters supplied at runtime.
  2. Create Dynamics CRM custom workflow activities to launch the Tropo application. [more]

Tropo Configuration

Tropo has two different APIs for interoperability - scripting and the WebAPI. With the WebAPI, you host Tropo scripts on your own server, and with the scripting API Tropo hosts your scripts on its servers. To keep things simple, we'll be using the scripting API to host a voice script and an SMS script. The voice script will call a specified phone number and read a message via text-to-speech. The SMS script will send an input message to a specified phone number.

First you'll need to sign up for a Tropo account here: https://www.tropo.com/account/register.jsp.

As soon as you've registered and logged in, you need to verify your account before you can make outbound calls. Directions for how to do that are on this page: https://www.tropo.com/docs/scripting/making_outgoing_calls.htm.

Once you've opened the verification ticket, it's time to create a new scripting API application as shown in the quickstart guide here: https://www.tropo.com/docs/scripting/creating_first_application.htm.

Finally, you need to create a hosted JavaScript files for both the voice and SMS sides of your application. Tropo supports multiple languages for its scripting API, but JavaScript is the quickest way to do what we need here (and my PHP is a bit rusty).

This is the script for the voice component of your application:

call('+' + to, {network:"PSTN", callerID:"XXXXXXXXXX"}); //replace XXXXXXXXXX with your Tropo phone number  
say(msg);

This is the script for the SMS component of your application:

call('+' + to, {network:"SMS", callerID:"XXXXXXXXXX"}); //replace XXXXXXXXXX with your Tropo phone number  
say(msg);

After you've loaded your scripts, you need to get the outbound voice and messaging tokens for your application so that you can launch the application from CRM. Click each link and copy the full token from the window that opens.

A note to my international friends: I have only worked with Tropo applications that take/make calls and send/receive SMS messages using U.S. numbers, so I can't speak to Tropo's internation features. You can read more about them here: https://www.tropo.com/docs/scripting/international_features.htm.

Custom Workflow Activity Code

When you launch your Tropo application from CRM, you need to pass in the recipient's phone number and the text of the message that will be spoken over the phone or contained in the body of the SMS message. There are a few different ways to pass those parameters, but my approach POSTs a JSON message from a custom workflow activity. I chose to use custom workflow activities instead of a straight plug-in because it allows for input parameters to be edited inside a workflow, and CRM UR12 allows custom workflow activities to run in the sandbox for both online and on-premise deployments.

Because JSON is just specially formatted text, you could build your JSON messages using .Net's standard string-manipulation classes, but I prefer to use a custom request object and serialize it to JSON and then deserialize the JSON response from Tropo to a custom respose object. Here is a great overview of JSON serialization/deserialization in .Net: http://pietschsoft.com/post/2008/02/NET-35-JSON-Serialization-using-the-DataContractJsonSerializer.aspx.

Here are my request and response classes:

[DataContract]  
public class TropoResponse  
{  
 [DataMember(Name = "success")]  
 public bool Success { get; set; }  
  
 [DataMember(Name = "token")]  
 public string Token { get; set; }  
  
 [DataMember(Name = "reason")]  
 public string Reason { get; set; }  
  
 [DataMember(Name = "id")]  
 public string Id { get; set; }  
  
 public TropoResponse() { }  
}  
  
[DataContract]  
public class TropoRequest  
{  
 [DataMember(Name = "msg")]  
 public string Message { get; set; }  
  
 [DataMember(Name = "to")]  
 public string To { get; set; }  
  
 [DataMember(Name = "token")]  
 public string Token { get; set; }  
  
 public TropoRequest() { }  
}

I have two classes for my custom workflow activities - SendSMS and SendCall. They are basically identical except for the tokens used in each one. The SendSMS class has the Tropo messaging token embedded in it, and the SendCall class has the Tropo voice token. I did consider passing the token to the custom activities at workflow runtime, but I decided against that approach because I felt it would overly complicate workflow creation and management.

In each class there are two public members and two private members:

[Input("Recipient phone number")]  
public InArgument<String> RecipientIn { get; set; }  
  
[Input("Message")]  
public InArgument<String> MessageIn { get; set; }  
  
private string _webAddress = "https://api.tropo.com/v1/sessions";  
private string _tropoToken = "YOUR_TOKEN_HERE"; //voice or messaging token as appropriate

The execute method is then the same in each class (except for the tracing messages):

protected override void Execute(CodeActivityContext executionContext)  
{  
 // Create the tracing service  
 ITracingService tracingService = executionContext.GetExtension<ITracingService>();  
  
 if (tracingService == null)  
 {  
 throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");  
 }  
  
 tracingService.Trace("Entered SendSMS.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",  
 executionContext.ActivityInstanceId,  
 executionContext.WorkflowInstanceId);  
  
 // Create the context  
 IWorkflowContext context = executionContext.GetExtension<IWorkflowContext>();  
  
 if (context == null)  
 {  
 throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");  
 }  
  
 tracingService.Trace("SendSMS.Execute(), Correlation Id: {0}, Initiating User: {1}",  
 context.CorrelationId,  
 context.InitiatingUserId);  
  
 IOrganizationServiceFactory serviceFactory = executionContext.GetExtension<IOrganizationServiceFactory>();  
 IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);  
  
 try  
 {  
 //create a new troporequest object  
 TropoRequest tropoReq = new TropoRequest  
 {  
 To = RecipientIn.Get(executionContext),  
 Message = MessageIn.Get(executionContext),  
 Token = _tropoToken  
 };  
  
 //serialize the troporequest to json  
 System.Runtime.Serialization.Json.DataContractJsonSerializer serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(tropoReq.GetType());  
 MemoryStream ms = new MemoryStream();  
 serializer.WriteObject(ms, tropoReq);  
 string jsonMsg = Encoding.Default.GetString(ms.ToArray());  
  
 //create the json webrequest and execute it  
 System.Net.WebRequest req = System.Net.WebRequest.Create(_webAddress);  
 req.ContentType = "application/json";  
 req.Method = "POST";  
 byte[] bytes = System.Text.Encoding.ASCII.GetBytes(jsonMsg.ToString());  
 req.ContentLength = bytes.Length;  
 System.IO.Stream os = req.GetRequestStream();  
 os.Write(bytes, 0, bytes.Length);  
 os.Close();  
  
 //get the response  
 System.Net.WebResponse resp = req.GetResponse();  
  
 //deserialize the response to a troporesponse object  
 TropoResponse tropoResponse = new TropoResponse();  
 System.Runtime.Serialization.Json.DataContractJsonSerializer deserializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(tropoResponse.GetType());  
 tropoResponse = deserializer.ReadObject(resp.GetResponseStream()) as TropoResponse;  
  
 //if the success value is false, throw an error - not strictly necessary as tropo returns an HTTP error code, but still good to have just in case  
 if (!tropoResponse.Success)  
 {  
 throw new Exception("SendSMS failure: " + tropoResponse.Reason);  
 }  
 }  
 catch (WebException exception)  
 {  
 string str = string.Empty;  
 if (exception.Response != null)  
 {  
 using (StreamReader reader =  
 new StreamReader(exception.Response.GetResponseStream()))  
 {  
 str = reader.ReadToEnd();  
 }  
 exception.Response.Close();  
 }  
 if (exception.Status == WebExceptionStatus.Timeout)  
 {  
 throw new InvalidPluginExecutionException(  
 "The timeout elapsed while attempting to issue the request.", exception);  
 }  
 throw new InvalidPluginExecutionException(String.Format(CultureInfo.InvariantCulture,  
 "A Web exception ocurred while attempting to issue the request. {0}: {1}",  
 exception.Message, str), exception);  
 }  
 catch (FaultException<OrganizationServiceFault> e)  
 {  
 tracingService.Trace("Exception: {0}", e.ToString());  
  
 // Handle the exception.  
 throw;  
 }  
 catch (Exception e)  
 {  
 tracingService.Trace("Exception: {0}", e.ToString());  
 throw;  
 }  
  
 tracingService.Trace("Exiting SendSMS.Execute(), Correlation Id: {0}", context.CorrelationId);  
}

Here are the classes I use in my workflows assembly:

Wrapping Up

This just scratches the surface of what's possible to do with Tropo and Dynamics CRM. In future posts I'll show some more advanced integration possibilities that include handling inbound calls and intelligent processing of user responses.

comments powered by Disqus