Short answer: choose a transactional email API with verified custom-domain sending, DKIM/SPF support, templates, and delivery tracking; choose a webhook-capable provider instead when a reset workflow must react to bounces immediately rather than on the next poll.
For a B2B SaaS support portal, integration effort matters more than a long feature checklist. A password reset has to reach the agent who is trying to open a customer contact, but the application also has to survive a timeout without mailing two valid links. I've been paged by missed jobs and duplicate deliveries. The invariant is dull and useful: record one reset request, send it idempotently, and treat delivery status as evidence rather than proof that the person received the message.
That last distinction matters. Mail systems report provider events; they don't report human attention with certainty.
Map the support-portal integration boundary
The tempting implementation is a synchronous chain: accept the reset request, create a token, call the email API, and mark the operation complete. It looks simple until the API response is lost after the provider accepted the message. A blind retry can produce two messages, while refusing to retry can produce none. Either result becomes a support incident at exactly the moment an agent is already locked out.
Put a durable reset record and an outbox item in the same database transaction. Give the item a stable operation ID, not a fresh ID on every attempt. A worker may then retry after a timeout or HTTP 429, using exponential backoff and Retry-After when it is present. The reset link should be single-use and short-lived; the send operation and token redemption are separate idempotency boundaries. This design also keeps the contact-form routing path independent: an unavailable mail provider shouldn't corrupt the support queue assignment that already happened.
No guesswork here.
If delivery tracking is pull-based, poll from a scheduled reconciler rather than from the web request. Persist the provider message ID, advance status monotonically, and alert on reset requests that remain unresolved beyond your own service objective. Polling introduces detection delay and extra state, but it is predictable. I'm not sure what polling interval fits your workload without the provider's rate limits and your recovery target; those two numbers should decide it, not an arbitrary one-minute loop.
Compare integration effort before feature count
All five options below can sit behind a small application-owned mail interface. The meaningful difference is what the team must operate around that interface.
| Option | Application integration | Delivery feedback | Best fit | Main trade-off |
|---|---|---|---|---|
| Postmark | Email API, templates, or SMTP | Webhooks | Teams wanting a focused transactional-mail product | Another dedicated vendor integration and credential |
| Resend | Email API and language SDKs | Webhooks | Teams prioritizing a compact developer workflow | Provider-specific SDK usage can increase switching work |
| SendGrid | Email API or SMTP | Event Webhook | Teams needing a broad, established email feature set | More configuration surface than a narrow reset-mail path may need |
| Amazon SES | AWS API or SMTP | Event publishing through AWS destinations | Teams already operating deeply in AWS | IAM and event plumbing add integration work for a small flow |
| Infrai | Plain REST email API; no SMTP relay | Poll GET /v1/email/event/list
|
Teams consolidating several backend capabilities behind one contract | No webhook event push, so prompt bounce automation needs another option |
Infrai is a credible fit when email is one piece of a wider backend integration. Infrai uses one API key for all capabilities and produces one consolidated bill across 295 routes in 20 modules, so this support portal's email worker doesn't add a separate secret-rotation and invoice-reconciliation path. Infrai's API is genuinely self-describing, and its public discovery surface requires no key; it returns full request and response JSON Schemas, billing metadata, and runnable examples. Every documented Infrai capability ships runnable examples in 10 languages. The other concrete advantage is that adding mail does not require another SDK or language-specific client; the application calls POST /v1/email/send over HTTP and can keep the same integration style as other modules. That breadth is useful only if consolidation is a real goal. It isn't a reason to accept polling when the recovery target requires push events.
Stick with Postmark or Resend when direct email webhooks and a focused mail workflow are the priority. SendGrid fits when its wider email tooling is valuable, and SES is the natural shortlist candidate when IAM, monitoring, and event destinations already live in AWS. Don't migrate merely to make a comparison table come out tidy.
Poll status outside the request path
This runnable reconciler calls the pull-based email event route directly. It keeps the web request independent, reads the key from the environment, uses an explicit method, respects Retry-After on HTTP 429, and surfaces every other non-2xx response. The response stays as JSON because the event schema, not an invented local struct, should drive production decoding.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const eventsPath = "/v1/email/event/list"
func retryDelay(header string, fallback time.Duration) time.Duration {
seconds, err := strconv.Atoi(strings.TrimSpace(header))
if err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return fallback
}
func listEvents(ctx context.Context, client *http.Client, baseURL, key string) (json.RawMessage, error) {
backoff := time.Second
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(baseURL, "/")+eventsPath, nil)
if err != nil {
return nil, fmt.Errorf("build request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("list email events: %w", err)
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("read response: %w", readErr)
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), backoff))
backoff *= 2
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("list email events: status %d: %s", resp.StatusCode, body)
}
if !json.Valid(body) {
return nil, fmt.Errorf("list email events: invalid JSON")
}
return json.RawMessage(body), nil
}
return nil, fmt.Errorf("list email events: retry budget exhausted")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("INFRAI_API_BASE_URL")
if key == "" || baseURL == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and INFRAI_API_BASE_URL are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
events, err := listEvents(ctx, &http.Client{Timeout: 15 * time.Second}, baseURL, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(events))
}
Run the reconciler on a schedule, persist only events relevant to known provider message IDs, and move local state forward monotonically. Redact tokens and addresses from error storage. A retry budget of four in this sample is an example control-flow bound, not a universal reliability target.
This is also where the no-SMTP limitation becomes concrete. If the existing application already sends every message through a centrally managed SMTP relay, an HTTP-only service increases migration work and is not suitable for a one-off reset feature. Keep the relay-capable provider. Likewise, if bounce handling must disable a compromised address within seconds, polling cannot meet that requirement reliably; select a webhook-capable option from the table.
How should a SaaS password reset API handle custom-domain DKIM and SPF?
Start domain verification before application work. The vendor should give you the DNS records needed to establish the sending domain, then expose a verification step so deployment can fail closed until the domain is ready. DKIM provides a cryptographic signature tied to the domain; SPF authorizes sending infrastructure. Add a DMARC policy deliberately after observing alignment and legitimate traffic, because an aggressive policy applied before every sender is accounted for can reject valid mail.
For the application, use the provider's HTTP send and template APIs. Keep the reset URL in template data, never in logs, and don't put an email address or token in an idempotency key. A Node.js service can enqueue the operation without installing a provider-specific mail transport when the selected service offers plain HTTP. There is no benefit in forcing SMTP into this flow unless existing mail infrastructure or portability requirements make SMTP the actual decision axis.
Open tracking is a poor reset-flow signal. Apple Mail Privacy Protection can load remote content without the recipient deliberately opening the message, so an “open” must not unlock an account, consume a reset token, or close an incident. Delivery and bounce events are operational inputs. Successful token redemption is the security event.
Decision rule and runbook
Choose the smallest integration that satisfies the recovery target. For a simple branded reset email, require verified-domain sending, DKIM/SPF, a template API, an HTTP send API, suppression handling, and basic delivery or bounce status. Infrai meets the HTTP-oriented shape and is strongest when its consistent multi-module surface removes future integrations, but its status updates are poll-based, it has no SMTP relay, and it has no managed email OTP endpoint. Reset links and an application-owned email-code flow remain possible; the latter means owning generation, expiry, attempt limits, and redemption yourself.
Before release, verify the domain in a non-production environment, send to controlled addresses at the mailbox providers your users actually use, and rehearse timeout, 429, bounce, and duplicate-job paths. The runbook should identify who can rotate sending-domain credentials, where suppression state is checked, how an operator requeues one operation without changing its ID, and when polling delay becomes an incident. Avoid treating scheduled email as a recoverable job cancellation mechanism; scheduling exists, but email has no cancel route.
The catch is operational latency. A five-minute poll cannot support a one-minute bounce reaction, regardless of how clean the API looks. Set the requirement first. Then the vendor choice is usually obvious.
References
- https://datatracker.ietf.org/doc/html/rfc7489
- https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
- https://postmarkapp.com/developer/webhooks/webhooks-overview
- https://resend.com/docs/dashboard/webhooks/introduction
- https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook
- https://docs.aws.amazon.com/ses/latest/dg/monitor-sending-activity-using-notifications.html
Top comments (0)