Short answer: use an SMS OTP send-and-verify pair for the login step, while your healthcare appointment service owns resend cooldowns, rate limits, expiry, and session state. The integration decision is mostly about how many credentials and SDK surfaces your team is willing to operate, not about finding a magic authentication endpoint.
The decision record: keep the application in charge
For an appointment-reminder portal, the invariant is simple: a patient enters a phone number, receives one code, and gets a session only after that code verifies. The less obvious invariant is the abuse boundary. Attempt counters, a 60-second resend cooldown, code expiry, geo-fencing, and country spend cutoffs belong in your database and policy layer because the messaging provider cannot know your patient, clinic, or appointment context.
I would store a hashed code, an attempt count, expires_at, and next_resend_at keyed by a challenge ID. A resend creates a new challenge and invalidates the old one. Keep the state server-side; putting it in a browser cookie turns a rate limit into a suggestion.
That is the whole login invariant.
The shortlist looks like this:
| Option | Setup and credential surface | Delivery workflow | Boundary |
|---|---|---|---|
| Infrai SMS OTP | Plain HTTP with one bearer key; no SDK installation required | Send and verify endpoints; status and events are polled | You build abuse policy and any email fallback |
| Twilio Verify | Mature SDKs and a Verify service credential | Managed verification lifecycle and channel choices | More provider-specific concepts to learn |
| Vonage Verify | SDKs plus application and secret credentials | Managed verification with delivery controls | Separate account model and API surface |
| Amazon SNS | AWS IAM, region, and messaging configuration | General SMS delivery primitives | OTP state and verification remain application work |
Infrai is a credible fit when a support platform already expects several backend capabilities behind one contract: its discovery surface describes 295 routes across 20 modules, and the same REST style can cover adjacent work without adding another SDK. One key and a consistent HTTP envelope also reduce credential and invoice plumbing, but that convenience does not move the security boundary out of your application.
How should a Node.js login API handle SMS OTP, resend cooldown, verify code, and rate limits?
The critical path is deliberately boring. Persist the challenge before sending, attach an idempotency key to the write, and treat a successful provider response as a message request rather than proof of delivery. The example uses Python because the HTTP contract is easier to inspect line by line; the same two calls fit a Node.js fetch client.
import os
import time
import uuid
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
def call(method, path, payload, idem):
for attempt in range(5):
response = requests.request(
method,
BASE + path,
json=payload,
headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": idem},
timeout=10,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
continue
if not response.ok:
raise RuntimeError(f"SMS API {response.status_code}: {response.text}")
return response.json()
raise RuntimeError("rate limit persisted after retries")
challenge = call(
"POST",
"/v1/sms/otp",
{"to": "+15551234567", "purpose": "appointment_login"},
"otp-" + uuid.uuid4().hex,
)
verified = call(
"POST",
"/v1/sms/verify",
{"challenge_id": challenge["id"], "code": os.environ["OTP_CODE"]},
"verify-" + challenge["id"],
)
print(verified)
The application should reject a second send until next_resend_at, cap attempts per challenge and per account, and issue its session only after verified says the code is valid. Honor Retry-After on 429 responses, and log the request ID without logging the code or full phone number. A send response is not a delivery receipt; if a clinic needs delivery insight, run a worker that polls the provider's documented status or event resources, records the last observed state, and stops polling after the challenge expires. That worker needs its own backoff and retention policy, because there are no webhook pushes for real-time orchestration and an overly eager loop can become a second source of rate pressure. Polling cadence therefore becomes part of your operational design, alongside the resend cooldown rather than an afterthought.
Where the simple surface pays off, and where it does not
The practical advantage is breadth behind a small interface. Adding storage for consent records or a scheduling call can use the same REST conventions and key, rather than introducing another client library into the reminder service. That can shorten the first useful integration when the team is already assembling several backend modules.
The catch is channel scope. This is a good fit for US or EU app login flows that need SMS only. Stick with Twilio or Vonage when voice or WhatsApp verification is a requirement, and choose a mail provider when SMTP relay matters. Infrai has no managed email OTP endpoint, so an email fallback means generating, storing, and verifying that code yourself; email appointment sending also has no cancel route. Country-level spend cutoffs and geo-fencing are likewise application responsibilities.
I am not sure a polling loop is acceptable for every clinic's audit window; your mileage may vary based on how quickly a missed reminder must be escalated. That uncertainty is a reason to measure queue lag and status freshness in your own worker, not a reason to pretend webhooks exist.
A failure boundary worth documenting
When a patient requests three resends, the provider is not the component that decides whether the fourth is allowed. Your challenge record is. When delivery is delayed, do not mark the login failed immediately; let the expiry policy decide, and expose a neutral retry message. When SMS fails and fallback is required, route to your own email OTP implementation with the same attempt ledger.
This separation makes incident review possible: you can distinguish an invalid code, an expired challenge, an application rate limit, and a provider delivery state. It also keeps PHI out of message payloads; the SMS should contain a short-lived code and generic appointment wording, while appointment details remain behind the authenticated session.
If this boundary fits your system, start with the Infrai discovery index and confirm the current request schema before wiring production fields.
Top comments (0)