Skip to main content
Docs

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.

Option 1: X-API-Key header
X-API-Key: npot_live_1a2b3c4d5e6f7g8h9i0j
Option 2: Authorization header
Authorization: Bearer npot_live_1a2b3c4d5e6f7g8h9i0j

Key rotation

You can have up to 3 active API keys at any time. When rotating keys:

  1. Generate a new key in the dashboard
  2. Deploy your application with the new key
  3. 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

1

Generate

Server creates OTP

2

Send

SMS delivered

3

Verify

User submits code

4

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

POST /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
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

200 OK
{
  "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

422 VALIDATION_ERROR

The phone field is missing, or isn't a valid E.164 number.

402 INSUFFICIENT_BALANCE

Your wallet balance can't cover this OTP's cost (production apps only — sandbox is always free).

429 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

POST /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
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

200 OK
{
  "success": true,
  "message": "OTP verified successfully",
  "data": {
    "phone": "+9779841234567",
    "reference": "user_signup_abc123",
    "verified_at": "2026-01-15T10:32:10+05:45"
  }
}

Failure response

400 Bad Request
{
  "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

  1. Complete your integration testing in sandbox mode
  2. Navigate to Dashboard → Settings → Live Mode
  3. Submit a request describing your use case and expected volume
  4. Add funds to your wallet once approved
  5. 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

  1. Submit template via Dashboard → Templates → Create
  2. Include template name, content, and use case description
  3. 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
  4. Approved templates receive a template_id

Example template submission

Template Content
Your order {{order_id}} has been shipped. 
Expected delivery: {{delivery_date}}. 
Thank you for shopping with {{brand_name}}.

Sending a transactional SMS

POST /v1/sms/send
cURL
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

200 OK
{
  "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

POST /v1/sms/bulk-send
cURL
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)

200 OK
{
  "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.

422 Unprocessable Entity
{
  "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

202 Accepted
{
  "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:

GET /v1/sms/bulk/{batch_id}

Limits

  • Up to 500 recipients per request (mobiles is 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

  1. Open Dashboard → Apps → Manage
  2. Enable Webhooks and provide a webhook_url
  3. Set a webhook_secret to sign requests (recommended)
  4. Return a 2xx response 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

JSON
{
  "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.

PHP
$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

400 · Missing required variable
{
  "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

429 Too Many Requests
{
  "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

422 VALIDATION_ERROR

A required field is missing or malformed (invalid phone format, missing template_id, etc). Field-level details are in data.errors.

400 TEMPLATE_NOT_APPROVED

The template exists but hasn't been approved yet (system templates are exempt from this check).

400 MISSING_VARIABLES

The template needs variables that weren't included in your request. See data.missing_variables.

400 INVALID_OTP

The submitted OTP code is incorrect.

400 OTP_EXPIRED

The OTP has expired. Request a new one.

400 MAX_ATTEMPTS_EXCEEDED

Too many failed verification attempts. OTP is invalidated.

401 INVALID_API_KEY

API key is missing, malformed, or revoked.

402 INSUFFICIENT_BALANCE

Wallet balance is too low to complete the request.

403 API_KEY_ENVIRONMENT_MISMATCH

A sandbox key was used against a production app, or vice versa.

403 APPLICATION_SUSPENDED

The app this key belongs to is disabled.

403 IP_NOT_ALLOWLISTED

The app has an IP allowlist configured, and the request didn't come from an allowed address.

404 OTP_NOT_FOUND

The specified otp_id does not exist.

404 TEMPLATE_NOT_FOUND

The specified template_id does not exist or is not approved.

429 RATE_LIMIT_EXCEEDED

Too many requests. Check the retry_after field.

500 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.com

When 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