Toll-free 1800 410 4433
SMSAPI
— Verification

SMS OTP best practices: expiry, rate limits, and auto-read

· Updated · 8 min read · The SMSAPI team

SMSAPI guide cover: OTP flow

The core SMS OTP best practices are: let the provider generate, store and verify the code so your app never sees it; keep the time-to-live to five to ten minutes; cap verify attempts and rate-limit send requests; offer resend and voice fallback; use Android auto-read; and keep the message link-free on its registered DLT template.

A one-time passcode is a small thing that fails in expensive ways. Too loose, and it's a security hole; too strict, and real users get locked out and abandon signup. Most of these practices you get for free if you let the provider handle the code, and a few you own regardless.

At a glance

PracticeWhat to doWhy it matters
Code storageLet the provider generate, store and verify the code; your app holds only a verification IDA code that never touches your database or logs can't leak from them
Time-to-liveFive to ten minutes, shown to the userLimits the window in which an intercepted code is useful
Verify attemptsLock the code after a handful of wrong guessesWith a short TTL and six digits, brute force is impractical
Send rate limitsCap requests per number and per accountStops SMS bombing and runaway bills
ResendReuse the same verificationKeeps the attempt counter meaningful
Voice fallbackRead the same code out on a call if the SMS isn't delivered in a windowTurns a failed verification into a completed one
Auto-readUse the Android SMS Retriever API with your app's hashRemoves the read-and-type step
Message contentCode first, purpose and validity stated, no link, on the registered templateFaster scanning, less phishing exposure, no DLT scrubbing

Don't store the code — verify it

The single most important rule: your application should never see, store, or log the OTP. Generate the code on the provider's side, deliver it to the user, and verify the user's input against a verification ID. If the code never touches your database or your logs, it can't leak from them.

This also removes a pile of code you'd otherwise write and secure: generation, hashing, expiry, attempt counting. You call send, you get a verification ID back, and you call verify with what the user typed. That's the whole flow. The OTP API overview shows the two calls end to end.

How long should an OTP stay valid?

A code should be valid for minutes, not hours. A short time-to-live (TTL) limits the window in which an intercepted or shoulder-surfed code is useful. Five to ten minutes is typical; long enough for a real user to switch apps and type it, short enough that a stale code is worthless.

Make the expiry visible to the user — "valid for 10 minutes" — so the deadline is part of the experience rather than a surprise.

How do you stop OTP brute force and SMS bombing?

Two limits matter. First, cap the number of verify attempts per code: after a handful of wrong guesses, lock the code. Combined with a short TTL and a six-digit space, that makes brute force impractical — a guessed code expires long before it can be found.

Second, rate-limit send requests per number and per account: how many codes a single number can request in a window, and how fast. This stops someone using your OTP endpoint to pump SMS at a target's phone (an "SMS bomb") or to run up your bill. Read the rate-limit headers the API returns and back off cleanly rather than retrying blindly.

What if the OTP SMS doesn't arrive?

Delivery is never 100%. The flow has to handle the code that doesn't show up, or you lose the user at the last step.

Two tools help. Resend — let the user request the code again, but reuse the same verification rather than starting a new one, so your attempt counter stays meaningful and the first code isn't orphaned. And voice fallback — if the SMS isn't delivered within a window, the same code is read out on a voice call, with the verification ID unchanged so your verify call doesn't branch. Voice fallback turns a failed verification into a completed one, and it costs nothing until it's used.

Use auto-read on Android

The biggest source of drop-off in a verification isn't security — it's the user switching to their SMS app, reading the code, and typing it back. On Android, you can remove that step entirely with the SMS Retriever API: include your app's hash in the request, format the message correctly, and the code fills itself in without the user reading or typing anything, and without the app holding the SMS permission.

It's a convenience layer on the same flow — the verification ID and verify call don't change — so there's little reason not to use it. The auto-read guide covers the setup, and the iOS and web equivalents (keyboard AutoFill and the WebOTP API) that get most of the way there on other platforms.

Get the message right

A few content details lift completion rates:

  • Put the code early. Lead with the digits so a user scanning a notification sees them without opening the message.
  • Say what it's for and how long it's valid. "Your SMSAPI login code is 481920. Valid for 10 minutes."
  • Don't include a link. Codes and links together train users to tap, which phishers exploit. Keep OTP messages link-free.
  • Match the registered template. Under DLT, OTPs are service-implicit traffic, exempt from DND so they reach users at any hour — but only if the template is registered in the right category. Content that drifts from the approved template gets scrubbed.

Think about the second factor

If you're using OTP for two-factor authentication, target the friction at the risk rather than applying it everywhere. Require a code at login from a new device, and as step-up before a sensitive action — a payment, a password change, adding a payee — rather than on every action. That keeps trusted sessions smooth and reserves the friction for the moments that matter. The 2FA guide goes deeper on where a second factor belongs.

Test the whole flow before you ship

A verification flow has more moving parts than it looks — send, deliver, auto-read, type, verify, resend, fallback — and the failures live in the seams between them. Test the whole path before real users hit it, not just the happy case.

Use a test API key against the same endpoints. Good providers accept and verify test sends without touching the operator network, so you can wire up send, verify, your webhook handler, and your retry interface end to end, then switch to a live key once your DLT template is approved. Nothing about the calls changes between test and live except the key, so what you test is what you ship.

Exercise the unhappy paths deliberately. Enter a wrong code and confirm the mismatch status and the attempts-remaining count behave. Let a code expire and check the expired state. Request too many codes and confirm the rate limit kicks in with a clear signal. Trigger the voice fallback and make sure your verify call handles a code that arrived by call exactly as one that arrived by SMS. Each of these is a place a real user will eventually land, and each is cheap to get right in testing and expensive to get wrong in production.

Finally, watch the metrics once you're live. Track send-to-verify completion rate by channel and device; a sudden drop usually means a deliverability issue or a broken auto-read integration, and the per-attempt log — channel, status, timing — is what tells you where to look before it costs you signups.

The short version

Let the provider generate, store, expire, and verify the code; keep the TTL short; cap attempts and rate-limit sends; handle resend and voice fallback so delivery failures don't lock users out; use auto-read to remove the typing step; and keep the message lean, link-free, and on its registered template. Do those, and you get a verification flow that's hard to attack and easy to pass — which is exactly the balance an OTP is supposed to strike. And because most of it — generation, storage, expiry, and verification — is handled by the provider rather than your code, the secure path is also the one with less to build and maintain, which is a rare and welcome alignment.

Frequently asked

Should my app store OTP codes in its database?

No. The safest design keeps the OTP out of your application entirely. The provider generates the code, delivers it, and checks the user's input against a verification ID you hold. If the code never touches your database or logs, it can't leak from them, and you skip writing generation, hashing, expiry and attempt counting.

Should an OTP SMS include a link?

No. Keep OTP messages link-free, because codes and links together train users to tap, which phishers exploit. A good OTP message puts the code first and says what it is for and how long it is valid, for example 'Your SMSAPI login code is 481920. Valid for 10 minutes.' It should also match its registered DLT template.

Are OTP messages blocked by DND in India?

No, provided the template is registered correctly. Under DLT, OTPs are service-implicit traffic, which is exempt from DND so codes reach users at any hour. That only holds if the template is registered in the right category; content that drifts from the approved template gets scrubbed.

How do I test an OTP flow without sending real SMS?

Use a test API key against the same endpoints. Good providers accept and verify test sends without touching the operator network, so you can wire up send, verify, your webhook handler and retries end to end, then switch to a live key once your DLT template is approved. Only the key changes.

Can OTP codes fill in automatically on iPhone and the web?

Mostly. On Android, the SMS Retriever API fills the code with no reading or typing and without the SMS permission. On iOS, keyboard AutoFill suggests the code from the message, and on the web the WebOTP API does a similar job. Both get most of the way there and use the same verify call.

Start building.