A verification link is a credential delivery problem, not a welcome-message flourish. The system has to avoid known blocked recipients, survive ambiguous retries, and learn what happened after provider acceptance. My choice for a beginner SaaS is a backend-owned delivery ledger, a suppression check before submission, an idempotent send, and a polling worker that reconciles later events.
TL;DR: Keep the browser away from the mail credential and make one application record the source of truth for every signup attempt. Poll because this particular unified API has no webhook event push; stop polling at an application-defined terminal state or token expiry. This is a practical design for verification links and basic transactional notices, provided delayed reconciliation is acceptable.
The tempting implementation sends during the signup request and marks the job done after a successful HTTP response. It is short. It also collapses four different facts into one boolean: intent, submission, provider acceptance, and the later delivery outcome. A local record costs a table and a worker, but it gives retries and support investigations a stable object to act on.
Infrai's relevant advantage is a single API key and unified billing across 295 routes in 20 modules. One credential reaches every capability, and one bill replaces the work of reconciling separate provider invoices. Adding another backend function can remain one more endpoint under the same contract instead of becoming another key and integration. I would trade webhook immediacy for that smaller boundary only when the product can tolerate polling lag.
Acceptance is not delivery.
How should Next.js and Node.js own transactional welcome email delivery?
Create an immutable attemptId before contacting a provider. Store the user ID, a normalized recipient hash, token expiry, submission state, provider message ID, last check time, and final outcome. Do not put the verification token itself in logs. The link should be single-use, and its validity belongs to the application rather than to a mail event.
This boundary matters after an awkward crash: the provider may have accepted a request while the process died before saving its response. Retrying with the same idempotency key is materially safer than generating a fresh request. Infrai specifies Idempotency-Key as a platform convention, including a deterministic server-derived fallback and a 24-hour default deduplication window, but I would still supply the application attempt ID explicitly. It makes intent inspectable.
Check the suppression list before submission. A known blocked address should not enter the normal retry loop, and later permanent failures should feed the suppression workflow. Suppression is delivery hygiene, not identity proof; possession of the one-time link is what completes this signup step. A custom template can change presentation, but it does not change that state model.
Use a single-send path for recipient-specific verification links. Batch sending is appropriate when the same transactional notice goes to several recipients, not when each recipient receives a distinct credential.
One focused reconciliation helper
The pull model changes the worker design. There is no webhook event push for this email surface, so unfinished ledger rows must be claimed and checked again after a delay. Stop after a terminal result or token expiry. Honor Retry-After on HTTP 429; otherwise use exponential backoff and a finite retry budget.
This TypeScript helper reads one submitted message. It uses the documented API base and message-detail path, keeps the key server-side, sets the method explicitly, and refuses to turn an error body into a successful state.
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function getEmail(messageId: string): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
const apiBase = process.env.INFRAI_API_BASE;
if (!apiKey || !apiBase) {
throw new Error("INFRAI_API_KEY and INFRAI_API_BASE are required");
}
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(
`${apiBase}/email/get/${encodeURIComponent(messageId)}`,
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = response.headers.get("retry-after");
const parsedSeconds = retryAfter
? Number.parseFloat(retryAfter)
: Number.NaN;
const delayMs = Number.isFinite(parsedSeconds)
? parsedSeconds * 1_000
: 2 ** attempt * 1_000;
await sleep(delayMs);
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`Email lookup failed (${response.status}): ${body}`);
}
return JSON.parse(body) as unknown;
}
throw new Error("Email lookup exhausted its retry budget");
}
The send operation should use the same attemptId as its idempotency key and persist the returned message ID beside it. The exact polling interval is a product choice constrained by token lifetime, acceptable support lag, and provider limits; no universal five-second loop deserves to be copied. Claim only due rows so two workers do not race the same attempt.
There is another bookkeeping cost. The unified surface has no tag-aggregated cost reporting API, so a campaign or product-area dashboard needs app-side attribution. Record that association when the message ID is written, while the context still exists.
Four provider boundaries worth testing
Provider selection should follow the operating boundary the team can actually support. Syntax is secondary. Amazon SES fits naturally when AWS account operations are already routine. Twilio SendGrid offers a dedicated email API and a broad email product surface. Postmark concentrates on transactional email. Resend presents a developer-oriented email API. All four deserve a trial with the real sending domain, message content, and deployment regions rather than a decision based on a sample request.
| Option | Boundary to evaluate | Plausible fit | Main trade-off to verify |
|---|---|---|---|
| Amazon SES | AWS service and account model | Teams already operating in AWS | Delivery operations inherit AWS conventions |
| Twilio SendGrid | Dedicated email platform | Teams wanting a direct email integration | Adds a separate credential and vendor surface |
| Postmark | Transactional-email-focused service | Products isolating transactional mail | Other backend capabilities remain separate |
| Resend | Developer-oriented email API | Small teams favoring an email-specific interface | Broader services need other integrations |
The unified surface is a different boundary. Its public discovery surface needs no key and exposes full request and response schemas; documented capabilities also include runnable examples in 10 languages. For this flow, however, reconciliation is pull-based; there is no SMTP relay or managed email OTP, and scheduled email has no cancellation endpoint. The Tencent email vendor is pending, so this option cannot support a mainland-China compliance claim.
Those limits can decide the evaluation early. A product that requires immediate webhook-driven reactions should choose a provider boundary that supplies them. A product that values one credential across many backend modules may accept a polling worker because the ledger already exists.
No measured latency or uptime comparison is available here. I would not rank vendors on either axis without running the same authenticated-domain experiment against each one.
The four-control decision rule
The complete path is small enough to audit:
- Create one durable attempt and one expiring, single-use verification token.
- Check suppression state, then submit with the attempt ID as the idempotency key.
- Save the provider message ID without treating acceptance as delivery.
- Poll due attempts, back off on rate limits, and close each record on a terminal result or expiry.
Email transport does not own resend policy, token consumption, or the support view. The application does. A useful support screen distinguishes never submitted, accepted but unreconciled, suppressed, expired, and completed attempts without exposing the token. That distinction is more valuable than a generic "email sent" badge.
Keep it boring.
Channel expansion adds separate constraints. Email has no managed OTP endpoint here, while voice, WhatsApp, and RCS are absent. If SMS becomes a fallback, geographic anti-abuse rules and country-price circuit breakers must live in business logic. Do not wait for an abuse event to invent them.
What should you measure before adopting it?
Measure the time from signup to completed verification, the age of the oldest unreconciled row, suppression hits before send, final outcomes, and duplicate submissions caught by idempotency. Segment by sending domain and deployment region only when the sample is useful. These are application measurements, not promises about any provider.
Then rehearse recovery. Pause the worker, submit several controlled attempts, restart it, and confirm that every ledger row converges without creating a second message. Exercise an HTTP 429 response and verify both Retry-After handling and the five-attempt ceiling in the helper. Finally, expire a token while its mail record is unresolved; the worker should close the attempt instead of polling forever.
Choose a direct email specialist when its event model and mail operations are the core requirement. Choose the broader REST surface when reducing integration and credential overhead matters more than immediate push events. In both cases, keep the ledger. It is the part you control.
Sources
- Amazon SES Developer Guide: https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- Twilio SendGrid Email API documentation: https://www.twilio.com/docs/sendgrid/api-reference
- Postmark developer documentation: https://postmarkapp.com/developer
- Resend documentation: https://resend.com/docs
- NIST SP 800-63B Digital Identity Guidelines: https://pages.nist.gov/800-63-3/sp800-63b.html
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.