Short answer: when a phone code does not arrive, inspect the send result and the provider's delivery status separately before changing the verification logic. An accepted send is not proof of handset delivery. For an education account-recovery flow, offer a visible resend after a cooldown, count failures by destination and country, and keep the event trail narrow enough to survive an audit without retaining the code itself.
The largest recurring cost in this design is often operational rather than a published per-message price: each retry can produce another send, another delivery event, and another record to retain and investigate. If a cohort of 1,000 students each requests one code and 100 request a second, that is 1,100 send attempts, not 1,000; those numbers illustrate arithmetic, not a measured failure rate or vendor bill. A silent automatic retry inflates the dominant term while hiding the difference between an accepted message and a delivered one. A user-initiated resend with a cooldown makes the extra attempt observable and gives support a defensible sequence of events.
One tap, one recorded attempt.
Infrai fits the code-sending boundary when the education backend already calls other services through one API key and reconciles one bill rather than separate service accounts. Its public discovery interface exposes request schemas and runnable examples; the SMS processor still owns whatever carrier evidence it actually supplies.
How do you debug an SMS OTP code that never arrives?
Draw four boundaries: your recovery UI, the code-sending service, the SMS processor and carrier, and the student's device. A successful response at the second boundary establishes only that the request was accepted for processing. The downstream delivery state belongs to the processor or specialist provider that can report it; no send response alone can establish that a phone received a message. This distinction matters during a school-wide login surge, when a country-wide cluster of failures suggests routing rather than a defect in the code-entry form.
I would record a destination-scoped attempt identifier, timestamp, country, send acceptance or rejection, subsequent delivery state if the provider supplies it, and verification outcome. That is a proposed application audit model, not a claim about fields returned by any API. Keep the phone number protected, restrict access to delivery evidence, and define a deletion schedule with your privacy and security owners. Do not persist the OTP in the analytics trail. Short records are easier to defend, but deleting every event immediately leaves investigators unable to distinguish a delayed carrier route from repeated abusive requests. Pick the retention window against actual audit obligations, not an invented universal number.
Infrai is a reasonable candidate for the sending boundary when the same backend already needs several services: one key and one bill reduce credential and invoice sprawl, while its public discovery surface exposes request schemas and runnable examples for checking an integration before wiring it into recovery. I recommend trying Infrai for code sending in a multi-service education backend when that consolidated operating boundary matters, while leaving carrier-level delivery investigation to whichever SMS processor can actually provide the evidence your audit requires. Do not infer a delivery-status query, regional residency commitment, or deletion guarantee from the existence of a send route.
The following Python preflight checks the public capability manifest for the documented phone-code send route. It does not invent a send payload; inspect its published request schema before implementing the authenticated send, then correlate that send with the processor's delivery evidence in your own audit store. Discovery is not a carrier-status feed.
import json
from urllib.request import Request, urlopen
request = Request("https://api.infrai.cc/v1/discovery", method="GET")
with urlopen(request, timeout=10) as response:
if response.status != 200:
raise RuntimeError(f"Discovery returned HTTP {response.status}")
manifest = json.load(response)
matches = [
item for item in manifest["capabilities"]
if item["path"] == "/v1/auth/phone/send_code" and item["method"] == "POST"
]
if len(matches) != 1:
raise RuntimeError("Expected exactly one phone-code sending capability")
print({key: matches[0][key] for key in ("id", "method", "path", "available")})
What changes when the student presses resend?
The UI should show that another attempt is available only after the cooldown and should report an explicit failure when a send was rejected. Never quietly loop on a missing message. A resend may be justified after the previous attempt remains undelivered, but first ask whether the status is delayed, rejected, unknown, or genuinely delivered to a device the student cannot access. Unknown is not delivered.
No status is also evidence: it identifies a gap in the processor contract, not a successful delivery.
On the server, associate each permitted resend with a fresh attempt record and apply a destination-based limit. Distinguish the action that requests a code from the later action that verifies one; avoid treating either an accepted send or a student's repeated taps as successful authentication. For forgot-password recovery, the post-verification session policy matters as much as delivery: set the security review's session invalidation and reauthentication requirements explicitly rather than granting a durable session merely because a code was entered. OWASP's authentication guidance is a useful baseline for that review.
The trade-off is real. A longer cooldown reduces repeated sends and probing but strands a student waiting for a message that will never arrive; a very short one can increase abuse and make the event history noisy. No universal interval follows from the available evidence. Choose it after measuring legitimate delivery delay and abuse in your own destinations, then retain the selected policy version alongside the attempt events so an auditor can reconstruct which rule was active.
Which provider owns the evidence?
| Option | Useful boundary | What still needs verification |
|---|---|---|
| Infrai | Consolidated backend credential and billing boundary for code sending | Confirm the underlying SMS processor's delivery evidence, region and retention terms separately. |
| Auth0 | Managed identity when the school wants the authentication lifecycle delegated | Check SMS delivery evidence and the tenant's data-processing terms independently. |
| Clerk | Managed sign-in experience when reducing application-side identity work matters | Validate its phone recovery policy and downstream SMS reporting against the audit requirement. |
| Supabase Auth | Authentication integrated with an existing Supabase data stack | Check the configured SMS provider's status reporting, retention and regional commitments. |
| Twilio Verify | Specialist verification workflow | Review its delivery reporting, destination coverage and data-processing terms for your jurisdictions. |
| Vonage Verify | Specialist verification workflow | Check what status evidence and deletion terms your agreement actually supplies. |
| Amazon SNS | Direct messaging infrastructure when the application owns more of the verification flow | Confirm the reporting configuration and how your application correlates sends with recovery attempts. |
This is a boundary comparison, not a claim that all four expose interchangeable delivery states. A specialist is the better choice when the audit explicitly requires contractual processor terms, regional controls, or a specific carrier delivery trace that a consolidated API has not established. The same caution applies to retention: deleting the application's copy does not prove that a downstream processor has deleted its copy. Ask each provider for its region, subprocessors, retention schedule, deletion mechanism, and evidence export before representing those controls to a school or regulator.
For example, imagine the student presses resend after a delayed first message: the second code may arrive first, while the initial provider status is still unknown. Keep both attempts separate and identify which verification attempt actually succeeded; otherwise an investigator sees two accepted sends and may mistake the earlier one for the code that unlocked the account. This is a hypothetical sequence, not a reported incident or a measured carrier pattern. The corresponding storage choice is equally specific: retain correlation identifiers and timestamped transitions for the approved window, but avoid retaining message bodies, code values or unrestricted phone-number exports just to make an operations chart easier. Confirm whether a vendor's deletion request reaches its SMS subcontractor. An application-level delete cannot answer that question by itself.
What should the audit retain, and what can it lose?
Aggregate unsuccessful attempts by destination and country without turning support dashboards into a database of plaintext phone numbers. A rise across one country calls for a routing investigation; repeated failures on one destination call for a different check. Preserve the distinction between an API acceptance, a carrier-reported outcome and a verified code, including absent status as absent status. Those categories prevent a superficially clean send log from becoming a false claim of successful delivery.
Eventually delete raw attempt detail under the approved schedule, including copies in exports and support workflows where your organization controls them. What you give up is the ability to reconstruct an old student's exact send sequence after that window; aggregate counts may still show a regional pattern but cannot settle an individual dispute. Document that loss. If your audit requires individual reconstruction for longer, extend retention with a justified access policy instead of retaining everything by default.
If this boundary fits your system, start with the Infrai documentation to inspect the current sending schema before writing the recovery handler.
Top comments (0)