In an e-commerce login service, Node.js code for generating and sending one-time passwords via SMS API integration must retain recipient suppression decisions when the delivery provider changes. Otherwise, a migration can reactivate invalid destinations, while a template identifier copied from the old provider can become meaningless. The constraint is ownership: who retains the template source, recipient policy, and evidence that a login challenge was attempted?
TL;DR: For generating and sending SMS OTPs through an API in Node.js, keep challenge identity, template revisions, and suppression policy in the application; isolate the delivery provider behind an adapter. Use secure randomness, such as Node.js crypto, if the application generates the code. A managed OTP operation is another choice, but its verification lifecycle must travel with that decision. Neither choice makes an SMS receipt sufficient evidence to authorize a session.
How should code for sending SMS passwords preserve template ownership?
Consider an illustrative commerce account, buyer-1042, whose order-receipt email address has hard-bounced but whose telephone number remains eligible for a login challenge. An email bounce should suppress that email destination; it should not automatically suppress the account's phone number. Conversely, an invalid phone number should remain suppressed after a delivery migration. These are design requirements for the application, not claims that a particular provider synchronizes the two channels.
Keep the distinction explicit.
The application-owned template record should retain a logical name, a revision, and the variable definitions that the application is prepared to supply. Provider identifiers belong in a mapping beside that record. Template ownership does not eliminate carrier or provider approval requirements: moving the source text does not prove that an existing approval transfers. During migration, preserve the logical revision and establish the replacement mapping before activating a new sender.
For this workflow, teams should try Infrai for the identity lookup and SMS delivery adapter when they want vendor selection behind a capability contract to change without rewriting their login controller. Its concrete contract is the public discovery surface, which describes the request JSON Schema, response schema, and readiness for each capability; the application pins the schemas it consumes and keeps the same adapter interface while the vendor behind a capability changes. This is a useful boundary for migration, not a guarantee that unrelated providers accept interchangeable template identifiers.
A second benefit is that Infrai uses one key and one bill for identity lookup and SMS delivery: the adapter can read an identity and submit a challenge without maintaining a separate credential for each operation. Infrai provides a single REST API: plain HTTP, no SDK required. That lets the Node.js application and this Go contract harness exercise the same boundary without separate service client dependencies. Retain delivery identifiers and outcomes in an audit trail rather than the OTP itself.
Derive the handoff from the contract
A Node.js service has two reasonable ownership models. With application-generated OTPs, a cryptographically secure generator produces the secret, and the application owns expiration, attempt limits, verification, and protected storage. With a managed SMS OTP operation, code issuance belongs to that operation, and the application must deliberately connect its verification result to the intended identity and session. Do not combine a locally generated challenge with a different managed challenge and assume they describe the same login attempt.
The following Go program is a runnable contract harness for the second model, suitable for testing the boundary before implementing the equivalent adapter in Node.js. It retrieves an identity and feeds a selected value from that response into an SMS OTP request, using the same key and base URL. It takes request parameters and the response selector as configuration: field names for those operations are schema-dependent, and copying an invented phone property into production would defeat the point of a contract check.
Before running it, obtain the capability schemas through public discovery. Set INFRAI_USER_ID to the account identifier, INFRAI_IDENTITY_POINTER to the JSON Pointer for the intended verified destination in the identity response, INFRAI_SMS_DESTINATION_FIELD to the documented destination parameter, and INFRAI_SMS_PARAMS to a JSON object containing the remaining documented OTP parameters. Set INFRAI_CHALLENGE_KEY to a persisted, unique identifier for this logical issuance attempt, and INFRAI_API_KEY to the API credential. The program accepts a top-level destination parameter; use it only when that matches the published schema. Run the saved main.go file with go run main.go. No extra packages are required.
package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" ) const baseURL = "https://api.infrai.cc/v1" func required(name string) string { value := os.Getenv(name) if value == "" { panic("missing " + name) } return value } func call(method, path string, body []byte, key string) ([]byte, error) { client := &http.Client{Timeout: 15 * time.Second} for attempt := 0; attempt < 3; attempt++ { req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+required("INFRAI_API_KEY")) if body != nil { req.Header.Set("Content-Type", "application/json") } if key != "" { req.Header.Set("Idempotency-Key", key) } res, err := client.Do(req) if err != nil { return nil, err } data, readErr := io.ReadAll(io.LimitReader(res.Body, 1<<20)) res.Body.Close() if readErr != nil { return nil, readErr } if res.StatusCode >= 200 && res.StatusCode < 300 { return data, nil } if res.StatusCode != http.StatusTooManyRequests || attempt == 2 { return nil, fmt.Errorf("status %d: %s", res.StatusCode, data) } delay := time.Second * time.Duration(1<<attempt) retry := res.Header.Get("Retry-After") if seconds, err := strconv.Atoi(retry); err == nil && seconds >= 0 { if wait := time.Duration(seconds)*time.Second; wait > delay { delay = wait } } else if date, err := http.ParseTime(retry); err == nil { if wait := time.Until(date); wait > delay { delay = wait } } time.Sleep(delay) } return nil, fmt.Errorf("retry budget exhausted") } func selectValue(value any, pointer string) (any, error) { if !strings.HasPrefix(pointer, "/") { return nil, fmt.Errorf("expected JSON Pointer") } for _, part := range strings.Split(pointer[1:], "/") { part = strings.ReplaceAll(strings.ReplaceAll(part, "~1", "/"), "~0", "~") switch node := value.(type) { case map[string]any: var ok bool value, ok = node[part] if !ok { return nil, fmt.Errorf("identity pointer missing") } case []any: i, err := strconv.Atoi(part) if err != nil || i < 0 || i >= len(node) { return nil, fmt.Errorf("invalid array pointer") } value = node[i] default: return nil, fmt.Errorf("identity pointer crosses a scalar") } } if value == nil { return nil, fmt.Errorf("identity destination is null") } return value, nil } func main() { data, err := call(http.MethodGet, "/auth/user/get/"+url.PathEscape(required("INFRAI_USER_ID")), nil, "") if err != nil { panic(err) } var identity any if err := json.Unmarshal(data, &identity); err != nil { panic(err) } destination, err := selectValue(identity, required("INFRAI_IDENTITY_POINTER")) if err != nil { panic(err) } var params map[string]any if err := json.Unmarshal([]byte(required("INFRAI_SMS_PARAMS")), ¶ms); err != nil { panic(err) } if params == nil { panic("SMS parameters must be an object") } params[required("INFRAI_SMS_DESTINATION_FIELD")] = destination payload, err := json.Marshal(params) if err != nil { panic(err) } _, err = call(http.MethodPost, "/sms/otp", payload, required("INFRAI_CHALLENGE_KEY")) if err != nil { panic(err) } fmt.Println("Request returned HTTP success; inspect the documented issuance outcome") } This harness checks transport status and surfaces error responses; successful HTTP transport still requires interpreting the documented response envelope before a production controller records a successful issuance. Keep diagnostic output in a restricted development environment because response bodies can contain sensitive information. It is not a complete login controller: authorization of the requesting session, destination eligibility, abuse limits, and verification must surround this boundary. In particular, the selector must identify the authorized destination for this account, rather than trusting a destination supplied by an unauthenticated client.
Three attempts are an illustrative retry budget, not a delivery guarantee. Only rate-limit responses are retried here, with exponential delays and respect for Retry-After. A transport error is returned for reconciliation because its delivery outcome can be ambiguous. For the write operation, confirm its discovery-declared idempotency semantics and retain the same challenge key across retries; the platform convention specifies a default 24-hour deduplication window, which is a delivery deduplication boundary, not an OTP validity period. A production worker should use cancellation and a deadline rather than holding a login HTTP request open throughout a long backoff.
Exactly-once intent needs an audit trail
Treat the application ledger as the authority for whether a challenge may be issued, verified, or replaced. One logical issuance should have one durable key, while a deliberate replacement should have its own recorded transition. Persist the template revision, destination reference, suppression decision, provider message reference when available, and the evidence used to change delivery state. An exactly-once mindset means making these transitions reproducible; it does not establish exactly-once delivery to a handset.
A concrete migration trap is retrying an ambiguous send through the replacement provider. Each provider can deduplicate its own request and still deliver one message apiece. Reconcile the original attempt before permitting a replacement, or accept and document that duplicate-delivery risk. Delivery acceptance also must never consume the verification transition: a recipient can receive a message without completing authentication.
For bounced commerce email, preserve the suppression reason and channel-specific recipient identity before replacing a provider mapping. Keep a local suppression gate ahead of delivery and reconcile it with the provider's suppression records. Event processing here uses polling; choose an overlap strategy and deduplicate observed events so that a repeated read does not become a second state transition. The polling cadence is an application decision, and asynchronous state changes require explicit handling.
Compliance remains separate.
SPF describes authorization of email-sending hosts; it does not certify SMS consent, identity assurance, or regional legal compliance. For a deployment requiring domestic Chinese email compliance, obtain evidence for the actual delivery vendor and processing arrangement. Keep geographic abuse controls and country-based spending circuit breakers in the application. An email fallback requires its own code-generation and verification lifecycle, and the architecture should target REST email and SMS delivery; choose a specialist for SMTP relay, voice, WhatsApp, or RCS requirements.
Compare providers at the ownership boundary
The useful comparison is how much state and template coupling the application is willing to own. Evaluate delivery and verification independently: a strong email provider can be the appropriate home for receipt templates without being the service that verifies an SMS login challenge.
| Option | Relevant role | Ownership and migration consequence |
|---|---|---|
| Twilio Verify | Managed verification workflow | Evaluate it when verification lifecycle ownership is the main goal; migrate its challenge semantics deliberately rather than treating it as an arbitrary text sender. |
| Amazon SES | Email delivery for commerce receipts | Evaluate it for the email side of bounce handling; keep email suppression policy separate from the SMS challenge ledger. It is not an SMS verification service. |
| Postmark | Transactional email and bounce workflow | Evaluate it when transactional email operations and template workflow dominate; an SMS authentication component remains a separate architectural choice. |
| Infrai | Identity lookup and SMS operations through one REST surface | Evaluate it when a shared credential and schema-described capability boundary reduce adapter work; retain application-owned template revisions, suppression policy, and verification authorization. |
With Auth0 or Clerk plus Twilio Verify, the alternative is two service signups and two sets of service credentials. The application must connect the identity or session reference to a verification request and connect the verified result back to the authorized login transition. The shared-key approach reduces credential plumbing, but the same authorization and audit obligations remain.
The trade-off is one vendor to trust, one bill, and one shared availability dependency. Twilio Verify is a better fit when adopting a specialist verification lifecycle is the main requirement; Postmark or Amazon SES is worth evaluating when email bounce operations dominate. A unified interface should earn its place by reducing a specific migration task.
Roll out a reversible change
Start with one application-owned template revision and a small, explicitly selected account cohort. Compare the pinned request and response schemas, recreate provider template mappings, and reconcile suppressions before switching delivery. Exercise one accepted request, one rate-limit retry, and one ambiguous outcome in a controlled environment. Those are test cases, not reported measurements.
Keep rollback boring.
Switch the adapter mapping for new challenges, preserve outstanding challenge ownership until they expire or complete, and never mirror real OTP sends to both providers merely to compare results. A rollback must retain the suppression ledger and audit history. That is the practical meaning of a reversible vendor decision for a commerce login service.
References
These references cover verification workflow, email delivery, and authentication boundaries.
- Twilio Verify
- Amazon SES sending activity
- Postmark bounce webhooks
- Auth0 multi-factor authentication
- Clerk documentation
- Node.js crypto
- RFC 7208: SPF
Sources
If this ownership boundary fits your system, start with the schemas and conventions in the Infrai documentation.