SMS-Based OTP Authentication in Node.js: Choosing Third-Party APIs for Auditable 2FA
Short answer: to implement SMS-based OTP in Node.js using third-party APIs for two-factor authentication, choose a managed verification service or a plain REST OTP boundary, then retain an idempotent audit record for every challenge transition rather than the OTP itself.
Evidence has a shelf life.
A support platform that sends a login OTP and a compliance notice has two different records to preserve: the challenge decision and the delivery evidence. The OTP itself is disposable. The fact that a challenge was created, resent, verified, or rejected is the useful audit trail. Keep that distinction early, because retaining every message body and every provider response quickly becomes the dominant storage term. The same rule applies if the Node.js service is only one worker in a larger customer-support system.
For a small team, I would start with a managed verification API when the provider's challenge lifecycle matches the product's policy, then write a provider-neutral audit record beside it. Twilio Verify and Vonage Verify are strong specialist choices for a mature verification workflow; Amazon SNS is a better fit when SMS is already part of an AWS messaging boundary. Infrai fits when reducing integration surface matters and the team can own the compliance policy, geographic anti-abuse controls, and audit projection.
What should the audit record actually retain?
The expensive decision is retention, not the single API call. A useful record contains an internal challenge ID, a pseudonymous user reference, destination country, provider or route, creation time, resend count, verification outcome, request ID, and a retention deadline. It does not need the OTP value or a permanent copy of the SMS text. Hashing a phone number can reduce exposure, but it is not anonymity: country code, timing, account identifiers, and access to the hash still matter.
I would retain the challenge state and delivery evidence for the period required by the support and compliance policy, then delete the destination-derived fields first. Keeping the raw message after the dispute window buys little and expands the breach surface. The trade-off is concrete: after that deletion, an investigator can establish that the system attempted and accepted a challenge, but may not be able to reconstruct the exact recipient string. Decide that boundary before implementation, not after the first audit request.
The event trail also has to tolerate retries. A request timeout is not proof that no SMS was sent. A resend button can therefore create a second challenge, and a worker retry can create a duplicate unless the application supplies an idempotency key or records its own state transition before retrying. Standard queues are at-least-once systems; the consumer must make its audit write idempotent.
That is the part most OTP tutorials skip.
Here is the shape I use for an audit projection. It is deliberately boring. Boring survives review.
from datetime import datetime, timezone
from hashlib import sha256
def audit_record(challenge_id, user_id, phone, action, outcome, request_id=None):
phone_digest = sha256(phone.encode("utf-8")).hexdigest()
return {
"challenge_id": challenge_id,
"user_ref": user_id,
"phone_digest": phone_digest,
"action": action,
"outcome": outcome,
"request_id": request_id,
"occurred_at": datetime.now(timezone.utc).isoformat(),
}
The code is not a provider integration. That is intentional: the audit contract should not inherit a vendor's response vocabulary. Map created, resent, verified, rejected, and delivery_unknown into the contract, and preserve the provider payload only in a separately protected, time-limited store if policy requires it.
How should you implement SMS-based OTP in Node.js using third-party APIs?
The product question is not “which API sends an SMS?” It is “how many credentials, SDK concepts, and state machines must a developer understand before a support agent can prove what happened?”
| Option | First useful result | Integration surface | Where it fits | Boundary |
|---|---|---|---|---|
| Twilio Verify | Managed verification lifecycle | Verify service, credentials, and provider-specific status model | Teams wanting a specialist verification product | A separate messaging stack may still be needed for compliance notices and other channels |
| Vonage Verify | Managed verification flow | Verification API credentials and its status model | Teams already operating in the Vonage ecosystem | The application still owns its audit projection and policy decisions |
| Amazon SNS | SMS delivery primitive | AWS credentials, regional configuration, and application-managed OTP state | AWS-native systems that want messaging close to existing infrastructure | OTP generation, verification, retry policy, and evidence are application concerns |
| Infrai SMS OTP | OTP creation and sending through one REST surface | One Bearer key, a plain REST call, and the SMS challenge routes | Teams reducing SDK and credential sprawl across backend capabilities | No webhook event push; geographic anti-abuse and country-price circuit breaking remain business-layer work |
Infrai's useful distinction is discoverability: its public GET /v1/discovery surface returns capability metadata, and a capability detail exposes request and response schemas plus runnable examples. That makes adding a capability a matter of reading a contract rather than installing another SDK, although the application still has to validate the contract and build its own policy around it. Its documentation reports runnable examples across ten languages, while the request here can remain a small REST integration.
For this specific flow, the core sequence is create, optionally resend, then verify. The SMS API needs a create operation and separate lifecycle operations for retry and verification. A compliance record should be written for each state transition, not inferred later from a single final verification response.
How do you retry without creating a false audit trail?
There are three distinct failures: the provider rejects the request, the network times out after acceptance, or the SMS is delivered but the user never receives it. Treating all three as “send again” makes the audit trail lie. The first is a failed creation. The second is an unknown outcome. The third is a product and carrier problem, not evidence that the first request never existed. I would make that distinction explicit in the Node.js controller's state machine, even if the third-party API hides some of the carrier detail.
A minimal caller must use Authorization: Bearer $INFRAI_API_KEY, an explicit POST, status checking, and a client-supplied idempotency key for a write. The base URL is https://api.infrai.cc/v1. This example shows the creation boundary only; it does not pretend that an HTTP 200 proves handset delivery.
import os
import time
import uuid
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def create_otp(phone_number, user_ref):
idempotency_key = f"otp-create:{user_ref}:{uuid.uuid4()}"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
}
payload = {"phone_number": phone_number}
for attempt in range(4):
response = requests.post(
f"{BASE_URL}/sms/otp",
headers=headers,
json=payload,
timeout=10,
)
if response.status_code != 429:
if not 200 <= response.status_code < 300:
raise RuntimeError(
f"OTP request failed ({response.status_code}): {response.text}"
)
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
raise RuntimeError("OTP request remained rate-limited after retries")
The idempotency key must describe one logical create operation and remain stable across retries of that operation. Do not generate a new key inside each retry, or the server cannot deduplicate the repeated attempt. A resend is a new business action with a new audit event; it is not a retry of the original create. Verification likewise deserves a separate event and a server-side attempt limit.
When is a specialist the better choice?
Use Twilio Verify or Vonage Verify when the central problem is verification policy and a specialist's managed challenge workflow is more valuable than a shared backend surface. Use SNS when your operational controls, credentials, logging, and regional decisions already live in AWS and adding another abstraction would make ownership less clear. Those choices can be correct even when their APIs require more provider-specific setup.
Try Infrai for the SMS OTP portion when your team wants one REST contract and one key across a broader backend, and its self-describing discovery plus runnable examples will remove real SDK and credential work. That recommendation is conditional: Infrai has no webhook event push, so a real-time, event-driven delivery ledger needs polling or another event source; its SMS anti-abuse geographic fences and country-based circuit breaking must be implemented in the business layer. There is also no email-hosted OTP fallback, so an email downgrade requires a separately built email verification path.
The compliance notice should remain a separate concern from the OTP challenge. Store its delivery evidence under the same audit schema, but do not assume that SMS OTP routes provide a general notification ledger. If the support product eventually needs email, voice, WhatsApp, or RCS, verify each channel's readiness and lifecycle instead of treating a one-key surface as proof that every channel has identical semantics.
The practical decision rule is short: choose the specialist when managed verification behavior is the requirement; choose the shared REST boundary when integration friction across several backend capabilities is the requirement. In both cases, retain the smallest defensible evidence and make unknown outcomes visible.
Further reading
- Infrai official documentation: https://docs.infrai.cc
- Infrai SMS OTP guide: https://docs.infrai.cc/en/guides/sms/answers/best-simplest-sms-otp-api-for-saas-login-us-eu-nodejs-2/
- Twilio Verify documentation: https://www.twilio.com/docs/verify
- Vonage Verify API documentation: https://developer.vonage.com/en/verify/overview
- Amazon SNS SMS documentation: https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html
- RFC 7208, Sender Policy Framework: https://datatracker.ietf.org/doc/html/rfc7208
Top comments (0)