I’m excited to share my recent experience integrating Twilio Voice into a Django application to deliver one-time passwords (OTPs) via phone calls. When I first set up my /outbound/
endpoint, it rendered valid TwiML XML perfectly in the browser. However, triggering a call through Twilio resulted in a frustrating “Sorry, an application error has occurred” message.
- The original code that led to the error
- My diagnosis of what went wrong
- A corrected implementation that works
- Extra “practice” enhancements to deepen the integration
Error Code
Here’s the initial view code I wrote in views.py: <details> <summary><strong>views.py (original)</strong></summary>
django.conf import settings
from django.http import HttpResponse
from twilio.rest import TwilioRestClient # legacy import
import twilio.twiml
def voice_call(otp, mobile_no):
client = TwilioRestClient(settings.ACCOUNT_SID, settings.AUTH_TOKEN)
client.calls.create(
from_=settings.OTP_FROM_NUMBER,
to=mobile_no,
url='http://localhost:8000/outbound/',
method='POST'
)
def outbound(self):
response = twiml.Response()
response.say("Thank you for contacting our department", voice='alice')
return HttpResponse(response, content_type="application/xml")
</details>
This looked straightforward: trigger a call to mobile_no
and have Twilio POST to my /outbound/
endpoint, which returns TwiML telling it to speak a message.
Define “Application Error”
After some investigation, I identified three root issues:
- Incorrect view signature
- Django views must accept a
request
object as their first parameter. Definingoutbound(self)
meant Django couldn’t match the signature, resulting in an internal server error (HTTP 500).
- Django views must accept a
- Legacy Twilio client import
- Using
TwilioRestClient
is deprecated. The moderntwilio.rest.Client
class is more reliable and up to date.
- Using
- Localhost not publicly accessible
- When Twilio’s servers attempt to fetch your TwiML at
http://localhost:8000/outbound/
, they cannot reach your local machine. Twilio requires a publicly accessible URL (for example, via an ngrok tunnel or your own domain).
- When Twilio’s servers attempt to fetch your TwiML at
Correct Implementation
Here’s the revised views.py that addresses all three problems: <details> <summary><strong>views.py (fixed)</strong></summary>
django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from twilio.rest import Client
from twilio.twiml.voice_response import VoiceResponse
def voice_call(request, otp, mobile_no):
"""
Initiates a Twilio voice call to deliver an OTP.
"""
client = Client(settings.ACCOUNT_SID, settings.AUTH_TOKEN)
# Use a publicly accessible URL (e.g., via ngrok)
twiml_url = settings.TWILIO_TWIML_URL # e.g. 'https://abcd1234.ngrok.io/outbound/'
client.calls.create(
from_=settings.OTP_FROM_NUMBER,
to=mobile_no,
url=twiml_url,
method='POST'
)
return HttpResponse("Call initiated", status=202)
@csrf_exempt
def outbound(request):
"""
Responds to Twilio’s webhook with TwiML instructions.
"""
resp = VoiceResponse()
resp.say("Thank you for contacting our department. Your OTP is:", voice='alice')
# Optionally inject the OTP dynamically via query string or storage
otp = request.GET.get('otp', 'unknown')
resp.say(otp, voice='alice')
return HttpResponse(str(resp), content_type="application/xml")
</details>
Key fixes explained:
outbound(request)
: Replacedself
withrequest
so Django can invoke it properly.Client
: Swapped out the deprecatedTwilioRestClient
for the modernClient
class.@csrf_exempt
: Added to allow Twilio’s POST without a CSRF token.- Public URL: Set
TWILIO_TWIML_URL
in settings (for example, an ngrok tunnel) so Twilio can reach your TwiML.
Additional “Practice” Functionality
Once you have the basics working, you can supercharge your integration. Here are a few ideas to practice:
Error Handling & Logging
client.calls.create(…)
except Exception as e:
logger.error(f"Twilio call failed: {e}")
return HttpResponse("Error initiating call", status=500)
Gather User Input
Let callers press a digit and handle their response:
twilio.twiml.voice_response import Gather
@csrf_exempt
def outbound(request):
resp = VoiceResponse()
gather = Gather(num_digits=1, action='/gather-response/', method='POST')
gather.say("Press 1 to confirm receipt of the OTP.", voice='alice')
resp.append(gather)
resp.redirect('/outbound/') # repeat if no input
return HttpResponse(str(resp), content_type="application/xml")
Play an Audio File
.play(url="https://example.com/hold-music.mp3")
Record the Call
Tell Twilio to record the conversation:
.calls.create(…, record=True)
Dynamic TwiML Generation
Store the OTP in your database or session, then look it up in outbound()
instead of passing it in the URL.
Final Thoughts
By correcting my view signature, updating to the modern Twilio Python client, and exposing a public TwiML URL, I eliminated the mysterious “application error.” From here, you can layer on advanced TwiML features,user input, recordings, audio playback, and more to build an interactive voice experience.