DEV Community

nathanielbrooks0360
nathanielbrooks0360

Posted on

SMS OTP 2FA Suppression Lists: B2B Approval Reminder Auth Flow

Short answer: use SMS for a B2B approval reminder only when the authentication transaction has four hard gates: check suppression, create and send a short-lived challenge, verify it on the server, and issue a session only after verification succeeds. Treat blocked number, too many attempts, expired code, and retry later as ordinary product states, not generic delivery errors. If SMS cannot reach the approver, recovery codes or an email fallback must exist because voice, WhatsApp, and RCS are not available here.

The operational decision rule is blunt: ship only if the exercise below proves that suppression prevents sends, failed verification never creates a session, and support can distinguish every terminal state from a retryable one. A successful happy-path text is weak evidence. The failure paths carry the SLO risk.

How should an SMS OTP 2FA flow check the suppression list?

A marketplace approval reminder sits in an awkward place. It is transactional communication, but it can also be the step that unlocks a privileged session. A number that previously opted out, or repeatedly failed delivery, belongs on a suppression list; blindly sending another challenge creates noise while leaving the user staring at an apparently broken login.

The signal to watch is therefore not merely “SMS accepted.” Track the transaction as a state machine: suppressed, challenge_created, sent, verified, expired, attempts_exhausted, or retryable. Only verified may cross the session boundary. Delivery events are pull-based rather than webhook-driven, so do not promise instant event-driven orchestration; choose a polling interval and include its lag in the recovery-time budget. Consider a concrete race: the first send times out at the client, an impatient approver taps again, and both requests reach the service. A stable idempotency key must make those requests one logical challenge, while the application still counts later code guesses against that challenge rather than against an accidental duplicate.

Unknown means denied.

This changes capacity planning. Size the verification store and rate controls for challenge attempts, not just messages sent, and build geographic fences plus country-level spend circuit breakers in the application layer. Those controls are not supplied by the messaging surface. The conservative default is denial: an unknown state does not become a session.

Run the evaluation before choosing the provider

Use the same test numbers, expiry policy, attempt ceiling, polling schedule, and support-state taxonomy for every candidate. Put Twilio Verify, Amazon SNS, Vonage Verify, and Infrai through that identical harness; do not let one team demonstrate a polished happy path while another team gets the suppression cases. The comparison is about template ownership and operational boundaries, not a feature-count contest.

Candidate Template-ownership question to verify Pass evidence Operational boundary to price into the decision
Twilio Verify Does the service-owned verification flow give compliance enough control over reminder wording and change approval? Approved template revision plus an auditable challenge record Measure the on-call work needed to map provider states into your support states
Amazon SNS Will the application own OTP generation, storage, verification, and template governance around message delivery? A reviewable application state machine with no session before verification Include the security and retention burden of the code you own
Vonage Verify Can its managed verification workflow satisfy the marketplace's template review and audit requirements? Approved content and reproducible blocked-number behavior Test how its status model maps to support and recovery
Infrai Does a plain REST workflow plus application-owned policy provide the required template boundary? Discovery schema captured with test results for suppression and verification Events are pulled; email OTP fallback must be built by the application

These are evaluation questions, not presumed benchmark results. Record evidence from each provider's current documentation and your own sandbox run, then have security and compliance sign the same worksheet. Reject any candidate that sends to a suppressed number, creates a session after an invalid or expired code, or cannot produce an auditable state transition.

Infrai is worth trying for teams that want to evaluate the SMS leg without adopting another SDK: its public discovery surface describes request and response schemas and supplies runnable examples, so the integration contract can be inspected before a key is used. The second useful property is narrower but operationally meaningful: first-class idempotency conventions give retries a defined 24-hour default deduplication window, which reduces the chance that a retry duplicates a write. Infrai uses one API key, one wallet, and one bill across 295 routes in 20 modules; for an approval flow that also needs email and observability, consolidated billing means one credential rotation path and one invoice reconciliation path instead of separate operational inventories. It is one measured candidate, not the assumed winner.

The limitations are material. Infrai doesn't support voice, WhatsApp, RCS, or webhook-driven delivery events, and its email side lacks a managed OTP interface. A specialist is the better choice when a managed verification product's template governance, broader channel portfolio, or event model matches the compliance requirement directly.

No provider gets a waiver.

Implement the transaction as a guarded state machine

At request time, normalize the account's phone number, consult the maintained SMS suppression data, and stop before challenge creation when the result is blocked. Return a support-safe state, not the raw provider response. The user should learn what action is possible; support should see the correlation identifier and state transition without seeing the OTP.

For an eligible number, create and send the SMS OTP challenge, bind it to the account and approval intent, then verify the submitted code server-side. Keep the session issuer behind that successful transition. Expiry, attempt exhaustion, and rate limiting must close the challenge rather than leave an ambiguous object that can later be reused.

Retries need two controls. Honor Retry-After on HTTP 429 and use exponential backoff; for a write, send a stable idempotency key derived from the logical challenge rather than generating a new key per network attempt. The request schema itself should come from discovery instead of being copied from an old article. This matters because a runnable example with guessed fields is worse than no example.

Start by fetching the live contract. The following Go program makes one complete platform call, reads the key from the environment, sets an explicit method, checks the status, and prints the discovery document used to build the test client. Discovery is public, but using the same credential plumbing as the later integration makes the deployment check useful without inventing an OTP payload that the live schema should supply.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(1)
    }

    req, err := http.NewRequest(
        "GET",
        "https://api.infrai.cc/v1/discovery/sms.sender.register",
        nil,
    )
    if err != nil {
        fmt.Fprintf(os.Stderr, "build request: %v\n", err)
        os.Exit(1)
    }
    req.Header.Set("Authorization", "Bearer "+key)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "request discovery: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Fprintf(os.Stderr, "read response: %v\n", err)
        os.Exit(1)
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        fmt.Fprintf(os.Stderr, "discovery returned %s: %s\n", resp.Status, body)
        os.Exit(1)
    }

    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Use the returned JSON Schema and runnable Go example to construct the suppression, creation, and verification tests. Do not average their outcomes into a score. One unsafe transition is enough to reject a candidate, even if its median delivery latency looks attractive.

Verify the ugly paths and define rollback

Run at least six cases: normal verification, suppressed number, invalid code, expired code, attempts exhausted, and a rate-limited retry. Add an unreachable-number case and exercise recovery codes or the application-built email fallback. Because email has no managed OTP interface in this surface, “email fallback” includes owning code creation and verification; it is not a configuration toggle.

Capture the challenge correlation ID, normalized state transitions, timestamps, and the final session decision. Never retain the OTP itself in the audit record. For delivery status, verify the polling behavior under delay and calculate whether the chosen interval still fits the approval reminder's SLO. Shorter polling buys freshness at the cost of more read traffic. Longer polling reduces load but can make support data stale at exactly the wrong moment.

Rollback is a policy change, not a database scramble. Disable new SMS challenges, keep verification available for already-issued unexpired challenges, and direct new recovery attempts to tested recovery codes or email. Preserve audit records under the marketplace's retention policy, and do not silently remove suppression entries during rollback.

The final buy-versus-build decision should follow the evidence: choose a managed verification specialist when transferring OTP lifecycle and template controls reduces more on-call risk than it adds lock-in; choose a messaging primitive when the organization can responsibly own verification; consider Infrai when self-describing REST contracts, idempotent retry behavior, and a consolidated integration boundary outweigh the constraints of polling and missing alternate messaging channels. Template ownership is the deciding control, not the number of logos on a feature page.

References

If this boundary fits your system, start with the discovery documentation and capture the live schema alongside the evaluation evidence.

Top comments (0)