This tutorial shows a practical SMS OTP implementation pattern you can adapt to signup and login verification.

It focuses on backend safety first, then user experience.

Step 1: Create a verification session

When user submits a phone number:

  1. normalize to E.164 format,
  2. create verification session record,
  3. generate OTP token,
  4. store hashed token + expiry + attempt counters.

Return a session identifier to the client, not the token.

Step 2: Send OTP over SMS

Dispatch OTP through your SMS provider abstraction.

Store send metadata:

  • provider message ID,
  • route/provider used,
  • timestamp,
  • delivery status updates (if available).

This data is essential for support and incident triage.

Step 3: Verify OTP input

On code submission:

  1. load active verification session,
  2. reject if expired/locked,
  3. compare submitted token securely,
  4. increment attempts on failure,
  5. mark session verified and invalidate token on success.

Never allow token reuse after success.

Step 4: Handle resend safely

Resend should be constrained:

  • minimum cooldown,
  • maximum resends per time window,
  • optional risk checks (IP/device anomalies),
  • clear user feedback on wait state.

When resending, define explicit policy on whether prior codes remain valid (most teams invalidate previous codes).

Step 5: Integrate with auth lifecycle

After verification succeeds, decide one of:

  • complete registration,
  • issue authenticated session,
  • unlock high-risk action.

Keep OTP verification result scoped to the intended action to prevent token reuse across workflows.

Minimal backend pseudocode

Test plan you should not skip

  1. success verification path,
  2. wrong code lockout path,
  3. expired code path,
  4. resend + old-code invalidation,
  5. provider delay and retry behavior.

For deterministic automation, use test phone workflows instead of manual handset checks.

Final take

OTP SMS verification is straightforward to prototype and easy to get subtly wrong in production. Build it as a stateful verification system with explicit limits, replay resistance, and test coverage from day one.