Code Examples
Ready-to-use integration examples for popular frameworks and platforms.
Before you start
All examples assume you have a NepalOTP account and API key. Visit the Getting Started guide to set up your account.
Replace your_api_key_here with your actual API key from the dashboard.
Laravel (PHP)
Installation
Composer
composer require guzzlehttp/guzzle
Configuration (.env)
.env
NEPALOTP_API_KEY=npot_live_sk_1a2b3c4d5e6f7g8h9i0j
NEPALOTP_API_URL=https://api.nepalotp.com
Service Class
app/Services/NepalOTPService.php
<?php
namespace App\Services;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
class NepalOTPService
{
protected $client;
protected $apiKey;
protected $baseUrl;
public function __construct()
{
$this->apiKey = config('services.nepalotp.api_key');
$this->baseUrl = config('services.nepalotp.api_url');
$this->client = new Client([
'base_uri' => $this->baseUrl,
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]);
}
public function sendOTP(string $phone, ?string $reference = null)
{
try {
$response = $this->client->post('/v1/otp/send', [
'json' => [
'phone' => $phone,
'reference' => $reference,
],
]);
return json_decode($response->getBody()->getContents(), true);
} catch (GuzzleException $e) {
return [
'success' => false,
'error' => [
'message' => $e->getMessage(),
],
];
}
}
public function verifyOTP(string $otpId, string $code)
{
try {
$response = $this->client->post('/v1/otp/verify', [
'json' => [
'otp_id' => $otpId,
'code' => $code,
],
]);
return json_decode($response->getBody()->getContents(), true);
} catch (GuzzleException $e) {
return [
'success' => false,
'error' => [
'message' => $e->getMessage(),
],
];
}
}
}
Controller Example
app/Http/Controllers/AuthController.php
<?php
namespace App\Http\Controllers;
use App\Services\NepalOTPService;
use Illuminate\Http\Request;
class AuthController extends Controller
{
protected $otpService;
public function __construct(NepalOTPService $otpService)
{
$this->otpService = $otpService;
}
public function sendOTP(Request $request)
{
$request->validate([
'phone' => 'required|regex:/^(97|98)[0-9]{8}$/',
]);
$result = $this->otpService->sendOTP(
$request->phone,
'user_login_' . time()
);
if ($result['success']) {
session(['otp_id' => $result['data']['otp_id']]);
return response()->json([
'success' => true,
'message' => 'OTP sent successfully',
'expires_at' => $result['data']['expires_at'],
]);
}
return response()->json([
'success' => false,
'message' => $result['error']['message'] ?? 'Failed to send OTP',
], 400);
}
public function verifyOTP(Request $request)
{
$request->validate([
'code' => 'required|digits:6',
]);
$otpId = session('otp_id');
if (!$otpId) {
return response()->json([
'success' => false,
'message' => 'OTP session expired',
], 400);
}
$result = $this->otpService->verifyOTP($otpId, $request->code);
if ($result['success'] && $result['data']['verified']) {
session()->forget('otp_id');
return response()->json([
'success' => true,
'message' => 'OTP verified successfully',
]);
}
return response()->json([
'success' => false,
'message' => $result['error']['message'] ?? 'Invalid OTP',
'attempts_remaining' => $result['error']['attempts_remaining'] ?? 0,
], 400);
}
}
Routes
routes/api.php
Route::post('/auth/send-otp', [AuthController::class, 'sendOTP']);
Route::post('/auth/verify-otp', [AuthController::class, 'verifyOTP']);
Node.js / Express
Installation
npm
npm install axios express express-session
NepalOTP Service
services/nepalOTP.js
const axios = require('axios');
class NepalOTPService {
constructor() {
this.apiKey = process.env.NEPALOTP_API_KEY;
this.baseURL = process.env.NEPALOTP_API_URL || 'https://api.nepalotp.com';
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
});
}
async sendOTP(phone, reference = null) {
try {
const response = await this.client.post('/v1/otp/send', {
phone,
reference,
});
return response.data;
} catch (error) {
return {
success: false,
error: {
message: error.response?.data?.error?.message || error.message,
},
};
}
}
async verifyOTP(otpId, code) {
try {
const response = await this.client.post('/v1/otp/verify', {
otp_id: otpId,
code,
});
return response.data;
} catch (error) {
return {
success: false,
error: {
message: error.response?.data?.error?.message || error.message,
attempts_remaining: error.response?.data?.error?.attempts_remaining,
},
};
}
}
}
module.exports = new NepalOTPService();
Routes & Controllers
routes/auth.js
const express = require('express');
const router = express.Router();
const nepalOTP = require('../services/nepalOTP');
// Send OTP
router.post('/send-otp', async (req, res) => {
const { phone } = req.body;
if (!phone || !/^(97|98)[0-9]{8}$/.test(phone)) {
return res.status(400).json({
success: false,
message: 'Invalid phone number format',
});
}
const result = await nepalOTP.sendOTP(
phone,
`user_login_${Date.now()}`
);
if (result.success) {
req.session.otpId = result.data.otp_id;
return res.json({
success: true,
message: 'OTP sent successfully',
expires_at: result.data.expires_at,
});
}
return res.status(400).json({
success: false,
message: result.error.message || 'Failed to send OTP',
});
});
// Verify OTP
router.post('/verify-otp', async (req, res) => {
const { code } = req.body;
const otpId = req.session.otpId;
if (!otpId) {
return res.status(400).json({
success: false,
message: 'OTP session expired',
});
}
if (!code || !/^[0-9]{6}$/.test(code)) {
return res.status(400).json({
success: false,
message: 'Invalid OTP code format',
});
}
const result = await nepalOTP.verifyOTP(otpId, code);
if (result.success && result.data.verified) {
delete req.session.otpId;
return res.json({
success: true,
message: 'OTP verified successfully',
});
}
return res.status(400).json({
success: false,
message: result.error.message || 'Invalid OTP',
attempts_remaining: result.error.attempts_remaining || 0,
});
});
module.exports = router;
Python / Flask
Installation
pip
pip install flask requests python-dotenv
NepalOTP Service
services/nepal_otp.py
import os
import requests
from typing import Dict, Optional
class NepalOTPService:
def __init__(self):
self.api_key = os.getenv('NEPALOTP_API_KEY')
self.base_url = os.getenv('NEPALOTP_API_URL', 'https://api.nepalotp.com')
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
}
def send_otp(self, phone: str, reference: Optional[str] = None) -> Dict:
try:
response = requests.post(
f'{self.base_url}/v1/otp/send',
headers=self.headers,
json={
'phone': phone,
'reference': reference,
}
)
return response.json()
except requests.exceptions.RequestException as e:
return {
'success': False,
'error': {
'message': str(e),
}
}
def verify_otp(self, otp_id: str, code: str) -> Dict:
try:
response = requests.post(
f'{self.base_url}/v1/otp/verify',
headers=self.headers,
json={
'otp_id': otp_id,
'code': code,
}
)
return response.json()
except requests.exceptions.RequestException as e:
return {
'success': False,
'error': {
'message': str(e),
}
}
Flask Routes
app.py
from flask import Flask, request, jsonify, session
from services.nepal_otp import NepalOTPService
import re
import time
app = Flask(__name__)
app.secret_key = 'your-secret-key-here'
otp_service = NepalOTPService()
@app.route('/api/auth/send-otp', methods=['POST'])
def send_otp():
data = request.get_json()
phone = data.get('phone')
if not phone or not re.match(r'^(97|98)[0-9]{8}', phone):
return jsonify({
'success': False,
'message': 'Invalid phone number format'
}), 400
result = otp_service.send_otp(phone, f'user_login_{int(time.time())}')
if result.get('success'):
session['otp_id'] = result['data']['otp_id']
return jsonify({
'success': True,
'message': 'OTP sent successfully',
'expires_at': result['data']['expires_at']
})
return jsonify({
'success': False,
'message': result.get('error', {}).get('message', 'Failed to send OTP')
}), 400
@app.route('/api/auth/verify-otp', methods=['POST'])
def verify_otp():
data = request.get_json()
code = data.get('code')
otp_id = session.get('otp_id')
if not otp_id:
return jsonify({
'success': False,
'message': 'OTP session expired'
}), 400
if not code or not re.match(r'^[0-9]{6}', code):
return jsonify({
'success': False,
'message': 'Invalid OTP code format'
}), 400
result = otp_service.verify_otp(otp_id, code)
if result.get('success') and result.get('data', {}).get('verified'):
session.pop('otp_id', None)
return jsonify({
'success': True,
'message': 'OTP verified successfully'
})
return jsonify({
'success': False,
'message': result.get('error', {}).get('message', 'Invalid OTP'),
'attempts_remaining': result.get('error', {}).get('attempts_remaining', 0)
}), 400
if __name__ == '__main__':
app.run(debug=True)
Django
NepalOTP Service
services/nepal_otp.py
import requests
from django.conf import settings
from typing import Dict, Optional
class NepalOTPService:
def __init__(self):
self.api_key = settings.NEPALOTP_API_KEY
self.base_url = settings.NEPALOTP_API_URL
self.headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
}
def send_otp(self, phone: str, reference: Optional[str] = None) -> Dict:
try:
response = requests.post(
f'{self.base_url}/v1/otp/send',
headers=self.headers,
json={'phone': phone, 'reference': reference}
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {
'success': False,
'error': {'message': str(e)}
}
def verify_otp(self, otp_id: str, code: str) -> Dict:
try:
response = requests.post(
f'{self.base_url}/v1/otp/verify',
headers=self.headers,
json={'otp_id': otp_id, 'code': code}
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
return {
'success': False,
'error': {'message': str(e)}
}
Views
views.py
from django.http import JsonResponse
from django.views import View
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from .services.nepal_otp import NepalOTPService
import json
import re
import time
otp_service = NepalOTPService()
@method_decorator(csrf_exempt, name='dispatch')
class SendOTPView(View):
def post(self, request):
try:
data = json.loads(request.body)
phone = data.get('phone')
if not phone or not re.match(r'^(97|98)[0-9]{8}', phone):
return JsonResponse({
'success': False,
'message': 'Invalid phone number format'
}, status=400)
result = otp_service.send_otp(
phone,
f'user_login_{int(time.time())}'
)
if result.get('success'):
request.session['otp_id'] = result['data']['otp_id']
return JsonResponse({
'success': True,
'message': 'OTP sent successfully',
'expires_at': result['data']['expires_at']
})
return JsonResponse({
'success': False,
'message': result.get('error', {}).get('message', 'Failed to send OTP')
}, status=400)
except Exception as e:
return JsonResponse({
'success': False,
'message': str(e)
}, status=500)
@method_decorator(csrf_exempt, name='dispatch')
class VerifyOTPView(View):
def post(self, request):
try:
data = json.loads(request.body)
code = data.get('code')
otp_id = request.session.get('otp_id')
if not otp_id:
return JsonResponse({
'success': False,
'message': 'OTP session expired'
}, status=400)
if not code or not re.match(r'^[0-9]{6}', code):
return JsonResponse({
'success': False,
'message': 'Invalid OTP code format'
}, status=400)
result = otp_service.verify_otp(otp_id, code)
if result.get('success') and result.get('data', {}).get('verified'):
del request.session['otp_id']
return JsonResponse({
'success': True,
'message': 'OTP verified successfully'
})
return JsonResponse({
'success': False,
'message': result.get('error', {}).get('message', 'Invalid OTP'),
'attempts_remaining': result.get('error', {}).get('attempts_remaining', 0)
}, status=400)
except Exception as e:
return JsonResponse({
'success': False,
'message': str(e)
}, status=500)
Next.js
API Route - Send OTP
app/api/auth/send-otp/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { phone } = await request.json();
if (!phone || !/^(97|98)[0-9]{8}$/.test(phone)) {
return NextResponse.json(
{ success: false, message: 'Invalid phone number format' },
{ status: 400 }
);
}
const response = await fetch(`${process.env.NEPALOTP_API_URL}/v1/otp/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NEPALOTP_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone,
reference: `user_login_${Date.now()}`,
}),
});
const data = await response.json();
if (data.success) {
return NextResponse.json({
success: true,
message: 'OTP sent successfully',
otp_id: data.data.otp_id,
expires_at: data.data.expires_at,
});
}
return NextResponse.json(
{ success: false, message: data.error?.message || 'Failed to send OTP' },
{ status: 400 }
);
} catch (error) {
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}
API Route - Verify OTP
app/api/auth/verify-otp/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const { otp_id, code } = await request.json();
if (!otp_id || !code || !/^[0-9]{6}$/.test(code)) {
return NextResponse.json(
{ success: false, message: 'Invalid OTP data' },
{ status: 400 }
);
}
const response = await fetch(`${process.env.NEPALOTP_API_URL}/v1/otp/verify`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NEPALOTP_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ otp_id, code }),
});
const data = await response.json();
if (data.success && data.data.verified) {
return NextResponse.json({
success: true,
message: 'OTP verified successfully',
phone: data.data.phone,
});
}
return NextResponse.json(
{
success: false,
message: data.error?.message || 'Invalid OTP',
attempts_remaining: data.error?.attempts_remaining || 0,
},
{ status: 400 }
);
} catch (error) {
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}
Client Component
components/OTPForm.tsx
'use client';
import { useState } from 'react';
export default function OTPForm() {
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [otpId, setOtpId] = useState('');
const [step, setStep] = useState<'phone' | 'verify'>('phone');
const [loading, setLoading] = useState(false);
const sendOTP = async () => {
setLoading(true);
try {
const res = await fetch('/api/auth/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone }),
});
const data = await res.json();
if (data.success) {
setOtpId(data.otp_id);
setStep('verify');
alert('OTP sent successfully!');
} else {
alert(data.message);
}
} catch (error) {
alert('Failed to send OTP');
} finally {
setLoading(false);
}
};
const verifyOTP = async () => {
setLoading(true);
try {
const res = await fetch('/api/auth/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ otp_id: otpId, code }),
});
const data = await res.json();
if (data.success) {
alert('OTP verified successfully!');
// Redirect or set auth state
} else {
alert(data.message);
}
} catch (error) {
alert('Failed to verify OTP');
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto p-6">
{step === 'phone' ? (
<div>
<input
type="tel"
placeholder="98XXXXXXXX"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="w-full px-4 py-2 border rounded mb-4"
/>
<button
onClick={sendOTP}
disabled={loading}
className="w-full bg-blue-500 text-white py-2 rounded"
>
{loading ? 'Sending...' : 'Send OTP'}
</button>
</div>
) : (
<div>
<input
type="text"
placeholder="Enter 6-digit OTP"
value={code}
onChange={(e) => setCode(e.target.value)}
maxLength={6}
className="w-full px-4 py-2 border rounded mb-4"
/>
<button
onClick={verifyOTP}
disabled={loading}
className="w-full bg-green-500 text-white py-2 rounded"
>
{loading ? 'Verifying...' : 'Verify OTP'}
</button>
</div>
)}
</div>
);
}
React
OTP Component with Hooks
components/OTPAuth.jsx
import React, { useState } from 'react';
import axios from 'axios';
const OTPAuth = () => {
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [otpId, setOtpId] = useState('');
const [step, setStep] = useState('phone');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const sendOTP = async (e) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
const response = await axios.post('/api/auth/send-otp', { phone });
if (response.data.success) {
setOtpId(response.data.otp_id);
setStep('verify');
setMessage('OTP sent successfully!');
} else {
setMessage(response.data.message);
}
} catch (error) {
setMessage(error.response?.data?.message || 'Failed to send OTP');
} finally {
setLoading(false);
}
};
const verifyOTP = async (e) => {
e.preventDefault();
setLoading(true);
setMessage('');
try {
const response = await axios.post('/api/auth/verify-otp', {
otp_id: otpId,
code
});
if (response.data.success) {
setMessage('OTP verified successfully!');
// Handle successful authentication
} else {
setMessage(response.data.message);
}
} catch (error) {
setMessage(error.response?.data?.message || 'Failed to verify OTP');
} finally {
setLoading(false);
}
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6">Phone Authentication</h2>
{message && (
<div className={`p-3 mb-4 rounded ${
message.includes('success') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{message}
</div>
)}
{step === 'phone' ? (
<form onSubmit={sendOTP}>
<label className="block mb-2 font-medium">Phone Number</label>
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="98XXXXXXXX"
className="w-full px-4 py-2 border rounded mb-4"
required
/>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Sending...' : 'Send OTP'}
</button>
</form>
) : (
<form onSubmit={verifyOTP}>
<label className="block mb-2 font-medium">Enter OTP</label>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="123456"
maxLength={6}
className="w-full px-4 py-2 border rounded mb-4"
required
/>
<button
type="submit"
disabled={loading}
className="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600 disabled:opacity-50"
>
{loading ? 'Verifying...' : 'Verify OTP'}
</button>
<button
type="button"
onClick={() => setStep('phone')}
className="w-full mt-2 text-gray-600 hover:text-gray-800"
>
Change phone number
</button>
</form>
)}
</div>
);
};
export default OTPAuth;
Flutter
Installation
pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.1.0
NepalOTP Service
lib/services/nepal_otp_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
class NepalOTPService {
final String apiKey = 'npot_live_sk_1a2b3c4d5e6f7g8h9i0j';
final String baseUrl = 'https://api.nepalotp.com';
Future<Map<String, dynamic>> sendOTP(String phone, {String? reference}) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/v1/otp/send'),
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
body: jsonEncode({
'phone': phone,
'reference': reference ?? 'app_login_${DateTime.now().millisecondsSinceEpoch}',
}),
);
return jsonDecode(response.body);
} catch (e) {
return {
'success': false,
'error': {'message': e.toString()},
};
}
}
Future<Map<String, dynamic>> verifyOTP(String otpId, String code) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/v1/otp/verify'),
headers: {
'Authorization': 'Bearer $apiKey',
'Content-Type': 'application/json',
},
body: jsonEncode({
'otp_id': otpId,
'code': code,
}),
);
return jsonDecode(response.body);
} catch (e) {
return {
'success': false,
'error': {'message': e.toString()},
};
}
}
}
OTP Screen Widget
lib/screens/otp_screen.dart
import 'package:flutter/material.dart';
import '../services/nepal_otp_service.dart';
class OTPScreen extends StatefulWidget {
@override
_OTPScreenState createState() => _OTPScreenState();
}
class _OTPScreenState extends State<OTPScreen> {
final _phoneController = TextEditingController();
final _codeController = TextEditingController();
final _otpService = NepalOTPService();
String? _otpId;
bool _isLoading = false;
String _message = '';
bool _showVerifyStep = false;
Future<void> _sendOTP() async {
setState(() {
_isLoading = true;
_message = '';
});
final result = await _otpService.sendOTP(_phoneController.text);
setState(() {
_isLoading = false;
if (result['success'] == true) {
_otpId = result['data']['otp_id'];
_showVerifyStep = true;
_message = 'OTP sent successfully!';
} else {
_message = result['error']?['message'] ?? 'Failed to send OTP';
}
});
}
Future<void> _verifyOTP() async {
setState(() {
_isLoading = true;
_message = '';
});
final result = await _otpService.verifyOTP(_otpId!, _codeController.text);
setState(() {
_isLoading = false;
if (result['success'] == true && result['data']['verified'] == true) {
_message = 'OTP verified successfully!';
// Navigate to next screen or handle success
} else {
_message = result['error']?['message'] ?? 'Invalid OTP';
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Phone Verification')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_message.isNotEmpty)
Container(
padding: EdgeInsets.all(12),
margin: EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: _message.contains('success')
? Colors.green.shade100
: Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(_message),
),
if (!_showVerifyStep) ...[
TextField(
controller: _phoneController,
decoration: InputDecoration(
labelText: 'Phone Number',
hintText: '98XXXXXXXX',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.phone,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _sendOTP,
child: Text(_isLoading ? 'Sending...' : 'Send OTP'),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 48),
),
),
] else ...[
TextField(
controller: _codeController,
decoration: InputDecoration(
labelText: 'Enter OTP',
hintText: '123456',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
maxLength: 6,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _verifyOTP,
child: Text(_isLoading ? 'Verifying...' : 'Verify OTP'),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 48),
),
),
TextButton(
onPressed: () {
setState(() {
_showVerifyStep = false;
_message = '';
});
},
child: Text('Change phone number'),
),
],
],
),
),
);
}
}
React Native
Installation
npm
npm install axios @react-native-async-storage/async-storage
NepalOTP Service
services/nepalOTP.js
import axios from 'axios';
const API_KEY = 'npot_live_sk_1a2b3c4d5e6f7g8h9i0j';
const BASE_URL = 'https://api.nepalotp.com';
const apiClient = axios.create({
baseURL: BASE_URL,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
export const sendOTP = async (phone, reference = null) => {
try {
const response = await apiClient.post('/v1/otp/send', {
phone,
reference: reference || `app_login_${Date.now()}`,
});
return response.data;
} catch (error) {
return {
success: false,
error: {
message: error.response?.data?.error?.message || error.message,
},
};
}
};
export const verifyOTP = async (otpId, code) => {
try {
const response = await apiClient.post('/v1/otp/verify', {
otp_id: otpId,
code,
});
return response.data;
} catch (error) {
return {
success: false,
error: {
message: error.response?.data?.error?.message || error.message,
attempts_remaining: error.response?.data?.error?.attempts_remaining,
},
};
}
};
OTP Screen Component
screens/OTPScreen.js
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from 'react-native';
import { sendOTP, verifyOTP } from '../services/nepalOTP';
const OTPScreen = ({ navigation }) => {
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [otpId, setOtpId] = useState('');
const [step, setStep] = useState('phone');
const [loading, setLoading] = useState(false);
const handleSendOTP = async () => {
if (!phone || !/^(97|98)[0-9]{8}$/.test(phone)) {
Alert.alert('Error', 'Please enter a valid Nepali phone number');
return;
}
setLoading(true);
const result = await sendOTP(phone);
setLoading(false);
if (result.success) {
setOtpId(result.data.otp_id);
setStep('verify');
Alert.alert('Success', 'OTP sent successfully!');
} else {
Alert.alert('Error', result.error.message);
}
};
const handleVerifyOTP = async () => {
if (!code || !/^[0-9]{6}$/.test(code)) {
Alert.alert('Error', 'Please enter a valid 6-digit OTP');
return;
}
setLoading(true);
const result = await verifyOTP(otpId, code);
setLoading(false);
if (result.success && result.data.verified) {
Alert.alert('Success', 'Phone verified successfully!');
// Navigate to home screen or save auth state
// navigation.navigate('Home');
} else {
Alert.alert(
'Error',
`${result.error.message}\nAttempts remaining: ${result.error.attempts_remaining || 0}`
);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Phone Verification</Text>
{step === 'phone' ? (
<View style={styles.form}>
<Text style={styles.label}>Phone Number</Text>
<TextInput
style={styles.input}
value={phone}
onChangeText={setPhone}
placeholder="98XXXXXXXX"
keyboardType="phone-pad"
maxLength={10}
/>
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleSendOTP}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Send OTP</Text>
)}
</TouchableOpacity>
</View>
) : (
<View style={styles.form}>
<Text style={styles.label}>Enter OTP</Text>
<TextInput
style={styles.input}
value={code}
onChangeText={setCode}
placeholder="123456"
keyboardType="number-pad"
maxLength={6}
/>
<TouchableOpacity
style={[styles.button, styles.buttonVerify, loading && styles.buttonDisabled]}
onPress={handleVerifyOTP}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>Verify OTP</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.linkButton}
onPress={() => setStep('phone')}
>
<Text style={styles.linkText}>Change phone number</Text>
</TouchableOpacity>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#fff',
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 30,
textAlign: 'center',
},
form: {
width: '100%',
},
label: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
marginBottom: 16,
},
button: {
backgroundColor: '#3b82f6',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonVerify: {
backgroundColor: '#10b981',
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
linkButton: {
marginTop: 12,
alignItems: 'center',
},
linkText: {
color: '#6b7280',
fontSize: 14,
},
});
export default OTPScreen;
Android (Kotlin)
Gradle Dependencies
build.gradle (app)
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0'
}
API Service
NepalOTPService.kt
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.POST
data class SendOTPRequest(val phone: String, val reference: String?)
data class VerifyOTPRequest(val otp_id: String, val code: String)
data class OTPResponse(
val success: Boolean,
val data: OTPData?,
val error: ErrorData?
)
data class OTPData(
val otp_id: String?,
val phone: String?,
val expires_at: String?,
val attempts_remaining: Int?,
val verified: Boolean?
)
data class ErrorData(
val code: String?,
val message: String?,
val attempts_remaining: Int?
)
interface NepalOTPApi {
@POST("v1/otp/send")
suspend fun sendOTP(@Body request: SendOTPRequest): OTPResponse
@POST("v1/otp/verify")
suspend fun verifyOTP(@Body request: VerifyOTPRequest): OTPResponse
}
object NepalOTPService {
private const val BASE_URL = "https://api.nepalotp.com/"
private const val API_KEY = "npot_live_sk_1a2b3c4d5e6f7g8h9i0j"
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $API_KEY")
.addHeader("Content-Type", "application/json")
.build()
chain.proceed(request)
}
.addInterceptor(loggingInterceptor)
.build()
val api: NepalOTPApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NepalOTPApi::class.java)
}
Activity Example
OTPActivity.kt
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class OTPActivity : AppCompatActivity() {
private var otpId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_otp)
findViewById<Button>(R.id.btnSendOTP).setOnClickListener {
sendOTP()
}
findViewById<Button>(R.id.btnVerifyOTP).setOnClickListener {
verifyOTP()
}
}
private fun sendOTP() {
val phone = findViewById<EditText>(R.id.etPhone).text.toString()
if (!phone.matches(Regex("^(97|98)[0-9]{8}$"))) {
Toast.makeText(this, "Invalid phone number", Toast.LENGTH_SHORT).show()
return
}
lifecycleScope.launch {
try {
val response = NepalOTPService.api.sendOTP(
SendOTPRequest(phone, "app_login_${System.currentTimeMillis()}")
)
if (response.success) {
otpId = response.data?.otp_id
Toast.makeText(this@OTPActivity, "OTP sent successfully!", Toast.LENGTH_SHORT).show()
// Show OTP input field
} else {
Toast.makeText(
this@OTPActivity,
response.error?.message ?: "Failed to send OTP",
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Toast.makeText(this@OTPActivity, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
private fun verifyOTP() {
val code = findViewById<EditText>(R.id.etCode).text.toString()
if (otpId == null) {
Toast.makeText(this, "Please send OTP first", Toast.LENGTH_SHORT).show()
return
}
if (!code.matches(Regex("^[0-9]{6}$"))) {
Toast.makeText(this, "Invalid OTP code", Toast.LENGTH_SHORT).show()
return
}
lifecycleScope.launch {
try {
val response = NepalOTPService.api.verifyOTP(
VerifyOTPRequest(otpId!!, code)
)
if (response.success && response.data?.verified == true) {
Toast.makeText(this@OTPActivity, "OTP verified successfully!", Toast.LENGTH_SHORT).show()
// Navigate to next screen
} else {
Toast.makeText(
this@OTPActivity,
"${response.error?.message}\nAttempts: ${response.error?.attempts_remaining}",
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Toast.makeText(this@OTPActivity, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
iOS (Swift)
NepalOTP Service
NepalOTPService.swift
import Foundation
struct OTPResponse: Codable {
let success: Bool
let data: OTPData?
let error: ErrorData?
}
struct OTPData: Codable {
let otp_id: String?
let phone: String?
let expires_at: String?
let attempts_remaining: Int?
let verified: Bool?
}
struct ErrorData: Codable {
let code: String?
let message: String?
let attempts_remaining: Int?
}
class NepalOTPService {
static let shared = NepalOTPService()
private let apiKey = "npot_live_sk_1a2b3c4d5e6f7g8h9i0j"
private let baseURL = "https://api.nepalotp.com"
func sendOTP(phone: String, reference: String? = nil, completion: @escaping (Result<OTPResponse, Error>) -> Void) {
guard let url = URL(string: "\(baseURL)/v1/otp/send") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"phone": phone,
"reference": reference ?? "app_login_\(Int(Date().timeIntervalSince1970))"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else { return }
do {
let otpResponse = try JSONDecoder().decode(OTPResponse.self, from: data)
completion(.success(otpResponse))
} catch {
completion(.failure(error))
}
}.resume()
}
func verifyOTP(otpId: String, code: String, completion: @escaping (Result<OTPResponse, Error>) -> Void) {
guard let url = URL(string: "\(baseURL)/v1/otp/verify") else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"otp_id": otpId,
"code": code
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else { return }
do {
let otpResponse = try JSONDecoder().decode(OTPResponse.self, from: data)
completion(.success(otpResponse))
} catch {
completion(.failure(error))
}
}.resume()
}
}
SwiftUI View
OTPView.swift
import SwiftUI
struct OTPView: View {
@State private var phone = ""
@State private var code = ""
@State private var otpId: String?
@State private var showVerifyStep = false
@State private var isLoading = false
@State private var message = ""
@State private var showAlert = false
var body: some View {
VStack(spacing: 20) {
Text("Phone Verification")
.font(.title)
.fontWeight(.bold)
if !showVerifyStep {
VStack(alignment: .leading, spacing: 8) {
Text("Phone Number")
.font(.subheadline)
TextField("98XXXXXXXX", text: $phone)
.keyboardType(.phonePad)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: sendOTP) {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Send OTP")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.disabled(isLoading)
} else {
VStack(alignment: .leading, spacing: 8) {
Text("Enter OTP")
.font(.subheadline)
TextField("123456", text: $code)
.keyboardType(.numberPad)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: verifyOTP) {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Text("Verify OTP")
.fontWeight(.semibold)
}
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
.disabled(isLoading)
Button("Change phone number") {
showVerifyStep = false
message = ""
}
.foregroundColor(.gray)
}
}
.padding()
.alert(isPresented: $showAlert) {
Alert(title: Text("Message"), message: Text(message), dismissButton: .default(Text("OK")))
}
}
func sendOTP() {
guard phone.range(of: "^(97|98)[0-9]{8}$", options: .regularExpression) != nil else {
message = "Invalid phone number format"
showAlert = true
return
}
isLoading = true
NepalOTPService.shared.sendOTP(phone: phone) { result in
DispatchQueue.main.async {
isLoading = false
switch result {
case .success(let response):
if response.success {
otpId = response.data?.otp_id
showVerifyStep = true
message = "OTP sent successfully!"
showAlert = true
} else {
message = response.error?.message ?? "Failed to send OTP"
showAlert = true
}
case .failure(let error):
message = error.localizedDescription
showAlert = true
}
}
}
}
func verifyOTP() {
guard let otpId = otpId else { return }
guard code.range(of: "^[0-9]{6}$", options: .regularExpression) != nil else {
message = "Invalid OTP code"
showAlert = true
return
}
isLoading = true
NepalOTPService.shared.verifyOTP(otpId: otpId, code: code) { result in
DispatchQueue.main.async {
isLoading = false
switch result {
case .success(let response):
if response.success && response.data?.verified == true {
message = "OTP verified successfully!"
showAlert = true
// Navigate to next screen
} else {
message = response.error?.message ?? "Invalid OTP"
showAlert = true
}
case .failure(let error):
message = error.localizedDescription
showAlert = true
}
}
}
}
}
struct OTPView_Previews: PreviewProvider {
static var previews: some View {
OTPView()
}
}