Laravel Phone Verification via NepalOTP
Learn the cleanest, most idiomatic way to implement OTP-based phone verification in your Laravel application using custom validation rules, service classes, and the native HTTP Client.
Laravel provides unparalleled scaffolding for email-based authentication, but verifying phone numbers requires orchestrating external API calls to a reliable SMS gateway. Wiring this logic directly into your controllers creates bloated, untestable code.
In this advanced guide, we will architect a robust phone verification flow. We will build a dedicated Service Class, utilize the powerful Illuminate\Support\Facades\Http facade, and create custom validation rules to ensure only valid Nepali phone numbers hit your database.
1. Configuration & Setup
First, secure your API credentials. Never hardcode API keys in your classes. Add your NepalOTP keys to your environment file:
NEPALOTP_API_KEY=notp_live_sk_your_secret_key_here
Next, map the environment variable in your config/services.php file. This allows Laravel to cache the configuration for production performance and makes it available globally via the config() helper.
return [
// ... mailgun, postmark, etc.
'nepalotp' => [
'key' => env('NEPALOTP_API_KEY'),
],
];
2. Creating the Service Class
To adhere to the Single Responsibility Principle (SRP), we will abstract all HTTP communication with NepalOTP into a dedicated service. This makes your application easily testable—you can mock this service during PHPUnit/Pest testing without actually firing network requests.
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
class NepalOtpService
{
protected string $baseUrl = 'https://api.nepalotp.com/v1';
/**
* Configure the base HTTP client with authentication and timeouts.
*/
protected function client(): PendingRequest
{
return Http::withToken(config('services.nepalotp.key'))
->acceptJson()
->timeout(5); // Prevent hanging requests if network fails
}
/**
* Dispatch an OTP to a Nepali phone number.
*
* @throws RequestException
*/
public function sendOtp(string $phone): array
{
return $this->client()
->post("{$this->baseUrl}/otp/send", [
'phone' => $phone
])
->throw()
->json();
}
/**
* Validate an OTP code against its corresponding ID.
*
* @throws RequestException
*/
public function verifyOtp(string $otpId, string $code): array
{
return $this->client()
->post("{$this->baseUrl}/otp/verify", [
'id' => $otpId,
'code' => $code
])
->throw()
->json();
}
}
3. Custom Validation Rule
Before we hit the API, we must ensure the phone number conforms to the E.164 standard required by telecom networks. Let's create a reusable Laravel Rule object.
php artisan make:rule NepaliPhone
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class NepaliPhone implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// Ensures the format strictly matches +977 followed by 10 digits
if (!preg_match('/^\+977\d{10}$/', $value)) {
$fail('The :attribute must be a valid Nepali phone number starting with +977.');
}
}
}
4. Orchestrating in the Controller
Now, we orchestrate the entire flow. We use Laravel's powerful Dependency Injection (IoC container) to automatically instantiate our NepalOtpService within the controller methods.
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Rules\NepaliPhone;
use App\Services\NepalOtpService;
use Illuminate\Http\Request;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Auth;
class PhoneVerificationController extends Controller
{
/**
* Handle the incoming request to dispatch an OTP.
*/
public function store(Request $request, NepalOtpService $otpService)
{
// 1. Validate Input
$validated = $request->validate([
'phone' => ['required', 'string', new NepaliPhone]
]);
try {
// 2. Dispatch OTP
$result = $otpService->sendOtp($validated['phone']);
// 3. Store the OTP ID in the session temporarily
session()->put('verification_otp_id', $result['data']['otp_id']);
session()->put('verification_phone', $validated['phone']);
return redirect()->route('verification.notice')
->with('status', 'Verification code sent!');
} catch (RequestException $e) {
// 4. Safely extract API error messages (e.g. Rate Limits)
$errorMessage = $e->response->json('message', 'Failed to send OTP. Please try again.');
return back()->withErrors(['phone' => $errorMessage])->withInput();
}
}
/**
* Verify the code submitted by the user.
*/
public function update(Request $request, NepalOtpService $otpService)
{
$request->validate([
'code' => ['required', 'numeric', 'digits:6']
]);
$otpId = session('verification_otp_id');
if (!$otpId) {
return redirect()->route('register')->withErrors('Session expired. Please restart registration.');
}
try {
$otpService->verifyOtp($otpId, $request->code);
// Success! The API returned 200 OK.
// Create user, clear session, and login.
/* Example:
$user = User::create(['phone' => session('verification_phone')]);
Auth::login($user);
*/
session()->forget(['verification_otp_id', 'verification_phone']);
return redirect()->route('dashboard');
} catch (RequestException $e) {
// Handle invalid codes, expirations, or max-attempt lockouts
$errorMessage = $e->response->json('error.message', 'Invalid verification code.');
return back()->withErrors(['code' => $errorMessage]);
}
}
}
Testing & Mocking
Because we abstracted the logic into a Service Class, testing our controller in Pest or PHPUnit is incredibly easy. We can utilize Laravel's powerful Http::fake() method to simulate API responses without hitting the actual NepalOTP servers.
use Illuminate\Support\Facades\Http;
test('it redirects to verification notice on successful otp dispatch', function () {
// Fake the NepalOTP endpoint to always return success
Http::fake([
'api.nepalotp.com/v1/otp/send' => Http::response([
'success' => true,
'data' => ['otp_id' => 'test_id_123']
], 200)
]);
$response = $this->post('/register/phone', [
'phone' => '+9779812345678'
]);
$response->assertRedirect(route('verification.notice'));
$response->assertSessionHas('verification_otp_id', 'test_id_123');
});
Start integrating today
Get your sandbox API keys instantly and integrate our endpoints into your Laravel application in minutes.
Create Developer Account