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:
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.
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.
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.
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.
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:
| Aspect | SMS OTP | TOTP (authenticator app) |
|---|---|---|
| What the user needs | Any phone that receives texts | A smartphone with an authenticator app, enrolled in advance |
| Works offline | No, needs mobile signal | Yes, codes are computed on the device |
| SIM swap exposure | Real risk for targeted accounts: the attack moves the number, then receives the codes | None, nothing is sent over the carrier network |
| Onboarding friction | None, everyone already has SMS | Noticeable: install an app, scan a QR code, store recovery codes |
| Also proves | Possession of a working phone number | Possession of the enrolled device only |
| Typical role | Signup verification and reach-everyone 2FA | Stronger 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.
