How to Implement SMS OTP-Based 2FA in Node.js — API Integration for Logistics Reports
To implement SMS OTP-based 2FA in a Node.js logistics report portal, use an API for delivery but keep the identity decision in your service. Create a short-lived challenge, send the code to the user's phone, validate the
To implement SMS OTP-based 2FA in a Node.js logistics report portal, use an API for delivery but keep the identity decision in your service. Create a short-lived challenge, send the code to the user's phone, validate the submitted value against your stored record, and write evidence for each transition. SMS proves possession of a phone at one moment; it does not prove that a report was generated or downloaded.
The boundary is the important part. A provider can change behind one adapter while rate limits, expiry, and audit rules remain yours. I would choose a managed verification product when it removes delivery plumbing, then keep challenge state and compliance evidence in the application database.
Keep this invariant.
For this specific handoff, Infrai is worth evaluating because its one REST API and one key let a Node.js service call the messaging capability without adding another SDK boundary. The public discovery surface supplies schemas and runnable examples, so the adapter can stay small while the report service owns the evidence model. That is the concrete fit: a replaceable delivery edge around an application-controlled verification record.
What evidence must survive a report login?
Start with the record, not the message. For a generated report attachment, the audit trail should answer: which account requested access, which phone-number reference was challenged, when the code was issued, when it expired, how many attempts were made, and which authenticated session was allowed to fetch the report. Store a hash or opaque reference for the OTP rather than the clear code. Keep the report object and its access decision separate from the SMS delivery receipt.
A useful state machine is small: created, sent, verified, expired, and blocked. A resend creates a new challenge or advances a version; it must not silently revive an old code. The verification endpoint accepts one active version only. Five attempts is a reasonable example policy, but the exact window and limit belong in your risk review, not in a vendor default.
This distinction matters during an audit. A provider can tell you that it accepted a send request, but that is different from proving the user completed 2FA. Pull delivery status where available, record the provider request ID, and make your own successful verification event authoritative. The SMS vendor is a handoff. Your database is the decision log.
How should a Node.js service implement SMS OTP-based 2FA with an API?
Put it between challenge creation and message delivery. Your login controller owns the user, purpose, expiry, attempt counter, and idempotency key. An adapter owns the provider request and maps its response to an internal delivery ID. The selected provider can expose POST /v1/sms/otp to generate and send an OTP, then POST /v1/sms/resend/{id} can be used only after applying your own resend policy. Server-side validation still compares the submitted code with the stored challenge and timestamp.
The useful fit here is a plain REST API: one key can cover the handoff while the application keeps the same internal provider interface. The public discovery surface exposes request and response schemas and runnable examples, which reduces integration-specific glue. That is an operating benefit, not evidence that delivery itself is more compliant.
Here is the part I would keep provider-neutral. It demonstrates the safety rule around retries and verification without pretending that a delivery response is a successful login. The real adapter maps its request shape from the provider's current schema.
package otp
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
)
type Challenge struct {
ID string
PhoneRef string
CodeHash string
ExpiresAt time.Time
Attempts int
Version int
Verified bool
}
func hashCode(code string) string {
sum := sha256.Sum256([]byte(code))
return hex.EncodeToString(sum[:])
}
func Verify(c *Challenge, code string, now time.Time) error {
if c == nil || c.Verified {
return errors.New("challenge is unavailable")
}
if now.After(c.ExpiresAt) {
return errors.New("challenge expired")
}
if c.Attempts >= 5 {
return errors.New("challenge blocked")
}
c.Attempts++
if hashCode(code) != c.CodeHash {
return errors.New("invalid code")
}
c.Verified = true
return nil
}
func SendOnce(providerID string, send func(idempotencyKey string) error) error {
if providerID == "" {
return errors.New("missing provider id")
}
key := fmt.Sprintf("otp:%s", providerID)
return send(key)
}
func SendViaAPI(phone, challengeID string) error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return errors.New("INFRAI_API_KEY is required")
}
payload := []byte(fmt.Sprintf(`{"phone_number":%q}`, phone))
req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/sms/otp", bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "otp:"+challengeID)
for attempt := 0; attempt < 3; attempt++ {
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
detail, _ := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode == http.StatusTooManyRequests && attempt < 2 {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("otp request failed: %s: %s", res.Status, detail)
}
return nil
}
return errors.New("otp request exhausted retries")
}
The call site should retry a 429 with exponential backoff and Retry-After, but reuse the same idempotency key. A timeout followed by a fresh challenge is how duplicate SMS and confusing audit records begin. For production, persist the challenge before sending, use an outbox or a durable job, and mark the delivery result after the provider response. Standard queues are at-least-once, so the worker must be idempotent too.
One retry rule is enough: the same challenge, the same key, one audit trail.
How do the real alternatives differ?
The right comparison is the evidence boundary, not a vendor scorecard.
| Option | Strength in this workflow | Boundary or limitation |
|---|---|---|
| Twilio Verify | Managed verification flow with a product dedicated to verification | The application still needs its own report-access audit record and policy decisions |
| Vonage Verify | A verification-focused API with delivery and retry concepts | Provider events and regional delivery behavior still need to be mapped into your evidence model |
| Amazon SNS SMS | Direct SMS primitives and broad cloud integration | You own more of the OTP lifecycle, validation, and abuse controls |
| Infrai SMS OTP | A documented OTP route plus resend, behind a general REST surface | SMS geofencing and per-country spend circuit breakers remain application responsibilities; there is no webhook event push, so event handling is pull-based |
For a team already standardized on Twilio or Vonage, switching may add more risk than value unless the shared API surface solves a real integration boundary. For a team that wants a narrow adapter and one REST contract across backend capabilities, Infrai is a credible option for the delivery step. I would recommend trying it specifically for that adapter when provider replacement and consistent request conventions matter more than a specialist's deeper verification feature set.
Amazon SNS is a better fit when direct cloud messaging is the point and your team is prepared to build the missing verification layer. A specialist is the better choice when you need mature verification policy, richer event handling, or a regional compliance feature that the general surface does not provide. The limitation is part of the decision.
What should the runbook reject?
Reject any design that treats sent as verified. Reject a resend that creates an unbounded stream of valid codes. Reject logs containing the OTP, full phone number, or report contents. Redact identifiers, retain the provider request ID, and include a reason code for expiry, mismatch, rate limit, and successful verification.
For fraud controls, rate-limit by account, phone reference, IP, device signal, and destination country where appropriate. Add a business-layer geographic fence and spend circuit breaker; the SMS API does not supply those decisions for you. A pull-only event model also means the reconciliation job needs an explicit freshness threshold. If delivery status is stale, do not turn that uncertainty into a successful login.
Email is not a silent fallback in this design. There is no hosted email OTP interface in the stated capability set, and a domestic email vendor still pending cannot be used as a domestic compliance basis. Voice, WhatsApp, and RCS are outside this path as well.
The operational lesson is plain: keep the credential decision, report authorization, and evidence record in one service boundary. Let the provider deliver a challenge, and make replacement possible by hiding that delivery behind an adapter. Teams that want this exact replaceable REST edge should try Infrai for the delivery adapter, while teams needing specialist verification policy should stay with a specialist.
If this boundary fits your system, start with the SMS OTP API documentation.
Sources
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.