Unable to Send OTP via Twilio in Django

I'm working on implementing an OTP system in my Django project using Twilio. I've successfully obtained a Twilio phone number, but the OTP message is not being sent to the user's mobile number. Below are the details of my setup:

Code Implementation

  1. views.py:
def send_otp_view(request):
    if request.method == "POST":
        data = json.loads(request.body)
        phone_number = data.get('phone_number')

        # Generate a random 6-digit OTP
        otp = random.randint(100000, 999999)

        # Send OTP via Twilio (or any other SMS provider)
        try:
            client = Client(settings.TWILIO_ACCOUNT_SID, settings.TWILIO_AUTH_TOKEN)
            message = client.messages.create(
                body=f"Your OTP is {otp}. Please use this to verify your number.",
                from_=settings.TWILIO_PHONE_NUMBER,
                to=phone_number
            )

            # Optionally, store the OTP in a session or database
            request.session['otp'] = otp
            request.session['phone_number'] = phone_number

            return JsonResponse({'status': 'success', 'otp': otp})  # Return OTP for validation
        except Exception as e:
            return JsonResponse({'status': 'error', 'message': str(e)})

    return JsonResponse({'status': 'error', 'message': 'Invalid request'})



def address_view(request):
    if request.user.is_authenticated:
        user_addresses = Address.objects.filter(user=request.user)

        if request.method == "POST":
            # Get the OTP from the form
            entered_otp = request.POST.get("otp")

            # Check if the OTP matches
            if entered_otp == str(request.session.get('otp')):
                # Proceed with saving the address if OTP is correct
                mobile = request.POST.get("mobile")
                email = request.POST.get("email")
                pin = request.POST.get("pin")
                region = request.POST.get("region")
                address = request.POST.get("address")
                landmark = request.POST.get("landmark")
                name = request.POST.get("name")

                new_address = Address.objects.create(
                    user=request.user,
                    mobile=mobile,
                    email=email,
                    pin=pin,
                    region=region,
                    address=address,
                    landmark=landmark,
                    name=name,
                )
                messages.success(request, "Address Added Successfully.")
                return redirect("core:address")
            else:
                messages.error(request, "Invalid OTP. Please try again.")
                return redirect("core:address")

        context = {
            "user_addresses": user_addresses,
        }
        return render(request, 'others/address-book.html', context)
    else:
        print("User is not authenticated")
        return redirect("core:login")

2. HTML Form:

<div class="gl-inline">
    <div class="u-s-m-b-30">
        <label class="gl-label" for="address-phone">PHONE *</label>
        <input 
            class="input-text input-text--primary-style" 
            type="text" 
            id="address-phone" 
            name="mobile" 
            placeholder="Enter 10-digit phone number" 
            oninput="validatePhoneNumber()" 
            maxlength="10">
        <button type="button" id="send-otp-btn" style="display:none;" onclick="sendOTP()">Send OTP</button>
    </div>
    <div id="otp-box" style="display: none;" class="u-s-m-b-30">
        <label class="gl-label" for="otp-input">ENTER OTP *</label>
        <input 
            class="input-text input-text--primary-style" 
            type="text" 
            id="otp-input" 
            name="otp" 
            placeholder="Enter OTP" 
            maxlength="6" 
            oninput="validateOTP()">
        <span id="otp-feedback" style="display: none; font-size: 14px;"></span>
    </div>
    <div class="u-s-m-b-30">
        <label class="gl-label" for="address-street">STREET ADDRESS *</label>
        <input class="input-text input-text--primary-style" type="text" id="address-street" placeholder="House Name and Street" name="address">
    </div>
</div>

3. Twilio Settings (settings.py):

TWILIO_ACCOUNT_SID = 'YOUR_ACCOUNT_SID'
TWILIO_AUTH_TOKEN = 'YOUR_AUTH_TOKEN'
TWILIO_PHONE_NUMBER = '+1234567890'  # Twilio number with country code

4. Frontend Validation (JavaScript):

console.log("OTP");

let sentOTP = null; // Store the sent OTP

function validatePhoneNumber() {
    const phoneInput = document.getElementById('address-phone');
    const otpBox = document.getElementById('otp-box');
    const sendOtpBtn = document.getElementById('send-otp-btn');

    // Check if the phone number is 10 digits long
    if (phoneInput.value.length === 10) {
        otpBox.style.display = 'block'; // Show OTP box
        sendOtpBtn.style.display = 'inline-block'; // Show OTP button
    } else {
        otpBox.style.display = 'none'; // Hide OTP box
        sendOtpBtn.style.display = 'none'; // Hide OTP button
    }
}

function sendOTP() {
    const phoneNumber = document.getElementById('address-phone').value;

    // Send an AJAX request to the Django view to generate and send OTP
    fetch('/send-otp/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken') // Handle CSRF token for security
        },
        body: JSON.stringify({ phone_number: phoneNumber })
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'success') {
            sentOTP = data.otp; // Save the OTP for later validation
            console.log("OTP sent successfully");
        } else {
            alert("Error sending OTP");
        }
    })
    .catch(error => {
        console.error('Error:', error);
        alert('Failed to send OTP');
    });
}

function validateOTP() {
    const otpInput = document.getElementById('otp-input');
    const feedback = document.getElementById('otp-feedback');

    // Compare entered OTP with the sent OTP
    if (otpInput.value === sentOTP) {
        feedback.style.display = 'block';
        feedback.style.color = 'green';
        feedback.textContent = 'OTP is correct!';
    } else if (otpInput.value.length === sentOTP.length) {
        feedback.style.display = 'block';
        feedback.style.color = 'red';
        feedback.textContent = 'Incorrect OTP, please try again.';
    } else {
        feedback.style.display = 'none'; // Hide feedback when incomplete
    }
}

// Utility to get the CSRF token from cookies
function getCookie(name) {
    const cookieValue = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
    return cookieValue ? cookieValue.pop() : '';
}

Problem:

  1. The form submits the correct mobile number, and the POST request reaches the Django view.
  2. Twilio's Client and messages.create methods are invoked, but no SMS is sent.
  3. I see the following error in my Django logs:
Error sending OTP: TwilioRestException: [Error message here]

What I've Tried:

  1. Verified that the Twilio credentials (Account SID, Auth Token, and Phone Number) are correct.
  2. Ensured the phone number is in E.164 format with the correct country code.
  3. Confirmed that the Twilio trial account has verified the recipient's phone number.
  4. Checked Twilio message logs, but no entries appear for the failed message.
  5. Tested the Twilio functionality with a standalone script, which works:
from twilio.rest import Client

client = Client('YOUR_ACCOUNT_SID', 'YOUR_AUTH_TOKEN')
message = client.messages.create(
    body="Test message",
    from_='+1234567890',
    to='+9876543210'
)
print(message.sid)

Expected Behavior: The OTP message should be sent to the entered mobile number.

Question: What could be causing the issue, and how can I resolve it? Could it be related to my Twilio account setup or Django configuration? Any help or insights would be appreciated!

Additional Notes:

  1. I'm using a Twilio trial account.
  2. The recipient's phone number is verified in Twilio's dashboard.
  3. There are no network or firewall restrictions in my setup.

Key improvements and troubleshooting steps:

1. Phone Number Formatting:

-Added proper phone number validation and formatting using the phonenumbers library

-Ensures numbers are in E.164 format required by Twilio

-Handles different country codes properly

from django.http import JsonResponse
from django.conf import settings
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException
import random
import json
import phonenumbers

def format_phone_number(phone_number, country_code='US'):
    try:
        parsed_number = phonenumbers.parse(phone_number, country_code)
        if not phonenumbers.is_valid_number(parsed_number):
            return None
        return phonenumbers.format_number(parsed_number, 
phonenumbers.PhoneNumberFormat.E164)
    except phonenumbers.NumberParseException:
        return None

def send_otp_view(request):
    if request.method != "POST":
        return JsonResponse({'status': 'error', 'message': 'Invalid 
request method'})

    try:
        data = json.loads(request.body)
        phone_number = data.get('phone_number')
    
        if not phone_number:
            return JsonResponse({'status': 'error', 'message': 'Phone 
number is required'})
    
        formatted_number = format_phone_number(phone_number)
        if not formatted_number:
            return JsonResponse({
                'status': 'error', 
                'message': 'Invalid phone number format. Please include 
country code.'
            })
    
        otp = str(random.randint(100000, 999999))
    
        try:
            client = Client(settings.TWILIO_ACCOUNT_SID, 
settings.TWILIO_AUTH_TOKEN)
            message = client.messages.create(
                body=f"Your verification code is: {otp}",
                from_=settings.TWILIO_PHONE_NUMBER,
                to=formatted_number
            )
        
            request.session['otp'] = otp
            request.session['phone_number'] = formatted_number
        
            return JsonResponse({
                'status': 'success',
                'message': 'OTP sent successfully',
                'otp': otp
            })
        
        except TwilioRestException as e:
            print(f"Twilio Error: {str(e)}")
            error_message = 'Failed to send OTP. '
            if e.code == 20003:
                error_message += 'Authentication failed. Check Twilio 
credentials.'
            elif e.code == 21211:
                error_message += 'Invalid phone number format.'
            elif e.code == 21608:
                error_message += 'Phone number not verified in trial 
account.'
            else:
                error_message += str(e)
            
            return JsonResponse({'status': 'error', 'message': 
error_message})
        
    except Exception as e:
        return JsonResponse({'status': 'error', 'message': f'Server error: 
{str(e)}'})

2. Error Handling:

-Added specific error handling for common Twilio error codes

-Provides clear error messages for debugging

-Properly catches and reports authentication issues

3. Security Improvements:

-Better session handling for OTP storage

-More robust input validation

-Proper error message sanitization

To implement this solution:

# Install required packages:

pip install twilio phonenumbers

# In your frontend JavaScript, modify the error handling:

function sendOTP() {
    const phoneNumber = document.getElementById('address-phone').value;
    // Add country code if not provided by user
    const formattedNumber = phoneNumber.startsWith('+') ? phoneNumber : 
`+${phoneNumber}`;

    fetch('/send-otp/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken')
        },
        body: JSON.stringify({ phone_number: formattedNumber })
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'success') {
            alert('OTP sent successfully!');
        } else {
            alert(`Error: ${data.message}`);
        }
    })
    .catch(error => {
        console.error('Error:', error);
        alert('Failed to send OTP. Please try again.');
    });
}

Additional troubleshooting steps:

# Check your Twilio Console to ensure:

-Your account is active and has sufficient credits

-The recipient's number is verified (required for trial accounts)

-Your Twilio phone number is capable of sending SMS

# Verify your Django settings:

-All Twilio credentials are correctly set

-DEBUG mode is enabled during testing

-Your server has internet access to reach Twilio's API

Вернуться на верх