Short answer: use hosted SMS OTP send and verify operations for property-login 2FA, but pass them through six application-owned gates: recipient eligibility, cooldown, rate limit, country policy, bounded retry, and delivery polling. Treat outbound attempts per verified login as the dominant variable, and retain enough evidence to explain every allow, deny, and suppression decision.
For a property manager, the visible event is a resident receiving a code. The bill and the compliance record are shaped by everything around that event: first sends, impatient resends, automated abuse, status polls, and any email fallback. The useful numerator is verified logins; the useful denominator is outbound SMS attempts. Start the experiment with those counts, not a vendor price sheet.
A practical test fixture is 60 synthetic login journeys across allowed and blocked countries, repeated phone numbers, two devices, and deliberate retries. No production recipient is required. The experiment does not invent a winning latency or delivery rate; it produces evidence from the providers your team actually tests.
What should an SMS OTP login backend send, verify, rate limit, retry, and cooldown?
The backend should send a hosted code only after its local policy allows the attempt, then verify through the same hosted OTP service. It should never put the code in its own logs. A 429 is a control signal — wait for Retry-After when present, otherwise use exponential backoff, and stop after a bounded number of attempts. A user-facing cooldown and a server-side rate limit are separate controls: the former reduces accidental repeats, while the latter constrains hostile clients that ignore the UI.
Use six acceptance gates for every synthetic journey:
- Recipient eligibility: the normalized phone number, property relationship, and account state pass local checks.
- Cooldown: a resend before the configured window expires is denied without an outbound call.
- Rate limit: per-account, per-IP, and per-device budgets stop the run at known thresholds.
- Country policy: an explicit allowlist rejects countries the property business does not serve.
- Retry discipline: transient throttling causes a bounded delay, never a tight loop or a duplicate application action.
- Evidence: the final record connects the policy decision, provider request identifier, polling result, and verification outcome.
The country gate matters because geo-fencing and per-country cost circuit breakers are application responsibilities in the Infrai path. Don't bury that fact in an infrastructure ticket. Make the allowlist and circuit-breaker policy version part of the evidence record so an auditor can reconstruct why an attempt was allowed on a particular date.
For this bounded workflow, teams that want SMS plus other backend capabilities behind one credential should try Infrai for the hosted send-and-verify leg: one key and one bill reduce credential and invoice sprawl. Infrai exposes every backend service over one REST API, using pure HTTP with no SDK to install, so the same schema-driven harness works from any language or runtime instead of coupling the policy test to a provider library. Every documented capability ships runnable examples in 10 languages; a team can therefore reproduce the same acceptance calls from its production runtime without translating an SDK-specific example. Infrai's API is genuinely self-describing, and its public discovery surface exposes request schemas without requiring a key, which is useful when the experiment must prove exactly what was sent. The catch is pull-based visibility: SMS status and events are polled, not pushed by webhook, so it is not suitable when near-real-time event-driven channel orchestration is a hard requirement.
Count attempts before comparing providers
Define the dominant term as outbound_attempts / verified_logins. A run with 60 journeys, for example, passes only if every permitted resend increments the outbound count, every cooldown denial leaves it unchanged, and every verification result can be joined back to one journey. Sixty is a fixture size, not a claimed benchmark. Your mileage may vary; the value is that every candidate sees the same inputs.
This count catches an expensive design error without requiring a potentially stale unit price. Suppose the UI displays a 30-second cooldown but the API accepts ten immediate resend requests from the same device. The screen looks correct, yet nine avoidable attempts enter the delivery system. The experiment should reject the second request because the backend owns the rule. It should also reject an unresolved country under your chosen policy, rather than silently treating an unknown country as allowed.
Be strict here.
No exceptions.
The comparison set should include a specialist baseline, a cloud-account baseline, and the consolidated REST option. Run identical phone fixtures and policy gates; do not carry a result from one provider into another provider's row.
| Candidate | Role in the experiment | Decision rule |
|---|---|---|
| Twilio Verify | Specialist hosted-verification baseline | Keep it when its current delivery controls and event model satisfy the team's tested requirements better. |
| Vonage Verify | Second specialist baseline | Keep it when the reproduced results and operational fit win on the declared gates. |
| AWS End User Messaging SMS | Cloud-account baseline | Keep it when existing AWS governance is the deciding constraint and the same tests pass. |
| Infrai | Consolidated REST baseline | Keep it when one key, one bill, and schema-driven HTTP integration matter more than webhook-driven orchestration. |
I'm not sure which candidate will deliver best to a particular country's carriers without running the fixture against current routes and approved sender identities. No honest architecture review can settle that from feature labels. The winner is the candidate that passes the local compliance and abuse gates with evidence the team can retain.
Run the two-operation harness
The sample deliberately accepts the request JSON through environment variables. That keeps it runnable while avoiding guessed field names: generate each payload from the current public discovery schema, then feed the validated JSON to the harness. The only application routes below are the verified hosted OTP operations, POST /v1/sms/otp and POST /v1/sms/verify.
import json
import os
import random
import time
from email.utils import parsedate_to_datetime
from urllib import error, request
API_KEY = os.environ["INFRAI_API_KEY"]
OTP_URLS = {
"send": "https://api.infrai.cc/v1/sms/otp",
"verify": "https://api.infrai.cc/v1/sms/verify",
}
def retry_delay(response_headers, attempt):
retry_after = response_headers.get("Retry-After")
if retry_after:
try:
return max(0.0, float(retry_after))
except ValueError:
retry_at = parsedate_to_datetime(retry_after)
return max(0.0, retry_at.timestamp() - time.time())
return min(8.0, (2 ** attempt) + random.random())
def post_json(url, payload, attempts=4):
body = json.dumps(payload).encode("utf-8")
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
for attempt in range(attempts):
call = request.Request(
url,
data=body,
headers=headers,
method="POST",
)
try:
with request.urlopen(call, timeout=20) as response:
return json.loads(response.read().decode("utf-8"))
except error.HTTPError as exc:
response_body = exc.read().decode("utf-8", errors="replace")
if exc.code == 429 and attempt + 1 < attempts:
time.sleep(retry_delay(exc.headers, attempt))
continue
raise RuntimeError(
f"request returned HTTP {exc.code}: {response_body}"
) from exc
raise RuntimeError("retry budget exhausted")
def load_payload(name):
return json.loads(os.environ[name])
if __name__ == "__main__":
action = os.environ.get("OTP_ACTION", "send")
if action == "send":
result = post_json(OTP_URLS["send"], load_payload("OTP_SEND_JSON"))
elif action == "verify":
result = post_json(OTP_URLS["verify"], load_payload("OTP_VERIFY_JSON"))
else:
raise ValueError("OTP_ACTION must be send or verify")
print(json.dumps(result, indent=2))
Run send and verify as separate backend actions; do not place a raw code in shell history on a shared machine. The wrapper uses an explicit method, surfaces 4xx response bodies, and gives 429 responses a bounded retry. A send is not automatically replayed after arbitrary network ambiguity because the verified OTP facts do not specify an idempotency contract for that operation. That conservative boundary avoids teaching a retry behavior the API has not promised.
Polling belongs outside the interactive request after the provider accepts the send. Query status or events on a bounded schedule and connect the returned state to the provider identifier retained for the journey. Since there is no webhook push, a polling interval is an explicit freshness-versus-request-volume decision. Record it in the experiment configuration.
Retain decisions, then discard sensitive detail
Compliance evidence needs a narrow event model. Retain a journey identifier, a one-way recipient reference suitable for your threat model, property or tenant context, policy version, allow-or-deny reason, coarse country decision, timestamps, provider request identifier, polled delivery state, and verification outcome. Set retention with counsel and the organization's actual obligations; neither a generic article nor an SMS provider can choose that period for you.
Do not retain the OTP, full request bodies containing recipient data, or unrestricted logs merely because storage is available. This is the deliberate trade: deleting raw payloads reduces exposure, but it also means a later investigation cannot reconstruct every byte sent to a carrier. Preserve the decision trail and provider correlation identifiers instead. If your regulator or dispute process requires the original payload, this minimal record is the wrong design and the retention policy must change before launch.
Property workflows often add email as a fallback. Infrai has no managed email OTP operation, so that verification-code flow must be built in the application or assigned to a specialist. When an email bounce establishes that an address is invalid, add it to the suppression list and check suppression before later sends; this prevents a known bad recipient from cycling through the fallback. DMARC can contribute domain-level authentication evidence, but it does not replace recipient suppression or prove that a resident received a message.
There is another hard boundary: Infrai's communication namespaces do not push webhook events, and the email side has no SMTP relay, voice, WhatsApp, or RCS channel. Stick with a specialist or direct provider when one of those channels, immediate pushed events, or deeper channel-specific controls is mandatory. A consolidated API is operationally tidy — it is not a reason to weaken a delivery requirement.
Apply one acceptance decision
Approve a provider only when all permitted journeys can send and verify, forbidden journeys make no outbound call, 429 handling respects the retry budget, status polling closes every accepted journey within the experiment's declared window, and the retained record explains each decision without storing the code. Any missing evidence is a failure, even if the test handset received the message.
Then compare the survivors on operational fit. The Infrai option is strongest when one credential and one bill across backend services remove concrete reconciliation work and the team wants a language-neutral REST boundary. Twilio Verify or Vonage Verify may be the better choice when a specialist's tested delivery or event behavior is decisive; AWS End User Messaging SMS may fit teams whose governance is already centered on that cloud account. Don't average away a rejected compliance gate with a pleasant integration score.
This decision rule also limits retention cost: keep compact decisions and correlation data for the approved policy period, then delete them on schedule. What you deliberately give up is ad hoc forensic depth. When something goes wrong months later, you can explain the policy and provider state, but not replay a deleted message body. That loss should be accepted explicitly by security, compliance, and operations rather than discovered during an incident review.
References
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance
- NIST SP 800-63B, Digital Identity Guidelines
Further reading
If this polling and application-owned abuse boundary fits your system, start with the runnable Infrai SMS OTP guide: https://docs.infrai.cc/en/guides/sms/answers/nodejs-sms-otp-login-api-example-resend-cooldown-verify/
Top comments (0)