NepalOTP Documentation
OTP-first verification infrastructure for Nepal.
What is NepalOTP?
NepalOTP is a purpose-built OTP verification platform designed specifically for the Nepali market. It handles the complete lifecycle of one-time passwords: generation, delivery via SMS, and verification. The platform treats OTP as critical infrastructure rather than a messaging feature.
Why OTP is treated as infrastructure
OTP verification is a security primitive. It requires predictable delivery, strict expiration enforcement, and abuse protection. NepalOTP separates OTP from general-purpose SMS gateways because verification demands different reliability guarantees than transactional messaging.
What NepalOTP provides
- Server-side OTP generation with cryptographic randomness
- OTP delivery via SMS to Nepali phone numbers
- Server-side OTP verification with attempt limits
- Sandbox mode for development and testing
- Rate limiting and abuse protection
- Approved transactional SMS templates for non-OTP use cases
- Bulk / promotional SMS to many recipients at once, screened by AI content moderation
What NepalOTP does not support
- Email delivery
- Content that fails automated AI moderation (links/URLs, betting, fraud or phishing)
Getting Started
1. Create an account
Register at nepalotp.com/login,
verify your email, and complete your profile. Enable two-factor authentication from
Settings → Two-Factor for additional security.
2. Create your first app
Go to Dashboard → Apps & API Keys and create an
application. Choose the environment (sandbox or production), configure rate limits, and
optionally add a webhook URL. Each app has a public app_id used in logs and
webhook payloads.
3. Generate an API key
Open your app and create an API key. Keys are environment-specific and must match the app environment.
API Key Format
npot_test_1a2b3c4d5e6f7g8h9i0j
4. Send your first sandbox OTP
Use the API key to send an OTP. Sandbox mode always returns OTP 123456 and does not deduct
credits.
5. Go live
Switch your app environment to production, top up credits, and generate a live API key
(prefixed with npot_live_). Use the live
key in production traffic.
Authentication
All API requests require authentication via an API key, passed either as an X-API-Key header or as
a Bearer token in
the Authorization
header. Either works identically — pick whichever your HTTP client makes easier.
X-API-Key: npot_live_1a2b3c4d5e6f7g8h9i0j
Authorization: Bearer npot_live_1a2b3c4d5e6f7g8h9i0j
Key rotation
You can have up to 3 active API keys at any time. When rotating keys:
- Generate a new key in the dashboard
- Deploy your application with the new key
- Revoke the old key once migration is complete
Security best practices
- Never expose API keys in client-side code
- Store keys in environment variables or secure vaults
- Use different keys for staging and production
- Rotate keys periodically and after team member departures
Core Concepts
OTP Lifecycle
Generate
Server creates OTP
Send
SMS delivered
Verify
User submits code
Expire
OTP invalidated
Server-side generation and verification
OTPs are generated and verified exclusively on NepalOTP servers. Your application never stores OTPs. When a user submits a code, your backend sends it to NepalOTP for verification.
Expiry and attempt limits
| Parameter | Default | Description |
|---|---|---|
expiry |
5 minutes | Time until OTP becomes invalid |
max_attempts |
3 | Failed attempts before OTP is invalidated |
length |
6 digits | OTP length (fixed) |
Why clients should never store OTPs
Storing OTPs client-side creates security vulnerabilities. Client storage can be inspected, logged, or intercepted. Server-side verification ensures the OTP is validated against a secure source of truth.
Sending an OTP
/v1/otp/send
Generates a new OTP and sends it via SMS to the specified phone number. The OTP message uses a fixed template format.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
phone |
string | Yes | E.164 phone number, e.g. +9779812345678
(an optional leading + followed by
2–15 digits) |
reference |
string | No | Your internal reference ID for tracking (max 255 characters) |
OTP message format
Your OTP is: 482910. Valid for 5 minutes. Do not share this code.
SMS-only delivery, using this fixed system message — it is not a customizable template. The code
and expiry minutes shown here are dynamic, based on your app's configured OTP length (6 digits by
default) and expiry time (otp_expiry_seconds,
default 300 seconds / 5 minutes).
Example request
curl -X POST https://nepalotp.com/api/v1/otp/send \
-H "Authorization: Bearer npot_live_1a2b3c4d5e6f7g8h9i0j" \
-H "Content-Type: application/json" \
-d '{
"phone": "+9779841234567",
"reference": "user_signup_abc123"
}'
Success response
{
"success": true,
"message": "OTP sent successfully",
"otp_id": "otp_8f9a7b6c5d4e3f2a",
"data": {
"expires_at": "2026-01-15T10:35:00+05:45",
"expires_in_seconds": 300
}
}
Common errors
VALIDATION_ERROR
The phone
field is missing, or isn't a valid E.164 number.
INSUFFICIENT_BALANCE
Your wallet balance can't cover this OTP's cost (production apps only — sandbox is always free).
RATE_LIMIT_EXCEEDED
Too many OTP requests for this phone number, or the app's
cooldown period hasn't elapsed yet. Check data.retry_after.
Verifying an OTP
/v1/otp/verify
Verifies an OTP submitted by the user. The OTP is invalidated after successful verification or when the maximum attempt limit is reached.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
otp_id |
string | Yes | The OTP ID returned from the send endpoint |
otp |
string | Yes | The code entered by the user (4–8 digits; 6 by default) |
Example request
curl -X POST https://nepalotp.com/api/v1/otp/verify \
-H "Authorization: Bearer npot_live_1a2b3c4d5e6f7g8h9i0j" \
-H "Content-Type: application/json" \
-d '{
"otp_id": "otp_8f9a7b6c5d4e3f2a",
"otp": "847293"
}'
Success response
{
"success": true,
"message": "OTP verified successfully",
"data": {
"phone": "+9779841234567",
"reference": "user_signup_abc123",
"verified_at": "2026-01-15T10:32:10+05:45"
}
}
Failure response
{
"success": false,
"code": "INVALID_OTP",
"message": "The submitted OTP code is incorrect",
"data": {
"attempts_remaining": 2
}
}
Sandbox Mode
Sandbox mode allows you to integrate and test the NepalOTP API without sending real SMS messages or incurring charges.
Sandbox behavior
No SMS delivery
Messages are logged but not sent to real phones.
Fixed OTP: 123456
All sandbox OTPs are 123456 for
predictable testing.
No balance deduction
Your wallet balance is not affected by sandbox requests.
Identical API responses
Response structure matches production exactly.
Identifying sandbox mode
Sandbox keys belong to apps with the Sandbox
environment and are prefixed with npot_test_, while
live keys belong to Production apps and use npot_live_. A key's
prefix must match its app's environment — using a test key against a production app (or vice
versa) is rejected.
Moving to live mode
- Complete your integration testing in sandbox mode
- Navigate to Dashboard → Settings → Live Mode
- Submit a request describing your use case and expected volume
- Add funds to your wallet once approved
- Generate a live API key and deploy
Transactional SMS Templates
Transactional SMS templates allow you to send non-OTP messages for specific operational use cases. All templates must be manually approved before use.
Transactional SMS is not OTP
OTP messages use a fixed system template. Transactional SMS is a separate feature for sending approved operational messages like order confirmations or appointment reminders.
Allowed use cases
Order confirmations
Order placed, shipped, delivered
Payment receipts
Payment received, refund processed
Appointment reminders
Upcoming appointments, schedule changes
Account alerts
Security alerts, password changes
Template restrictions
- No links or URLs
- No promotional content or offers
- No free-text composition at runtime
- No bulk campaigns (use Bulk SMS API for bulk/promotional campaigns)
Template approval process
- Submit template via Dashboard → Templates → Create
- Include template name, content, and use case description
- Reviewed by AI moderation the moment you submit — most templates are approved or rejected instantly; a genuinely ambiguous template is escalated to manual review, completed within one business day
- Approved templates receive a
template_id
Example template submission
Your order {{order_id}} has been shipped.
Expected delivery: {{delivery_date}}.
Thank you for shopping with {{brand_name}}.
Sending a transactional SMS
/v1/sms/send
curl -X POST https://nepalotp.com/api/v1/sms/send \
-H "Authorization: Bearer npot_live_1a2b3c4d5e6f7g8h9i0j" \
-H "Content-Type: application/json" \
-d '{
"phone": "+9779841234567",
"template_id": "tpl_order_shipped",
"variables": {
"order_id": "ORD-2024-0847",
"delivery_date": "Jan 18, 2024",
"brand_name": "ShopNP"
}
}'
Success response
{
"success": true,
"message": "SMS sent successfully",
"sms_id": "sms_8f9a7b6c5d4e3f2a",
"data": {
"sent_at": "2026-01-15T10:32:10+05:45"
}
}
Bulk / Promotional SMS
Bulk SMS sends one free-text message to many recipients in a single call — the promotional/marketing channel, separate from pre-approved transactional templates. No template is required, but every message is screened automatically before anything is charged or sent.
How content moderation works
Every message is checked by AI moderation the moment you call the endpoint:
- Fair, legitimate promotional content is approved and sent instantly
- Links/URLs, betting or gambling content, and fraud or phishing content are always rejected — nothing is charged
- A message the AI can't confidently judge is queued for manual review — completed within one business day — instead of being sent or rejected outright, and nothing is charged until it actually sends
Sending a bulk SMS
/v1/sms/bulk-send
curl -X POST https://nepalotp.com/api/v1/sms/bulk-send \
-H "Authorization: Bearer npot_live_1a2b3c4d5e6f7g8h9i0j" \
-H "Content-Type: application/json" \
-d '{
"message": "Get 20% off your next order this weekend only!",
"mobiles": ["+9779841234567", "+9779801112223"]
}'
Success response (sent instantly)
{
"success": true,
"message": "Bulk SMS sent",
"batch_id": "bulk_8f9a7b6c5d4e3f2a",
"data": {
"recipient_count": 2,
"sent_count": 2,
"invalid_count": 0,
"segments": 1,
"ntc": 1,
"ncell": 1,
"smartcell": 0,
"invalid_numbers": [],
"sent_at": "2026-01-15T10:32:10+05:45"
}
}
Invalid recipients (all-or-nothing)
Our SMS gateway rejects the entire request if even one number in mobiles is invalid or
unreachable — it does not partially deliver to the valid numbers. If this happens, nothing is
sent to anyone in the batch and you are refunded in full automatically, never charged partially.
Validate phone numbers before submitting to avoid this.
{
"success": false,
"code": "INVALID_RECIPIENTS",
"message": "Some numbers in this request are invalid, so nothing was sent to anyone. You have been fully refunded.",
"batch_id": "bulk_8f9a7b6c5d4e3f2a",
"data": {
"invalid_numbers": ["9849059160"]
}
}
Queued for manual review
{
"success": true,
"message": "Content queued for manual review",
"batch_id": "bulk_8f9a7b6c5d4e3f2a",
"data": {
"status": "pending_review",
"recipient_count": 2,
"estimated_review_time": "within 1 business day"
}
}
Check the outcome of a queued batch any time with:
/v1/sms/bulk/{batch_id}
Limits
-
Up to 500 recipients per request (
mobilesis a JSON array of E.164 numbers) - Each recipient counts against your app's daily SMS limit
- Invalid numbers reported by the carrier are automatically refunded
Webhooks
Webhooks let you receive real-time delivery and verification updates for OTP and SMS without polling. Configure a webhook URL per app and we will POST signed events whenever activity occurs.
Enable webhooks
- Open Dashboard → Apps → Manage
- Enable Webhooks and provide a
webhook_url - Set a
webhook_secretto sign requests (recommended) - Return a
2xxresponse quickly to acknowledge delivery
Events
otp.sent
OTP delivered to provider successfully.
otp.failed
OTP delivery failed.
otp.verified
OTP verified by the end user.
sms.sent
Transactional SMS delivered to provider.
sms.failed
Transactional SMS delivery failed.
Example payload
{
"id": "evt_9d8a7c6b5a4f3e2d",
"event": "otp.sent",
"created_at": "2026-01-04T16:42:00+05:45",
"app": {
"app_id": "app_k2Y9s0XrT3aB",
"name": "Demo App",
"environment": "sandbox"
},
"data": {
"otp_id": "otp_8f9a7b6c5d4e3f2a",
"phone": "+9779812345678",
"reference": "login-123",
"status": "sent",
"is_sandbox": true,
"cost": "0.0000",
"expires_at": "2026-01-04T16:47:00+05:45",
"sent_at": "2026-01-04T16:42:00+05:45"
}
}
Signature verification
We sign webhook requests when a webhook_secret is set. Verify the
signature using the raw request body and the X-Npot-Signature header.
$signature = $_SERVER['HTTP_X_NPOT_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_NPOT_TIMESTAMP'] ?? '';
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $webhookSecret);
$header = "t={$timestamp},v1={$expected}";
if (! hash_equals($header, $signature)) {
http_response_code(400);
exit('Invalid signature');
}
Retries & timeouts
We retry failed webhook deliveries up to 2 times (3 attempts total). Your endpoint should respond within 10
seconds and return a 2xx status. On cPanel, you can keep delivery synchronous by using the
sync queue driver or run a
cron-based queue worker for higher throughput.
Template Variables & Validation
Template variables allow dynamic content insertion within approved templates. Variables are validated at runtime to ensure message integrity.
Variable syntax
Variables use double curly brace syntax: {{variable_name}}
Naming rules
- Letters, numbers, and underscores (word characters)
Value rules
| Constraint | Limit |
|---|---|
| Maximum value length | 500 characters per variable |
Template content itself (not the variable values you send at runtime) goes through spam and content review during template approval — see Template restrictions above.
Runtime enforcement
When sending a transactional SMS, the API validates that every variable referenced in the
template's content (e.g. {{order_id}}) is present in the
variables object and is a string
of 500 characters or fewer. Extra keys you send beyond what the template uses are accepted and
simply ignored.
Validation error example
{
"success": false,
"code": "MISSING_VARIABLES",
"message": "Missing required variables",
"data": {
"missing_variables": ["order_id"]
}
}
Rate Limits & Abuse Protection
Rate limits are enforced at two levels: a fixed platform-wide throttle on every request, and a per-app business limit on OTP/SMS volume that you configure per phone number when you create the app (and can change any time from Dashboard → Apps → Manage).
Platform-wide request throttle
Applies to every API call regardless of endpoint, before your app's own limits are checked.
| Scope | Limit | Window |
|---|---|---|
| Per API key | 120 requests | 1 minute |
| Per IP address | 300 requests | 1 minute |
OTP rate limits (default, per app)
These are the values a newly created app starts with — every row is configurable.
| Scope | Default limit | Window |
|---|---|---|
| Per phone number | 1 OTP | 1 minute |
| Per phone number | 5 OTPs | 1 hour |
| Per phone number | 20 OTPs | 24 hours |
| Cooldown between requests (same phone number) |
60 seconds | — |
| Per app, across all phone numbers | 1,000 OTPs | 24 hours |
Transactional SMS limits (default, per app)
| Scope | Default limit | Window |
|---|---|---|
| Per phone number | 1 SMS | 1 minute |
| Per phone number | 5 SMS | 1 hour |
| Per phone number | 20 SMS | 24 hours |
| Per app, across all phone numbers | 1,000 SMS | 24 hours |
Why limits exist
Rate limits prevent SMS bombing attacks against individual phone numbers, protect carrier relationships, and ensure fair usage across all customers.
Rate limit response
{
"success": false,
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many OTP requests for this phone number",
"data": {
"retry_after": 3600
}
}
Pricing & Billing
NepalOTP uses a prepaid wallet model. You add funds to your account and usage is deducted in real time.
Prepaid wallet
Add funds via the dashboard using supported payment methods. Your wallet balance is displayed on the dashboard and available via API.
Billing behavior
| Message type | Sandbox | Live |
|---|---|---|
| OTP SMS | No charge | Deducted from wallet |
| Transactional SMS | No charge | Deducted from wallet |
| Bulk / Promotional SMS | No charge | Deducted per recipient, only once a batch actually sends |
Message segments
Live SMS (transactional templates and bulk) is billed per segment, not per message — 1 credit per segment, direct pass-through with no markup on length.
| Encoding | Single segment | Each additional segment |
|---|---|---|
| GSM-7 (plain English text) | 160 characters | 153 characters |
| Unicode (any Nepali/Devanagari script, emoji, most accented characters) | 70 characters | 67 characters |
A message switches to Unicode the moment it contains any character outside the GSM-7 alphabet —
this is all-or-nothing per message, not per character, so a single Nepali word in an otherwise-English
message drops the whole message's budget from 160 to 70 characters per segment. Both
/v1/sms/send and
/v1/sms/bulk-send return a
segments field so you always know
exactly what a send was billed as.
No postpaid usage
API requests fail when wallet balance is insufficient. There is no credit or postpaid billing. Monitor your balance and set up low-balance alerts in the dashboard.
Errors & Status Codes
All API errors return a consistent structure with an error code and human-readable message.
Error response structure
{
"success": false,
"code": "ERROR_CODE",
"message": "Human readable description",
"data": {}
}
code,
message, and
data are always top-level
fields — errors are never nested under a separate error object.
Error code reference
VALIDATION_ERROR
A required field is missing or malformed (invalid phone
format, missing template_id,
etc). Field-level details are in data.errors.
TEMPLATE_NOT_APPROVED
The template exists but hasn't been approved yet (system templates are exempt from this check).
MISSING_VARIABLES
The template needs variables that weren't included in
your request. See data.missing_variables.
INVALID_OTP
The submitted OTP code is incorrect.
OTP_EXPIRED
The OTP has expired. Request a new one.
MAX_ATTEMPTS_EXCEEDED
Too many failed verification attempts. OTP is invalidated.
INVALID_API_KEY
API key is missing, malformed, or revoked.
INSUFFICIENT_BALANCE
Wallet balance is too low to complete the request.
API_KEY_ENVIRONMENT_MISMATCH
A sandbox key was used against a production app, or vice versa.
APPLICATION_SUSPENDED
The app this key belongs to is disabled.
IP_NOT_ALLOWLISTED
The app has an IP allowlist configured, and the request didn't come from an allowed address.
OTP_NOT_FOUND
The specified otp_id does not exist.
TEMPLATE_NOT_FOUND
The specified template_id does not exist or is not approved.
RATE_LIMIT_EXCEEDED
Too many requests. Check the retry_after field.
INTERNAL_ERROR
An unexpected error occurred. Contact support if persistent.
Best Practices
OTP UX recommendations
- Show a countdown timer displaying time until expiry
- Display remaining verification attempts
- Provide a "Resend OTP" button with a cooldown (30-60 seconds)
- Use auto-focus on OTP input fields
Retry and resend handling
- Implement client-side rate limiting for resend buttons
- Handle 429 errors gracefully with user-friendly messages
- Use exponential backoff for automatic retries
When to use OTP vs transactional SMS
| Use case | Recommended |
|---|---|
| Phone verification at signup | OTP |
| Two-factor authentication | OTP |
| Password reset | OTP |
| Order shipped notification | Transactional SMS |
| Appointment reminder | Transactional SMS |
What not to do
- Never log OTP codes in your application
- Never store OTPs in client-side storage
- Never expose API keys in frontend code
- Never allow unlimited OTP resends without cooldown
FAQ
Why is NepalOTP OTP-first?
OTP verification has different requirements than general messaging: strict delivery timing, abuse protection, and security guarantees. Building OTP as the core product allows us to optimize for these requirements.
Can I send marketing SMS?
Yes — use Bulk / Promotional
SMS (POST
/v1/sms/bulk-send) to send free-text messages to many recipients at once. Content
is screened automatically by AI moderation — links/URLs, betting, and fraud/phishing are
always rejected, fair promotional content is approved and sent instantly, and anything
genuinely ambiguous is queued for manual review (within one business day) rather than
being rejected outright. Transactional messages (OTPs and approved templates) remain a
separate channel with its own restrictions — see Transactional SMS Templates.
Can I edit templates after approval?
No. Approved templates cannot be modified. To change a template, submit a new template for approval and update your code to use the new template_id.
How many templates can I create?
Standard accounts can have up to 20 active templates. Contact support if you need more.
How long do OTPs last?
OTPs expire after 5 minutes. This is a fixed value and cannot be configured.
Will WhatsApp OTP be supported?
WhatsApp as a delivery channel is planned for a future release. Currently, only SMS delivery is available.
Support & Contact
Contact support
For technical issues, integration questions, or account inquiries:
support@nepalotp.comWhen contacting support, include:
- Your account email or organization name
- Request ID from the API response (if applicable)
- Timestamp of the issue
- Error code and message received
- Steps to reproduce the issue