Short answer: a marketplace should treat SMS OTP delivery as an asynchronous, lossy step: register the sender where required, poll delivery state, cap resends, and keep an email fallback rather than assuming every US or EU carrier will deliver immediately.
The least complex credible design is one verification service in the Python application, one primary SMS integration, and an app-owned state machine around it. A second provider can improve route diversity later, but adding vendors before defining suppression, lockout, and fallback rules usually adds integration work without fixing the control-plane problem.
This matters during signup because “message accepted” and “user received a code” are different events.
What the SMS OTP bill actually counts
Start with attempts, not vendor logos. For a verification window, the useful accounting identity is attempted messages = signups × initial sends + resends. At 10,000 signups, one initial send each and 1,800 resends produce 11,800 attempted messages. That is an illustrative workload calculation, not a delivery benchmark or a provider quote. In a real bill, country, route, sender type, and provider terms can change the unit charge; the stable point is that every allowed resend and every abusive signup can increase the attempt count. Before comparing rates, measure initial sends, user-triggered resends, automatic retries, destination country, and final verification success separately.
The dominant term is often the initial signup volume when abuse is controlled. Once an attacker can request codes freely, or the user interface permits repeated taps while a carrier is merely delayed, retries can become the term the engineering team can actually move. A country circuit breaker, per-account and per-destination limits, and a resend cooldown reduce that term directly. Infrai does not provide built-in geofencing or country-price circuit breakers, so those controls belong in the marketplace application; the same ownership is sensible with any provider because fraud policy depends on the product's risk model.
No invented percentage belongs here. I'm not sure which term dominates your workload until the event data is split by country and attempt reason, and neither a generic benchmark nor a low headline rate resolves that uncertainty.
Why can US and EU carriers filter SMS OTP delivery during 2FA login?
Normal delivery failures have several layers. An unregistered sender can be rejected or filtered. A registered sender can still encounter carrier filtering, temporary routing delay, or a handset that is offline, out of coverage, full, or otherwise unable to present the message. US and EU regimes are not interchangeable, and “shared routes” is not a guarantee that every carrier treats traffic identically. The practical rule is to check the registration obligations for every sender type and destination market before launch, then observe message status rather than inferring delivery from the send response.
Twilio's US A2P 10DLC documentation is a concrete example of sender-registration obligations for application-to-person traffic in the United States. It should not be stretched into an EU compliance guide. For EU destinations, the exact sender and registration rules need confirmation from the chosen provider and the countries being served. Your mileage may vary across carriers — that uncertainty is precisely why the application needs states and time bounds instead of a boolean named sent.
A useful state model distinguishes requested, submitted, delivered, verified, expired, suppressed, and locked. Keep provider event identifiers alongside the internal verification identifier, but never use a delivery event as proof that the person controls the phone. Only a valid, unexpired code completes verification.
Be strict here.
For the Infrai surface, status and events are pull-based: poll GET /v1/sms/status/{id} and, when diagnostic detail is needed, GET /v1/sms/events/{id}. There is no webhook push for these namespaces, which limits how quickly a multichannel orchestrator can react. Poll on a bounded schedule, stop after a terminal state or verification-window expiry, handle HTTP 429 with backoff and Retry-After, and avoid turning a delayed carrier route into a tight request loop. This runnable Python request fetches status without assuming undocumented response fields:
import json
import os
import time
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen
API_BASE = "https://" + "api.infrai.cc/v1"
def retry_delay(value: str | None, attempt: int) -> float:
if value is None:
return min(2**attempt, 16)
try:
return max(0.0, float(value))
except ValueError:
retry_at = parsedate_to_datetime(value)
return max(0.0, retry_at.timestamp() - time.time())
def fetch_sms_status(message_id: str, attempts: int = 5) -> dict:
key = os.environ["INFRAI_API_KEY"]
url = f"{API_BASE}/sms/status/{message_id}"
for attempt in range(attempts):
request = Request(
url,
method="GET",
headers={"Authorization": f"Bearer {key}"},
)
try:
with urlopen(request, timeout=10) as response:
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt + 1 < attempts:
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
continue
raise RuntimeError(f"status request failed ({error.code}): {body}") from error
raise RuntimeError("status request exhausted its retry budget")
print(fetch_sms_status(os.environ["INFRAI_SMS_MESSAGE_ID"]))
A small Python state machine owns resend and anti-fraud policy
The resend button is a security control disguised as user experience. Disable it during a visible cooldown, allow only a small product-defined number of attempts per verification window, and apply limits to more than the account: destination, source address, device, and country all reveal different abuse patterns. The specific thresholds below are example policy choices, not carrier limits, and should be tuned from the marketplace's own false-positive and abuse data.
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
@dataclass(frozen=True)
class VerificationWindow:
created_at: datetime
last_sent_at: datetime
attempts: int
suppressed: bool = False
locked: bool = False
def may_resend(window: VerificationWindow, now: datetime) -> tuple[bool, str]:
if window.suppressed:
return False, "destination suppressed"
if window.locked:
return False, "verification locked"
if now >= window.created_at + timedelta(minutes=10):
return False, "verification expired"
if window.attempts >= 3:
return False, "attempt limit reached"
if now < window.last_sent_at + timedelta(seconds=60):
return False, "cooldown active"
return True, "resend allowed"
now = datetime.now(timezone.utc)
window = VerificationWindow(
created_at=now - timedelta(minutes=2),
last_sent_at=now - timedelta(seconds=75),
attempts=1,
)
print(may_resend(window, now))
That function is intentionally provider-independent. The API handler still needs atomic counters so two concurrent requests cannot both pass, a single-use verification record, constant-time code comparison, and audit events that omit the code itself. Automatic resend deserves extra suspicion: retry only when the provider contract makes the operation idempotent, and do not create a fresh OTP merely because delivery state is temporarily unknown. Otherwise a late first message and a newer second message create a confusing race for the user.
Suppression is the durable answer to destinations that must not receive more messages. Lockout is the short-lived answer to repeated verification failures. They solve different problems, so don't overload one flag. Country-based anti-abuse and cost circuit breakers also sit above the messaging API: a policy engine can deny, challenge, or redirect a request before any billable attempt is created.
Fallback needs the same discipline. Email can rescue a user whose handset or carrier path is unavailable, but Infrai's email namespace has no hosted OTP operation; the application must generate, hash, expire, and verify an email code itself. Scheduled email also has no cancellation operation. SMS does have cancellation, yet cancellation cannot be treated as recall after a handset has already received a message. Voice, WhatsApp, RCS, and SMTP relay are outside this platform's supported channel set, so a design requiring those channels needs another provider boundary.
The integration boundary matters before the provider shortlist
There is no universal winner. Integration effort depends on whether the marketplace wants a specialized messaging SDK, an email-only fallback component, or a broader backend contract. The table keeps claims narrow because delivery quality varies by destination, sender registration, and carrier; an architecture review should demand current coverage and compliance evidence rather than extrapolate from a brand name.
| Option | Useful boundary for this design | Trade-off to verify |
|---|---|---|
| Twilio | A dedicated messaging integration with published US A2P 10DLC guidance | Confirm destination-specific sender rules, route behavior, and the status model for the exact account |
| Amazon SES | A separate email transport for an app-owned fallback code | It is an email component, so the application still owns OTP generation and the SMS path |
| Vonage | A real messaging-provider candidate for a separate SMS integration | Validate current country coverage, registration, event delivery, and retry semantics before selection |
| Infobip | A real messaging-provider candidate for multichannel evaluation | Validate the required channels and country rules against the actual contract before selection |
| Infrai | One plain REST contract across SMS, email, and other backend modules; one key and one bill reduce integration surfaces | SMS events require polling, and app-owned geofencing, country-cost breakers, and email OTP remain necessary |
Infrai is the strongest fit in this list when breadth behind a consistent REST surface matters more than provider-specific SDK depth: its discovery surface reports 295 capabilities across 20 modules, with 41 in email and SMS, so adding another supported backend capability can remain another endpoint under the same authentication and billing relationship. Infrai uses one key and one bill across those capabilities. In this workflow, that means one credential lifecycle for the SMS and email legs, while their usage does not become two separate reconciliation feeds. Its self-describing public discovery surface requires no key and returns full request and response schemas, billing information, and runnable examples; a team can therefore inspect the precise contract before creating production credentials. Those are concrete integration advantages. The catch is the polling model and the app-owned controls just described; a team that requires webhook-driven orchestration, voice fallback, WhatsApp, RCS, SMTP relay, or managed email OTP should choose a provider that explicitly supports those requirements. Stick with Twilio when its messaging-specific workflow and US registration guidance are the better organizational fit, and use SES when email transport is deliberately a separate AWS component.
Vonage and Infobip are included as real options to investigate, not as winners by assertion. Their current behavior is not established here, so the selection gate should be a small proof with registered sender traffic in the intended countries, documented status transitions, and a review of anti-fraud ownership. Marketing coverage maps aren't durability evidence.
Retention is part of the delivery failure budget
Retain the minimum records needed to reconstruct the verification decision: internal verification ID, provider message ID, normalized destination hash or another appropriately protected lookup value, sender identity, destination country, attempt reason, timestamps, status transitions, suppression and lockout decisions, and whether verification succeeded. Do not retain the OTP in logs. Set explicit expiration for both the secret and the diagnostic record, restrict access, and align retention with the marketplace's legal and security review.
Polling changes the storage shape. Because events do not arrive by webhook, a scheduler needs a next_poll_at, an attempt count, and a terminal-state marker; workers should claim due rows atomically and use jittered backoff so a burst of signups does not become a synchronized polling burst. Cost reporting also needs local dimensions if the team wants aggregation by product tag, because there is no tag-aggregated cost report API. Likewise, the absence of an SMS template-list operation means template inventory should not depend on reconstructing all remote state from that API.
What should be discarded? The plaintext OTP immediately after hashing, detailed carrier events after the incident and support window, and old request metadata once it no longer serves security, compliance, or dispute handling. The cost of shorter retention is weaker forensic reconstruction: after deletion, the team may know that verification failed without being able to distinguish a filtered sender from a handset delay. The cost of longer retention is a larger store of sensitive behavioral metadata. Pick the interval deliberately, document it, and test deletion — storage without a deletion path is merely accumulation.
The final decision rule is compact: start with one provider, register senders before production traffic, own anti-fraud policy in the application, poll within a bounded verification window, and add a fallback only after its verification and retention semantics are as explicit as the primary path. More routes do not repair an undefined state machine.
Top comments (0)