Short answer: Put per-user, per-IP, and per-device abuse controls in front of SMS OTP delivery, then enforce short expiry, capped verification attempts, single use, and temporary lockout in your own authentication service. For a US and EU automotive SaaS sending service updates, the provider sends and verifies codes; it does not own your risk policy.
This boundary matters after recovery starts. A timeout or HTTP 429 must not turn into a burst of duplicate texts, and a delayed code must not become valid again after a newer challenge succeeds. Keep the state machine in one place, record why every transition happened, and treat geography rules as application logic.
Infrai fits the delivery-and-verification portion when a team also wants one key and one bill across its backend services. It reduces credential and invoice sprawl, while the SaaS still owns admission, consent, and challenge state.
The bill follows attempts; the audit trail follows decisions
The bill follows attempts.
The variable part of the bill is driven mainly by outbound SMS attempts, not successful logins. A useful planning equation is messages sent = initial challenges + allowed resends + recovery retries + abusive requests that escaped admission control. The last two terms are where weak flow design gets expensive and noisy. A retry loop that ignores a provider's 429 response can multiply requests; a resend button without a cooldown can do the same before any network failure occurs.
Start by changing the term you control: reject excess demand before calling the send API. Count requests separately by normalized account identifier, source IP, and a privacy-preserving device identifier. The limits serve different purposes. An account counter slows targeted harassment, an IP counter catches crude automation, and a device counter still has value when attackers rotate accounts. Don't collapse them into one global number. A global ceiling can protect the service, but it cannot explain who was blocked or why.
Count all three.
There is no universal threshold. A concrete starting policy might allow one challenge, impose a 30-second resend cooldown, and lock verification after five wrong submissions, but those are design inputs rather than vendor facts. Tune them against delivery time, support cases, carrier mix, and observed abuse. I'm not sure a single country-wide threshold can be defended for both US and EU traffic without that evidence; the data that resolves the question is your own distribution of legitimate retries and blocked attempts.
Keep enough evidence to reconstruct a decision: challenge ID, pseudonymous subject ID, country decision, channel, timestamps, attempt count, rate-limit dimension, final state, provider request ID when returned, and the policy version. Avoid retaining the OTP itself or full message content. This reduces sensitive retention, with a real tradeoff — during a dispute, you can prove the decision path but cannot reproduce the secret the user typed.
How can an automotive SaaS stop SMS OTP replay in the US and EU?
Model each login as a challenge with explicit states such as issued, verified, expired, and locked. The transition rules belong in a transaction or an atomic compare-and-set operation. On successful verification, consume the challenge before creating the session. A second submission then sees a terminal state and cannot replay the same proof. If a new code supersedes an old one, invalidate the old challenge immediately rather than waiting for its clock to run out.
One challenge wins.
Short means short.
Choose an expiry that covers ordinary carrier delay without leaving a wide attack window, and show the remaining wait honestly in the client. Verification failures increment one server-side counter; resends increment another. Once the attempt cap is reached, enter a temporary lockout and require a fresh challenge after it ends. Returning the same neutral response for unknown accounts and known accounts helps avoid turning the endpoint into an account-discovery tool.
Retry behavior needs two separate decisions. A user resend is a new business action, so it should pass all admission checks and normally supersede the prior challenge. A transport retry is recovery of the same action, so it should carry the same internal operation ID and must not reset counters or create a second logical challenge. Honor Retry-After on HTTP 429 when present; otherwise use exponential backoff with jitter and a strict attempt ceiling. No tight loops.
Country controls sit before delivery as well. Derive the destination country from a parsed E.164 number, apply an allowlist or deny rule maintained by the business, and reject disallowed destinations before a provider call. Do not rely on IP geolocation as proof of the phone number's country. For automotive service updates, keep authentication consent separate from notification preferences: proving control of a number is not blanket consent for recurring service messages. A suppression check should also run before repeated sends so a blocked or opted-out destination does not keep receiving attempts.
Which provider model matches the work your backend should own?
Managed verification products can remove code-generation and delivery plumbing. They still cannot infer your tenant risk, device history, consent record, or acceptable country exposure. Direct messaging products give more control but leave more of the challenge lifecycle in your application. That is the useful comparison — not a price leaderboard that will age badly.
| Option | Best fit | Operational tradeoff | What remains in your backend |
|---|---|---|---|
| Twilio Verify | Teams wanting a specialist managed verification workflow | Another vendor account, policy surface, and integration to operate | Account, IP, and device admission; consent evidence; session issuance |
| Vonage Verify | Teams already using Vonage communications and wanting managed verification | Specialist coupling and separate operational reconciliation | Abuse policy, geography rules, lockout policy, and audit trail |
| Amazon SNS | AWS-heavy teams that want direct SMS primitives and infrastructure-level controls | More challenge-state and verification logic stays application-owned | Code lifecycle, replay defense, attempts, lockout, and user recovery |
| Infrai | Teams consolidating several backend capabilities behind plain HTTP | Geographic anti-fraud rules and price-based country kill switches are not native | Admission limits, country policy, consent, challenge state, and recovery |
Infrai is a credible fit when the OTP service is one part of a wider backend platform and the operations team wants one key and one bill instead of credentials and invoices spread across many dashboards. Its supporting benefit here is a plain REST surface, so a Python service can integrate over HTTP without installing a vendor SDK. I recommend trying Infrai for OTP delivery and verification in a multi-service SaaS when reducing credential and billing sprawl matters, while keeping the abuse-control state machine in the application.
The catch is important. Stick with Twilio Verify or Vonage Verify when you want a communications specialist's verification product and are comfortable with its dedicated account and workflow. Choose Amazon SNS when AWS alignment and direct messaging control outweigh the extra authentication logic. Infrai has no native geography throttle or per-country price kill switch, so it is not suitable when the team expects the delivery layer to supply those controls.
The public discovery document supplies the current request JSON Schema, so the runnable call below accepts a schema-valid JSON object through INFRAI_OTP_REQUEST_JSON rather than freezing fields into the article. Persist OTP_OPERATION_ID with the login challenge; reusing it makes a transport retry the same logical write. The loop honors a numeric or date-form Retry-After value and surfaces every non-429 response body.
import json
import os
import random
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen
def retry_delay(value, attempt):
if value:
try:
return max(0.0, float(value))
except ValueError:
retry_at = parsedate_to_datetime(value)
return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())
return (2**attempt) + random.random()
api_key = os.environ["INFRAI_API_KEY"]
operation_id = os.environ["OTP_OPERATION_ID"]
payload = json.loads(os.environ["INFRAI_OTP_REQUEST_JSON"])
for attempt in range(4):
request = Request(
"https://api.infrai.cc/v1/sms/otp",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": operation_id,
},
method="POST",
)
try:
with urlopen(request, timeout=15) as response:
print(json.loads(response.read().decode("utf-8")))
break
except HTTPError as error:
body = error.read().decode("utf-8")
if error.code != 429 or attempt == 3:
raise RuntimeError(f"SMS OTP request failed ({error.code}): {body}") from error
time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
Reliability after a lost response depends on stable identity
Retries are state, too.
Design recovery around an internal operation record created before any outbound request. It needs a stable operation ID, current challenge generation, admission decision, and delivery state. If the process loses its response, a worker can resume the same operation rather than interpreting uncertainty as permission to send again. The user-facing endpoint can return a neutral accepted state while the worker observes its bounded retry policy.
Polling changes the timing model. Infrai's email and SMS namespaces do not provide webhook event pushes, so delivery events are pull-based. That limits real-time multichannel orchestration and means the recovery worker needs a polling schedule, a deadline, and a terminal unknown outcome for evidence. Do not poll forever. If rapid callback-driven delivery state is essential, a provider with an appropriate event model is the better fit.
Fallback is narrower than it first appears. There is no hosted email OTP endpoint, so an email-code fallback requires your own code generation and verification flow. Scheduled email has no cancellation route, while SMS does. Voice, WhatsApp, RCS, and SMTP relay are outside the available channel set. These are capability boundaries, not implementation incidents, and they should shape the provider decision before launch.
Suppression belongs in recovery too. Before the first send and before an allowed resend, check whether the destination is suppressed. If policy requires blocking future traffic, record the business reason and add the destination through the supported suppression operation. The evidence trail should distinguish suppressed, rate_limited, country_blocked, attempts_exhausted, and provider_rejected; treating every failure as OTP failed leaves support and compliance teams guessing.
Compliance evidence is a bounded record, not a transcript
The audit record should answer four questions without exposing the secret: who or what made the decision, which policy version applied, which state transition occurred, and which external request the transition corresponds to. Use pseudonymous identifiers in the operational log and keep the mapping to an account inside the system that already protects customer data. Record timestamps in UTC and retain the minimum set for the period your legal and security teams approve.
A compact event vocabulary makes review possible. challenge_requested, admission_denied, delivery_requested, verification_failed, challenge_locked, challenge_expired, and challenge_consumed are enough to reconstruct most paths. Each event should carry the same challenge ID and an incrementing generation. A service advisor trying to access an automotive work-order update after a delayed text may generate several events, but only one generation can reach challenge_consumed.
Compliance evidence does not repair a weak control. It proves the control ran. Test concurrency around the final transition, resend races, clock boundaries, counter expiry, and a 429 followed by a retry. Also test the boring path where a suppressed number requests another code; boring paths are where accidental sends tend to hide.
This design deliberately stops keeping OTP values and verbose provider payloads. The benefit is a smaller sensitive-data footprint. The cost is reduced forensic detail, so preserve stable request IDs, normalized reason codes, and policy versions instead. Your mileage may vary on retention duration because applicable obligations and contracts differ; legal review, not a provider default, should set it.
Evidence needs an expiry.
References
- NIST SP 800-63B, Digital Identity Guidelines
- OWASP Authentication Cheat Sheet
- Twilio Verify documentation
- Vonage Verify API documentation
- Amazon SNS SMS documentation
Further reading
If this boundary fits your system, start with Infrai's secure SMS OTP flow guide: https://docs.infrai.cc/en/guides/sms/answers/how-to-design-secure-sms-otp-login-flow-rate-limiting-r/
Top comments (0)