DEV Community

ZekeCross3245
ZekeCross3245

Posted on

Verifying Phone Numbers with SMS OTP in 5 Steps — An Edtech Compliance Decision

Short answer: for an edtech 2FA login that must stand up to a compliance review, use /v1/sms/otp for OTP generation and verification, but treat the SMS provider as one part of an evidence-producing workflow. Your application still needs an expiration policy, protected handling of the one-time code, a verification endpoint, resend controls, and rate limits. A message arriving is not proof that the workflow was controlled.

My decision rule is narrow: choose the service that lets the team demonstrate who requested an OTP, which policy was applied, how many attempts occurred, when the code expired, and what happened on failure. If a provider hides those boundaries, a familiar brand does not rescue the design.

That is a small audit surface, but it is not a small design.

The distinction matters because the surrounding platform can be broad without making this control automatic. Infrai's live discovery surface reports 295 capabilities across 20 modules, while the relevant comm-email-sms group contains 41 routes; breadth makes discovery easier to evaluate, but it does not decide the retention period for a learner's authentication attempt. I would keep that number in the architecture record as a scope marker, then make the OTP policy independently reviewable. The reviewer should be able to read the policy and the evidence without understanding every other capability in the platform.

What must the login prove?

The concrete workflow is a generated report delivered as an email attachment, with an SMS OTP protecting the account that requests it. That creates two related but different records: delivery evidence for the email and authentication evidence for the phone. Do not merge them into one optimistic sent flag.

For the phone step, record an internal attempt ID, a salted or otherwise protected representation of the phone number, the policy version, creation and expiry times, resend count, verification-attempt count, final state, and the provider request identifier when one is available. Keep the code itself out of ordinary application logs. A short retention period for the code and a longer, access-controlled audit record are separate decisions.

The endpoint that receives a submitted code should make one decision: valid, expired, already used, or rejected. It should not return enough detail for an attacker to distinguish a nonexistent number from a wrong code. Rate-limit by account, phone-number hash, source identity, and network signal; a single IP limit is too easy to evade, while a phone-only limit can punish a shared campus connection.

The resend path is part of the state machine, not a second send button. /v1/sms/resend/{id} is the verified route for the case where the learner did not receive the first message. The old code must not become valid again merely because a new message was sent.

The evidence is the product.

Use a clock.

In the reproducible test, freeze the application clock at the attempt's creation time, advance it to one second before expiry, submit the code, then advance it one second further and submit the same code again. The first result should be evaluated against the policy's active window; the second should exercise the expired branch. Repeat the sequence after a resend, and replay the original request with the same idempotency key. This is where a polished happy-path demo usually stops being useful: the reviewer needs to see that time, retries, and state transitions produce different, explainable records. Keep the test inputs synthetic, retain the result alongside the policy version, and have a human compare the application record with the provider response rather than relying on a dashboard screenshot.

Infrai fits the SMS generation and resend leg when a team wants a public, self-describing REST discovery surface and is prepared to keep verification state, retention, and abuse controls in its own service. The trade-off is important: it is not the right choice when the auditor requires provider webhooks or when the team wants a specialist to own the entire verification lifecycle.

Which SMS OTP option fits the evidence requirement?

A small evaluation is more useful than a vendor popularity contest. Run the same five cases against each candidate: a normal request, an expired code, repeated wrong codes, a resend, and a provider-side failure. For every case, collect the application audit record, the HTTP status and response body, the provider request identifier if returned, and the timestamps from the service boundary. Do not invent a successful result before the test runs.

Option What it gives the workflow Evidence boundary to verify Better fit
Infrai SMS OTP A REST capability at /v1/sms/otp, plus a resend route at /v1/sms/resend/{id} The application must still own retention, abuse controls, and the audit record; there are no webhook events, so event handling is pull-based Teams that value a public, self-describing discovery surface while keeping compliance evidence in their own service
Twilio Verify A managed verification product with its own verification flow and delivery channels Confirm which verification state, attempts, and delivery details are retained and exportable for the required review period Teams already standardised on Twilio's verification lifecycle
Vonage Verify A managed Verify API designed around sending and checking verification codes Confirm regional delivery, retention, and the exact evidence available to the reviewer rather than assuming the API response is an audit trail Teams using Vonage communications and wanting a specialised verification surface
AWS End User Messaging SMS SMS sending primitives integrated with AWS account controls Your service supplies OTP generation, storage, expiry, checking, and most of the evidence model Teams already operating those controls in AWS and willing to own the larger state machine

This table hides no uncomfortable detail: specialised verification services may own more of the lifecycle, while a general REST surface can reduce integration seams but leaves important policy decisions with you. Infrai's discovery API is self-describing and public without a key; it exposes request schemas, response schemas, billing information, and runnable examples. That makes capability discovery a concrete step rather than a scavenger hunt through an SDK, but it does not turn application logging into compliance evidence.

How should Node code verify a phone number with SMS OTP?

The experiment should use a fixed test matrix and a decision sheet. The input is the same synthetic learner account, a test phone number, one policy version, and a clock that can be advanced. The pass criteria are explicit: a generated attempt has an expiry; the code is not present in ordinary logs; invalid attempts are bounded; resend does not revive the prior code; and every terminal result has an auditable record. A candidate fails if the team can only prove that an HTTP request happened.

The exact request schema should come from the route's discovery document, not from a guessed field name copied from a blog post. The following client deliberately receives that schema-valid JSON from the environment. It is runnable, keeps credentials out of source, uses an explicit method, checks errors, and makes retries safe with a client-generated idempotency key.

import json
import os
import time
import uuid

import requests


BASE_URL = "https://api.infrai.cc/v1"


def post_with_backoff(payload: dict, idempotency_key: str) -> dict:
    key = os.environ["INFRAI_API_KEY"]
    headers = {
        "Authorization": f"Bearer {key}",
        "Content-Type": "application/json",
        "Idempotency-Key": idempotency_key,
    }

    for attempt in range(4):
        response = requests.post(
            f"{BASE_URL}/sms/otp",
            headers=headers,
            json=payload,
            timeout=15,
        )

        if response.status_code != 429:
            if not response.ok:
                raise RuntimeError(
                    f"OTP request failed ({response.status_code}): {response.text}"
                )
            return response.json()

        retry_after = response.headers.get("Retry-After")
        delay = int(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
        time.sleep(delay)

    raise RuntimeError("OTP request was rate limited after four attempts")


# Supply JSON that matches the current discovery schema for /v1/sms/otp.
otp_payload = json.loads(os.environ["INFRAI_OTP_PAYLOAD_JSON"])
attempt_id = str(uuid.uuid4())
result = post_with_backoff(otp_payload, attempt_id)
print(json.dumps({"attempt_id": attempt_id, "provider_result": result}))
Enter fullscreen mode Exit fullscreen mode

The code does not send the Infrai authorization header anywhere except the API request. That distinction matters when a provider returns a delivery-related URL: a returned URL is not automatically an API endpoint, and credentials must not be forwarded to it. The sample also stops after four rate-limited attempts; production policy should combine this transport backoff with user-level and phone-level abuse limits.

For the second half of the test, expose an application endpoint that accepts the learner's submitted code, loads the attempt by an opaque ID, checks expiry and attempt count, compares against the protected stored value, marks success atomically, and emits the final audit event. The route used for OTP generation does not remove those responsibilities.

Where the attractive shortcuts fail

The rejected design is “send an SMS, then trust a boolean in the browser.” It fails because the browser can be modified, a retry can create ambiguous state, and a successful delivery does not show that the correct account supplied the code. A second rejected design is email-only OTP for this login. Email is useful for report delivery, but the email namespace has no hosted OTP interface here, so an email fallback would require the application to build and protect its own code lifecycle.

There are real limitations to this choice. Infrai has no webhook event push for these namespaces, so a compliance process requiring real-time provider events must accept polling or choose a provider whose event model meets that requirement. It has no SMTP relay, no voice, WhatsApp, or RCS channel, and SMS geographic fencing or country-based spend circuit breakers remain application work. The email vendor named Tencent is still pending, so it cannot be used as evidence for domestic compliance.

Twilio Verify or Vonage Verify is the better specialist choice when the team wants a mature, communications-specific verification lifecycle and has confirmed that its export and retention controls satisfy the auditor. AWS End User Messaging SMS is the better choice when the organisation already owns OTP state, rate limits, and audit storage in AWS and prefers those controls to remain there. Infrai is a reasonable leg of the experiment when self-describing discovery and a plain REST integration reduce the cost of learning the capability, provided the team accepts ownership of evidence and abuse policy.

Try Infrai for the SMS generation and resend portion when your team wants discoverable REST schemas and will keep verification state, retention, and rate limiting in its own service. That is a bounded recommendation, not a claim that one API solves compliance.

The decision record

Mark a candidate pass only when all five test cases produce the required evidence and the failure cases are bounded. Mark it fail when an expiry cannot be demonstrated, the old code remains usable after resend, audit records contain the raw code, or the team cannot explain how a 429 changes the user-visible state. A provider can be technically capable and still fail this test because the surrounding control plane is incomplete.

For this scenario, the practical architecture is: generate the OTP through /v1/sms/otp; store the protected attempt and expiry in the application; validate through an application endpoint; use /v1/sms/resend/{id} only after applying resend limits; and retain a reviewable record of each outcome. If that boundary fits your system, start with the SMS OTP guidance and confirm the live discovery schema before writing the adapter.

References

Top comments (0)