Get Appointment

Blog Single

How to Verify Contact Form with OTP on Phone and Stop Spam in Laravel

  • Vfix Technology
  • 31 May 2025
  • Laravel
  • 271 Views

Spam submissions can ruin your contact form's effectiveness. This guide shows you how to implement OTP verification via SMS in Laravel to ensure only genuine users can submit forms while blocking bots and spammers.

Step 1: Apply for SMS Sender ID in India

For Indian phone numbers, you need an approved sender ID from telecom providers:

  • Visit websites of Tata, Jio, Airtel, or BSNL

  • Apply for a sender ID (6-character alphanumeric)

  • Submit required business documents for verification

  • Approval typically takes 3-7 working days

Step 2: Create Approved SMS Template

Once sender ID is approved, register your OTP template with the provider. Example:

Your OTP for Vfix Technology is ##var##. Don't share with anyone else.

Templates must follow strict guidelines - no promotional content, only transactional.

Step 3: Link Sender ID with Message Gateway

We'll use MSG91 as our SMS gateway. Link your sender ID provider (Tata/Airtel/etc.) with MSG91 by:

  1. Providing MSG91 with your sender ID credentials

  2. Completing KYC documentation if required

Step 4: Set Up MSG91 Account

  1. Create account at MSG91

  2. Add your approved sender ID

  3. Upload and approve your OTP template

  4. Note your:

    • API key

    • Template ID
       

Step 5: Install MSG91 Laravel Package

Add the official package to your Laravel project:

composer require craftsys/msg91-laravel

Add MSG91 API key in .env file:

Msg91_KEY=*******************

Step 6: Register Service Provider (Laravel 12)

In config/app.php, add to providers array:

'providers' => [
    // Other service providers...
    Craftsys\Msg91\Msg91LaravelServiceProvider::class,
],

Step 7: Add Facade Alias (Laravel 12)

In same config/app.php, add to aliases:

'aliases' => [
    // other aliases here
    'Msg91' => Craftsys\Msg91\Facade\Msg91::class,
],

Step 8: Create Controller Logic

Create HomeController with these key methods:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\Mail;
use Msg91;

class HomeController extends Controller
{
    public function index()
    {
        return view('home');
    }

    public function sendOtp(Request $request)
    {
        $request->validate([
            'name' => 'required|max:75',
            'email' => 'required|email|max:40',
            'phone' => 'required|digits:10',
            'message' => 'required|string|max:255',
        ]);

        // Prevent duplicate submissions within 24h
        if (Cookie::has('contact_form_submitted')) {
            return response()->make("
                <script>
                    alert('Your form has already been submitted. You can only submit the form once every 24 hours.');
                    window.history.back();
                </script>
            ");
        }

        // Block after 3 OTP attempts
        if (session('resend_count', 0) >= 3) {
            Cookie::queue('otp_blocked', true, 1440);
            return response()->make("
                <script>
                    alert('You have exceeded the maximum OTP requests. Try again in 24 hours.');
                    window.history.back();
                </script>
            ");
        }

        $otp = rand(1000, 9999);

        session([
            'contact_form_data' => $request->only('name', 'email', 'phone', 'message'),
            'otp_phone' => $request->phone,
            'otp' => $otp,
            'resend_count' => session('resend_count', 0),
        ]);

        try {
            $response = Msg91::otp($otp)
                ->to('91' . $request->phone)
                ->template('YOUR_TEMPLATE_ID')
                ->send();

            return redirect()->route('otp.form')->with('success', 'OTP sent successfully.');
        } catch (\Exception $e) {
            return back()->with('error', 'Failed to send OTP: ' . $e->getMessage());
        }
    }

    // Additional methods (verifyOtp, showOtpForm, resendOtp, thanks) 
    // as shown in your original code
}

Step 9: Create Views

index.blade.php

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Contact form</title>
    <!-- Tailwind CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
</head>

<body class="bg-gray-100">
    <section class="py-10">
        <div class="container mx-auto px-4">
            <div class="flex justify-center">
                <div class="w-full max-w-xl bg-white rounded-lg shadow p-6">
                    <form id="contactForm" method="post" action="{{ route('send.otp') }}">
                        @csrf
                        <div class="grid md:grid-cols-2 gap-4 mb-4">
                            <div>
                                <label class="block text-gray-700 text-sm font-bold mb-2" for="name">
                                    Full name <span class="text-red-500 font-bold">*</span>
                                </label>
                                <input
                                    class="border border-gray-300 rounded w-full py-2 px-3 text-gray-700 focus:outline-none focus:ring-1 focus:ring-black"
                                    name="name" type="text" placeholder="Your full name"
                                    value="{{ old('name') }}" required>
                                @error('name')
                                    <span class="text-red-500 text-sm">{{ $message }}</span>
                                @enderror
                            </div>

                            <div>
                                <label class="block text-gray-700 text-sm font-bold mb-2" for="email">
                                    Email <span class="text-red-500 font-bold">*</span>
                                </label>
                                <input
                                    class="border border-gray-300 rounded w-full py-2 px-3 text-gray-700 focus:outline-none focus:ring-1 focus:ring-black"
                                    name="email" type="email" placeholder="Your email"
                                    value="{{ old('email') }}" required>
                                @error('email')
                                    <span class="text-red-500 text-sm">{{ $message }}</span>
                                @enderror
                            </div>
                        </div>

                        <div class="mb-4">
                            <label class="block text-gray-700 text-sm font-bold mb-2" for="phone">
                                Phone no. <span class="text-red-500 text-xs">[Only 10 digits no space]*</span>
                            </label>
                            <input
                                class="border border-gray-300 rounded w-full py-2 px-3 text-gray-700 focus:outline-none focus:ring-1 focus:ring-black"
                                name="phone" type="number" placeholder="Your phone number"
                                value="{{ old('phone') }}" required>
                            @error('phone')
                                <span class="text-red-500 text-sm">{{ $message }}</span>
                            @enderror
                        </div>

                        <div class="mb-4">
                            <label class="block text-gray-700 text-sm font-bold mb-2" for="message">
                                Explain your requirement <span class="text-red-500 font-bold">*</span>
                            </label>
                            <textarea
                                class="border border-gray-300 rounded w-full py-2 px-3 text-gray-700 focus:outline-none focus:ring-1 focus:ring-black"
                                name="message" id="message" rows="4"
                                placeholder="Please share anything that will help prepare for our meeting...."
                                required>{{ old('message') }}</textarea>
                            @error('message')
                                <span class="text-red-500 text-sm">{{ $message }}</span>
                            @enderror
                        </div>

                        <div class="mt-6">
                            <button id="submit-btn"
                                class="w-full bg-green-600 hover:bg-green-700 text-white text-lg font-semibold py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-green-500">
                                >> SUBMIT
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </section>
</body>

</html>


otp-form.blade.php

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Verify OTP</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
    <style>
        .otp-input {
            letter-spacing: 10px;
            font-size: 1.5rem;
            text-align: center;
        }

        #resendBtn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
    </style>


</head>

<body class="bg-light">

    <section>
        <div class="container">
            <div class="row min-vh-100 align-items-center justify-content-center py-5">
                <div class="col-md-6 col-lg-5">
                    <div class="card shadow-sm">
                        <div class="card-body p-4 p-md-5">
                            <!-- Status Messages -->
                            @if ($errors->any())
                                <div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
                                    <strong>Error!</strong> Please fix the following:
                                    <ul class="mb-0 mt-2">
                                        @foreach ($errors->all() as $error)
                                            <li>{{ $error }}</li>
                                        @endforeach
                                    </ul>
                                    <button type="button" class="btn-close" data-bs-dismiss="alert"
                                        aria-label="Close"></button>
                                </div>
                            @endif

                            @if (session('error'))
                                <div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
                                    <strong>Error:</strong> {{ session('error') }}
                                    <button type="button" class="btn-close" data-bs-dismiss="alert"
                                        aria-label="Close"></button>
                                </div>
                            @endif

                            @if (session('success'))
                                <div class="alert alert-success alert-dismissible fade show mb-4" role="alert">
                                    <strong>Success:</strong> {{ session('success') }}
                                    <button type="button" class="btn-close" data-bs-dismiss="alert"
                                        aria-label="Close"></button>
                                </div>
                            @endif

                            <div class="text-center mb-4">
                                <h4 class="fw-bold">Verify OTP</h4>
                                <p class="text-muted">Enter the 4-digit code sent to
                                    {{ session('otp_phone') ?? 'your phone' }}</p>
                            </div>

                            <!-- OTP Verification Form -->
                            <form id="verifyOtpForm" method="POST" action="{{ route('verify.otp') }}"
                                autocomplete="off">
                                @csrf
                                <div class="mb-4">
                                    <label for="otp" class="form-label">OTP Code</label>
                                    <input type="text" name="otp" class="form-control form-control-lg otp-input"
                                        id="otp" placeholder="••••" required maxlength="4" pattern="\d{4}"
                                        inputmode="numeric" oninput="this.value = this.value.replace(/[^0-9]/g, '')">
                                </div>

                                <input type="hidden" name="phone" value="{{ session('otp_phone') }}">

                                <button id="verifyBtn" type="submit" class="btn btn-primary btn-lg w-100 py-2">
                                    Verify & Continue
                                </button>
                            </form>

                            <!-- Resend OTP Section -->
                            <div class="text-center mt-4 pt-3">
                                @if (session('resend_count', 0) < 3)
                                    <p class="mb-2">
                                        Didn't receive code?
                                        <span id="timer" class="text-muted">Resend in 30s</span>
                                    </p>
                                    <form id="resendOtpForm" method="POST" action="{{ route('resend.otp') }}">
                                        @csrf
                                        <input type="hidden" name="phone" value="{{ session('otp_phone') }}">
                                        <button id="resendBtn" type="button" class="btn btn-link p-0" disabled>
                                            Resend OTP
                                        </button>
                                    </form>
                                @else
                                    <p class="text-danger">
                                        You have exceeded the maximum OTP requests. Try again in 24 hours.
                                    </p>
                                @endif
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous">
    </script>

    <script>
        // Resend OTP Timer
        let timer = 30;
        const timerElement = document.getElementById('timer');
        const resendBtn = document.getElementById('resendBtn');

        if (timerElement && resendBtn) {
            const countdown = setInterval(() => {
                if (timer <= 0) {
                    clearInterval(countdown);
                    timerElement.textContent = '';
                    resendBtn.disabled = false;
                    resendBtn.addEventListener('click', function() {
                        document.getElementById('resendOtpForm').submit();
                    });
                } else {
                    timerElement.textContent = `Resend in ${timer--}s`;
                }
            }, 1000);
        }
    </script>

    <script>
        // Prevent double click on "Verify & Continue" button
        const form = document.getElementById('verifyOtpForm');
        const verifyBtn = document.getElementById('verifyBtn');

        if (form && verifyBtn) {
            form.addEventListener('submit', function(e) {
                verifyBtn.disabled = true;
                verifyBtn.innerText = 'Verifying...';
            });
        }
    </script>

   

</body>

</html>

thanks.blade.php

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Thanks</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
</head>

<body>
      <section class="bg-white">
        <div class="container">
            <div class="row d-flex align-items-center justify-content-center text-center">
                <div class="col-md-8">

                    <i style="font-size: 70px;" class="bi bi-check2-circle text-success"></i>

                    <h1>Thanks, you are all set!</h1>
                    <p>
                        We received your message, one of our team mate will contact you soon. Thank
                        You!
                    </p>
                    <a href="/" class="btn btn-success py-0 mt-3 fs-3">Back to Home</a>
                </div>
            </div>
        </div>
    </section>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous">
    </script>
</body>

</html>

Step 10: Set Up Routes

Add these routes in routes/web.php:

Route::get('/', [App\Http\Controllers\HomeController::class, 'index']);
Route::get('/contact/verify/otp', [App\Http\Controllers\HomeController::class, 'showOtpForm'])->name('otp.form');
Route::post('/contact/send-otp', [App\Http\Controllers\HomeController::class, 'sendOtp'])->name('send.otp'); 
Route::post('/contact/verify-otp', [App\Http\Controllers\HomeController::class, 'verifyOtp'])->name('verify.otp');
Route::post('/contact/resend-otp', [App\Http\Controllers\HomeController::class, 'resendOtp'])->name('resend.otp');
Route::get('thanks', [App\Http\Controllers\HomeController::class, 'thanks'])->name('thanks');

Key Security Features

  1. 24-hour submission limit - Prevents duplicate submissions

  2. OTP attempt limiting - Blocks after 3 failed attempts

  3. Session validation - Ensures OTP matches original request

Conclusion

This implementation provides robust spam protection while maintaining user experience. The OTP verification adds a critical layer of security to ensure only genuine users with valid phone numbers can contact you. Customize the templates and messaging to match your brand voice.

For production use, consider adding:

  • Rate limiting middleware

  • CAPTCHA fallback

  • SMS delivery failure handling

  • Analytics integration

Tags
Share :


+91 8447 525 204 Request Estimate