A one-time passcode has a short useful life. A promo that lands four minutes late still gets read. A login code that lands four minutes late gets a support ticket, a second "resend" tap, and sometimes a user who gives up. Most OTP delivery problems come from setup decisions made months earlier: which campaign the codes were registered under, which numbers they share with marketing, and how much traffic is ahead of them in the queue.
This post is for product and engineering teams sending verification codes at volume. It covers registering the right use case, keeping OTP traffic separate from marketing, sizing throughput, and what to expect from delivery speed when you send through our API. We make Ready, so the API examples are ours. The setup advice applies to any registered US sender.
"Bulk" OTP is not a bulk send
People search "bulk SMS for OTP," but verification traffic is the opposite of a campaign. Every code is unique, every send is triggered by one user action, and every message is time-critical. That shapes the API call.
On Ready, the bulk endpoint (POST /v1/messages/bulk, up to 100 recipients per call) sends the same body to many numbers. That is wrong for codes. Each OTP goes out as its own POST /v1/messages request, fired the moment the user taps "send code":
curl -X POST https://api.tryready.com/v1/messages \
-H "X-API-Key: $READY_API_KEY" \
-H "Idempotency-Key: 6f1c2b8e-3d4a-4f7e-9a21-0c5d8e7b1a44" \
-H "Content-Type: application/json" \
-d '{"to": "+12125550199", "body": "Your Acme code is 482913. It expires in 10 minutes. Do not share it.", "consent_attested": true, "from": "+12125550142"}' Three details matter here.
Idempotency-Key: generate one per code request, and reuse it if your HTTP client retries after a timeout. A retried request never sends twice, so the user doesn't receive two different codes and type the stale one. We go deeper on this in how to stop duplicate texts from your SMS API.from: pin codes to the number(s) on your verification campaign (more on that below), instead of letting them go out from whatever number marketing uses.consent_attested: this is your promise that the recipient agreed to receive the text. For OTP, the user just asked for it. Ready records the attestation per send (who sent, to how many, when). It is not a per-contact consent record, so your app should keep its own log of the request: the timestamp, the phone number and the screen it came from.
Full request options are in the messages API docs.
Register a verification use case, not "mixed" or "marketing"
Unregistered A2P traffic gets carrier-filtered, and OTP traffic is no exception. When you register a 10DLC campaign, you pick a use case. Carriers offer a dedicated two-factor authentication category, and that is where login codes, signup verification and password resets belong.
On Ready, the app and the registration API don't offer the 2FA use case yet: the app has no use-case choice, and POST /v1/registration/campaign takes MARKETING, CUSTOMER_CARE, MIXED, DELIVERY_NOTIFICATIONS, ACCOUNT_NOTIFICATIONS or FRAUD_ALERTS. To file a separate verification campaign, email support@tryready.com before you submit.
Why not fold codes into an existing marketing or mixed campaign?
- Carriers judge content against the declared use case. A campaign registered for promotions that suddenly sends "Your code is 482913" looks like a mismatch, and mismatches get filtered. The reverse also happens: a 2FA campaign that starts carrying discount offers is a quick route to throttling. Our guide on matching use case to what you actually send covers this in detail.
- Sample messages are easy to write. A 2FA campaign's samples are your actual templates: "Your Acme code is 123456. It expires in 10 minutes." No guessing.
- Opt-in is clean. The user typed their number and requested a code. That flow is simple to describe in the registration form.
On Ready, registration happens in the app or through the API (status is readable at GET /v1/registration). There is no setup fee. The monthly cost is $25/mo for registration + $2 per number, and the charge at submit is the first month, $25. Most approvals take 2 to 7 days. If you're a platform registering each customer's brand under its own sub-account, the 10DLC registration docs walk through the flow.
Before you register, an account can send up to 500 texts a month at its normal per-text rate. That is enough to build and test your OTP flow. The first 20 texts are free one-to-one sends (a card on file is required), and the free texts end once a registration is paid for, so use them during development before you submit.
Keep OTP traffic separate from marketing
If you register only one thing, register a dedicated verification campaign with its own number. Here is why, with math.
Throughput is per campaign
Carriers meter throughput in segments per second, per campaign. A fresh standard campaign with no vetting typically gets around 1 to 4 segments per second. Higher use-case tiers get tens, roughly 10 to 75. Carriers publish no official rate card, so treat these as round approximations.
Worked example (assumptions stated): your marketing team sends a 175-character promo with an emoji to 5,000 contacts. The emoji drops the limit to 67 characters per multipart segment, so that's 3 segments each, or 15,000 segments. At an assumed 4 segments per second on a single shared campaign, the promo takes 15,000 ÷ 4 = 3,750 seconds, a little over an hour to drain.
Any login code requested during that hour joins the same queue. If your codes expire in 10 minutes, some of them arrive dead.
Put codes on their own campaign and that promo can't delay a password reset, because each campaign's per-second allowance is separate.
The daily brand cap is shared
There is one catch. T-Mobile's daily cap applies to the brand, not the campaign. Depending on trust score, it is roughly 2,000, 10,000, 40,000, 100,000 or 200,000 messages a day, lowest score first. AT&T throttles per second and per minute rather than with a hard daily cap. A separate campaign protects your per-second lane, but a huge marketing send can still eat into the brand's daily T-Mobile allowance that your codes also draw from. If you're near the bottom tier and your verification volume matters, plan marketing volume around it.
Brand vetting ($79 Standard / $179 Enhanced, one-time) raises the trust score. It moves the daily cap only when the new score crosses a tier, and it does not change the per-second rate. For a small app sending a few hundred codes a day, standard registration is usually all you need.
Cost of the separation
A second campaign is $20/mo plus $2 for its number. Compared with lost logins, that's usually an easy call.
Write codes that stay at one segment
Every segment is billed, and a single segment also delivers as one piece. GSM-7 text fits 160 characters in one segment. One emoji or other non-GSM character drops the limit to 70.
Your Acme code is 482913. It expires in 10 minutes. Do not share it.→ about 68 characters, GSM-7, 1 segment.- Add a lock emoji and a "Didn't request this? Ignore this message." line and you are past 70 unicode characters, which means 2 segments.
Ready swaps curly quotes, long dashes and ellipses for plain ones before sending, so a pasted apostrophe won't silently double your count. Emoji still will. Keep codes plain.
Worked cost example: 50,000 one-segment codes a month on Standard costs 50,000 × $0.0075 at the Standard rate, plus 50,000 × $0.0045 in carrier fee, plus $20/mo for the verification campaign and $2/mo for its number. Brand fees are shared with your other campaigns.
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.
To be honest about price: if OTP is the only thing you send, at high volume, a raw CPaaS can make more sense. Twilio's list rate is around $0.0083 per segment, well below our $0.0075 Standard rate. The case for Ready rests on registration handled in-app, sub-accounts for multi-tenant platforms, and opt-outs, quiet hours and number validation built in, not on the per-segment price.
Delivery speed: what to actually expect
On a registered route with a clean campaign, codes typically arrive within seconds. No carrier guarantees a delivery time, though, and these things slow codes down:
- Queueing behind your own traffic (the shared-campaign problem above).
- Bursts beyond your per-second rate. If 600 users hit "log in" in the same minute after a push notification, an assumed 4 segments per second clears 240 a minute, so the tail waits roughly two and a half minutes. Spread your push notifications, or size throughput for your peak. Our post on SMS API rate limits covers this.
- Handset issues: phone off, no signal, full inbox. You can't fix those.
Set your product up to absorb the variance:
- Expire codes at 5 to 10 minutes, not 60 seconds.
- Show "resend" after 30 to 60 seconds, and make the newest code valid without breaking the previous one for its remaining life, or invalidate it clearly in the UI.
- Rate-limit resends per number so a stuck user (or an attacker) can't burn your daily cap.
- Offer a non-SMS fallback: email, an authenticator app, or a voice call.
Two Ready behaviors that affect OTP
- Opt-outs are refused up front and not charged. A user who once texted STOP to your number won't receive codes from it. Your UI should detect that case (you can check
GET /v1/opt-outs/:phone) and route them to the fallback instead of looping on "resend." - Quiet-hours enforcement holds sends outside permitted local hours. A held text is charged only when it sends. That is the right default for marketing and the wrong behavior for a code someone requested at 11 PM. A code requested during the recipient's quiet hours is held or refused, so a login code asked for at 11 PM can come back as a 409 QUIET_HOURS_BLOCK. If your users sign in at night, email support@tryready.com before launch and send your codes from a fallback channel until we reply. A held code is a useless code. Campaigns, drip campaigns and automations wait up to 24 hours for the window; a one-to-one or API text is held only if the window opens within 6 hours, otherwise it is refused with 409 QUIET_HOURS_BLOCK and must be resent; a one-to-one reply to someone who texted you in the last 24 hours goes out at any hour.
Validating numbers at signup (GET /v1/validate/:phone) catches landlines and disconnected numbers before a user waits for a code that can't arrive. Each answered lookup costs $0.003. The standalone litigator scrub is less relevant here, since a user requesting their own code is about as consented as traffic gets.
One more limitation: the blue line (iMessage) isn't built for OTP volume. Each line can start up to 50 new conversations a day, and that cap rules it out for signups. Keep codes on SMS.
The practical checklist
- Email support@tryready.com before you submit to file a dedicated 2FA/verification campaign with its own number; send codes with
fromset to it. - Send each code as its own
POST /v1/messageswith a per-requestIdempotency-Key. - Keep the template plain GSM-7 and under 160 characters.
- Size throughput for your login peak, and remember the T-Mobile daily cap is per brand.
- Expire codes at 5 to 10 minutes, throttle resends, and offer a non-SMS fallback.
- Handle opted-out numbers and confirm quiet-hours behavior before launch.
For the broader picture on verification flows, fallbacks and compliance, read our SMS OTP and verification API guide. If you want to try the flow end to end, create an account, wire up a test send during development, and register the verification campaign once your templates are final.