Gathering User Input from Code
In Gathering User Input we showed how to set up an XML bin to collect input from users. The approach we showed had a limitation: we couldn't take choices based on the user's input.
In this article, we are going to see how to build an input-gathering application using Compatibility XML and SDKs. Using the SDKs will allow us to perform complex choices. For the sake of this guide, we are going to build a very simple IVR.
We will take for granted that you know how to set up a basic application using the Compatibility SDKs. If not, make sure to read Handling Calls from Code first.
Entrypoint
We'd like to offer callers a set of choices, such as:
- Press (1) to speak with Support
- Press (2) to speak with Sales
- Press (3) to speak with Marketing
We can implement this using the <Gather>
verb. The following is the XML that we'd like to use as entry point:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Gather
action="https://example.com/choice"
timeout="15"
numDigits="1">
<Say>
Welcome! Please press 1 to speak with Support. Press 2 to speak with
Sales. Press 3 to speak with Marketing.
</Say>
</Gather>
<Say>We did not receive any input. Goodbye!</Say>
<Hangup />
</Response>
Instead of using this XML document directly (for example as a hosted XML bin),
in this case we are going to generate it from our server. Let's define an
endpoint at /
:
- Python
- PHP
- Node.js
from flask import Flask, request, Response
from twilio.twiml.voice_response import VoiceResponse, Gather
app = Flask(__name__)
@app.route("/", methods=['GET', 'POST'])
def call_handler():
"""
This endpoint will be queried by SignalWire whenever an incoming call is received.
We respond with an XML document which specifies the next instructions.
"""
response = VoiceResponse()
gather = Gather(action='https://example.com/choice', method='POST', numDigits=1)
gather.say('Welcome! Please press 1 to speak with Support. ' +
'Press 2 to speak with Sales. ' +
'Press 3 to speak with Marketing.')
response.append(gather)
response.say('We did not receive any input. Goodbye!')
return Response(response.to_xml(), mimetype='text/xml')
<?php
require_once 'vendor/autoload.php';
use \SignalWire\LaML\VoiceResponse;
$response = new VoiceResponse;
$gather = $response->gather(array(
'action' => 'https://example.com/choice.php',
'method' => 'POST',
'numDigits' => 1
));
$gather->say('Welcome! Please press 1 to speak with Support. ' .
'Press 2 to speak with Sales. ' .
'Press 3 to speak with Marketing.');
$response->say('We did not receive any input. Goodbye!');
header('Content-type: text/xml');
echo $response->asXML();
import express from "express";
import { RestClient } from "@signalwire/compatibility-api";
const app = express();
app.use(express.urlencoded({ extended: true }));
/**
* @param {express.Request} req
* @param {express.Response} res
*/
function callHandler(req, res) {
// This endpoint will be queried by SignalWire whenever an incoming call is received.
// We respond with an XML document which specifies the next instructions.
const response = new RestClient.LaML.VoiceResponse();
const gather = response.gather({
action: "https://example.com/choice",
method: "POST",
numDigits: 1,
});
gather.say(
"Welcome! Please press 1 to speak with Support. " +
"Press 2 to speak with Sales. " +
"Press 3 to speak with Marketing."
);
response.say("We did not receive any input. Goodbye!");
res.set("Content-Type", "text/xml");
res.send(response.toString());
}
app.get("/", callHandler);
app.post("/", callHandler);
app.listen(8088);
As soon as the user inputs a number, we execute the XML script at
https://example.com/choice
. This is an endpoint which will receive the user
input as an HTTP parameter, and will emit new XML: make sure to replace this URL
with your actual public URL (for example, the ngrok URL). Keep the /choice
path though: we are going to show the implementation for this endpoint in the
next section.
Branching
After the user makes a choice, SignalWire will fetch our server at /choice
including the input as parameter. Let's implement the /choice
endpoint to read
the input and reply accordingly.
- Python
- PHP
- Node.js
# ... previous code
@app.route("/choice", methods=['GET', 'POST'])
def choice_handler():
choice = request.values.get('Digits')
response = VoiceResponse()
if choice == '1':
response.say("You selected: support")
elif choice == '2':
response.say("You selected: sales")
elif choice == '3':
response.say("You selected: marketing")
else:
response.say("Invalid input. Please try again.")
response.redirect("https://example.com/")
return Response(response.to_xml(), mimetype='text/xml')
<?php
// NOTE: This is a different file: choice.php
require_once 'vendor/autoload.php';
use \SignalWire\LaML\VoiceResponse;
$choice = $_REQUEST['Digits'];
$response = new VoiceResponse;
if ($choice == "1") {
$response->say("You selected: support");
} else if ($choice == "2") {
$response->say("You selected: sales");
} else if ($choice == "3") {
$response->say("You selected: marketing");
} else {
$response->say("Invalid input. Please try again.");
$response->redirect("https://example.com/");
}
header('Content-type: text/xml');
echo $response->asXML();
// ... previous code
/**
* @param {express.Request} req
* @param {express.Response} res
*/
function choiceHandler(req, res) {
/** @type string */
const choice = req.body?.["Digits"] ?? "";
const response = new RestClient.LaML.VoiceResponse();
if (choice === "1") {
response.say("You selected: support");
} else if (choice === "2") {
response.say("You selected: sales");
} else if (choice === "3") {
response.say("You selected: marketing");
} else {
response.say("Invalid input. Please try again.");
response.redirect("https://example.com/");
}
res.set("Content-Type", "text/xml");
res.send(response.toString());
}
app.post("/choice", choiceHandler);
app.listen(8088);
This time, we read the Digits
parameter and we reply with a different message
depending on its value. If the value is not one of the expected ones, we let the
user try again by redirecting the script to the initial endpoint /
: make sure to
replace https://example.com/
with your actual public URL.
After configuring one of your numbers to handle incoming calls using the webhook that we implemented, you will be able to test the application. Refer to Handling Calls from Code to learn how to configure your number.
Conclusion
We have shown how to gather input from the user, and take decisions accordingly. We did all this in the context of the Compatibility API.
For more advanced, real-time applications, you'll want to check out our Realtime SDK. Find more details in the guide about First Steps with Voice in the Realtime SDK.