Course 3 / Lecture 6

How to build an IVR with Ozeki VoIP SIP SDK

How to create IVR with DTMF authentication

This article is a detailed guide on how you can create an authentication for your customers using DTMF signals. Of course you can use the DTMF signals for several purposes. If you are not familiar with the expression DTMF you should read the following article: How to use DTMF signaling? You can find out more about a basic and a multi-level IVR system that uses XML code in the previous example. If you click on the following link, you can find the full description of the basic and multi-level IVRs example code. Both of these examples are written in C#.


The present sample is an updated version of the sample program, which can be found at the How to develop an IVR system written in C# by using XML code. That is why here you can only read about the new solutions. To be able to have sufficient knowledge on this topic you should read the earlier mentioned description, too.

This sample program demonstrates such an implemetation of the several possibilities of usage of the DTMF which is for the authentication of the users. If the User enters his PIN code with the help of his device and if the PIN code has been provided correctly, he can access his data. The process is fully automated as the information is provided by an IVR.

DTMF authentication example

When the User sends such a DTMF signal to which there is not any command assigned in the actual IVR, this signal along with the ID of the User will be forwarded with the help of an HTTP request to a simple webpage written in PHP.

This sample code uses HTTP request, because this is a platform-independent solution that is why the IVR can be integrated with any application because the HTTP request can be received with the help of any programming language. This way you can add an internal menu to your system dynamically. The Ozeki Phone System XE also includes a similar solution to this; you can find more information about this on the webpage of the Ozeki Phone System software product.

The PHP stores the PIN codes of the Users, to keep the example simple in this case the PIN codes and other data of the Users are stored in a multidimensional array, but any kind of database can be used for this purpose. This PHP also checks whether the given User has provided his code correctly and according to this it sends back the IVR.

  • If the User provided an incorrect code, in this case the IVR is made up of a speak node only, which informs the User that his PIN code cannot be accepted. In this case the progam can only handle play or speak nodes, then it gets back to the original IVR menu.
  • If the User has provided the code correctly, the IVR which is sent back includes a complete menu, with the help of this menu the User can query his own private data.

To make it able for the program to interpret the PIN code properly, it needed to be prepared to handle multiple DTMF signals which are received one after another as if they were coherent.

Source code analysis in C#

The methods connected to the processing of the IVR XML had been taken out from the Program.cs class and they have been put into a separate class, this is IVRFactory.cs.

The CreateIVR method can be found here, which is responsible for parsing the default IVR and the response IVR XML. The latter will be included in the response received for the HTTP request if the User provided such DTMF signs which cannot be defined.

public static ICommand CreateIVR(string ivrXml, Menu menu = null)
        {
            if (menu == null)
            {
                var responseMenu = new Menu();

                try
                {
                    var xelement = XElement.Parse(ivrXml);
                    var menuElement = xelement.Element("menu");

                    if (menuElement != null)
                    {
                        InitElement(menuElement, responseMenu);
                        KeyElement(menuElement, responseMenu, false);
                    
                        return responseMenu;
                    }
                    else
                    {
                        return PlaySpeakElement(xelement);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    Console.WriteLine("Invalid ivr xml");
                }
            }
            else
            {
                try
                {
                    var xelement = XElement.Parse(ivrXml);
                    var menuElement = xelement.Element("menu");

                    if (menuElement == null)
                    {
                        PlaySpeakElement(xelement, menu);
                        return menu;
                    }

                    ForwardToUrl(menuElement, menu);
                    InitElement(menuElement, menu);
                    KeyElement(menuElement, menu, true);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    Console.WriteLine("Invalid ivr xml");
                }

                return menu;
            }
            return null;
        }
	

Here many kinds of possibilities should be handled:

  • If the CreateIVR method does not receive a menu parameter, it checks whether the received XML includes a menu node:
    • If there is a menu node, the CreateIVR method will call the Initelement() and KeyElement() methods for parsing and it will return with the new menu.

      	        static void InitElement(XElement element, Menu menu)
      	        {
      	            var menuInit = element.Element("init");
      	
      	            if (menuInit == null)
      	            {
      	                Console.WriteLine("Wrong XML code!");
      	                return;
      	            }
      	            PlaySpeakElement(menuInit, menu);
      	        }
      	        static void PlaySpeakElement(XElement element, Menu menu)
      	        {
      	            foreach (var initElements in element.Elements())
      	            {
      	                switch (initElements.Name.ToString())
      	                {
      	                    case "play":
      	                        menu.AddInitCommand(new PlayCommand(initElements.Value.ToString()));
      	                        break;
      	                    case "speak":
      	                        menu.AddInitCommand(new SpeakCommand(initElements.Value.ToString()));
      	                        break;
      	                    default:
      	                        break;
      	                }
      	            }
      	        }
      	
      	        static void KeyElement(XElement xelement, Menu menu, bool isInnerMenu)
      	        {
      	            var menuKeys = xelement.Element("keys");
      	
      	            if (menuKeys == null)
      	            {
      	                Console.WriteLine("Missing <keys> node!");
      	                return;
      	            }
      	
      	            foreach (var key in menuKeys.Elements("key"))
      	            {
      	                var pressedKeyAttribute = key.Attribute("pressed");
      	
      	                if (pressedKeyAttribute == null)
      	                {
      	                    Console.WriteLine("Invalid ivr xml, keypress has no value!");
      	                    return;
      	                }
      	
      	                int pressedKey;
      	
      	                if (!Int32.TryParse(pressedKeyAttribute.Value, out pressedKey))
      	                {
      	                    Console.WriteLine("You did not add any number!");
      	                }
      	
      	                foreach (var element in key.Elements())
      	                {
      	                    switch (element.Name.ToString())
      	                    {
      	                        case "play":
      	                            menu.AddKeypressCommand(pressedKey, new PlayCommand(element.Value.ToString()));
      	                            break;
      	                        case "speak":
      	                            menu.AddKeypressCommand(pressedKey, new SpeakCommand(element.Value.ToString()));
      	                            break;
      	                        case "menu":
      	                            if (isInnerMenu)
      	                            {
      	                                Menu innerMenu = new Menu();
      	                                menu.AddKeypressCommand(pressedKey, innerMenu);
      	                                CreateIVR(key.ToString(), innerMenu);
      	                            }
      	                            break;
      	                        default:
      	                            break;
      	                    }
      	                }
      	            }
      	        }
      				

      This possibility will run, when the User has provided his Pin code correctly, that is why he should receive a new menu.

    • If there is not any menu node, the CreateIVR method calls the PlaySpeakElement() method.

      			static ICommand PlaySpeakElement(XElement element)
      	        {
      	            var multipleCommand = new MultipleCommandHandler();
      	            foreach (var initElements in element.Elements())
      	            {
      	                switch (initElements.Name.ToString())
      	                {
      	                    case "play":
      	                        multipleCommand.AddCommand(new PlayCommand(initElements.Value.ToString()));
      	                        break;
      	                    case "speak":
      	                        multipleCommand.AddCommand(new SpeakCommand(initElements.Value.ToString()));
      	                        break;
      	                    default:
      	                        break;
      	                }
      	            }
      	            return multipleCommand;
      	        }
      				

      In this case the User has provided his PIN code incorrectly, as in this case the PHP sends only such an IVR in which only a speak node can be found with a notification of the error.

  • If during the calling of the CreateIVR method it receives a menu parameter, the methods will be called which are necessary for the generation of the default IVR menu. In this case the CreateIVR method will only receive a menu parameter, when it receives the burnt-in ivrXml for processing which includes the default menu. At this time the calling of the ForwardToUrl() method is also necessary which saves the parameter of the menu node forwardToUrl to the ForwardToUrl variable of the Menu class. This way, this variable will include that to which URL the HTTP request should be sent to.

    		static void ForwardToUrl(XElement element, Menu menu)
            {
                var forwardToUrl = element.Attribute("forwardToUrl");
    
                if (forwardToUrl != null)
                {
                    string[] strings = forwardToUrl.ToString().Split('"');
                    menu.ForwardToUrl = strings[1];
                }
            }
    			

    (The forwardToUrl parameter should be provided in the IVR XML because this parameter provides the URL address of the destination where the HTTP request will be sent.)


In the Menu.cs class some changes have been made, too:

The method which is responsible for sending the HTTP request and for processing the response which is received for the mentioned request can be found here. This is the CreateHttpRequest() method. This method receives the URL as a parameter where it should send the request, additionally it receives the phone number and the DTMF signals provided by the User.

		string CreateHttpRequest(string url, int dtmf, string phoneNumber)
        {
            // Create a request using a URL that can receive a post.
            WebRequest request = WebRequest.Create(url);
            // Set the Method property of the request to POST.
            request.Method = "POST";
            
            // Create POST data and convert it to a byte array.
            string postDtmf = dtmf.ToString();
            string callerInfo = phoneNumber;
            string postData = postDtmf + " " + callerInfo;
            byte[] byteArray = Encoding.UTF8.GetBytes(postData);

            // Set the ContentType and ContentLength property of the WebRequest.
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = byteArray.Length;

            // Get the request stream and write the data in it.
            Stream dataStream = request.GetRequestStream();
            dataStream.Write(byteArray, 0, byteArray.Length);

            // Close the Stream object.
            dataStream.Close();

            // Get the response.
            WebResponse response = request.GetResponse();
            
            // Get the stream containing content returned by the server.
            dataStream = response.GetResponseStream();
            StreamReader reader = new StreamReader(dataStream);
            // Read the content.
            string responseFromServer = reader.ReadToEnd();

            // Clean up the streams.
            reader.Close();
            dataStream.Close();
            response.Close();

            return responseFromServer;
        }
	

The CreateHttpRequest() method creates a Webrequest object with the provided URL, then it will send the data (DTMF, phone number) in a POST message as a byteArray. After that, it will create an object for the response, too. Then, with the help of a StreamReader it saves the data which was received as a response in a string. After closing the streams it will return this string.

In this class it is also handled that it should take the buttons which were pressed one after another as only one command. That is why, if it receives multiple DTMF signals one after another, here will make a chain of them and in this case this will represent the PIN code of the User. For the implementation of this a timer is running, which checks in every second whether a DTMF signal has been received. This is the InitKeyPressTimeoutTimer() method, which is called in the constructor of the Menu class.

		void InitKeypressTimeoutTimer()
        {
            keypressTimeoutTimer = new Timer(1000);
            keypressTimeoutTimer.AutoReset = true;
            keypressTimeoutTimer.Elapsed += KeypressTimeoutElapsed;
            keypressTimeoutTimer.Start();
        }

        void KeypressTimeoutElapsed(object sender, ElapsedEventArgs e)
        {
            if (!dtmfPressed)
            {
                call_DtmfReceived(sender, dtmfChain);
                dtmfChain = null;
            }

            dtmfPressed = false;
        }
	

If the DTMF signals are continuously received, in this situation if the User presses the buttons of the device continuously in order to enter his PIN code, the program will add the values of the DTMF to the chain. The DtmfReceived() method is called in case of every DtmfReceived event.

		void DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)
        {
            dtmfPressed = true;
            dtmfChain += DtmfNamedEventConverter.DtmfNamedEventsToString(e.Item.Signal.Signal);
        }
	

If no more DTMF signals are received within a second, the KeypressTimeoutElapsed() method will send the chain by calling the call_DtmfReceived() method. This method determines whether the DTMF signal can be defined as an existing command.

		void call_DtmfReceived(object sender, string dtmfChain)
        {
            if (dtmfChain != null)
            {
                int pressedKey;
                if (!Int32.TryParse(dtmfChain, out pressedKey))
                {
                    Console.WriteLine("You did not add a valid number!");
                }

                MultipleCommandHandler command;
                if (keys.TryGetValue(pressedKey, out command))
                {
                    StartCommand(command);
                }
                else
                {
                    if (ForwardToUrl != null)
                    {
                        ResponseXml = CreateHttpRequest(ForwardToUrl, pressedKey, call.DialInfo.UserName);

                        var responseIvr = IVRFactory.CreateIVR(ResponseXml);

                        StartCommand(responseIvr);
                    }
                    else
                    {
                        Console.WriteLine("This is a not used option! Please try again!");
                    }
                }
            }
        }
	

As it was mentioned above:

  • If the given event which is triggered by the pressing of the given button exists in the current IVR menu, it will be executed.
  • However, if this kind of event does not exist and the value of the ForwardToUrl is not null, the call_DtmfReceived method will send the given sign along with the phone number of the User with the help of the HTTP request.

PHP implementation

The PHP receives the HTTP request and by using the incoming data it checks whether the PIN code which was provided by the given User is valid or not.

  • In case the PIN code which was provided by the User is valid the PHP will send a complete IVR menu in the response.
  • If the PIN code is not valid, the PHP will only send an IVR which only includes a speak node in the response.

The data of the Users is stored in a multidimensional array, the PHP will go through this and it will look up for the data which is necessary.

	$userdb=Array
	(
		(0) => Array
			(
				('uid') => '1001',
				('balance') => '23012312',
				('pin') => '6544'
				
			),

		(1) => Array
			(
				('uid') => '1002',
				('balance') => '11021021',
				('pin') => '1234'
			),

		(2) => Array
			(
				('uid') => '1003',
				('balance') => '1012',
				('pin') => '7658'
			)
	);
	

With the help of the file_get_contents('php://input') function the HTTP request can be stored in a variable, after that it will be possible to tokenize the HTTP request, this way the values of the DTMF and the phone number can be obtained.

$rawdata = file_get_contents('php://input');
	
$dtmf = strtok($rawdata, ' ');
$phoneNumber = strtok(' ');
	

Firstly, the searchForId() function checks that the data belonging to the received phone number in what block with a given ID can be found.

	function searchForId($id, $array) {
	   foreach ($array as $key => $val) {
		   if ($val['uid'] === $id) {
			   return $key;
		   }
	   }
	   return null;
	}
	$id = searchForId($phoneNumber, $userdb);
	

Then according to this, the array_walk_recursive() function check in the block belonging to the given User whether the received PIN code is valid and it will continue the process according to the above mentioned cases.

	array_walk_recursive($userdb[$id], function ($item, $key) {
		global $dtmf;
		global $money;

		if($key == 'balance'){
			$money = $item;
		}
		if($key == 'pin'){
			if($item == $dtmf){
				echo "<response>
						<menu>
						<init>
							<speak>
								Access Granted to your bank account. If you want to know your balance, please press one.
							</speak>
						</init>
						<keys>
							<key pressed='1'>
								<speak>
										You pressed button one. Your balance is $" . $money .
								"</speak>
							</key>
		                </keys>
					</menu>
				</response>";
			}
			else {
				echo "<response>
						<speak>
								Access denied to your bank account.
						</speak>
				  </response>";
			}
		}
	});
	

Conclusion

This article introduced you the basic knowledge about DTMF authentication and showed how Ozeki VoIP SIP SDK can help you to fulfill your wishes about this topic. If you have read through this page carefully, you already have all the knowledge you need to start on your own solution.

Related Pages

More information