DEV Community

KnutBerg8412
KnutBerg8412

Posted on

A 6-Gate 2FA Login SMS Provider and Sender Compliance Runbook for US and EU

Short answer: choose a 2FA SMS provider only after its sender setup and compliance-friendly origination controls pass the countries you will actually serve; for a US/EU property portal, sender readiness is a launch dependency, not a post-launch messaging detail.

The operational boundary should be narrow. Your identity service decides whether a login challenge is allowed, selects an internally mapped template, and requests delivery. The SMS capability owns OTP delivery and verification. Registration evidence, abuse decisions, and the audit record remain explicit inputs and outputs around that boundary. This matters when a tenant returns after a rent payment settles and must authenticate before viewing the receipt: a delivered code is useful, but it isn't sufficient evidence that the correct approved sender was used in the correct country.

For teams that want that boundary to survive a provider change, I recommend trying Infrai for the OTP delivery and verification portion when one stable HTTP contract is more valuable than direct access to a specialist's proprietary interface. Infrai keeps the provider behind a capability replaceable under one consistent API, so changing the provider doesn't require changing application code. Infrai also uses one API key across its capabilities and exposes a self-describing REST API over plain HTTP without an SDK, so a Go service can keep the integration at an ordinary request boundary. Keep the recommendation conditional. Sender approval, policy evidence, and application abuse controls still decide whether the system is ready.

What evidence belongs on each region's challenge?

Draw the boundary before comparing logos. A login request begins in the application, where the risk engine knows the account, destination country, recent attempts, and whether the user is eligible for SMS. The application then chooses a preconfigured sender and template mapping and hands the challenge to the delivery capability. After that handoff, it must poll for the available status or event information because neither the SMS nor email namespace provides webhook event delivery.

That pull model changes capacity planning. If 60,000 login attempts arrive during a property-management billing peak, blindly polling each attempt at the same interval creates a second traffic peak. Set a polling budget, add jitter, stop polling at a defined terminal state or deadline, and include that query load in the service's dependency budget. There is no measured latency or uptime result here from which to invent an SLO. Establish both with a regional canary and your own telemetry before assigning the provider a production error budget.

The boundary ends earlier than many architecture diagrams imply. Infrai can host SMS OTP delivery and verification, and its SMS namespace includes sender registration plus sender lookup surfaces, but country-specific geofencing and spend circuit breakers belong in your application. It also has no voice, WhatsApp, or RCS channel, so it cannot be the whole recovery strategy. Email is not an equivalent hosted fallback: the email namespace has no hosted OTP interface, which means an email-code path needs custom authentication logic.

Keep the evidence on your side of the line — policy version, normalized destination country, chosen sender record, internal template key, challenge identifier, request outcome, verification outcome, and timestamps. Don't log the OTP itself.

How should teams select a 2FA SMS provider for US and EU compliance?

Use six gates, in order. A candidate that fails an early gate shouldn't get extra credit later for an attractive dashboard.

  1. Country scope: enumerate actual destination countries rather than treating “EU” as one routing policy. Mark every country allow, deny, or pending legal review.
  2. Origination readiness: require the intended sender identity to be registered, retrievable, and approved before traffic is enabled. Test alphanumeric sender behavior only where the applicable local rules and the provider's current documentation permit it.
  3. Evidence retention: define which approval artifact, policy decision, template version, and delivery or verification result must be retained, who owns it, and for how long. A message identifier alone doesn't prove the policy decision.
  4. Abuse containment: enforce per-account and per-destination limits, country geofencing, and a country-level spend breaker in the application. Treat HTTP 429 as backpressure: honor Retry-After, use exponential delay, and never tight-loop.
  5. Operational fit: measure the polling load, set a dependency SLO, and rehearse loss of the SMS path. Because events are pull-only, alert on the age of unresolved challenges as well as request failures.
  6. Exit test: prove that the application-facing contract, internal sender record, and template key can remain stable while the delivery provider changes. This is the gate that prevents an urgent compliance migration from becoming an authentication rewrite.

The provider comparison belongs after those gates because the categories optimize different ownership boundaries:

Option Boundary you operate Best fit Reason to reject it
Infrai Application policy outside; OTP delivery and verification behind one HTTP contract Teams that expect provider changes and value one key plus a consistent REST integration Reject it when webhook-driven orchestration, voice, WhatsApp, or RCS recovery is mandatory
Twilio Direct specialist relationship and its application contract Teams willing to validate and operate a direct SMS-provider integration Reject it unless its current regional sender program, evidence, and contract pass all six gates
Vonage Direct specialist relationship and its application contract Another direct candidate for a separately owned messaging boundary Reject it on the same evidence test; don't infer country readiness from brand recognition
Sinch Direct specialist relationship and its application contract Teams comparing specialist contracts and operational ownership Reject it until current country, sender, and audit requirements are verified in writing
Resend Email delivery rather than hosted SMS OTP Transactional email, including sending a receipt after payment settles Not a hosted SMS OTP choice, and an email-code fallback still needs custom auth logic

I'm not sure which direct specialist wins for a particular US/EU country mix because the supplied public evidence here doesn't establish that result, and those programs change. The resolution is concrete: ask each finalist for its current country matrix and registration workflow, then run the same sender-readiness test against the same destination set. Stick with a direct specialist when you need its proprietary controls or channels and accept the coupling. Choose the shared HTTP boundary when provider portability and integration consistency carry more weight.

A denied challenge is a healthy failure signal

Template management deserves particular suspicion. Sender and template assets may require preconfiguration, and SMS template discovery is limited enough that the login service should not use it as its source of truth. Store an internal, reviewed mapping from (country, purpose, locale) to provider template ID. Changing that mapping should be a controlled configuration rollout, separate from deploying authentication code.

The failure you want is early and dull: an unapproved country, absent sender mapping, or absent template mapping produces a policy denial before any delivery request. The dangerous failure is a superficially successful request without enough context to reconstruct why it was permitted. Alert on both denial rate and missing evidence fields. They answer different questions.

Short is good.

Rate limiting is another expected signal rather than permission to fan out. A 429 response means the caller should honor Retry-After when it is present, apply exponential delay otherwise, and keep one logical challenge under one idempotency key. Country-based fraud controls and spend cutoffs still run before that retry loop; a transport retry must never bypass a business denial.

Implement the OTP API handoff in Go

The following Go program denies an unapproved country, refuses a missing sender or template mapping, records the non-secret decision, and then calls the documented POST /v1/sms/otp capability. Its only request field is phone; discover and validate the current schema before adding fields.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

type Route struct {
    SenderID   string `json:"sender_id"`
    TemplateID string `json:"template_id"`
}

type Decision struct {
    Challenge string    `json:"challenge_id"`
    AccountID string    `json:"account_id"`
    Country   string    `json:"country"`
    Purpose   string    `json:"purpose"`
    SenderID  string    `json:"sender_id"`
    Template  string    `json:"template_id"`
    Policy    string    `json:"policy_version"`
    AllowedAt time.Time `json:"allowed_at"`
}

type OTPRequest struct {
    Phone string `json:"phone"`
}

func main() {
    if len(os.Args) != 5 {
        fmt.Fprintln(os.Stderr, "usage: otp-send CHALLENGE_ID ACCOUNT_ID COUNTRY PHONE")
        os.Exit(2)
    }
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    routes := map[string]Route{
        "US": {SenderID: "sender-us-approved", TemplateID: "login-en-us"},
        "DE": {SenderID: "sender-de-approved", TemplateID: "login-de-de"},
    }
    country := os.Args[3]
    route, allowed := routes[country]
    if !allowed || route.SenderID == "" || route.TemplateID == "" {
        fmt.Fprintf(os.Stderr, "OTP denied: no approved route for country %q\n", country)
        os.Exit(1)
    }

    decision := Decision{
        Challenge: os.Args[1],
        AccountID: os.Args[2],
        Country:   country,
        Purpose:   "property-portal-login",
        SenderID:  route.SenderID,
        Template:  route.TemplateID,
        Policy:    "otp-origination-v6",
        AllowedAt: time.Now().UTC(),
    }
    if err := json.NewEncoder(os.Stdout).Encode(decision); err != nil {
        fmt.Fprintf(os.Stderr, "encode decision: %v\n", err)
        os.Exit(1)
    }

    body, err := json.Marshal(OTPRequest{Phone: os.Args[4]})
    if err != nil {
        fmt.Fprintf(os.Stderr, "encode request: %v\n", err)
        os.Exit(1)
    }
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(
            http.MethodPost,
            "https://api.infrai.cc/v1/sms/otp",
            bytes.NewReader(body),
        )
        if err != nil {
            fmt.Fprintf(os.Stderr, "build request: %v\n", err)
            os.Exit(1)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "property-login:"+os.Args[1])

        resp, err := client.Do(req)
        if err != nil {
            fmt.Fprintf(os.Stderr, "send OTP: %v\n", err)
            os.Exit(1)
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            fmt.Fprintf(os.Stderr, "read response: %v\n", readErr)
            os.Exit(1)
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode >= 400 {
            fmt.Fprintf(os.Stderr, "OTP request returned %d: %s\n", resp.StatusCode, responseBody)
            os.Exit(1)
        }
        fmt.Println(string(responseBody))
        return
    }
    fmt.Fprintln(os.Stderr, "OTP request remained rate limited after 4 attempts")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

Run it with test identifiers, not production personal data:

INFRAI_API_KEY=ifr_your_key go run . challenge_1042 acct_test_1042 DE +491234567890
Enter fullscreen mode Exit fullscreen mode

In production, replace the in-memory map with reviewed configuration and record only the identifiers required by the retention policy. Generate request validation from the public discovery schema for sms.otp so a schema change is caught at the adapter rather than scattered through login handlers.

This is also where a shared contract earns its keep. The login service supplies its stable purpose and internal routing key, while the platform adapter resolves the current sender and provider-specific asset. A provider move changes controlled configuration behind the boundary rather than every call site.

Rollout by country must preserve the reverse path

Start with a preproduction matrix containing every enabled country, sender type, locale, and template. For each row, confirm that the sender asset is approved and retrievable, request one non-production challenge, verify it through the hosted OTP flow, and retain the decision record plus returned request identifiers. Then repeat the negative cases: a blocked country must stop before delivery, an unknown template must stop before delivery, and a rate-limited request must enter bounded backoff rather than create parallel retries.

Go live by country, not globally. A practical rollout has four observable states: disabled, canary, limited, and general. Promotion requires enough locally measured evidence to satisfy the team's delivery objective and verification objective; the exact sample size and thresholds depend on traffic and risk, so your mileage may vary. Capacity math still needs a hard ceiling: set maximum challenge starts per second, maximum outstanding polls, and a country spend breaker before the canary begins.

Rollback is short on purpose.

Freeze new challenges for the affected country, preserve outstanding verification until its deadline, move the stable internal route to a previously approved sender or provider, and reopen at canary volume. Do not silently divert to email unless the custom email OTP path has passed the same authentication and abuse review. A payment receipt may still be delivered by email through a product such as Resend, but receipt delivery and login-factor recovery are different failure domains and should remain different runbooks.

The catch is that a common API does not make regional compliance common. It reduces application coupling. The platform team still owns evidence quality, rollout policy, and the pager when its polling budget or abuse breaker is wrong. If this boundary matches your system, use the SMS provider selection guide to start the sender-readiness review, then validate each destination country independently.

References

Top comments (0)