SMS two factor authentication sends a short one-time code to a user's phone, and they type it in to prove they hold that number. It beats a password on its own and almost every user can receive it, but SIM swaps and phishing pages can defeat it, so treat it as a baseline rather than your strongest option.
This post covers how an SMS one-time password (OTP) moves from your server to a phone, where it fails, and what each failure costs you. It also covers a mistake we see often: sending login codes from the same numbers and 10DLC campaign as marketing texts. We make Ready, a messaging API, so the sending examples use our API. We also say where Ready is the wrong tool for this job.
How SMS two factor authentication works
The flow is the same everywhere:
- The user enters their password (or just their phone number for passwordless login).
- Your server generates a random code, usually 6 digits. It stores a hash of the code with an expiry, typically 5 to 10 minutes.
- Your server calls an SMS API to send the code to the phone number on file.
- The message travels through carrier infrastructure to the handset.
- The user types the code. Your server compares it to the stored hash, checks the expiry and the attempt count, then lets the user in.
The SMS provider only handles step 3. Generating the code, storing it, expiring it, capping guesses and rate-limiting resend requests are all your application's job. Some CPaaS vendors sell a "verify" product that handles code generation and checking for you. Ready doesn't. We send the text, and you own the code logic. If you want a managed verify service that does both halves, a CPaaS verify product is a reasonable choice.
What a good OTP message looks like
Your Acme login code is 482913. It expires in 10 minutes. Don't share it with anyone. That's 86 GSM-7 characters, so one segment. Keep OTP texts plain. A single emoji switches the message to unicode, where a segment holds only 70 characters, and an 86-character code text becomes 2 segments. On Ready, curly quotes and long dashes are swapped for plain ones before sending, so they won't push you over the limit. Emoji will.
Starting the message with your brand name helps people spot a fake: a user can tell your code from one an attacker triggered. The "don't share it" line exists because the most common SMS 2FA attack works by talking the user into handing the code over.
The pros of SMS 2FA
SMS 2FA is still the default for good reasons:
- Nearly everyone can receive it. No app to install, no hardware key to lose. Users with basic phones, older users and people who will never set up an authenticator app can all type six digits.
- It's much better than a password alone. Credential-stuffing bots that replay leaked passwords hit a wall when they also need the phone.
- It doubles as phone verification. At signup, a delivered code proves the number is real and reachable. That keeps junk numbers off your contact list.
- Users already understand it. Support tickets about "how do I log in" stay low.
- Recovery is simple. A user who replaces their phone usually keeps their number.
The cons: SIM swap, phishing and delivery failures
SIM swap
In a SIM swap, an attacker convinces a carrier to move the victim's number to a SIM the attacker controls, usually by social engineering a support rep or a store employee. From then on, every text to that number, including your login codes, goes to the attacker. Your system can't see it happen. The code is delivered "successfully", just to the wrong person.
Number porting fraud works the same way, through a port-out request instead of a SIM change. So does number recycling: a user abandons a number, the carrier reassigns it months later, and the new owner starts receiving the old owner's codes.
Real-time phishing
A phishing page mimics your login and passes credentials to your real site live. Your server sends the code, the user types it into the fake page, and the attacker uses it within seconds. SMS codes can't stop this because nothing ties the code to your real domain. Passkeys and hardware security keys do tie authentication to the domain, which is why they hold up against phishing.
Network-level interception
Weaknesses in the older signaling networks that carriers use to route texts have been exploited to intercept SMS. The attack is rarer and more expensive than a SIM swap, but it's one reason NIST's digital identity guidelines (SP 800-63B) label SMS-delivered codes a "restricted" authenticator rather than banning them outright.
Delivery isn't guaranteed
A code that arrives after it expires, or never arrives, locks out a legitimate user. Common causes:
- The number is a landline or disconnected. Validate it when the user enrolls; our phone number validation API post covers this.
- The user texted STOP to your number at some point. On Ready, opt-outs are refused up front and not charged, which is correct for marketing but means that user gets no code. They need another way to authenticate.
- Your traffic is unregistered or misregistered and gets carrier-filtered.
- Your code is stuck in a queue behind a marketing campaign. The next section covers that one.
SMS pumping fraud
Attackers script your "send me a code" endpoint to fire texts at number ranges they profit from. You pay for every one. The defenses live in your app: per-IP and per-number rate limits, CAPTCHAs on resend, and blocking destinations you don't serve.
Why OTP traffic needs its own numbers and campaign
This is the part most 2FA guides skip. Under A2P 10DLC, every campaign is registered for a use case. Two-factor codes have their own use case (2FA), and marketing is a different one. If you send login codes on a campaign registered as marketing, you're sending traffic that doesn't match your registration. Our guide to matching the use case to what you actually send explains why that causes filtering. For the broader split between the two kinds of traffic, see transactional vs promotional SMS.
Throughput is the second reason. Carriers cap segments per second per campaign, and a fresh standard campaign without vetting gets roughly 1 to 4. Here is a worked example at an assumed 3 segments per second:
- A 5,000-contact promo at 2 segments per text is 10,000 segments.
- 10,000 ÷ 3 = about 3,333 seconds, or roughly 55 minutes to drain.
- A login code sent on the same campaign during that window waits in line behind it, and a 10-minute code is dead on arrival.
A separate 2FA campaign gets its own per-second lane. One caveat: T-Mobile's daily cap (roughly 2,000 to 200,000 messages a day depending on trust score) applies per brand, so both campaigns draw from the same daily total. More on how these limits work in SMS API rate limits.
The third reason is reputation. A marketing number collects STOP replies and spam reports. If it also carries your login codes, any filtering it attracts blocks those codes too.
What separation costs on Ready
Ready handles registration in-app. The monthly cost is $25/mo for registration and $2 per number, and most approvals take 2 to 7 days. Ready does not file a 2FA campaign on its own yet: in the app and the API a campaign is Marketing, Customer care, Mixed, Delivery notifications, Account notifications or Fraud alerts. If you need a dedicated 2FA campaign for login codes, email support@tryready.com before you register.
Sending OTP codes through Ready's API
Use the from field to pin codes to your 2FA number, and send an Idempotency-Key so a retried request never delivers two different codes:
curl -X POST https://api.tryready.com/v1/messages \
-H "X-API-Key: $READY_API_KEY" \
-H "Idempotency-Key: 3b1f8c2e-6a4d-4f7e-9c11-2d5e8a7b0f43" \
-H "Content-Type: application/json" \
-d '{"to": "+12125550199", "body": "Your Acme login code is 482913. It expires in 10 minutes. Don'\''t share it with anyone.", "consent_attested": true, "from": "+13125550142"}' consent_attested is your statement that the recipient agreed to receive the text. A user who just clicked "send me a code" has asked for that message. Full request details are in the messages API docs and the quickstart. For why retries create duplicate codes, see how to stop duplicate texts.
The quiet-hours caveat
Ready holds campaign and automation texts sent during the recipient's quiet hours for up to 24 hours, charging only when they go out, and holds an API text only if the window opens within 6 hours; otherwise the API text is refused. That protects promotional sends. It's the wrong behavior for a login code requested at 11 PM. Before you route OTPs through Ready (or any platform that enforces quiet hours), email support@tryready.com to confirm how quiet hours apply to your account. A code held until morning is useless.
Cost per code
A one-segment code costs $0.0075 plus a $0.0045 carrier fee. At 10,000 codes a month, that's 10,000 × $0.0075: $75 in rate plus the carrier fee. Resends count, so a 10% resend rate means about 11,000 segments. The rate is flat and doesn't change with volume. Raw CPaaS APIs charge less per segment. If OTP is your only traffic and you want the lowest per-code cost, they're the honest pick. Ready makes more sense when the same platform also handles registration, opt-outs and two-way texting for your customers.
Ready's all-in prices per segment include the carrier fee but not screening: Scrub lists and Number validation, the two switches in Settings under Compliance, each add $0.003 per segment when switched on, shown as screening on your bill, and both are off by default. Unless you turn the opt-out footer off, the first text to each contact, and one again from time to time, also gets "Reply STOP to opt out" and your business name if the message does not already have them, so a text close to the segment limit can bill one more segment.
When to use SMS 2FA, and what to offer next to it
A practical approach:
- Offer SMS as the floor, not the ceiling. Default to it for reach, and prompt users to upgrade to an authenticator app or passkey.
- Require stronger factors for high-value actions. Admin accounts, payout changes and anything touching money shouldn't rely on SMS alone.
- Have a fallback. Email codes or backup codes cover opted-out numbers and lost phones.
- Watch for SIM swap signals. A login from a new device right after a phone-number change deserves extra friction.
- Keep the infrastructure separate. Use a dedicated 2FA campaign and its own numbers, with plain-text codes that stay at one segment.
If you want to test the flow, sign up at tryready.com/signup. The first 20 one-to-one texts are free, so use them to test your OTP flow before you register (a card on file is required to send).
FAQ
Is SMS two factor authentication secure?
It's much more secure than a password alone, but it's the weakest of the common second factors. SIM swaps, real-time phishing and number recycling can all defeat it. Authenticator apps are stronger, and passkeys and hardware keys are stronger still.
Do I need 10DLC registration to send OTP codes?
Yes, for 10-digit long code traffic in the US. Register a campaign under the 2FA use case rather than reusing a marketing campaign. On Ready, the app and the API do not offer a 2FA campaign type yet, so email support@tryready.com before you route login codes through us. An account can send up to 500 texts a month before it registers, but production login traffic should run on a registered campaign.
Does Ready generate and verify the codes?
No. Ready sends the text through POST /v1/messages. Your application generates, stores, expires and checks the code. If you want a managed verify product that handles that logic, a CPaaS verify service is the better fit.
Should OTP codes use the same number as my marketing texts?
No. Use a separate campaign and number so codes don't queue behind campaigns, don't inherit spam filtering from marketing traffic, and match their registered use case. On Ready, ask support@tryready.com about a 2FA campaign before you route login codes through us.
Can I send 2FA codes over iMessage with Ready's blue line?
It's a poor fit. Each blue line can start up to 50 new conversations a day and send up to 2 texts to someone until they reply. Login codes go to many people who never reply, so a registered SMS number is the right channel.