Build a customer interaction workflow using Call Automation
Important
This feature of Azure Communication Services is currently in preview.
Preview APIs and SDKs are provided without a service-level agreement. We recommend that you don't use them for production workloads. Some features might not be supported, or they might have constrained capabilities.
For more information, review Supplemental Terms of Use for Microsoft Azure Previews.
In this quickstart, you'll learn how to build an application that uses the Azure Communication Services Call Automation SDK to handle the following scenario:
- handling the
IncomingCall
event from Event Grid - answering a call
- playing an audio file and recognizing input(DTMF) from caller
- adding a communication user to the call such as a customer service agent who uses a web application built using Calling SDKs to connect to Azure Communication Services
Sample code
You can download the sample app from GitHub.
Prerequisites
- An Azure account with an active subscription.
- Azure Communication Services resource. See Create an Azure Communication Services resource. Note the resource connection string for this quickstart by navigating to your resource selecting 'Keys' from the left side menu.
- Acquire a phone number for your Communication Service resource or connect your carrier using Azure direct routing. Note the phone number you acquired or provisioned using Azure direct routing for use in this quickstart.
- The latest .NET library for your operating system. .NET 6.0 or higher is recommended as this quickstart uses the minimal API feature.
- The latest version of Visual Studio 2022 (17.4.0 or higher)
- An audio file for the message you want to play in the call. This audio should be accessible via a url.
Create a new C# application
In the console window of your operating system, use the dotnet
command to create a new web application.
dotnet new web -n MyApplication
Install required packages
Configure NuGet Package Manager to use dev feed: During the preview phase, the CallAutomation package is published to the dev feed. Configure your package manager to use the Azure SDK Dev Feed from here.
Install the NuGet packages: Azure.Communication.CallAutomation and Azure.Messaging.EventGrid to your project.
dotnet add <path-to-project> package Azure.Communication.CallAutomation --prerelease
dotnet add <path-to-project> package Azure.Messaging.EventGrid
Use Visual Studio Dev Tunnels for your webhook
In this quick-start, you'll use the new Visual Studio Dev Tunnels feature to obtain a public domain name so that your local application is reachable by the Call Automation platform on the Internet. The public name is needed to receive the Event Grid IncomingCall
event and Call Automation events using webhooks.
Note by default the dev tunnels are disabled in Visual Studio. To enable dev tunnels, please go to Tools, than Options and enable dev tunnels in Preview Features menu.
If you haven't already configured your workstation, be sure to follow the steps in this guide. Once configured, your workstation will acquire a public domain name automatically allowing us to use the environment variable ["VS_TUNNEL_URL"]
as shown below.
Set up your Event Grid subscription to receive the IncomingCall
event by reading this guide.
Update Program.cs
Using the minimal API feature in .NET 6, we can easily add an HTTP POST map and answer the call. A callback URI is required so the service knows how to contact your application for subsequent calls state events such as CallConnected
and PlayCompleted
.
In this code snippet, /api/incomingCall is the default route that will be used to listen for and answer incoming calls. At a later step, you'll register this url with Event Grid. Since Event Grid requires you to prove ownership of your Webhook endpoint before it starts delivering events to that endpoint, the code sample also handles this one time validation by processing SubscriptionValidationEvent. This requirement prevents a malicious user from flooding your endpoint with events. For more information, see this guide.
The code sample also illustrates how you can control the callback URI by setting your own context/ID when you answer the call. All events generated by the call will be sent to the specific route you provide when answering an inbound call and the same applies to when you place an outbound call.
using Azure.Communication;
using Azure.Communication.CallAutomation;
using Azure.Messaging;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
var builder = WebApplication.CreateBuilder(args);
var client = new CallAutomationClient("<resource_connection_string"); //noted from pre-requisite step
var tunnelUrl = builder.Configuration["VS_TUNNEL_URL"]; // Visual Studio Dev Tunnel's dynamic FQDN
var mediaFileSource = new Uri("<link_to_media_file>"); //This URL should be public accessible and the file format should be WAV, 16KHz, Mono.
var applicationPhoneNumber = "<phone_number_acquired_as_prerequisite>";
var phoneNumberToAddToCall = "<phone_number_to_add_to_call>"; //in E.164 format starting +...
Console.WriteLine($"Tunnel URL:{builder.Configuration["VS_TUNNEL_URL"]}"); // echo Tunnel URL to screen to configure Event Grid webhook
var app = builder.Build();
app.MapPost("/api/incomingCall", async (
[FromBody] EventGridEvent[] eventGridEvents) =>
{
foreach (var eventGridEvent in eventGridEvents)
{
if (eventGridEvent.TryGetSystemEventData(out object eventData))
{
// Handle the webhook subscription validation event.
if (eventData is SubscriptionValidationEventData subscriptionValidationEventData)
{
var responseData = new SubscriptionValidationResponse
{
ValidationResponse = subscriptionValidationEventData.ValidationCode
};
return Results.Ok(responseData);
}
}
var jsonObject = JsonNode.Parse(eventGridEvent.Data).AsObject();
var callerId = (string)(jsonObject["from"]["rawId"]);
var incomingCallContext = (string)jsonObject["incomingCallContext"];
var callbackUri = new Uri(tunnelUrl + $"api/calls/{Guid.NewGuid()}?callerId={callerId}");
AnswerCallResult answerCallResult = await client.AnswerCallAsync(incomingCallContext, callbackUri);
}
return Results.Ok();
});
app.MapPost("/api/calls/{contextId}", async (
[FromBody] CloudEvent[] cloudEvents,
[FromRoute] string contextId,
[Required] string callerId) =>
{
foreach (var cloudEvent in cloudEvents)
{
CallAutomationEventBase @event = CallAutomationEventParser.Parse(cloudEvent);
if (@event is CallConnected)
{
// play audio then recognize 3-digit DTMF input with pound (#) stop tone
var recognizeOptions =
new CallMediaRecognizeDtmfOptions(CommunicationIdentifier.FromRawId(callerId), 3)
{
InterruptPrompt = true,
InterToneTimeout = TimeSpan.FromSeconds(10),
InitialSilenceTimeout = TimeSpan.FromSeconds(5),
Prompt = new FileSource(mediaFileSource),
StopTones = new[] { DtmfTone.Pound },
OperationContext = "MainMenu"
};
await client.GetCallConnection(@event.CallConnectionId)
.GetCallMedia()
.StartRecognizingAsync(recognizeOptions);
}
if (@event is RecognizeCompleted { OperationContext: "MainMenu" })
{
// this RecognizeCompleted correlates to the previous action as per the OperationContext value
var addThisPerson = new PhoneNumberIdentifier(phoneNumberToAddToCall);
var listOfPersonToBeAdded = new List<CommunicationIdentifier>();
listOfPersonToBeAdded.Add(addThisPerson);
var addParticipantsOption = new AddParticipantsOptions(listOfPersonToBeAdded);
addParticipantsOption.SourceCallerId = new PhoneNumberIdentifier(applicationPhoneNumber);
AddParticipantsResult result = await client.GetCallConnection(@event.CallConnectionId).AddParticipantsAsync(addParticipantsOption);
}
}
return Results.Ok();
}).Produces(StatusCodes.Status200OK);
app.Run();
Replace the placeholders with the actual values in lines 12-16. In your production code, we recommend using Secret Manager for storing sensitive information.
Run the app
Within Visual Studio select the Run button or press F5 on your keyboard. You should have a dynamic FQDN echoed to the screen as per the above Console.WriteLine()
command above. Use this FQDN to now configure your Event Grid webhook subscription to receive the inbound call.
Configure Event Grid webhook subscription
In order to receive the IncomingCall
event for the inbound PSTN call, you must configure an Event Grid subscription as described in this concepts guide. The most important thing to remember is that an Event Grid webhook subscription must be validated against a working web server. Since you've started the project in the previous step, and you have a public FQDN, set the webhook address in your subscription to the Dev Tunnel obtained by Visual Studio plus the path to your POST endpoint (i.e. https://<dev_tunnel_fqdn>/api/incomingCall
).
Once your webhook subscription has been validated, it will show up in the portal and you're now ready to test your application by making an inbound call.
Sample code
You can download the sample app from GitHub
Prerequisites
- Azure account with an active subscription.
- Azure Communication Services resource. See Create an Azure Communication Services resource. Note the resource connection string for this quickstart by navigating to your resource selecting 'Keys' from the left side menu.
- Acquire a PSTN phone number from Azure Communication Services. Note the phone number you acquired for use in this quickstart.
- Java Development Kit (JDK) version 8 or above.
- Apache Maven
- An audio file for the message you want to play in the call. This audio should be accessible via a url.
Create a new Java Spring application
Configure the Spring Initializr to create a new Java Spring application.
- Set the Project to be a Maven Project.
- Leave the rest as default unless you want to have your own customization.
- Add Spring Web to Dependencies section.
- Generate the application and it will be downloaded as a zip file. Unzip the file and start coding.
Install the Maven package
Configure Azure Artifacts development feed:
Since the Call Automation SDK version used in this QuickStart isn't yet available in Maven Central Repository, we need to configure an Azure Artifacts development feed, which contains the latest version of the Call Automation SDK.
Follow the instruction here for adding azure-sdk-for-java feed to your POM file.
Add Call Automation package references:
azure-communication-callautomation - Azure Communication Services Call Automation SDK package is retrieved from the Azure SDK Dev Feed configured above.
Look for the recently published version from here
And then add it to your POM file like this (using version 1.0.0-alpha.20221101.1 as example)
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-communication-callautomation</artifactId>
<version>1.0.0-alpha.20221101.1</version>
</dependency>
Add other packages’ references:
azure-messaging-eventgrid - Azure Event Grid SDK package: com.azure : azure-messaging-eventgrid. Data types from this package are used to handle Call Automation IncomingCall event received from the Event Grid.
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-messaging-eventgrid</artifactId>
<version>4.11.2</version>
</dependency>
gson - Google Gson package: com.google.code.gson : gson is a serialization/deserialization library to handle conversion between Java Objects and JSON.
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
Set up a public URI for the local application
In this quick-start, you'll use Ngrok tool to project a public URI to the local port so that your local application can be visited by the internet. The public URI is needed to receive the Event Grid IncomingCall
event and Call Automation events using webhooks.
First, determine the port of your java application. 8080
is the default endpoint of a spring boot application.
Then, install Ngrok and run Ngrok with the following command: ngrok http <port>
. This command will create a public URI like https://ff2f-75-155-253-232.ngrok.io/
, and it is your Ngrok Fully Qualified Domain Name(Ngrok_FQDN). Keep Ngrok running while following the rest of this quick-start.
Add Controller.java
In your project folder, create a Controller.java file and update it to handle incoming calls. A callback URI is required so the service knows how to contact your application for subsequent calls state events such as CallConnected
and PlayCompleted
.
In this code snippet, /api/incomingCall is the default route that will be used to listen for and answer incoming calls. At a later step, we'll register this url with Event Grid. Since Event Grid requires you to prove ownership of your Webhook endpoint before it starts delivering events to that endpoint, the code sample also handles this one time validation by processing SubscriptionValidationEvent. This requirement prevents a malicious user from flooding your endpoint with events. For more information, see this guide.
The code sample also illustrates how you can control the callback URI by setting your own context/ID when you answer the call. All events generated by the call will be sent to the specific route you provide when answering an inbound call and the same applies to when you place an outbound call.
package com.example.demo;
import com.azure.communication.callautomation.*;
import com.azure.communication.callautomation.models.*;
import com.azure.communication.callautomation.models.events.*;
import com.azure.communication.common.CommunicationIdentifier;
import com.azure.communication.common.CommunicationUserIdentifier;
import com.azure.messaging.eventgrid.EventGridEvent;
import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationEventData;
import com.azure.messaging.eventgrid.systemevents.SubscriptionValidationResponse;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.*;
import static org.springframework.web.bind.annotation.RequestMethod.POST;
@RestController
public class ActionController {
@Autowired
private Environment environment;
private CallAutomationAsyncClient client;
private String connectionString = "<resource_connection_string>"; //noted from pre-requisite step
private String callbackUri = "<public_url_generated_by_ngrok>";
private Uri mediaFileSource = new Uri("<link_to_media_file>");
private String applicationPhoneNumber = "<phone_number_acquired_as_prerequisite>";
private String phoneNumberToAddToCall = "<phone_number_to_add_to_call>"; //in format of +1...
private CallAutomationAsyncClient getCallAutomationAsyncClient() {
if (client == null) {
client = new CallAutomationClientBuilder()
.connectionString(connectionString)
.buildAsyncClient();
}
return client;
}
@RequestMapping(value = "/api/incomingCall", method = POST)
public ResponseEntity<?> handleIncomingCall(@RequestBody(required = false) String requestBody) {
List<EventGridEvent> eventGridEvents = EventGridEvent.fromString(requestBody);
for (EventGridEvent eventGridEvent : eventGridEvents) {
// Handle the subscription validation event
if (eventGridEvent.getEventType().equals("Microsoft.EventGrid.SubscriptionValidationEvent")) {
SubscriptionValidationEventData subscriptionValidationEventData = eventGridEvent.getData().toObject(SubscriptionValidationEventData.class);
SubscriptionValidationResponse subscriptionValidationResponse = new SubscriptionValidationResponse()
.setValidationResponse(subscriptionValidationEventData.getValidationCode());
ResponseEntity<SubscriptionValidationResponse> ret = new ResponseEntity<>(subscriptionValidationResponse, HttpStatus.OK);
return ret;
}
// Answer the incoming call and pass the callbackUri where Call Automation events will be delivered
JsonObject data = new Gson().fromJson(eventGridEvent.getData().toString(), JsonObject.class); // Extract body of the event
String incomingCallContext = data.get("incomingCallContext").getAsString(); // Query the incoming call context info for answering
String callerId = data.getAsJsonObject("from").get("rawId").getAsString(); // Query the id of caller for preparing the Recognize prompt.
// Call events of this call will be sent to an url with unique id.
String callbackUri = callbackUriBase + String.format("/api/calls/%s?callerId=%s", UUID.randomUUID(), callerId);
AnswerCallResult answerCallResult = getCallAutomationAsyncClient().answerCall(incomingCallContext, callbackUri).block();
}
return new ResponseEntity<>(HttpStatus.OK);
}
@RequestMapping(value = "/api/calls/{contextId}", method = POST)
public ResponseEntity<?> handleCallEvents(@RequestBody String requestBody, @PathVariable String contextId, @RequestParam(name = "callerId", required = true) String callerId) {
List<CallAutomationEventBase> acsEvents = EventHandler.parseEventList(requestBody);
for (CallAutomationEventBase acsEvent : acsEvents) {
if (acsEvent instanceof CallConnected) {
CallConnected event = (CallConnected) acsEvent;
// Call was answered and is now established
String callConnectionId = event.getCallConnectionId();
CommunicationIdentifier target = CommunicationIdentifier.fromRawId(callerId);
// Play audio then recognize 3-digit DTMF input with pound (#) stop tone
CallMediaRecognizeDtmfOptions recognizeOptions = new CallMediaRecognizeDtmfOptions(target, 3);
recognizeOptions.setInterToneTimeout(Duration.ofSeconds(10))
.setStopTones(new ArrayList<>(Arrays.asList(DtmfTone.POUND)))
.setInitialSilenceTimeout(Duration.ofSeconds(5))
.setInterruptPrompt(true)
.setPlayPrompt(new FileSource().setUri(mediaFileSource))
.setOperationContext("MainMenu");
getCallAutomationAsyncClient().getCallConnectionAsync(callConnectionId)
.getCallMediaAsync()
.startRecognizing(recognizeOptions)
.block();
} else if (acsEvent instanceof RecognizeCompleted) {
RecognizeCompleted event = (RecognizeCompleted) acsEvent;
// This RecognizeCompleted correlates to the previous action as per the OperationContext value
if (event.getOperationContext().equals("MainMenu")) {
CallConnectionAsync callConnectionAsync = getCallAutomationAsyncClient().getCallConnectionAsync(event.getCallConnectionId());
// Invite other participants to the call
CommunicationIdentifier target = new PhoneNumberIdentifier(phoneNumberToAddToCall);
List<CommunicationIdentifier> targets = new ArrayList<>(Arrays.asList(target));
AddParticipantsOptions addParticipantsOptions = new AddParticipantsOptions(targets)
.setSourceCallerId(new PhoneNumberIdentifier(applicationPhoneNumber));
Response<AddParticipantsResult> addParticipantsResultResponse = callConnectionAsync.addParticipantsWithResponse(addParticipantsOptions).block();
}
}
}
return new ResponseEntity<>(HttpStatus.OK);
}
}
Replace the placeholders with the actual values in lines 28-32. In your production code, we recommend using Secret Manager for storing sensitive information like this.
Run the app
To run your Java application, run maven compile, package, and execute commands.
mvn compile
mvn package
mvn exec:java -Dexec.mainClass=com.example.demo.DemoApplication -Dexec.cleanupDaemonThreads=false
Subscribe to IncomingCall event
IncomingCall is an Azure Event Grid event for notifying incoming calls to your Communication Services resource. To learn more about it, see this guide.
Navigate to your resource on Azure portal and select
Events
from the left side menu.Select
+ Event Subscription
to create a new subscription.Filter for Incoming Call event.
Choose endpoint type as web hook and provide the public url generated for your application by ngrok. Make sure to provide the exact api route that you programmed to receive the event previously. In this case, it would be <ngrok_url>/api/incomingCall.
If your application does not send 200Ok back to Event Grid in time, Event Grid will use exponential backoff retry to send the incoming call event again. However, an incoming call only rings for 30 seconds, and acting on a call after that will not work. To avoid retries after a call expires, we recommend setting the retry policy in the
Additional Features
tab as: Max Event Delivery Attempts to 2 and Event Time to Live to 1 minute. Learn more about retries here.Select create to start the creation of subscription and validation of your endpoint as mentioned previously. The subscription is ready when the provisioning status is marked as succeeded.
This subscription currently has no filters and hence all incoming calls will be sent to your application. To filter for specific phone number or a communication user, use the Filters tab.
Testing the application
- Place a call to the number you acquired in the Azure portal.
- Your Event Grid subscription to the
IncomingCall
should execute and call your application that will request to answer the call. - When the call is connected, a
CallConnected
event will be sent to your application's callback url. At this point, the application will request audio to be played and to receive input from the caller. - From your phone, press any three number keys, or press one number key and then # key.
- When the input has been received and recognized, the application will make a request to add a participant to the call.
- Once the added user answers, you can talk to them.
Clean up resources
If you want to clean up and remove a Communication Services subscription, you can delete the resource or resource group. Deleting the resource group also deletes any other resources associated with it. Learn more about cleaning up resources.
Next steps
- Learn more about Call Automation and its features.
- Learn how to redirect inbound telephony calls with Call Automation.
- Learn more about Play action.
- Learn more about Recognize action.
Feedback
Submit and view feedback for