Building a call centre is crucial for most businesses today. In this post, I take you through simple steps to setting up outbound calls from the browser. I will be using a React based frontend and a Django backend. The tutorial also assumes you have knowledge of the two frameworks and have set up your Africa's Talking account. If you don't have one, you can create one here: Africa's Talking

Django Rest Framework Backend

Setting Up

First, create your Django project and install Django Rest Framework. The backend will use Django Rest framework library to expose our callback URLs.

pip install djangorestframework
To learn more about drf: https://www.django-rest-framework.org/

After installing DRF, we need to set up a Django app. If you haven't set up your Django project yet, head on here to know how to create a Django project. Then install an app from the root folder of your Django project using the command (Ensure you add it to your apps in the settings file)

python3 manage.py startapp outbound_calls


Making calls from the browser via Africa's Talking (AT) requires you to have a virtual number. Before we continue, please make sure you set up you have a working phone number provided by Africa's Talking. The voice feature is not yet enabled for AT sandbox and you'll need a live number.
Now that all that is set up, let's dive into creating our REST endpoints.

Creating Views

On your outbound_calls app, navigate to the views file. Import viewsets from rest_framework and action from rest framework decorators

from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response


Next, we need to setup the callback route to handle our calls action logic.

class OutboundViewset(viewsets.ViewSet):

    @action(methods=["POST"], detail=False)
    def callaction(self, request):

        return Response(
            {
                "Dial": {
                    "phoneNumbers": request.data["clientDialedNumber"],
                    "callerId": request.data["destinationNumber"],
                }
            },
            status=status.HTTP_200_OK,
        )

So what's happening,

  • First, we set up our viewset class by subclassing the restframework viewsets class. In my code, I use the simple viewsets.ViewSet but you are free to use any other viewset type, e.g. model viewsets
  • Then finally we return a response to Africa's Talking on how to handle the call. Please note that in our case, I am only catering for the "Dial" action, but there are many other actions as documented here.

The "clientDialedNumber" and "destinationNumber" are sent within the payload from Africa's Talking.

destinationNumber is Your Africa’s Talking phone number. This will also be presented in an international format, starting with a +.

clientDialedNumber refers to the number dialled from the browser. It is the number you intend to call from the browser.

You can read more about the other information contained in the payload later on here.

When a call is made from the browser, AT will send the request to this callback endpoint. From this endpoint, we ask AT to perform a dial action which will then call the number we are trying to connect to. There's one problem though; AT expects an XML response meaning we need to use an XML renderer rather than the default JSON renderer. The minimum Dial action XML (read here) needs to look like:

<Response>
    <Dial
  phoneNumbers="+254711XXXYYY,+25631XYYZZZZ,test@ke.sip.africastalking.com"
    />
</Response>

The phoneNumbers refer to the phoneNumber(s) you intend to call. In our case, however, we also include the callerId since we are calling from the browser. From AT docs,

This (callerId) contains the Africa’s Talking number you want to dial out with. It is mainly important when you call using a sip number. If not specified, the number called by the user will be used.

Since we are calling from the browser,  the calls from the browser client come in the form of an SIP number. The clientId, therefore, helps the user receiving our call to view it as incoming from a user-friendly (regular) number defined by us which in this case is our destinationNumber. Therefore, at minimum our XML will have both a callerId attribute and a phoneNumbers attribute

<Response>
    <Dial
  phoneNumbers="+254711XXXYYY,+25631XYYZZZZ,test@ke.sip.africastalking.com" callerId="AT number to use"
    />
</Response>

XML Renderer

After creating the view, we need an XML renderer that will return an XML response instead of a JSON response. Create a file called custom_xml_renderer.py in your app directory.

from io import StringIO

import six
from django.utils.encoding import smart_text
from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework import renderers


class CustomRenderer(renderers.BaseRenderer):
    """
    Renderer which serializes to CustomXML.
    """

    media_type = "application/xml"
    format = "xml"
    charset = "utf-8"

    item_tag_name = "list-item"
    root_tag_name = "Response"

    def render(self, data, accepted_media_type=None, renderer_context=None):
        """
        Renders *obj* into serialized XML.
        """
        if data is None:
            return ""

        stream = StringIO()

        xml = SimplerXMLGenerator(stream, self.charset)
        xml.startDocument()
        xml.startElement(self.root_tag_name, {})

        self._to_xml(xml, data)

        xml.endElement(self.root_tag_name)
        xml.endDocument()
        return stream.getvalue()

    def _to_xml(self, xml, data):
        if isinstance(data, (list, tuple)):
            for item in data:
                xml.startElement(self.item_tag_name, {})
                self._to_xml(xml, item)
                xml.endElement(self.item_tag_name)

        elif isinstance(data, dict):
            for key, value in six.iteritems(data):
                if isinstance(value, dict) and key == "Dial":
                    xml.addQuickElement(key, attrs=value)
                    continue
                else:
                    xml.startElement(key, {})

                self._to_xml(xml, value)
                xml.endElement(key)

        elif data is None:
            pass

        else:
            xml.characters(smart_text(data))

Now, I will not explain in detail how the function works as that is beyond the scope of this tutorial. Notice that we subclass the Base renderer class.

media_type = "application/xml"
format = "xml"
charset = "utf-8"

item_tag_name = "list-item"
root_tag_name = "Response"

The media_type and format set our outcome to xml. The item_tag_name is used to set the xml key as list-item whenever a list is passed(in our case though we are not passing any lists). The root_tag_name is of importance. The default root tag name in xml is root in the AT's case as shown previously; the XML response needs to have the root tagged as Response; hence we override the default.

	elif isinstance(data, dict):
            for key, value in six.iteritems(data):
                if isinstance(value, dict) and key == "Dial":
                    xml.addQuickElement(key, attrs=value)
                    continue
                else:
                    xml.startElement(key, {})

                self._to_xml(xml, value)
                xml.endElement(key)

The section above is also another key section. As you notice in our views file, we were returning a response that performs a Dial action. Hence the dictionary with the key Dial in the response payload. In this XML renderer, we check for any dict item embedded within the Response that has a key of Dial and a value that's a dictionary. We then set the xml attributes for the Dial tag to be the keys of that dictionary.
Let's say our viewset returned a dictionary that looks like

 {
     "Dial": {
         "phoneNumbers": "+254711123456",
         "callerId": "+254701112233",
     }
 }

Then the outcome of our XML would be

<Response>
    <Dial
  phoneNumbers="+254711123456" callerId="+254701112233"
    />
</Response>

Since in the scope of this tutorial we are only using the Dial action, then its the only one that we check for, If you try to understand the custom XML renderer deeply, then keep in mind that the conversion happens recursively.

Add Custom Renderer to Views

Even though we now have a working custom renderer, we need to tell our views to use it. So customize your views file to look like

from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .custom_xml_renderer import CustomRenderer #This line added

class OutboundViewset(viewsets.ViewSet):

    renderer_classes = (CustomRenderer,) #This line added

    @action(methods=["POST"], detail=False)
    def callaction(self, request):

        return Response(
            {
                "Dial": {
                    "phoneNumbers": request.data["clientDialedNumber"],
                    "callerId": request.data["destinationNumber"],
                }
            },
            status=status.HTTP_200_OK,
        )

And that's it, guys. All you need to do now is to register your URL in your urls file and test it out. If you have been following along using viewsets and not Class Based Views or Function Based Views, I like to create a router.py file in the project folder then have it look something like


from rest_framework import routers

from outbound_calls import views

router = routers.DefaultRouter()

router.register("outbound", views.OutboundViewset, basename="outbound")

Then I would register that in the projects urls.py file

# import all needed packages
from myproject.router import router

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include((router.urls, "outbound")))
]

Register the URL endpoint as a callback on AT. Follow instructions here to do so. To configure events callback, set up a simple URL endpoint of POST method to receive events. Incase your callback endpoint refuses to work, ensure to also setup events endpoint as opposed to leaving it blank. If you'd love to register a localhost endpoint for callback, consider using ngrok for tunnelling,

See you on the next tutorial as we look on setting up the frontend with React.

Happy Coding :-)

You've successfully subscribed to Decoded For Devs
Welcome back! You've successfully signed in.
Great! You've successfully signed up.
Your link has expired
Success! Your account is fully activated, you now have access to all content.