SMS 2FA and OTP codes: when your own SIM is enough (and when it isn't)
Is SMS 2FA secure? An honest guide to SMS OTP from your own number: SIM swap risk, NIST guidance, when a phone gateway is enough, plus Node.js code for codes.

SMS codes at login are so familiar that sending one feels like the obvious choice for any app. This guide is for developers and small-business owners who build a small app, a client portal or an internal tool and wonder whether SMS OTP from your own number is a good idea. The honest answer is: sometimes, within clear limits, and I will show you where those limits are and give you working code.
Is SMS 2FA secure?
SMS as a second factor is clearly better than a password alone, and clearly weaker than an authenticator app or a passkey. The code travels through the carrier network to a phone number, not to a specific device, so its security depends on who controls that number.
The main threats:
- SIM swap: an attacker persuades the carrier to move your number to their SIM and starts receiving your texts.
- Number porting abuse: the number is ported to another carrier without the owner's knowledge.
- Real-time phishing: the user types the code into a fake site and the attacker replays it on the real one.
- Malware with permission to read SMS on the victim's phone.
NIST agrees. In SP 800-63B-4, using the public telephone network (PSTN) for out-of-band authentication, which includes SMS, is the one authenticator type marked restricted. An organization that uses it should accept the risk, offer at least one unrestricted alternative, give users meaningful notice of the risks and keep a migration plan. NIST also says verifiers should consider risk signals such as a recent SIM change before delivering a code that way.
Note: this article is not a security audit or professional advice. If you protect financial or health data or privileged access, choose a phishing-resistant method.
When is your own SIM enough for SMS codes?
Your own phone as a sender fits small scale, where SMS is a convenience and not the only barrier. Good examples: internal tools, portals for a few dozen customers, order confirmations by one-time code, and staff logins where a second factor already exists.
| Situation | Is SMS from your own number enough? |
|---|---|
| Internal tool for a team of up to a few dozen people | Yes, as one option |
| Small business client portal, a few dozen logins a day | Yes, with short code validity and rate limits |
| Confirming an order or appointment with a one-time code | Yes |
| Consumer app with thousands of logins | No: use a wholesale provider or another method |
| Banking, health data, administrator accounts | No: use passkeys or TOTP |
Why it does not scale to thousands of users:
- A phone sends one message every 5 seconds by default, about 12 per minute. Codes for many people at once queue up.
- The phone needs power, signal and internet. It is a single point of failure.
- Carriers may restrict bulk sending from consumer SIMs. See Android SMS gateway limits for the details.
Why is the 72-hour queue a drawback for OTP?
If the phone is off or has no signal, queued messages wait up to 72 hours and then expire. For ordinary notifications that is a feature. For a one-time code it is a flaw: the code might arrive an hour later, when the user has given up or requested a new one.
The fix lives in your application:
- Set a short code validity, such as 5 minutes. NIST requires out-of-band authentication to be invalid after 10 minutes.
- After expiry, require a fresh code.
- Tell the user a delay is possible and offer a "resend" button.
- Listen to
MESSAGE_FAILEDandMESSAGE_DELIVEREDevents so you know what happened (see receiving SMS with webhooks).
The device health screen in the app warns about issues such as battery optimisation that could pause sending.
How do you build SMS OTP step by step?
The flow is simple: generate a code, store its hash with an expiry, send the SMS through the API, and on verification compare the hash and count attempts. Never store the plain code.
- Generate a random 6-digit code with a cryptographic generator (not
Math.random). - Store an HMAC of the code with a server secret, an expiry time and an attempt counter.
- Send the SMS with
POST /gateway/send-sms. - On verification check expiry, attempt limit and hash, then delete the record on success.
import crypto from "node:crypto";
const API_URL = "https://smsportal.app/api/v1/gateway/send-sms";
const API_KEY = process.env.SMSPORTAL_API_KEY;
const PEPPER = process.env.OTP_PEPPER; // server-side secret
const TTL_MS = 5 * 60 * 1000; // code valid for 5 minutes
const MAX_ATTEMPTS = 5;
const digest = (phone, code) =>
crypto.createHmac("sha256", PEPPER).update(`${phone}:${code}`).digest("hex");
export async function sendCode(phone, store) {
const code = String(crypto.randomInt(0, 1_000_000)).padStart(6, "0");
store.set(phone, {
digest: digest(phone, code),
expiresAt: Date.now() + TTL_MS,
attempts: 0,
});
const res = await fetch(API_URL, {
method: "POST",
headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
recipients: [phone],
message: `Your login code: ${code}. Valid for 5 minutes. Never share it.`,
}),
});
if (!res.ok) throw new Error(`SMS send failed: ${res.status}`);
}
export function verifyCode(phone, code, store) {
const record = store.get(phone);
if (!record || Date.now() > record.expiresAt || record.attempts >= MAX_ATTEMPTS) {
return false;
}
store.set(phone, { ...record, attempts: record.attempts + 1 });
const a = Buffer.from(record.digest);
const b = Buffer.from(digest(phone, code));
const ok = a.length === b.length && crypto.timingSafeEqual(a, b);
if (ok) store.delete(phone); // one-time use
return ok;
}In production, store is Redis with key expiry or a database table. Use E.164 numbers (+1...). The message is plain ASCII, so it stays within 160 GSM-7 characters and a single segment.
A few rules that matter:
- Rate-limit sends per phone number and per IP, for example one code per minute and a few per hour. Without it, someone can flood a stranger's phone or exhaust your phone's queue.
- Limit verification attempts (5 here), then require a new code.
- Respond identically whether or not the account exists, so you do not reveal who has an account.
- Keep the text minimal. The code, your service name and a warning are enough.
If you have not sent a message through the API yet, start with SMS API without Twilio.
What should you use instead of, or next to, SMS?
If you can, give users a stronger option and keep SMS as the fallback.
| Method | SIM swap resistance | Phishing resistance | Notes |
|---|---|---|---|
| SMS code | Low | Low | Convenient, no install |
| Authenticator app (TOTP, RFC 6238) | High | Low | Works offline, cheap |
| Passkeys | High | High | Best protection and convenience |
A sensible setup for a small app is passkeys or TOTP by default, SMS as an option for people who cannot use anything else, with short code validity and rate limits.
Tip: AI agents that can read inbound SMS can also see codes that arrive on the same phone. If you connect an agent through MCP, keep your login codes off the number it reads, or use a separate phone for each purpose.
How can you check it will work for you?
Before you expose SMS OTP to real users, run a short test on your own phone:
- Send 20 codes a few seconds apart and measure how long each takes to arrive.
- Switch the phone off for a few minutes and see what the user experiences. Can they request a new code?
- Enter a wrong code six times and confirm the attempt limit works.
- Request a code twice in a row and confirm the send rate limit kicks in.
- Count how many logins per hour you realistically have. If it is hundreds, a single phone is not enough.
That last point is the most honest criterion: a phone is excellent at small scale and poor at large scale, and you find the boundary with numbers, not with intuition.
Get started
For a small or internal app, try it for free: create an account, pair a phone and send yourself a test code, then measure delivery time and what happens with the phone switched off. Request details are in the API documentation, and plans are in the pricing section. If you find the phone's queue is not enough, that is the signal to move to a wholesale provider or a method without SMS.
Frequently asked questions
Is SMS 2FA secure?
It is more secure than a password alone but not among the strongest methods. A code sent by SMS can be intercepted through a SIM swap, number porting abuse or malware, and users can be tricked into typing it on a fake site. Authenticator apps and passkeys are more resistant.
What is a SIM swap?
A SIM swap is an attack where a fraudster convinces a mobile carrier to move a victim's phone number to a SIM card the fraudster controls. From then on the attacker receives the victim's text messages, including login and account recovery codes.
Can I send OTP codes from my own Android phone?
At small scale, yes: an internal tool, a team portal or a small customer area with a few dozen logins a day. It is not suited to consumer apps, because a phone sends only a handful to a dozen messages per minute, needs power and signal, and is a single point of failure.
How long should an SMS code stay valid?
Keep it short. Five minutes is a sensible default, and NIST requires out-of-band authentication to be treated as invalid after 10 minutes. This matters because if the phone is offline, queued messages wait up to 72 hours and a code could arrive long after it expired.
What should I use instead of SMS for 2FA?
Passkeys (WebAuthn) give the strongest protection. Authenticator apps that generate TOTP codes are a cheap, solid alternative. Keep SMS as a fallback for users who cannot use anything else.

