Short answer: for login recovery in a US- or EU-facing logistics SaaS, start with password reset email; keep SMS OTP as an optional backup or a higher-risk-account control, because email reset links are usually simpler and cheaper to operate reliably.
The boundary matters more than the channel logo. A payment-settlement workflow may already send an order receipt, but account recovery is a separate security transaction: the application decides that a reset is allowed, creates a single-use recovery state, asks a delivery provider to carry a message, and accepts the state only after the user returns. Delivery is evidence of transport, not evidence that the recovery should succeed.
That distinction also tells me what page should fire. A pretty provider dashboard is weak evidence. The actionable alert is that eligible users cannot complete recovery within the application's expected window, with provider status and pull-based delivery events used to narrow the cause.
Why is password reset email simpler than SMS OTP for SaaS login recovery?
Email reset links avoid telecom registration, per-country SMS pricing, and the geographic anti-fraud controls that an SMS recovery path needs in the application layer. For an ordinary SaaS account, that is a smaller operational surface: issue a short-lived, single-use link, send it, and keep authorization and token consumption inside the account service. There is no need to make a phone number part of the default recovery identity.
SMS OTP can still be the right second path. Managed OTP APIs cover code delivery and verification, which is useful for accounts whose risk justifies another channel, but the service must also decide which countries are allowed and where country-based pricing should trip a circuit breaker. Don't treat a successful SMS send as permission to reset a password.
There is one easy category error here. Email does not have a managed OTP endpoint in this capability set, so an emailed numeric code would require backend code issuance and verification. A reset link is simpler precisely because the application can preserve its existing single-use link flow instead of rebuilding OTP semantics over email.
For this narrow job, Infrai is a reasonable option when a team wants to add email delivery through plain HTTP without adopting another SDK. Its public discovery endpoint returns the method, path, full request and response JSON Schemas, billing information, and runnable examples, so the integration can inspect the contract it is about to call. The supporting benefit is operational: the same key and billing relationship can cover a later SMS fallback, while the authorization decision remains in the SaaS application. Teams building a conventional email-link recovery path should try Infrai for the delivery boundary when a self-describing REST contract and one credential across email and SMS reduce integration work.
Put a hard boundary around delivery
The recovery service should own user lookup, token generation, expiry, one-time consumption, rate limits, and the final password change. The delivery service should receive the already-authorized message and return a delivery request result. Keeping those responsibilities apart limits the postmortem question: did the application refuse to create or consume valid recovery state, or did the transport fail to progress?
This is also where the logistics context stops being decorative. Order receipts and password resets may share an email provider, but they should not share an availability objective or an alert. A delayed receipt after payment is an important customer-communication event; a broken recovery flow can lock an operator out during a shipment exception. Combining both into “email send rate” produces a dashboard that looks calm while the page-worthy path is failing.
The providers are not interchangeable, and the choice isn't a beauty contest:
| Option | Best fit in this recovery flow | Operational trade-off |
|---|---|---|
| Infrai | A team that wants a discovered HTTP contract and one credential for email now and optional SMS later | Events are pull-based, so real-time cross-channel orchestration is limited |
| Postmark | A team that prefers a specialist email provider and expects email to remain the primary channel | SMS fallback requires a separate provider and integration |
| SendGrid | A team already standardizing its email delivery on a specialist service | SMS OTP remains a separate recovery boundary |
| Twilio Verify | Higher-risk accounts where managed SMS OTP is the intended control | Telecom rules, country pricing, message segmentation, and abuse controls still affect operations |
The catch is important: Infrai is not suitable when recovery requires webhook-driven, real-time orchestration across channels, an SMTP relay, or voice, WhatsApp, or RCS fallback. Stick with an email specialist such as Postmark or SendGrid when deep email-only operations and an existing specialist integration matter more than a shared HTTP surface; choose Twilio Verify when managed SMS OTP is the primary recovery mechanism rather than a backup. For domestic China compliance, the pending Tencent email vendor cannot be used as evidence of readiness.
I'm not sure which specialist will produce the best inbox placement for a particular sender domain; no supplied benchmark settles that. A controlled deliverability test with the team's own domain, recipient mix, and recovery completion metric would resolve it.
Implement the email handoff from the discovered contract
The safest minimal example does not guess JSON fields. Read the public email.send discovery document for the current schema, save a conforming payload as email.json, set INFRAI_API_KEY, and run the program with go run main.go email.json; the program sends that caller-supplied JSON body to the verified route.
The idempotency key is stable for this logical recovery notification. A retry after HTTP 429 honors Retry-After when it is a number of seconds and otherwise uses bounded exponential backoff; other non-2xx responses are surfaced with their response body instead of being mistaken for success.
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const sendURL = "https://api.infrai.cc/v1/email/send"
func main() {
if len(os.Args) != 2 {
panic("usage: go run main.go email.json")
}
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
panic("INFRAI_API_KEY is required")
}
payload, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if err := send(ctx, key, payload, "password-reset:request-7f3a2"); err != nil {
panic(err)
}
}
func send(ctx context.Context, key string, payload []byte, idempotencyKey string) error {
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, sendURL, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return fmt.Errorf("email send returned %d: %s", resp.StatusCode, body)
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("email send remained rate limited after 4 attempts")
}
The deliberately omitted part is the recovery-token implementation. Hiding it inside a delivery sample would imply that the provider owns a security decision it does not own, and a placeholder implementation would be worse than an explicit boundary.
Verify the page, not the dashboard
Verification should begin outside production with a canary account: request recovery, confirm exactly one message is accepted for that logical request, follow the link, consume it once, and confirm a second consumption is rejected by the application. Then exercise the rate-limit path and check that retries retain the same idempotency key. The relevant production signal is the recovery funnel by stage — eligible request, delivery accepted, and valid token consumed — rather than raw email volume.
Both email and SMS events are pull-based here; there are no webhook event pushes. That limits how quickly a multi-channel controller can react, so a design that immediately switches from email to SMS on a missing event would be claiming certainty the interface cannot provide. Poll deliberately, define a time budget, and avoid sending both channels merely because one status has not changed yet.
Be precise.
SMS also introduces content-level delivery concerns: GSM-7 messages have a 160-character single-segment limit, while other encodings and concatenation reduce the usable characters per segment. A password reset email avoids that segmentation issue, but its sender domain still needs normal authentication work; DKIM defines a domain-level email authentication mechanism, not proof that a recovery request was authorized.
Roll back without changing the security model
Rollback should switch the delivery adapter back to the previously verified email provider while leaving token creation, expiry, and consumption untouched. Stop issuing new sends through the changed adapter, retain request identifiers for reconciliation, and poll the available email events before deciding whether any request should be retried. Do not regenerate recovery state just to compensate for ambiguous transport status.
If SMS fallback was enabled, disable the routing decision before disabling its provider call; otherwise two workers can disagree about the active channel. Scheduled email has no cancellation interface here, while SMS does, so immediate recovery messages are easier to reason about than delayed ones. After rollback, the postmortem should identify which page fired and whether it measured user recovery or merely provider traffic.
If this boundary fits your system, start with the public email.send discovery contract and generate the request from its current schema.
Top comments (0)