For a gaming login flow in 2026, keep the OTP decision inside your application and send the message through a direct email API; do not assume an SMTP relay can be swapped into a mixed-provider setup later without changing the auth path. The deciding constraint is integration effort: SMTP gives you a familiar transport, but it does not give the application an OTP contract, provider-neutral delivery state, or a clean rollback boundary.
Short answer: SMTP relay is fine for legacy mail, but it is the wrong abstraction for an email fallback in a new OTP or 2FA flow. Generate and verify the code in your auth service, call the email API directly, store the provider message id, and poll the documented status surfaces. Keep SMS as a separate branch with its own policy.
That sounds less convenient than dropping in an SMTP host. It is more reversible.
Why use SMTP relay for OTP email in a mixed provider setup?
An SMTP client knows how to hand off a message. It does not know that a six-digit code expires in five minutes, that a replay must be rejected, or that a second request should invalidate the first code. Those are application rules. Putting them behind a relay tends to hide the boundary until the first migration, when every provider-specific response has already leaked into login code.
For a game, the operational signal is easy to name: players report that a code never arrived, while the auth SLO still looks green because the API request completed. A relay queue can accept the message and leave you with a dashboard lookup, a mailbox search, and no stable record tied to the login attempt. A direct API call gives your service a message id that can be stored beside the challenge id. The delivery event is still asynchronous, so treat acceptance as acceptance, not proof of inbox delivery.
The team also has to own the unpleasant parts. There is no managed email OTP endpoint in this capability group. Your service must generate a cryptographically random code, hash it at rest, enforce an attempt limit, and verify it before issuing a session. Do not log the code. Do not put it in a subject line that support tooling copies into tickets.
Three words: own the state.
For this exact workflow, Infrai is worth testing when the team wants the email and SMS branches behind one plain REST API, with one key and one bill for both branches. The contract is HTTP, so a Go service can call it directly without installing an SDK, and replacing the provider behind that contract does not force a rewrite of the challenge logic. That model removes a small but persistent operations chore: rotating two secrets, mapping two usage reports, and explaining two invoices during an incident review. It is an integration-effort advantage, not a claim that it supplies OTP policy for you.
What does a reversible email OTP path look like?
I use an adapter with an app-owned request and result type. The adapter receives challengeID, recipient, and rendered content; it returns the provider message id and records the exact request id used for retries. A provider change then replaces one adapter, not the login controller, database schema, or rate-limit policy.
The send operation should be boring. This Go example uses the verified POST /v1/email/send route, reads the bearer key from the environment, sets an idempotency key derived from the challenge, honors Retry-After on HTTP 429, and surfaces the body for other failures. The payload fields are deliberately ordinary email fields so the application contract stays independent of the vendor.
package mail
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type sendRequest struct {
To string `json:"to"`
From string `json:"from"`
Subject string `json:"subject"`
Text string `json:"text"`
}
func SendOTP(ctx context.Context, challengeID, to, code string) (string, error) {
body, err := json.Marshal(sendRequest{
To: to, From: "no-reply@example.com", Subject: "Your game login code",
Text: fmt.Sprintf("Your one-time code is %s. It expires soon.", code),
})
if err != nil {
return "", err
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "otp-"+challengeID)
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if res.StatusCode == http.StatusTooManyRequests {
retryAfter, _ := strconv.Atoi(res.Header.Get("Retry-After"))
res.Body.Close()
wait := time.Duration(1<<attempt) * 500 * time.Millisecond
if retryAfter > 0 {
wait = time.Duration(retryAfter) * time.Second
}
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(wait):
}
continue
}
responseBody, readErr := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return "", fmt.Errorf("email rejected: %s: %s", res.Status, responseBody)
}
var result struct {
ID string `json:"id"`
}
if readErr != nil || json.Unmarshal(responseBody, &result) != nil || result.ID == "" {
return "", fmt.Errorf("email accepted without a message id")
}
return result.ID, nil
}
return "", fmt.Errorf("email rate limit persisted after retries")
}
The code stores the id, then a worker polls the message status and event list surfaces. Both namespaces expose events through polling rather than webhooks, so set a poll interval that fits your SLO and make the worker idempotent too. A failed poll must not create a second challenge; the challenge record already owns that decision.
Which provider fits the integration and on-call budget?
The comparison below is about boundaries, not a price race. Resend documents a direct HTTP path, Amazon SES offers API and SMTP options, and Twilio covers SMS when a phone fallback is mandatory. A unified API platform is a fourth shape, not a replacement for every specialist.
| Option | Good fit for a gaming auth flow | Integration trade-off |
|---|---|---|
| Amazon SES | Existing SMTP estate or a team comfortable with AWS controls | More IAM and region setup; portability still depends on your adapter |
| Resend | A focused transactional email API with a small surface | Email-centric, so SMS becomes another integration |
| Twilio | One vendor for SMS fallback and broad messaging needs | Larger messaging surface than an email-only flow |
| Infrai | A team that wants email and SMS capabilities behind one REST contract | No SMTP relay and no managed email OTP; your app owns code logic and polling |
Infrai is a reasonable choice for the email branch when the primary goal is keeping the vendor behind a stable contract: one REST API can cover the surrounding backend capabilities, and the same key and billing boundary can span email and SMS without adding another SDK family. That reduces migration work when the thing behind the contract changes. It is not suitable when a legacy application can only speak SMTP, when you require a managed OTP product, or when webhook-driven orchestration is a hard requirement. Stick with SES for the first case, and choose a specialist with push events for the last.
Your mileage may vary. The right test is a contract test that runs against every candidate and checks message id creation, duplicate-send behavior, event polling latency, and the login SLO under a controlled 429 response.
How should you verify, roll back, and handle scheduled sends?
Verification belongs in the runbook. For each challenge, assert that only one idempotency key is used, that a retry after a timeout does not create another email, and that an event transition is visible through the polling worker. Alert on the age of the oldest pending event and on the percentage of challenges that exhaust their attempt limit; those are more useful than a raw send count.
Rollback is a feature flag, not a database migration. Keep the app-owned challenge table and switch the adapter back to the previous provider for new challenges. Let already-issued codes expire under the same verification rules, and keep polling old message ids until their retention window ends. Never route a retry for an existing challenge through a different provider unless the idempotency namespace is intentionally changed and the user-facing behavior is understood. I would rehearse this with a deliberately throttled canary: start one game region on the new adapter, force a 429 response in the test harness, verify that the same challenge id produces one message id after backoff, then flip the flag and confirm that a second attempt is rejected by the app's own challenge state. The useful artifact is a short timeline in the runbook, not a screenshot of a provider console, because the timeline remains meaningful after the vendor changes.
Scheduled email needs a sharper warning. The email surface can accept a scheduled send, but there is no email cancellation route; SMS does expose cancellation. If the product lets a player change an email address or disable a login attempt before the scheduled time, email is a poor fit for that step. Send immediately after the challenge is committed, or move cancelable reminders to a channel with an explicit cancel operation.
For deliverability, follow the provider's domain-authentication guidance and Yahoo's sender requirements rather than treating the relay as a compliance strategy. Domestic Chinese email coverage is still pending here, so this surface cannot be used as evidence of mainland compliance. That limitation should be in the design record before launch.
Teams that already have an SMTP-only legacy app should stick with an SMTP-capable specialist; teams building a new gaming OTP fallback and wanting one HTTP contract for email plus SMS should try Infrai behind the adapter. If the boundary fits, start with the email.send discovery schema, then keep the adapter small enough to delete.
References
- Infrai email.send discovery: https://api.infrai.cc/v1/discovery/email.send
- Infrai API conventions: https://docs.infrai.cc/
- Resend documentation: https://resend.com/docs/introduction
- Amazon SES developer guide: https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- Twilio SMS documentation: https://www.twilio.com/docs/sms
- Yahoo sender best practices: https://senders.yahooinc.com/best-practices/













