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 over whichever network your user is on, using our built-in template, keep only its hash, and tell you whether what your user typed is right.
10:42
Friday
YourApp: 482913 is your verification code. Valid for 5 minutes. Do not share it.
Enter your code
Sent to 98XX·XXX·567
The flow
Send, then verify. That is the whole integration.
No code generation, no storage, no comparison logic on your side.
- 1
Send
POST
/sms/otp/sendwith the number, a purpose and (optionally) the visitor’s IP. You get back anotpId, amessageIdandexpiresInSeconds: 300. - 2
Your user receives it
A one-SMS text from our pre-approved OTP template: “YourApp: 482913 is your verification code. Valid for 5 minutes. Do not share it.”
- 3
Verify
POST
/sms/otp/verifywith the number, purpose and code. The answer is always 200:verified: true, orfalsewith a reason.
# 1. Send a code (6 digits, valid 5 minutes)
curl https://api.smsray.com/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.com/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 sentYour sign-in screen
Their phone
Waiting for a code request
- queued
- sent
- 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.
| Response | Meaning | Suggested UI |
|---|---|---|
| verified: true | Right code, used once. It can’t be used again. | Continue the sign-in or action |
| invalid_code | Wrong code; attempts remain. | “That code isn’t right. Try again.” |
| too_many_attempts | Five wrong tries. The code is dead. | Offer to send a new code |
| no_active_otp | Expired, 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
OTP works on day one: no template to submit. Talk to sales, get your API key and make your first two calls. The guardrails come with it.
Already a customer? Log in