OTP Authentication: How One-Time Passwords Work

A one-time password (OTP) is a short code that proves one specific action, once. It expires in minutes, dies after a single use, and works even for users who will never install an authenticator app. This guide walks through how OTP authentication works, where SMS codes and app-generated codes each make sense, and how to build and test the flow properly.

What is OTP authentication?

OTP authentication verifies a user with a short-lived, single-use code instead of (or on top of) a static password. The server generates the code, delivers it over a channel the user controls, and accepts it only within a small time window. Because the code is worthless after one use, intercepting yesterday's SMS or reusing a leaked code achieves nothing.

You meet OTPs in three jobs: proving a phone number or email belongs to the person signing up, adding a second factor (2FA) on top of a password at login, and confirming sensitive actions such as payouts or password changes. The mechanics are the same in all three; what changes is when the code is demanded.

The OTP verification flow, step by step

Every OTP implementation, whatever the delivery channel, reduces to the same loop:

1

User submits an identifier

A phone number, email address or username arrives at your server. Normalise it before anything else so retries hit the same record.

2

Server generates the code

A cryptographically random code, typically six digits, bound to that identifier with an expiry of a few minutes. Store only a hash of it, exactly like a password.

3

Code travels to the user

Over SMS, an authenticator app, email or a push prompt. The channel is the main security and reach trade-off; the table below compares the two most common.

4

User types the code back

The server compares it against the stored hash, enforcing the expiry window and a strict attempt counter so codes cannot be brute forced.

5

Code is burned

On success the code is invalidated immediately and the action proceeds. On expiry the user requests a new code, which replaces the old one.

Nothing in this loop is exotic. Almost every real-world OTP incident traces back to skipping one of the boring parts: no attempt limit, codes that live too long, codes logged in plain text, or unlimited resends.

SMS OTP vs TOTP: an honest comparison

The two mainstream OTP channels are codes delivered by SMS and time-based codes (TOTP) generated by an authenticator app. Neither wins everywhere, and mature products usually offer both:

AspectSMS OTPTOTP (authenticator app)
What the user needsAny phone that receives textsA smartphone with an authenticator app, enrolled in advance
Works offlineNo, needs mobile signalYes, codes are computed on the device
SIM swap exposureReal risk for targeted accounts: the attack moves the number, then receives the codesNone, nothing is sent over the carrier network
Onboarding frictionNone, everyone already has SMSNoticeable: install an app, scan a QR code, store recovery codes
Also provesPossession of a working phone numberPossession of the enrolled device only
Typical roleSignup verification and reach-everyone 2FAStronger 2FA for accounts that opt in

The honest summary: TOTP is the stronger second factor because nothing crosses the carrier network, so SIM swapping and SS7-level interception simply do not apply. SMS remains unbeatable for reach and for the one job TOTP cannot do at all: proving that a real, working phone number belongs to the person signing up.

That is why most platforms still verify new accounts by SMS even when they push TOTP or passkeys for daily logins. Treat the two as complements, and let high-risk accounts upgrade beyond SMS.

OTP security best practices

Whichever channel delivers the code, the server-side rules do most of the security work:

Short expiry

Five to ten minutes is plenty. A code that lives for an hour is a standing invitation.

Single use, always

Invalidate the code the moment it is accepted, and replace it whenever a new one is issued.

Strict attempt limits

A six digit code survives brute force only if you lock after a handful of wrong guesses.

Throttle resends

Cap how often a new code can be requested per identifier and per IP. Resend abuse is also how SMS-pumping fraud drains messaging budgets.

Hash codes at rest

Store the hash, not the code. Anyone who can read your database should not be able to log in as your users.

Keep codes out of logs

Request logs, error trackers and analytics events all love to swallow query strings and request bodies. Scrub them.

Bind the code to context

A code issued for login on one session should not confirm a payout on another. Tie codes to the action that requested them.

Plan the fallback path

Device lost, number changed, app deleted. Recovery flows are part of the design, and they are the part attackers probe first.

Add OTP verification to your own app

Generating and checking codes is a few dozen lines in any framework, and delivering codes to your users is a job for your messaging provider. The part teams routinely under-build is the other direction: receiving codes, so you can pass phone verification on other platforms and test your own flow with real numbers instead of mocks.

That receive side is exactly what the SMSBulk SMS verification API does: your code buys a dedicated number, polls until the SMS lands, and reads the code as JSON. The snippet below is the production-verified loop; the Node.js and Python guides expand it with retries, timeouts and refund handling.

const BASE = 'https://smsbulk.net/api/v1';
const KEY = process.env.SMSBULK_API_KEY; // smsbulk_...

async function api(path, options = {}) {
  const res = await fetch(BASE + path, {
    ...options,
    headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
  });
  if (!res.ok) throw new Error(res.status + ' ' + (await res.text()));
  return res.json();
}

async function main() {
  // 1) Buy a number for Telegram in the United States
  const activation = await api('/activations', {
    method: 'POST',
    body: JSON.stringify({ serviceCode: 'tg', countryIso: 'US' }),
  });
  console.log('Number:', activation.phoneNumber);

  // 2) Poll until the verification code arrives
  let code = null;
  while (!code) {
    await new Promise((r) => setTimeout(r, 5000));
    const a = await api('/activations/' + activation.id);
    if (a.status === 'RECEIVED') code = a.smsCode;
    else if (['CANCELLED', 'EXPIRED', 'REFUNDED'].includes(a.status)) {
      throw new Error('Ended without SMS: ' + a.status);
    }
  }
  console.log('Verification code:', code);

  // 3) Confirm the code was used
  await api('/activations/' + activation.id + '/complete', { method: 'POST' });
}

main();

If your goal is specifically CI: registering test accounts, checking that your OTP emails and SMS actually arrive, and catching broken flows before users do, the SMS verification testing guide covers that end to end, and the API documentation specifies every endpoint used above.

OTP authentication FAQ

What does OTP mean in authentication?

OTP stands for one-time password: a short code that is valid for a single use within a short time window. It proves the user controls a delivery channel (a phone number, email or enrolled device) at the moment of the action.

Is OTP the same as 2FA?

Not quite. 2FA means requiring a second, independent factor on top of a password. An OTP is one common way to implement that second factor, but OTPs are also used alone, for example to verify a phone number at signup before any password exists.

Which is safer, SMS OTP or TOTP?

TOTP, for the second-factor job: codes never cross the carrier network, so SIM swapping and interception do not apply. SMS OTP remains the practical choice for reach and for verifying that a phone number is real, which TOTP cannot do. Serious products use both.

How long should an OTP be valid?

A few minutes. Five to ten minutes covers slow SMS delivery on bad networks without leaving a long window for replay. Combine the expiry with single use and an attempt limit; the three rules only work together.

Why do platforms still verify accounts by SMS?

Because a working phone number is a real-world cost that filters bulk fake signups, and because SMS needs no app and no enrollment. That is also why SMS verification survives even as logins move to TOTP and passkeys.

How do I test an OTP flow end to end?

With real numbers rather than mocks: buy a dedicated number through the SMS verification API, run your signup against it, and assert that the code arrives and validates. The SMS verification testing guide shows how teams wire that into CI.

Need real numbers for your verification flow?