Six pitfalls in OTP SMS implementation — from code design to SMS bombing defence
OTP is probably the feature most people have built and most people have built slightly wrong. It looks like “generate six digits and send them”. Then it hits production and a pile of problems nobody planned for surfaces.
Here are the six we run into most while helping clients integrate.
Pitfall 1: Treating the API response as “the customer got it”
rc: 100 means the platform accepted the request. Actual arrival is the callback returning DELIVRD.
This matters more for OTP than anywhere else, because a user is sitting on a page waiting. If your UI says “code sent” while the 1st callback returned IM (malformed number), that user stares at an empty inbox until timeout and then calls support.
What to do: handle the 1st callback at minimum. On a -100 failure code, tell the user immediately — “that number looks wrong, please check” — rather than letting them wait. The full code table is in DLR status codes explained.
Pitfall 2: Badly chosen validity windows
Two common extremes:
- Too short (30 seconds) — the user switches app, takes a call, hits a slow network, and it has expired. Resend rates climb and so does cost.
- Too long (30 minutes) — the attack window widens enough for brute force to become practical.
What to do: 3–5 minutes is the usual balance. Note that validity and resend interval are different settings — a code can be valid for 5 minutes while the resend button stays disabled for the first 60 seconds.
Pitfall 3: No rate limiting, and then SMS bombing
This is the expensive one.
An attacker scripts your “send verification code” endpoint against a large list of numbers. Your points burn down, the number owners get a pile of messages they never asked for, and your brand becomes the harassment.
The more refined version is AIT — Artificially Inflated Traffic. The attacker controls number ranges in high-rate destinations and pushes volume through your OTP endpoint to collect a share of the termination revenue. Your bill spikes and not one of those was a real user.
What to do (layered):
- Per-number frequency cap — five per hour, for example.
- Per-IP and per-session caps — stops the single-machine script.
- Destination allowlist — if you only serve Hong Kong and Taiwan, do not accept sends anywhere else. This single control blocks the most AIT.
- Anomaly alerting — a sudden spike concentrated on one number prefix should page someone.
- A pre-check — a low-friction human verification before the OTP is issued.
Point 3 gets skipped most often and has the best cost-benefit of the five.
Pitfall 4: OTP sharing a route with marketing
Marketing traffic can be delayed or filtered for content review, volume, or time-of-day restrictions. If OTP shares that queue and that route, your login experience gets dragged along by whatever campaign is running.
What to do: put OTP on a dedicated route and a dedicated queue, so no marketing volume can affect verification latency. We separate the two at the service level for exactly this reason.
Pitfall 5: The message itself is written badly
Details that measurably affect the experience:
- Put the code near the start. Many phones show only the first few dozen characters on the lock screen; a code at the end forces the user to unlock and open.
- No links in an OTP message. It trains users that verification texts contain links to tap, which is precisely the habit phishing depends on.
- Include the brand name. Someone registering on three apps at once cannot tell which “Your code is 482910” belongs to whom.
- Add a warning line. “We will never call you to ask for this code” works well for financial services.
A reasonable template:
[Your Brand] Your code is 482910, valid for 5 minutes.
We will never call you to ask for this code.
Note that any non-Latin character forces the whole message into Unicode encoding, which cuts the length limit sharply — covered in How Hong Kong SMS billing works.
Pitfall 6: Numbers never normalised
+852 9123 4567, 852-91234567, 09123 4567 — users will type all of it.
Our API splits the dialling code and the number into separate fields (country_code and mobile), and country_code must be 1–3 digits with no leading zero. Feed raw user input straight in and the 1st callback returns IM.
What to do: normalise before the write, not at send time. What lands in the database should always be a clean country_code + mobile pair, so it behaves the same no matter which system sends from it later.
Pre-launch checklist
- 1st callback handled; failure codes surface in the UI
- Code valid 3–5 minutes, resend gated at 60 seconds
- Per-number and per-IP frequency caps in place
- Only the destinations you actually serve are allowed
- OTP and marketing on separate routes
- Code near the start, no links, brand name present
- Numbers normalised at write time
- Points balance checked before large sends
- Callback handling is idempotent
Endpoint parameters and implementation detail are in the SMS API integration guide. If you are stuck on an integration, talk to us — our engineers have worked through most of these.