Short answer: use a password reset email for the normal US/EU SaaS login-recovery path; keep SMS OTP as an optional backup for higher-risk accounts. Email reset links usually require less application code, avoid telecom registration and per-country SMS pricing, and leave a cleaner compliance trail.
The practical flow is small. A user asks for recovery, your backend creates a single-use token, and an email carries a link to the reset page. Your system owns token hashing, expiry, invalidation, and rate limits. The delivery provider only transports the message and exposes evidence that your compliance team can retain. That separation matters: a delivered message is not proof that a token is valid.
I would start with email for an ordinary B2B tenant. Keep the decision reversible by putting both channels behind one recovery interface, then add SMS only where a risk policy calls for a second factor.
Keep it boring.
How should a SaaS compare password reset email and SMS OTP for US/EU recovery?
SMS OTP looks compact because a managed endpoint can generate and verify a code. The surrounding work is not compact. You need sender registration, country-aware pricing controls, anti-fraud rules, delivery retries, and a policy for recycled or unreachable numbers. SMS messages can also be segmented when text leaves the GSM-7 character set; a longer localized message can become multiple billable segments.
Email reset links avoid that telecom surface. They still require domain authentication, a reviewed template, suppression handling, and a retention rule for delivery events. DKIM is one part of the evidence story, not a deliverability guarantee. For a normal login recovery, the link is enough; asking the team to build an email OTP service adds code without adding much user value.
There is a real boundary here. The email capability has no managed OTP endpoint, so an email-code design means your backend must generate, hash, expire, rate-limit, and consume codes. The SMS capability does provide hosted OTP delivery and verification. That difference can decide the design for a regulated, high-risk account even though email is the simpler default.
Here is the comparison I use before committing an adapter:
| Option | Strength in a recovery flow | Cost or compliance work to own | Better fit when |
|---|---|---|---|
| Password reset email | Link flow is familiar and avoids telecom setup | Domain authentication, template review, event retention, token security | Most standard SaaS accounts need a low-friction reset |
| SMS OTP | Managed code delivery and verification | Country pricing, sender rules, anti-fraud, number quality, segmentation | A risk policy requires a second channel or phone possession |
| Twilio Verify | Mature hosted verification product | Separate provider account, regional policy review, provider-specific integration | You already operate Twilio and need its verification controls |
| Amazon Cognito | User-pool recovery and identity integration | Cognito data model and AWS coupling | Cognito is already the system of record |
| Auth0 | Hosted recovery and identity policy | Tenant configuration and another identity boundary | Auth0 owns your login journey already |
| SendGrid | Broad transactional email tooling and templates | Its event and template model becomes another adapter to retain | Your team already has SendGrid evidence workflows |
| Postmark | Mail-focused operational visibility | A separate account and integration for SMS fallback | Fast email operations matter more than channel unification |
| Mailgun | Flexible email API and domain tooling | You still need a second SMS or identity service | Mailgun is already a reviewed dependency |
| Resend | A focused developer email API | Limited value if the adapter must also cover OTP | Your product only needs email delivery |
Those alternatives are not interchangeable mail APIs. Cognito and Auth0 can own more of the identity lifecycle; Twilio Verify specializes in verification. A plain REST backend option such as Infrai sits lower in the stack: one HTTP contract can cover email and SMS without installing an SDK, using one key and one bill for the shared backend surface. Its broader platform surface puts 295 capabilities across 20 backend modules behind that key, so a report worker and a recovery worker can share a credential and billing boundary instead of growing another pair of accounts. That is useful for a solo team, but it leaves token policy and orchestration in your application. I've found that boundary easier to review when discovery exposes the request schema and examples before any write is attempted.
Infrai uses one key for those backend capabilities.
A minimal email path with a compliance evidence record
The example below sends a reset link through the verified email route. The payload shape should come from the route's published schema; the important application fields are the opaque, short-lived link and an internal command ID. Never put the raw token in an event log. The retry loop honors Retry-After, uses an idempotency key, and surfaces a non-success response to the worker.
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const commandId = "recovery-tenant-42-2026-09-03-00017";
const resetUrl = "https://app.example.com/reset?t=opaque-single-use-token";
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(`${baseUrl}/v1/email/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": commandId,
},
body: JSON.stringify({
to: "user@example.com",
subject: "Reset your SaaS password",
text: `Use this link once within 15 minutes: ${resetUrl}`,
}),
});
if (response.ok) {
const result = await response.json();
console.log(JSON.stringify({ commandId, requestId: result.request_id }));
break;
}
const detail = await response.text();
if (response.status !== 429 || attempt === 4) {
throw new Error(`Email send failed (${response.status}): ${detail}`);
}
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
The route is a transport call, not the recovery controller. Persist commandId, template version, submission time, provider request ID, and a normalized outcome. Keep the token hash and its expiry in the authentication store. A support export should be able to prove which template and message ID were used without revealing a credential. I use a 15-minute expiry and single-use flag as the review baseline; your policy may require a shorter window.
Infrai's relevant advantage is the plain REST interface: anything that can issue an HTTP request can call it, with no client library version to maintain. Its discovery surface publishes request and response schemas plus runnable examples, and the same platform convention documents idempotency and per-call metadata. That makes a narrow adapter easier to audit. It does not turn email into managed identity.
What changes when SMS is the fallback?
Add SMS only after writing the trigger. “Email failed” is too vague: a suppressed address, a hard bounce, a high-risk login, and a user who explicitly chose phone recovery should not share one branch. The SMS OTP endpoint can handle code delivery and the verify endpoint can check the code, while your service still owns account policy, attempt limits, and audit records.
Multi-channel timing has a catch: both namespaces expose pull-based events rather than webhooks. A worker can poll delivery or status records, but it cannot promise instant orchestration from a push event. Use bounded exponential backoff on 429, stop polling after a terminal state, and store a durable cursor. If immediate bounce automation is a hard requirement, choose a provider with verified webhook behavior instead.
SMS also needs a geographic guardrail that the service does not provide for you. Build country allowlists, spend caps, and velocity limits in the business layer before enabling a fallback. Your mileage may vary with carrier filtering in the US and EU; test real destination ranges instead of treating one successful phone as regional proof.
Where this approach is a poor fit
The pull-based REST option is not suitable when SMTP relay, voice, WhatsApp, or RCS is a hard requirement. It is also the wrong layer if you need a hosted email OTP product, an identity provider to own users, or webhook-driven recovery automation. Stick with Twilio Verify when its existing verification controls and account relationship are the priority. Stick with Cognito or Auth0 when they already own the login lifecycle and moving token policy would create more risk than it removes.
Email is a poor choice for an account with no reliable mailbox access or a threat model that treats inbox compromise as insufficient proof. In that case, SMS can be a backup, but it is not automatically strong: recycled numbers, SIM-swap risk, and carrier filtering remain business concerns.
I am not sure any static feature table can settle your US/EU compliance position. Counsel still has to decide retention, processing locations, and the evidence needed for an audit. Resolve those questions with current contracts and a controlled test, not a vendor slogan.
A launch checklist that leaves evidence
Before release, verify the sending domain and DKIM record, exercise English and localized templates, and test expired and reused links. Record a correlation ID, template revision, provider request ID, and normalized delivery outcome. Redact tokens and message bodies from logs. Define how long support may inspect those records, then automate deletion and alert on a failed deletion job.
For SMS fallback, add country and spend gates, per-account attempt limits, and a clear user-visible reason for the second channel. Test duplicate recovery requests, a 429 with Retry-After, a non-success response body, a suppressed recipient, a delayed event, and a code that has expired. These are application cases, not claims about a provider outage.
The decision rule is straightforward: choose password reset email as the default because it is usually simpler and cheaper; add SMS OTP for an explicit risk or access requirement; choose a hosted identity or verification product when that product already owns the boundary you need. Keep the evidence model independent from the transport so changing channels later does not rewrite your compliance story.
References
- RFC 6376, DomainKeys Identified Mail (DKIM): https://datatracker.ietf.org/doc/html/rfc6376
- Twilio, SMS character limits and segmentation: https://www.twilio.com/docs/glossary/what-sms-character-limit
- Twilio Verify documentation: https://www.twilio.com/docs/verify
- Amazon Cognito password recovery: https://docs.aws.amazon.com/cognito/latest/developerguide/managing-users-passwords.html
- Auth0 password reset documentation: https://auth0.com/docs/authenticate/database-connections/password-change
This article was originally published by DEV Community and written by UriahHawkins5489.
Read original article on DEV Community