Skip to content
SMSRay

OTP verification

Verification codes that arrive, checked for you

Two API calls. We generate a 6-digit code, send it with your app’s name from a real local number on NTC, Ncell, Smart, keep only its hash, and tell you whether what your user typed is right.

The flow

Send, then verify. That is the whole integration.

No code generation, no storage, no comparison logic on your side.

  1. 1

    Send

    POST /sms/otp/send with the number, a purpose and (optionally) the visitor’s IP. You get back an otpId, a messageId and expiresInSeconds: 300.

  2. 2

    Your user receives it

    A one-segment text from a local number: “YourApp: 482913 is your verification code. Valid for 5 minutes. Do not share it.”

  3. 3

    Verify

    POST /sms/otp/verify with the number, purpose and code. The answer is always 200: verified: true, or false with a reason.

# 1. Send a code (6 digits, valid 5 minutes)
curl https://api.smsray.in/api/sms/v1/sms/otp/send \
  -H "x-api-key: $SMSRAY_API_KEY" -H "content-type: application/json" \
  -d '{ "to": "9779801234567", "purpose": "login", "ip": "203.0.113.7" }'

# 2. Check what the user typed
curl https://api.smsray.in/api/sms/v1/sms/otp/verify \
  -H "x-api-key: $SMSRAY_API_KEY" -H "content-type: application/json" \
  -d '{ "to": "9779801234567", "purpose": "login", "code": "482913" }'

send · 200

{ "success": true,
  "otpId": "…",
  "messageId": "…",
  "expiresInSeconds": 300 }

verify · 200

{ "success": true,
  "verified": false,
  "reason": "invalid_code" }

Built-in protections

The guardrails are on by default

Everything an OTP system needs to resist abuse is part of the API. You don’t configure it, and you can’t forget it.

60 s resend cooldown

A second code for the same number and purpose can’t be requested within a minute. The 429 carries Retry-After so your UI can show a countdown.

5 attempts per code

After five wrong guesses the code is dead and verify answers too_many_attempts. Brute force goes nowhere.

3 codes per number per 10 min

Counted per phone number across every SMSRay customer, so nobody can use the platform to flood one person.

Per-IP limit

Pass your end user’s IP and we allow at most 10 codes per IP in 10 minutes, stopping one visitor cycling through numbers.

Hash-only storage

Verification compares against a SHA-256 hash of the code, never a stored copy, and the code record is deleted once it expires.

Hidden in logs

In the dashboard and message log, OTP text shows as “(OTP hidden)”. Teammates can see that it went out, never what it said.

Branded, and always one segment

The text uses your API client’s brand name and is kept to a single GSM-7 segment, so every code costs one message and arrives in one piece.

YourApp: 482913 is your verification code. Valid for 5 minutes. Do not share it.

Interactive

See the flow from both sides

Enter any made-up Nepal mobile number, watch the status move, then type the code. This runs entirely in your browser: it calls nothing and sends nothing.

Try the OTP flow

Simulation — no SMS is sent

Your sign-in screen

+977

Any made-up number works. Nothing is sent and nothing is stored.

Their phone

Messages

Waiting for a code request

  1. queued
  2. sent
  3. delivered

Simulated API calls

// Send a code to see the requests your server would make.

The simulation follows the real rules (six digits, five-minute expiry, 60 s resend cooldown, five attempts, three codes per number per ten minutes) and shows the response shapes your server would get.

What verify can tell you

A wrong code is a normal answer, not an HTTP error. Handle each reason in your UI.

ResponseMeaningSuggested UI
verified: trueRight code, used once. It can’t be used again.Continue the sign-in or action
invalid_codeWrong code; attempts remain.“That code isn’t right. Try again.”
too_many_attemptsFive wrong tries. The code is dead.Offer to send a new code
no_active_otpExpired, already used, or never sent.Offer to send a new code

Best practices

Getting OTP right for each use

The API handles the hard parts. These habits close the gaps on your side.

Login and sign-up

  • Use purpose "login" so a sign-up code can’t be used to sign in
  • Show the same message whether or not the number has an account
  • Pass the visitor’s IP to switch on the per-IP limit
  • Add autocomplete="one-time-code" to the input so phones can fill it

Two-factor authentication

  • Ask for the code after the password check, never before
  • Use a separate purpose such as "2fa" per action
  • On too_many_attempts, end the session and start over
  • Let users see and sign out other sessions after a 2FA change

Payment confirmation

  • Tie the purpose to the transaction, e.g. "pay-8841"
  • Show the amount and payee on the screen where the code is entered
  • Expire the pending payment when the code expires
  • Never ask users to read a code to a caller

Ship OTP sign-in this afternoon

Create a workspace, generate an API key and make your first two calls. The guardrails come with it.