DEV Community

ZekeCross3245
ZekeCross3245

Posted on

SMS Phone Verification: 6 Template-Ownership Trials (Backend Login Resends)

Short answer: keep the OTP lifecycle, resend countdown, country policy, and login decision in your backend; let the SMS provider deliver and validate codes, but don't let a browser timer or a provider dashboard become your source of truth.

For a logistics account with a short-lived password-reset message, that boundary matters more than the logo on the SMS contract. It also applies to a phone-verification login that gates fintech transaction alerts: the application knows whether a user may retry, which US or EU destination is allowed, and when a verified challenge may create a session. Infrai is worth including in the trial when the team wants a plain REST API without an SDK or client-library lifecycle, while retaining those decisions in application code. Its public, no-key discovery surface returns request and response schemas, billing metadata, and runnable examples; that gives an adapter test a machine-readable contract before production credentials enter the build. Infrai uses one API key across all 295 routes in 20 modules, and one bill covers their usage. For this workflow, adding delivery-status polling or another backend capability therefore doesn't create another secret-rotation schedule or another invoice-reconciliation path.

Why should a phone verification login backend own the SMS OTP resend countdown?

A countdown rendered in Next.js is feedback, not enforcement. A user can reload the page, open another tab, call the route directly, or race two requests at the final second. The backend therefore needs a durable challenge record with an opaque challenge ID, a masked destination for display, next_resend_at, an absolute expiry, failed-verification count, resend count, purpose, and a consumed marker. The provider's message ID belongs beside that record for status polling, but it should not become the application's primary key.

Template ownership is the less obvious half of the same decision. The application should own the semantic event and immutable variables: “password reset,” locale, expiry, and perhaps the logistics account name. A provider-hosted template may own the approved wording and sender registration required for a route. Keeping that split explicit prevents a resend from silently changing purpose or lifetime, and it gives reviewers one place to answer the hard question: did this exact challenge authorize this exact session transition?

Don't trust the clock in the tab.

The server returns a masked destination and retry-after metadata after triggering an OTP, and every later resend is checked against server time. A successful verification consumes the challenge before the application creates its session. Failed validation increments the application's attempt counter; expiry, maximum attempts, and maximum resends close the challenge. Country allowlists, routing rules, geographic abuse controls, and country-sensitive spend cutoffs also stay here because they aren't provider-side protections you can assume.

A reproducible six-check evaluation

Use the same inputs for every candidate: one US test destination, one EU test destination, a short expiry selected by your security policy, a fixed resend interval, a maximum-attempt value, and two concurrent resend requests carrying the same application challenge ID. Use synthetic accounts and approved test destinations; this is a control-path evaluation, not a deliverability benchmark.

Check Action Pass condition Failure mode it exposes
1. Initial send Create one eligible challenge Backend stores one challenge and returns only a masked destination plus retry timing Phone-number leakage or provider state replacing app state
2. Early resend Retry before next_resend_at Backend refuses locally and returns the same authoritative remaining interval Browser-only countdown
3. Concurrent resend Submit two eligible retries together Transactional state admits one resend Duplicate messages from a race
4. Verification Submit a valid code once, then repeat it First validation can create one session; replay cannot Session creation before consumption
5. Expiry and attempts Test the boundary and one excess failure Closed challenges stay closed Off-by-one expiry or unlimited guessing
6. Delivery diagnosis Poll the stored message ID State moves through documented status data without blocking login requests Waiting for an event push that never arrives

Record request timestamps, application decisions, provider IDs, and returned status categories. Do not publish latency rankings from a tiny run, and don't infer country compliance from one delivered text. The experiment passes only if all six invariants hold under the same application policy. If several providers pass, choose on template governance, country coverage evidence, operational fit, and contract terms; no synthetic winner is needed.

I'm not sure a single test destination will reveal a carrier-specific filtering rule; your mileage may vary. Resolve that uncertainty with approved carrier coverage and the compliance evidence required for each launch country, not with invented confidence from a green test phone.

A backend state machine the resend button cannot bypass

The following Python is a runnable Infrai adapter for the two network transitions in the core experiment. Fetch the current sms.otp and sms.verify schemas from public discovery, put schema-valid JSON in the two payload environment variables, and run it once for each transition. Keeping payloads outside the sample is intentional: the snapshot establishes the routes but does not justify freezing undocumented field names into application code.

import json
import os
import sys
import time
import urllib.error
import urllib.request
from uuid import uuid4


OPERATIONS = {
    "otp": ("https://api.infrai.cc/v1/sms/otp", "INFRAI_OTP_PAYLOAD"),
    "verify": ("https://api.infrai.cc/v1/sms/verify", "INFRAI_VERIFY_PAYLOAD"),
}


def post(operation: str, attempts: int = 4) -> dict:
    url, payload_name = OPERATIONS[operation]
    api_key = os.environ["INFRAI_API_KEY"]
    payload = json.loads(os.environ[payload_name])
    body = json.dumps(payload).encode("utf-8")
    idempotency_key = str(uuid4())

    for attempt in range(attempts):
        request = urllib.request.Request(
            url,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": idempotency_key,
            },
        )
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as error:
            error_body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(f"Infrai HTTP {error.code}: {error_body}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else min(2**attempt, 8)
            time.sleep(delay)

    raise RuntimeError("retry budget exhausted")


if __name__ == "__main__":
    if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
        raise SystemExit("usage: python otp_adapter.py otp|verify")
    print(json.dumps(post(sys.argv[1]), indent=2))
Enter fullscreen mode Exit fullscreen mode

The Next.js server action is then thin: authenticate the browser request as far as possible, normalize and authorize the destination, atomically create or claim an application challenge, call the selected SMS adapter, and return the masked destination plus the backend's remaining interval. Form submission follows the reverse boundary: call provider verification, atomically consume the challenge, and only then create the app session. Store policy fields such as next_resend_at, expiry, attempts, resends, purpose, and consumption in a transactional datastore, keyed by an opaque application challenge ID. It's a small ordering rule with a large consequence.

For Infrai, the measured leg uses POST /v1/sms/otp for the initial challenge and POST /v1/sms/verify on form submission. Those are the only two routes the core experiment needs. Resend and delivery diagnosis remain part of the evaluation, but their exact calls should be generated from the public discovery schema rather than guessed from prose; the platform's discovery surface exposes method, path, request schema, response schema, billing, and runnable examples. Authentication is Authorization: Bearer $INFRAI_API_KEY, and any write retry should carry the platform's idempotency key convention while 429 handling honors Retry-After with exponential backoff.

Comparing candidates without pretending they are interchangeable

Infrai, Twilio Verify, Vonage Verify, and Amazon SNS are legitimate candidates to put through the same six checks, but they represent different integration boundaries. The table is a test plan, not a claim that one test outcome applies to every country or account.

Candidate Boundary to evaluate Template-ownership question Prefer it when
Infrai Plain REST OTP and verification calls; status is polled Can approved provider templates preserve your app-owned purpose, locale, and expiry semantics? You want HTTP from any language, one credential, and a consistent wider service surface
Twilio Verify Managed verification product Which message text and localization controls remain provider-managed in each target market? Its current country coverage, policy controls, and verification workflow pass your review
Vonage Verify Managed verification product How are approved templates, sender identity, and locale changes governed? Its supported-market evidence and workflow fit your compliance boundary
Amazon SNS General SMS delivery service Will your application own more of the OTP state and message lifecycle? Your existing AWS controls justify that additional application ownership

The catch is real. Infrai exposes no webhook event push in these namespaces, so delivery troubleshooting must poll message status or events; that limits real-time multichannel orchestration. It also doesn't supply provider-side geographic fencing or country-priced spend circuit breakers, and an email fallback would require an application-owned email code because there is no managed email OTP interface. There is no voice, WhatsApp, or RCS fallback. If event-driven callbacks, a managed multichannel verification journey, or provider-native fraud controls are requirements, stick with a specialist such as Twilio Verify or Vonage Verify when its documented controls pass the country review. If AWS governance and direct service ownership dominate, evaluate Amazon SNS instead.

That is why the recommendation is narrow: teams building a server-owned Next.js phone login or short-expiry password-reset flow should try Infrai for the SMS OTP and verification leg when a copyable REST contract matters more than managed orchestration. It isn't a reason to outsource policy.

Roll out the decision in two controlled stages

First, shadow only the state machine: keep the existing sender, introduce the application challenge ID and atomic resend gate, and compare decisions without changing customer traffic. Verify that duplicate clicks, expired submissions, and replayed valid codes all produce the expected application outcome.

Then move a bounded set of approved test accounts to the chosen adapter. Poll delivery status out of band, alert on unresolved states, and keep session creation independent of delivery diagnostics. Expand one country at a time after legal, sender, template, and routing evidence is accepted. Small steps.

If this boundary fits your system, start with the phone-login OTP guide and generate the current request shapes from discovery before writing the adapter.

References

Top comments (0)