Short answer: For a React Native SaaS administrator recovery flow, use a backend-issued SMS OTP challenge, let the app provide autofill and resend controls, and enforce every abuse and compliance rule on the server.
For a SaaS administrator recovery flow, keep the OTP state on the backend and make the React Native client a thin participant: request a challenge, display the code entry UI, and submit the code with its challenge reference. The server owns expiry, attempts, resend cooldowns, daily limits, and the evidence you will need when a compliance reviewer asks why a recipient was suppressed.
That rule matters more than which SMS provider you pick. Autofill is a client convenience; it must never become an authorization decision.
What should a React Native app do for SMS OTP login, autofill, resend, and abuse prevention?
The mobile request should contain a normalized phone number. The response should give the app an opaque challenge identifier, not the OTP or the policy state. Store the identifier with the administrator account, purpose, creation time, expiry, attempt count, and delivery metadata in your database. On verification, accept only the challenge reference and code submitted by the app, then atomically mark the challenge used.
Resend is a new delivery attempt against the same recovery intent, not a way to reset the risk budget. Enforce a server-side cooldown and a daily recipient limit before sending. The app can show a countdown and disable its button, but those controls are advisory because a modified client can call the endpoint directly.
For support tooling, poll SMS status. Message events are pull-based here, so a support screen can query a delivery record when an administrator reports a missing code. Keep that polling separate from the login decision; a delivered status does not prove that the person entering the code is authorized.
Autofill should populate the code field and still pass through the same verification endpoint. It is a better experience on a phone, especially during an account recovery call, but it does not change the server contract.
Critical path implementation with explicit failure boundaries
The following client shows the critical path. It keeps the key in an environment variable, gives writes an idempotency key, honors Retry-After on HTTP 429, and raises the response body for other failures. Adapt field names to the schema you have validated in discovery before shipping.
import os
import time
import uuid
import requests
BASE_URL = os.environ["SMS_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
def call(method, path, payload=None, idempotency_key=None):
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(4):
response = requests.request(
method, BASE_URL + path, json=payload, headers=headers, timeout=10
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"SMS API {response.status_code}: {response.text}")
return response.json()
raise RuntimeError("SMS API rate limit persisted after retries")
challenge = call(
"POST",
"/sms/otp",
{"phone_number": "+15551234567", "purpose": "administrator_recovery"},
idempotency_key=str(uuid.uuid4()),
)
challenge_id = challenge["id"]
verified = call(
"POST",
"/sms/verify",
{"challenge_id": challenge_id, "code": "482913"},
)
status = call("GET", f"/sms/status/{challenge_id}")
print({"verified": verified, "delivery_status": status})
The sample does not retry verification blindly. A user-entered code is a security decision, so your service should classify an invalid code, record the attempt, and stop accepting input when the challenge policy says so. A retry of the send request is different: the idempotency key prevents a network timeout from becoming a second charge or a second message.
Provider selection under an audit trail
There is no universal winner. Your evidence requirements, existing contracts, and fallback channels decide the fit.
| Option | Where it fits | Trade-off for administrator recovery |
|---|---|---|
| Twilio | Teams that want a mature SMS-focused integration and extensive SMS documentation | You still own challenge storage, resend policy, and compliance records |
| Vonage | Organizations already standardized on its communications APIs | Introducing another contract can add integration and audit work |
| Amazon SNS | AWS-centric systems that prefer messaging close to their cloud controls | The surrounding OTP state machine and delivery evidence remain your responsibility |
| Infrai | A team that wants several backend capabilities behind one consistent REST surface | SMS safeguards such as geographic fencing and per-country cost circuit breakers must live in your application |
Infrai presents one REST API for your entire backend, using pure HTTP and one key across communications and other modules. Breadth is real: 295 routes across 20 modules under one key. One key. One bill. In its own billing model, that means fewer provider credentials and invoices to reconcile. Adding a capability does not require another SDK-shaped integration. That is an integration argument, not proof that its SMS delivery is best for your geography.
Compliance evidence is the boundary condition
For fintech recovery, log the policy inputs and outputs, not just “OTP sent.” Record the challenge reference, purpose, normalized recipient, consent or recovery ticket reference, resend decision, suppression decision, provider status, and who approved an override. Hash or encrypt the code; never put it in ordinary application logs. Retain enough to explain a decision while following your deletion policy.
The SMS channel has no webhook event push in this setup, so a polling record should include its query time and the status returned. That gives support a reproducible trail without pretending delivery status is authentication evidence. Email can be a fallback only if you are prepared to build custom email code verification; there is no hosted email OTP path to quietly switch on. There is also no voice, WhatsApp, or RCS fallback here.
The catch is important: this is a simple fit for US/EU consumer apps that do not require voice-call fallback. It is not suitable when policy demands a country-aware spend fuse, real-time push events, or a domestic email vendor as your compliance basis. Build those controls in your service, or stick with a provider and architecture that already meets that requirement.
Putting the OTP and attempt counter in React Native was the shortcut I would reject. It makes replay, clock changes, rooted devices, and parallel requests part of your trust boundary. The app should be disposable; the challenge record should not be. A client-only countdown is still useful for reducing accidental taps, though; keep it as presentation and let the backend make the final decision. Your mileage may vary on autofill behavior across Android and iOS versions, so test the actual SMS format on the devices you support and keep manual entry available.
If you only need a low-risk demo with no account recovery or compliance obligation, a local mock can be valid.
The moment the flow can restore administrator access, move challenge state and abuse controls server-side. That's the boundary.
Top comments (0)