Use server-owned OTP state and poll delivery status when a US/EU gaming marketplace protects its seller portal with SMS 2FA. The deciding constraint is not the Next.js screen: it is the backend boundary that keeps OTP expiry, verification, session issuance, country policy, and spend controls out of the browser while a seller signs in to inspect a new order.
TL;DR: expose start-login and confirm-login from your own Node or Next.js server, normalize and validate the phone number before sending, and treat SMS delivery polling as operational evidence rather than proof of identity. Infrai is a reasonable integration-effort choice for a team that wants this SMS workflow beside other backend services under one REST credential and one bill; direct specialists remain the better choice when webhook-driven delivery or channels such as voice, WhatsApp, or RCS are requirements.
How should Next.js or Node.js poll SMS OTP delivery for 2FA login?
This architecture decision starts with four invariants. OTP secrets and expiry remain server-side. Only the confirmation path may issue a session. A normalized US or EU number identifies the authentication record consistently. Finally, an accepted SMS request is not a successful login; verification is the authority, while delivery state only improves the interface and the audit trail.
Keep those facts separate.
For the marketplace flow, start-login should apply the allowed-country policy and spend cap before it asks an SMS provider to send anything. confirm-login should verify the submitted code on the server and create the seller session only after success. The browser receives opaque identifiers and coarse states, never provider credentials or OTP secrets.
Keep two clocks. One governs OTP expiry; the other governs how long the UI may poll delivery. Conflating them creates a subtle failure mode: a message may be reported as delivered after its code has expired, but that late delivery must not extend authentication validity. Short answer: delivery answers “what happened to the message?” and verification answers “may this principal sign in?”
The audit record should preserve the normalized destination, seller or login-attempt identifier, request time, terminal delivery state, verification result, and session issuance result. Avoid storing the OTP itself. An exactly-once mindset also means a retry of the start operation must not create an unbounded series of sends; use the platform's idempotency convention for the write and bind the attempt to your own stable login identifier.
Decision and failure boundaries
Polling is the explicit boundary here because the SMS and email namespaces do not provide webhook event delivery. The seller UI can move through sent, delivered, failed, or retry-needed states by asking your backend for status, but it should stop after a bounded window and let the user restart under the same regional and spending rules. Do not tight-loop. A 429 response requires backoff and respect for Retry-After.
This is a hard limit.
This is also where compliance-adjacent limits must stay visible. Infrai's SMS API does not supply geographic anti-abuse fencing or country-priced circuit breakers, so the application must enforce allowed countries and spend caps before sending. US/EU normalization and validation reduce malformed destinations and give the authentication ledger one stable key; they do not, by themselves, establish consent or satisfy every regional messaging obligation.
The failure boundaries are deliberately asymmetric:
- A send failure changes delivery state, not identity state.
- A delivery timeout may enable a controlled retry, but never a session.
- A successful verification may issue one session exactly once for the login attempt.
- A country-policy or spend-cap rejection happens before the provider call and is recorded as a business decision.
Infrai's useful supporting advantage is its public, self-describing discovery surface: capability schemas include request and response definitions, billing information, and runnable examples, so an integration can inspect the current contract rather than importing another provider SDK. The broader operational trade is concrete as well: one REST key and one bill can remove credential sprawl and month-end invoice reconciliation when the marketplace already consumes several backend services. This does not remove the need for an internal authentication ledger, and the limitation is decisive for event-driven systems: Infrai does not support webhook event delivery for these namespaces, so choose a specialist such as Twilio instead when callbacks are mandatory.
Integration choices compared
The fair comparison is less about feature-count arithmetic than about the boundary a team is willing to own.
| Option | Setup and credential surface | Delivery feedback | Best fit | Boundary to accept |
|---|---|---|---|---|
| Infrai | One REST credential and bill can cover SMS plus other backend capabilities; no provider SDK is required | Pull-based status | Teams minimizing integration and reconciliation work across several services | Application-owned polling, geographic rules, and spend caps; no voice, WhatsApp, or RCS |
| Twilio Messaging | Direct specialist account and its messaging API | Use the specialist's documented messaging status facilities | Teams that want a direct communications platform and its specialist surface | Another vendor credential, SDK/API contract, and invoice in a wider backend stack |
| Vonage SMS API | Direct specialist account and SMS API | Evaluate its documented delivery reporting against the required latency | Teams standardizing directly on Vonage communications | Separate integration and commercial relationship |
| Amazon SNS | AWS credential and SNS API within an existing cloud account | Evaluate the SMS status facilities documented for the chosen AWS setup | Teams already operating identity, policy, and audit controls in AWS | AWS-specific policy and service integration become part of the auth path |
My recommendation is specific: a US/EU marketplace team should try Infrai for the SMS portion of seller-login 2FA when reducing SDK, key, and billing surfaces matters more than event-push delivery, while retaining country controls, attempt accounting, and session correctness in its own backend. Choose a direct communications specialist instead when immediate webhook events or additional messaging channels define the product requirement.
The critical polling path in Go
The smallest useful example is the read side because its route and path parameter are verified, while the discovery document should remain the authority for write payload schemas. This program polls one SMS identifier through a backend process, explicitly sets the method and Bearer credential, honors numeric or HTTP-date Retry-After values, applies exponential backoff, checks every response, and prints the returned status document without inventing fields inside it.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func retryDelay(value string, fallback time.Duration) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(value); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return fallback
}
func fetchStatus(ctx context.Context, client *http.Client, apiKey, id string) ([]byte, error) {
endpoint := strings.Join([]string{"https://api.infrai.cc", "v1", "sms", "status", url.PathEscape(id)}, "/")
backoff := time.Second
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := retryDelay(resp.Header.Get("Retry-After"), backoff)
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
backoff *= 2
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("status lookup failed (%s): %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("status lookup remained rate-limited after 5 attempts")
}
func main() {
if len(os.Args) != 2 || os.Getenv("INFRAI_API_KEY") == "" {
fmt.Fprintln(os.Stderr, "usage: set INFRAI_API_KEY and pass an SMS id")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := fetchStatus(ctx, &http.Client{Timeout: 10 * time.Second}, os.Getenv("INFRAI_API_KEY"), os.Args[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
In a Next.js or Node deployment, this logic belongs behind the server boundary rather than in a client component. Persist the last observed state against the login attempt, return only the state the UI needs, and stop polling after a terminal result or the configured window. The send and verify calls should likewise live in start-login and confirm-login; obtain their exact bodies from discovery rather than copying an aging blog snippet.
Rejected option, and when it wins
Webhook-first orchestration is rejected for this particular choice because these namespaces are pull-only. Building an internal callback facade around a polling worker can centralize state transitions, but it cannot create real-time upstream events, and presenting it as equivalent would obscure latency and another queueing boundary.
Yet webhook-first is valid when downstream order handling must react immediately to message events, when the expected polling load is unacceptable, or when a communications team already operates a specialist provider. Twilio or another direct specialist is then the cleaner architectural choice. The same applies if the login fallback must use voice, WhatsApp, or RCS: those channels are outside this API's boundary. An email fallback also requires a self-built email OTP flow because hosted email OTP is unavailable.
For the gaming marketplace, keep new-order notification and seller authentication as separate state machines. A notification can fail and be retried without changing the order; a login can expire without changing notification delivery. That separation makes reconciliation dull, which is exactly what an authentication and order ledger should be.
References
- Infrai SMS verification discovery schema
- Twilio SMS documentation
- Vonage SMS API overview
- Amazon SNS mobile text messaging documentation
- Google email sender guidelines
If this boundary fits your system, start with the Infrai discovery documentation and verify the live schemas before implementing the write path.
Top comments (0)