How to Build a WhatsApp OTP Login and Password Reset System in Laravel

Passwordless authentication is rapidly replacing SMS OTPs across India and Pakistan. Local telecom operators like Jazz, Zong, Airtel, and Jio have strict, often unpredictable spam filters. SMS delivery rates frequently drop, leaving users waiting minutes for a verification code that expires before it arrives. This delay kills conversion rates.
WhatsApp OTPs solve this issue. Delivery is almost instant, and you only pay for successful sessions. However, setting this up in Laravel is not as simple as swapping an email driver. Laravel's default authentication system is heavily built around email addresses. To use WhatsApp for login and password resets, you must restructure how Laravel handles user identification, sanitizes phone numbers, and manages verification states.
What You Need Before You Start
Do not write a single line of PHP code until you have secured the following assets. Skipping these steps or using temporary shortcuts will cause your system to break within 24 hours.
- A Meta Developer Account: You must register an app under the Business type on the Meta Developers portal.
- A Dedicated Phone Number: This number must not be active on a personal or business WhatsApp app on any phone. If you try to use an active number, Meta will instantly disconnect your mobile app. The number must be clean and assigned exclusively to the Cloud API.
- A Permanent System User Access Token: The temporary access token generated on the Meta dashboard expires after 24 hours. For a production Laravel app, you must go to your Meta Business Suite, navigate to Users, select System Users, generate a token with the whatsapp_business_messaging permission, and save it.
- A Verified Meta Business Account: While you can test with an unverified account, you are limited to sending messages to a few registered test numbers. To go live to the public in Pakistan or India, your business must complete Meta's verification process.
Step 1: Creating the WhatsApp Authentication Template
Meta does not allow you to send arbitrary text messages containing OTPs. You must use an approved template. If you try to send a custom message like "Hey, here is your code," Meta will block the API call immediately.
Go to your WhatsApp Manager inside the Meta Business Suite, navigate to Message Templates, and click Create Template. Select the Authentication category. Do not select Utility or Marketing. The Authentication category has strict rules but benefits from faster approval and specific UI buttons.
Configure the template to use a One-Time Password format. Meta will auto-populate the template body to something similar to: "Your verification code is {{1}}."
Add a button to the template. Choose the Copy Code button type. This adds a clean button to the WhatsApp message that copies the OTP to the user's clipboard with a single tap. This design drastically improves the login experience on mobile devices.
Step 2: Designing the Database and Cache Strategy
Storing active OTPs in your main database is a bad idea. It creates unnecessary database write operations and leaves expired codes sitting in your storage until you run a cleanup script. Instead, use Laravel's Cache facade backed by Redis or Memcached.
When a user requests an OTP, generate a secure six-digit random integer. Store this integer in the cache using a key built from the user's phone number, such as otp_923001234567. Set the cache expiration time to exactly 300 seconds. This automatically destroys the OTP after five minutes without requiring database maintenance.
Your users table must also accommodate phone numbers. If you are replacing email login entirely, change the unique constraint in your migration from the email column to the phone column. Ensure the phone column is a string, not an integer, to prevent database systems from stripping away country codes.
Step 3: Sanitizing South Asian Phone Numbers
Users in Pakistan and India format their phone numbers in dozens of different ways when typing them into a form. A user in Karachi might type 03001234567, +923001234567, or 923001234567. A user in Mumbai might enter 09876543210 or +91 9876543210.
Meta's Cloud API rejects any phone number that contains spaces, plus signs, hyphens, or leading zeros. It requires the exact international format: country code followed by the subscriber number.
You must sanitize this input in Laravel before storing it or passing it to the API. Use a helper method or a custom Laravel validation rule to clean the input. First, strip away all non-numeric characters using a regular expression. Next, check if the number starts with a leading zero. If it does, strip that zero and prepend the appropriate country code, such as 92 for Pakistan or 91 for India. If the number already starts with the country code, leave it as is.
Step 4: Building the Meta API Integration
Instead of installing bloated, third-party PHP packages that often fall out of maintenance, use Laravel's native HTTP client to communicate with Meta. It is clean, secure, and easy to debug.
Add your credentials to your .env file first:
WHATSAPP_API_URL=https://graph.facebook.com/v20.0
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_ACCESS_TOKEN=your_permanent_system_user_token
WHATSAPP_TEMPLATE_NAME=your_approved_auth_template_name
Create a service class in your app directory named WhatsAppService. Inside this class, write a method to execute the API call. The payload must target the JSON endpoint and contain specific keys.
| JSON Parameter | Type | Required Value |
|---|---|---|
| messaging_product | String | Must be "whatsapp" |
| to | String | Sanitized phone number with country code |
| type | String | Must be "template" |
| template.name | String | The template name from your .env file |
| template.language.code | String | The language code used (e.g., "en_US") |
| template.components.0.type | String | "body" |
| template.components.0.parameters.0.text | String | The actual 6-digit OTP code |
| template.components.1.sub_type | String | "url" (for the copy code button) |
Use Laravel's Http::withToken() method to attach the permanent access token as a Bearer token in the request header. Send a POST request containing the payload to the API URL concatenated with your Phone Number ID and the messages endpoint.
Step 5: Overriding Login and Password Reset Flows
Laravel's default authentication scaffolding relies on email addresses. To implement WhatsApp OTP, you must bypass these default controllers and build a custom flow.
The Custom Login Flow
Create a LoginController with two primary actions. The first action accepts the sanitized phone number, verifies that a user with this number exists in your database, generates the OTP, stores it in the Cache, and calls your WhatsAppService to send the message. It then redirects the user to an OTP verification view, passing the phone number along in the request.
The second action handles the verification submission. It retrieves the submitted OTP and compares it to the value stored in the Cache under the user's phone number. If the codes match, pull the user record from the database, call Laravel's Auth::login() method to establish the session, and clear the Cache key immediately to prevent replay attacks.
The Custom Password Reset Flow
Laravel's default password reset system sends an email containing a signed token. To use WhatsApp, build a password reset controller that mimics the login flow but adds an extra step. First, verify the phone number belongs to an active account. Send the WhatsApp OTP and redirect the user to a verification form.
Once the user enters the correct OTP, do not log them in. Instead, store a temporary token in their session, such as password_reset_verified_phone. Redirect them to a secure reset password form. This form should check for the