Healthtech login recovery is an evidence problem before it is a delivery problem. Short answer: choose the SMS OTP design that can prove each password-reset decision, not the API that merely returns a send success. A short expiry, regional controls, and a durable audit record should be acceptance criteria in the same review.
I have been paged for missed cron jobs and duplicate deliveries. The lesson carries into reset messages: a provider response is not proof that a user received one valid code. For a healthtech tenant, the useful record is a timeline showing who requested a reset, which policy version applied, what was sent, how long it was valid, and why a verification attempt was accepted or rejected. During an incident review, I want to correlate the API request, queue attempt, carrier handoff, and verifier decision without opening a message body or exposing a phone number. That means assigning the request ID before enqueueing, carrying it through every retry, and writing an outcome even when the transport times out. It also means documenting the retention period and access role for those records, because an audit trail that only an on-call shell account can read is not useful evidence. Keep message content out of that audit trail; a salted request identifier and a redacted destination are enough for correlation.
No shortcut.
What should a US/EU SaaS SMS OTP flow record for verification?
Start with one immutable event per state transition. requested, throttled, dispatched, verified, expired, and revoked are easier to investigate than a single mutable row. Store an event timestamp in UTC, tenant and request identifiers, a hash of the OTP, expiry timestamp, attempt count, policy version, and the reason for every denial. A verifier can then answer an auditor's narrow question without reconstructing application logs.
The code below keeps the OTP single-use. It hashes the code before persistence, binds it to a reset request, and refuses a second successful verification. The repository and sender are deliberately generic so the same control can sit in front of any SMS API.
package reset
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"time"
)
var ErrInvalid = errors.New("invalid or expired code")
type Attempt struct {
RequestID string
CodeHash string
ExpiresAt time.Time
Used bool
Attempts int
}
type Store interface {
Load(ctx context.Context, requestID string) (*Attempt, error)
Save(ctx context.Context, requestID string, a *Attempt) error
}
func Verify(ctx context.Context, store Store, requestID, code string, now time.Time) error {
a, err := store.Load(ctx, requestID)
if err != nil || a == nil || a.Used || !now.Before(a.ExpiresAt) || a.Attempts >= 5 {
return ErrInvalid
}
a.Attempts++
h := sha256.Sum256([]byte(code))
got := hex.EncodeToString(h[:])
if subtle.ConstantTimeCompare([]byte(got), []byte(a.CodeHash)) != 1 {
return ErrInvalid
}
a.Used = true
return store.Save(ctx, requestID, a)
}
The five-attempt ceiling is a policy example, not a universal number. Your risk team should set it with the expiry window and account lockout rules, then record that decision as configuration evidence. A compare-and-swap or transactional update is required: two concurrent requests must not both observe Used == false and succeed. In one review, I would ask for the race test, the clock source, and the exact event emitted on each failed compare; those details turn a control statement into something an assessor can replay from logs.
How do rate limits and retries preserve a compliant audit trail?
Rate limiting has two layers. Apply a tenant and account policy before creating a code, then apply an IP or device policy to blunt enumeration. Return the same outward response for an unknown account and a known account. Internally, emit throttled with a reason code and the policy version.
Retries are where schedules turn into duplicate texts. Generate one request ID and reuse it for every transport attempt. Retry only transient transport outcomes, honor a provider's Retry-After when present, and stop at a bounded deadline shorter than the OTP lifetime. If the sender may deliver after a timeout, an idempotency key at your boundary prevents a second code from being created; a delivery receipt is evidence of handoff, not evidence of verification.
I once treated a timeout as a failed send and queued a fresh message. That made the pager quiet while two valid codes raced the user. The fix was boring: one state machine, one request ID, and a reconciliation job that marks an uncertain dispatch for review instead of inventing another credential.
Which evidence and standards belong in the design review?
NIST SP 800-63B describes requirements around authenticator secrets, rate limiting, and verifier behavior; use it to define the control objectives, then map each objective to an event and a test. For email fallback, DMARC (RFC 7489) governs domain authentication policy, but it does not make an SMS channel trustworthy. Keep those channels' evidence separate.
Test the unhappy paths first: clock skew at expiry, replay after success, five bad attempts, duplicate queue messages, provider timeouts, and a regional policy mismatch between US and EU tenants. In staging, assert that logs contain identifiers and decisions but never the OTP or full phone number. During deployment, treat a policy change like a schema change: version it, canary it, and retain the prior version for incident reconstruction.
This approach is not suitable when you need phishing-resistant authentication or users cannot reliably receive cellular messages; use a WebAuthn or hardware-backed factor as the primary method and retain SMS only for recovery where policy permits. It is also a poor fit for a team that cannot operate an audit store and reconciliation process. In that case, stick with a managed identity flow that exports the evidence your assessor requires, even if it limits customization.
The practical decision rule is simple: compare integrations on evidence fields, idempotency semantics, regional controls, and exportability before comparing delivery features. I'm not sure any single service will satisfy every jurisdiction or retention policy; your mileage will vary with the assessor and data residency contract. Prove the controls in a failure drill, then choose the API that leaves the clearest record.
Top comments (0)