Code Examples

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()
    }
}