Below is a full, clean tutorial specifically for Laravel + MySQL + Text.lk, using the official Text.lk Laravel package.
OTP (One-Time Password) verification is a critical feature for modern web applications. It is widely used for:
- User registration
- Login without passwords
- Password reset
- Sensitive actions (payments, profile changes)
In this tutorial, you’ll learn how to build a secure OTP verification system using:
- Laravel
- MySQL
- Text.lk SMS Gateway (Laravel Package)
We’ll use the official package:
👉 https://laravelpackages.net/textlk/textlk-laravel
OTP Verification Flow
- User enters mobile number
- System generates a 6-digit OTP
- OTP is saved in database (hashed)
- OTP is sent via Text.lk SMS
- User submits OTP
- OTP is verified and marked as used
Step 1: Install Text.lk Laravel Package
Run the following command:
composer require textlk/textlk-laravelLaravel will auto-discover the package.
Step 2: Configure Environment Variables
Add your Text.lk credentials to .env:
TEXTLK_API_KEY=your_textlk_api_key
TEXTLK_SENDER_ID=TEXTLK
OTP_EXPIRY_MINUTES=5Step 3: Create OTP Database Table
Create migration:
php artisan make:migration create_otp_verifications_tableMigration file:
public function up()
{
Schema::create('otp_verifications', function (Blueprint $table) {
$table->id();
$table->string('mobile', 15);
$table->string('otp_hash');
$table->timestamp('expires_at');
$table->boolean('is_verified')->default(false);
$table->timestamps();
});
}Run migration:
php artisan migrateStep 4: Create OTP Model
php artisan make:model OtpVerificationclass OtpVerification extends Model
{
protected $fillable = [
'mobile',
'otp_hash',
'expires_at',
'is_verified'
];
protected $casts = [
'expires_at' => 'datetime'
];
}Step 5: Create OTP Controller
php artisan make:controller OtpControllerStep 6: Generate & Send OTP Using Text.lk
use App\Models\OtpVerification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use TextLK\SMS;
use Carbon\Carbon;
class OtpController extends Controller
{
public function sendOtp(Request $request)
{
$request->validate([
'mobile' => 'required|string'
]);
$otp = rand(100000, 999999);
OtpVerification::create([
'mobile' => $request->mobile,
'otp_hash' => Hash::make($otp),
'expires_at' => Carbon::now()->addMinutes(env('OTP_EXPIRY_MINUTES', 5))
]);
$sms = new SMS(env('TEXTLK_API_KEY'));
$sms->send([
"recipient" => $request->mobile,
"sender_id" => env('TEXTLK_SENDER_ID', 'TEXTLK'),
"type" => "plain",
"message" => "Your OTP code is {$otp}. Valid for 5 minutes."
]);
return response()->json([
'status' => 'success',
'message' => 'OTP sent successfully'
]);
}Step 7: Verify OTP
public function verifyOtp(Request $request)
{
$request->validate([
'mobile' => 'required|string',
'otp' => 'required|string'
]);
$otpRecord = OtpVerification::where('mobile', $request->mobile)
->where('is_verified', false)
->latest()
->first();
if (!$otpRecord) {
return response()->json([
'status' => 'error',
'message' => 'OTP not found'
]);
}
if ($otpRecord->expires_at->isPast()) {
return response()->json([
'status' => 'error',
'message' => 'OTP expired'
]);
}
if (!Hash::check($request->otp, $otpRecord->otp_hash)) {
return response()->json([
'status' => 'error',
'message' => 'Invalid OTP'
]);
}
$otpRecord->update(['is_verified' => true]);
return response()->json([
'status' => 'success',
'message' => 'OTP verified successfully'
]);
}
}Step 8: Define Routes
use App\Http\Controllers\OtpController;
Route::post('/otp/send', [OtpController::class, 'sendOtp']);
Route::post('/otp/verify', [OtpController::class, 'verifyOtp']);Step 9: Simple Frontend Example
Request OTP
<form method="POST" action="/otp/send">
@csrf
<input type="text" name="mobile" placeholder="07XXXXXXXX" required>
<button type="submit">Send OTP</button>
</form>Verify OTP
<form method="POST" action="/otp/verify">
@csrf
<input type="text" name="mobile" required>
<input type="text" name="otp" placeholder="Enter OTP" required>
<button type="submit">Verify OTP</button>
</form>Security Best Practices
✔ Always hash OTPs
✔ Short expiry time (3–5 minutes)
✔ Limit OTP requests per mobile number
✔ Prevent brute-force attempts
✔ Use HTTPS
✔ Delete old OTP records with cron
Optional: Auto Cleanup Old OTPs
OtpVerification::where('expires_at', '<', now())->delete();Run via scheduled task.
Common Use Cases
- Login without password
- User registration verification
- Password reset
- Payment confirmation
- Admin approval flows
➕ Extra OTP Features for Laravel + Text.lk
This section covers advanced OTP features commonly used in modern SaaS and API-based applications.
1. OTP-Only Login (Passwordless Authentication)
OTP-only login allows users to log in without a password, using only their mobile number and OTP.
Flow
- User enters mobile number
- OTP is sent
- OTP is verified
- User is logged in automatically
Step 1: Ensure User Table Has Mobile Column
$table->string('mobile')->unique();Step 2: OTP Login Controller Method
use App\Models\User;
use Illuminate\Support\Facades\Auth;
public function otpLogin(Request $request)
{
$request->validate([
'mobile' => 'required',
'otp' => 'required'
]);
$otpRecord = OtpVerification::where('mobile', $request->mobile)
->where('is_verified', false)
->latest()
->first();
if (!$otpRecord || $otpRecord->expires_at->isPast()) {
return response()->json(['message' => 'OTP invalid or expired'], 401);
}
if (!Hash::check($request->otp, $otpRecord->otp_hash)) {
return response()->json(['message' => 'Invalid OTP'], 401);
}
$user = User::firstOrCreate(
['mobile' => $request->mobile],
['name' => 'User '.$request->mobile]
);
$otpRecord->update(['is_verified' => true]);
Auth::login($user);
return response()->json([
'status' => 'success',
'message' => 'Logged in successfully'
]);
}Route
Route::post('/otp/login', [OtpController::class, 'otpLogin']);2. API OTP Login with Laravel Sanctum
This is essential for mobile apps, SPA, and SaaS APIs.
Step 1: Install Sanctum
composer require laravel/sanctum
php artisan sanctum:install
php artisan migrateAdd middleware in api group (Laravel 10+ usually auto-enabled).
Step 2: API OTP Login with Token
public function apiOtpLogin(Request $request)
{
$request->validate([
'mobile' => 'required',
'otp' => 'required'
]);
$otp = OtpVerification::where('mobile', $request->mobile)
->where('is_verified', false)
->latest()
->first();
if (!$otp || $otp->expires_at->isPast()) {
return response()->json(['message' => 'OTP expired'], 401);
}
if (!Hash::check($request->otp, $otp->otp_hash)) {
return response()->json(['message' => 'Invalid OTP'], 401);
}
$user = User::firstOrCreate(
['mobile' => $request->mobile],
['name' => 'User '.$request->mobile]
);
$otp->update(['is_verified' => true]);
$token = $user->createToken('otp-login')->plainTextToken;
return response()->json([
'token' => $token,
'token_type' => 'Bearer'
]);
}API Routes
Route::post('/api/otp/login', [OtpController::class, 'apiOtpLogin']);Protect API Routes
Route::middleware('auth:sanctum')->get('/profile', function (Request $request) {
return $request->user();
});3. Resend OTP Logic (With Cooldown)
Prevent abuse by allowing resend only after X seconds.
Add Column (Optional but Recommended)
$table->timestamp('last_sent_at')->nullable();Resend OTP Controller Method
public function resendOtp(Request $request)
{
$request->validate([
'mobile' => 'required'
]);
$lastOtp = OtpVerification::where('mobile', $request->mobile)
->latest()
->first();
if ($lastOtp && $lastOtp->created_at->diffInSeconds(now()) < 60) {
return response()->json([
'message' => 'Please wait before requesting another OTP'
], 429);
}
$otp = rand(100000, 999999);
OtpVerification::create([
'mobile' => $request->mobile,
'otp_hash' => Hash::make($otp),
'expires_at' => now()->addMinutes(5)
]);
$sms = new \TextLK\SMS(env('TEXTLK_API_KEY'));
$sms->send([
"recipient" => $request->mobile,
"sender_id" => env('TEXTLK_SENDER_ID'),
"type" => "plain",
"message" => "Your new OTP is {$otp}. Valid for 5 minutes."
]);
return response()->json([
'status' => 'success',
'message' => 'OTP resent successfully'
]);
}Route
Route::post('/otp/resend', [OtpController::class, 'resendOtp']);
Additional Security Enhancements (Recommended)
✔ Limit OTP attempts (max 3 tries)
✔ Block mobile temporarily after failures
✔ Use Laravel Rate Limiting
✔ Log OTP requests per IP
✔ Expire all previous OTPs on resend
Ideal Use Cases
- Mobile app authentication
- SaaS dashboards
- Fintech & payment confirmation
- Admin & staff verification
- RapidAPI OTP services
Conclusion
You now have a fully functional OTP verification system using Laravel, MySQL, and Text.lk SMS, powered by the official Text.lk Laravel package.
This setup is:
- Secure
- Scalable
- Ready for SaaS & enterprise apps
- Perfect for Sri Lanka–based platforms
With extra features, your OTP system is now:
- Passwordless
- Scalable