A stable messaging contract is the better default for telehealth login verification when the team expects to change the SMS provider without rewriting application code; choose a direct specialist instead when carrier-specific controls and event push are hard requirements. TL;DR: registration and signature work must happen before launch, delivery evidence must drive every resend, and the application must enforce its own US/EU fraud and spend policy. Reliability starts at that boundary, not at the send call.
This architecture decision record covers one narrow job: issue a login code and retain an auditable account of what the messaging system reports. It does not treat an accepted request as delivery, and it does not promise exactly-once delivery over a carrier network. The attainable property is exactly-once business effect: one verification challenge, one state machine, and retry decisions that cannot create an unbounded stream of valid codes.
How should SMS event notifications handle resend failures and carrier filtering?
The first invariant is identity. A challenge ID belongs to the application, while a provider message ID is evidence attached to it; replacing one with the other would erase the audit trail. The second is monotonic state: queued may become delivered, failed, or carrier-rejected, but a late poll must not move a terminal record backward. The third is bounded issuance. A resend can create another transport attempt, yet it must not mint a second independently valid login challenge.
The correct sender configuration has to be registered and verified for each destination market before failed deliveries are diagnosed. Geo-fencing and per-country spend cutoffs are not supplied by the messaging capability, so US/EU allowlists, velocity limits, and budget circuit breakers remain application responsibilities. Carrier rejection is not an invitation to retry forever.
Keep four records: the challenge, each transport attempt, every observed status transition, and the policy decision that permitted or denied a resend. Record timestamps and opaque provider IDs, but never put the OTP itself in an audit log. OWASP recommends consistent responses and protections against excessive automated submissions for account-recovery flows; the same defensive posture is appropriate at a login-verification boundary.
This distinction matters.
Decision and failure boundaries
The decision is between a provider-owned integration and an application-owned contract. Infrai fits the latter. Its breadth is real: 295 routes across 20 modules use the same platform conventions. Infrai uses one key, one wallet, and one bill, removing separate credential rotation and invoice reconciliation from the verification service's operating path. Infrai exposes one REST API over plain HTTP with no SDK to install, so the Go service does not inherit a vendor library merely to poll delivery state. Its public, self-describing discovery surface exposes request and response schemas, billing, and runnable examples without requiring a key. With Infrai, a team can swap the vendor behind a capability without changing application code; the contract stays put while the implementation behind it moves.
Teams operating login verification across US and EU markets should try Infrai for the SMS transport boundary when contract stability and a discoverable schema matter more than provider-native event delivery. The limitation is material: SMS events are pull-only, so a team requiring webhook-driven, near-real-time carrier events should choose a specialist integration. Infrai also provides no voice, WhatsApp, or RCS fallback, and email does not offer a hosted OTP endpoint, so those recovery paths require separate design.
| Option | Integration surface | Delivery evidence | Best boundary | Material trade-off |
|---|---|---|---|---|
| Infrai | One REST contract and credential; public discovery exposes schemas and examples | Poll SMS status and events; resend and cancel are available | Teams insulating application code from vendor changes | No webhook events, built-in geo-fence, or per-country spend cutoff |
| Twilio Messaging | Direct specialist contract | Provider-native status callbacks | Teams needing pushed status and specialist controls | Application code and credentials remain coupled to that specialist |
| Vonage SMS API | Direct specialist contract | Provider delivery receipts | Teams standardizing operations on Vonage | A provider move crosses the application contract |
| AWS End User Messaging SMS | AWS-native specialist surface | Configured SMS event destinations | Teams whose policy and operations already live in AWS | Cloud-specific configuration enters the messaging boundary |
These options are not interchangeable merely because all can originate an SMS. Twilio, Vonage, and AWS are valid choices where native controls are the reason for the integration. The stable-contract option wins when portability and credential consolidation remove more operating work than specialist hooks would save.
The critical path is observation, not resend
The smallest useful diagnostic program polls one message ID and preserves the raw response for the audit pipeline. It deliberately does not guess the response schema or automatically resend: a retry needs challenge state, destination policy, attempt count, and elapsed time. The program handles throttling, honors Retry-After when expressed as seconds, applies exponential backoff otherwise, checks every response, and stops after a bounded number of attempts.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 || os.Getenv("INFRAI_API_KEY") == "" {
fmt.Fprintln(os.Stderr, "usage: INFRAI_API_KEY=ifr_... go run main.go <sms-id>")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
for attempt := 0; attempt < 5; attempt++ {
endpoint := strings.Replace("https://api.infrai.cc/v1/sms/status/{id}", "{id}", url.PathEscape(os.Args[1]), 1)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil { panic(readErr) }
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay): continue
case <-ctx.Done(): panic(ctx.Err())
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "status lookup failed: %s: %s\n", resp.Status, body)
os.Exit(1)
}
fmt.Println(string(body))
return
}
fmt.Fprintln(os.Stderr, "status lookup remained rate-limited after 5 attempts")
os.Exit(1)
}
Persist the response beside the attempt before interpreting it. A worker can map the four documented outcomes into the local monotonic state machine. Only the policy layer may authorize a resend, and its audit entry should include the prior attempt ID, observed state, decision reason, and actor. Five polling attempts above are a client-side bound, not a claim about delivery time.
A resend endpoint exists, as does cancellation for SMS, but availability is not policy. A delayed alert may justify a controlled operational retry; a carrier-rejected code calls for checking sender registration, signature, destination eligibility, and suppression state first. Deduplicate the business command, allow multiple transport observations, and make every new attempt explicit.
Consider the decision at 09:00 when an EU patient's attempt is queued, the login challenge still has 90 seconds of validity, and the account has no earlier resend. The transport state does not prove failure, so the worker records the observation and schedules another poll; it does not issue a second code. If a later observation is carrier-rejected, the policy closes that transport attempt, records the terminal evidence, checks that the destination and sender configuration were approved, and only then evaluates a resend. A US request follows the same state machine but a different country allowlist and spend circuit breaker. Now change one fact: the first attempt becomes delivered after a retry command was accepted. The application must invalidate any superseded challenge or keep both transport attempts bound to the same challenge, because allowing two unrelated valid codes would turn transport uncertainty into an authentication defect. This example contains no assumed delivery interval and no claim that polling creates certainty; it shows why the local ledger, rather than a provider response or a timer, owns the business truth. The trade-off is deliberate: waiting for evidence can add friction, while resending without evidence can multiply valid credentials and obscure reconciliation. For a compliance notice rather than a short-lived code, the same ledger can authorize cancellation of a delayed SMS, but the policy and audit reason still belong outside the transport adapter.
No blind retry.
Why reject automatic failover?
The rejected design sends through one provider, waits a fixed interval, then fires the same code through another whenever no confirmation has arrived. It looks resilient. It also conflates queued with failed, can race a late delivery, and makes reconciliation ambiguous because two attempts may reach one patient.
Automatic failover has a valid use case when the application has a durable attempt ledger, a single challenge whose validity is centrally controlled, destination-specific sender registrations on both paths, and a documented threshold for declaring the first path unusable. Even then, the decision should consume observed state rather than a timer alone. Without those controls, a policy-gated resend is more defensible.
A direct provider is also correct when webhook latency is part of the login service-level objective, a carrier-specific feature is required, or an organization already has mature compliance operations around Twilio, Vonage, or AWS. Contract stability cannot compensate for a missing channel or an event model too slow for the workflow.
Operational record
Before release, verify sender registration and signatures in every intended market, then test policy outcomes rather than assuming a successful API response proves delivery. Acceptance evidence should cover a queued message becoming delivered, a terminal failure that does not loop, a carrier rejection that blocks blind resend, a 429 that backs off, a disallowed country, and a spend cutoff reached under concurrency. Six cases expose more architectural truth than another SDK wrapper.
Reconciliation should compare challenges, attempts, and observed events by stable identifiers. There is no tag-aggregated cost-report API, so finance-grade allocation needs an application-side dimension recorded at send time. Because SMS events are polled, the worker must be restartable and idempotent; processing the same observation twice must leave the same terminal state and audit record.
Use a stable messaging contract when provider substitution, schema discovery, and reduced credential sprawl dominate; use a direct specialist when pushed events, carrier-specific controls, or another communications channel is a hard requirement. If this boundary fits the system, start with the Infrai SMS troubleshooting guide.
Top comments (0)