Welcome to the second entry in our two-part blog series about building, deploying, and using a bespoke contact center solution using the Telnyx Voice API.
To read about how we built an MVP using TeXML, check out part one.
Examples using the code from this project can be found on GitHub - try it out for yourself!
"So, we have some TeXML files. What do we do now?"
We needed more! To fulfill all the requirements of a fully-functional call center, we needed a more complex system. That's where the Python AIOHTTP server came in. The server would host the TeXML and audio files and essentially 'serve' them to the requesting TeXML service. It would also open up a way to build on our MVP and add additional requirements later on in development.
The AIOHTTP server is a backend HTTP server hosted on Kubernetes that handles incoming and outgoing HTTP requests. The server can be broken down into two main sections - server and client. The server handles incoming requests (from Telnyx) while the client sends requests to other servers.
Initially, the server-side was all that was needed. Incoming requests from the TeXML service were handled by the server-side, and a FileResponse object (TeXML file) was returned:
async def initiate_call_handler(request: web.Request) -> web.FileResponse:
"""
Return XML file that should dial Telnyx support line and redirect to
below function.
"""
return web.FileResponse("call_center/infrastructure/TeXML/inbound.xml")
To keep track of the 'state' of the calls coming in, we leveraged the call SID that was sent in the status callback events. For example, if an
answered
event came in, we knew that the call had been answered. This information was cached on our server, so when the next request came in, the server knew what file to give next. By tracking the state of calls, we could control the flow of the calls easily using the TeXML files returned. The server side was also responsible for handling requests for the media files specified in the TeXML instructions. For example, the support greeting upon dialing for support:
<Play><http://call-center:8080/TeXML/support_greeting></Play>
Once a request hit the server on the endpoint
/TeXML/support_greeting
, it needed to handle that request and immediately return the correct audio file. Here is an example of how this was done:async def hello_handler(request: web.Request) -> web.FileResponse:
"""
Return hello audio
"""
return web.FileResponse("call_center/infrastructure/audio/support_greeting.mp3")
The client was added to deal with the outgoing requests needed for additional features, like our Slack integration (more on that later). The client leveraged the AIOHTTP ClientSession module to send asynchronous
GET
and POST
requests:class SlackRequest:
headers = {"Content-Type": "application/json"}
def __init__(self, session, url):
self.session = session
self.url = url
async def slack_post(self, body):
async with self.session.post(
headers=self.headers, url=self.url, data=body
) as response:
if response.status == 200:
return
Once the central TeXML part was finished, we began adding more 'nice-to-haves' to our call center application. These features aren't essential to build a functioning contact center but were extremely helpful for our particular use-case. Luckily, the flexibility of Call Control made it easy to integrate dynamic changes to TeXML dials, Slack chat-bots, and account balance notifications.
For this project, we took full advantage of the Telnyx API, integrating with two Telnyx endpoints (aside from the previously-discussed TeXML files) for retrieving connection data and balance status.
We created a new Telnyx account dedicated specifically to the call center project and created a credential-based connection for each agent in our support team. These connections were gathered using scheduled jobs that ran on the server every day. The backend job would look for the relevant connections using filtering and prepare the new list of agents to be dialed. If the data had changed (i.e., a connection was modified or a new connection appeared), the TeXML files were changed accordingly by the server to dial the new set of users. This logic allowed for a dynamic set of agents to be dialed without the need to change the server every time a user changes.
The same principles were applied when adding balance notifications to inform us if the account balance needed topping up. A scheduled job ran to leverage the client-side to gather the account's balance information. If this balance fell under a certain threshold, a notification would be sent via Slack, pinging our management team with a reminder to top up the account. That leads us on to the very important step of our Slack integration:
Here at Telnyx, we use Slack as a central messaging hub for all employees. As such, we wanted to integrate our contact center with Slack to keep our team up-to-date with incoming calls and keep important call data at our fingertips.
Slack has a wide-ranging API of its own, but the particular area we were interested in was incoming webhooks. We created a Slack App to act as a gateway into the particular channel assigned to the app. Once we enabled incoming webhooks to this Slack app, we could use POST requests to send messages to specific channels using a unique URL created by Slack. Combining this with Slack's Block Kit framework gave us an easy way to format our messages:
def incoming_call(from_num) -> str:
blocks = [
header_section("Incoming Call Ringing"),
body_section("@here " + str(from_num) + " is calling. Let's jump on that!"),
]
return str(
{
"type": "modal",
"title": {"type": "plain_text", "text": "Call Center"},
"blocks": blocks,
}
)
def header_section(event) -> dict:
"""Main header"""
return {"type": "section", "text": {"type": "mrkdwn", "text": f">*{event}*"}}
def body_section(text) -> dict:
"""
Body of the message
"""
return {"type": "section", "text": {"type": "mrkdwn", "text": f"{text}"}}
But how do we know when to send these messages? Luckily for us, TeXML has a great way for the server to know the status of calls using Status Callbacks. All the server has to do is handle these callbacks and create a Slack event. Below we are using the 'in-progress' callback we receive to send a 'call was answered' webhook to Slack:
elif data["CallStatus"] == "in-progress":
# Set the call to answered
calls[sid] = (True, 1)
to = data["To"]
from_num = data["From"]
body = inbound_answered_call(to, from_num)
# Call in progress slack event
await request.config_dict["slack_client"].slack_post(body)
We took this logic and applied it to incoming calls, answered calls, missed calls, voicemails left, and completed calls for both inbound and outbound calls. This is how the finished integration looks on Slack:
Future tweaks to the call center solution will be required to cap off the project and fulfill the entire spec we initially laid out for ourselves. In particular, two things are on the short term roadmap: metric recordings and a WebRTC frontend client that our agents can use.
Calls are now being made from and received to our call center solution. Data associated with each call is present on the call center service at some moment in time. The question then becomes; how do we aggregate that data and allow for easy access elsewhere?
We intend to solve for this by implementing a PostgreSQL database and a separate service to pull this data into a dashboard for daily review. The ability to do this was kept in the back of our minds when building the service, as keeping track of this data gives us insights into how efficiently and effectively our support team is serving our customers. With this in mind, we set ourselves up for success with the original implementation, building in an easy way to cache relevant data, integrate a database, and write to the database tables on scheduled intervals. This approach will hopefully reduce the burden on the call center application, as it is already working a lot - receiving requests, sending requests, formatting Slack messages, etc.
Right now, each agent registers via a third-party frontend client to the SIP Connection associated with the call center's Telnyx account. However, Telnyx also provides SDKs that enable building web-based clients that can register to credential connections with WebRTC.
A WebRTC-based client would be especially advantageous in our use-case, as it would eliminate the need to provision physical desk phones or softphones for our agents, minimizing our hardware costs and allowing us to onboard new agents much more efficiently. Our engineers are hard at work building new functionality into our WebRTC offering, so this initiative is paused until we can get our hands on some of these exciting new features.
Building, deploying, and migrating to a bespoke contact center for our support team was a huge learning experience for our network operations team and an invaluable source of high-fidelity feedback for our engineers working to build the most developer-friendly enterprise-grade voice API on the market. If you're curious about how we did it or interested in implementing a call center of your own using the Telnyx Voice API, chat with an expert today.
Previously published at https://telnyx.com/resources/custom-support-call-center-ii