Send transactional email
Transactional emails are the one-to-one messages your app sends in response to a user action: password resets, magic links, receipts, order confirmations, "someone invited you." They're not marketing — they're load-bearing, and if they land in spam or never send, users get locked out or think you took their money for nothing.
Don't touch SMTP or roll your own mail server. Use a developer-first email API — Resend is the default in 2026 — send strictly from the server with the API key in an env var, and author templates as React Email components so you get a real HTML build with a plain-text fallback. The two things that actually make or break this are boring: authenticate a dedicated sending subdomain (SPF, DKIM, DMARC) so you don't hit spam, and don't block the user's HTTP request on the send — fire it into a background job so a slow provider never fails a signup.
Step by step
- 1
Pick a provider and a sending subdomain
Sign up for Resend (or Postmark if deliverability is your #1 concern, or Amazon SES if you're cost-sensitive at scale). Decide on a subdomain to send from — e.g. mail.acme.com or send.acme.com — never your bare root domain and never a domain you also use for marketing blasts.
- 2
Verify the domain with SPF, DKIM, and DMARC
The provider gives you 3-4 DNS records. Add them at your registrar/DNS host and wait for verification. This is the single biggest factor in whether you land in the inbox. Set a DMARC record (start with p=none) so mailbox providers trust you and you get reports.
- 3
Store the API key server-side only
Put RESEND_API_KEY in your environment (Vercel/Fly/Railway secrets, or a local .env that's gitignored). It must never be imported into client-side code or a NEXT_PUBLIC_ variable — anyone who reads your JS bundle could then send mail as you.
- 4
Build the template as a component
Use React Email to write the message as a component with props. You get a proper responsive HTML build and an auto-generated plain-text version, which improves deliverability. Keep it simple: one clear call-to-action, a real from-name, and a working reply-to address.
- 5
Send from a server route or action
Call the provider SDK from a route handler, server action, or backend endpoint. Return a real error status if the send fails, and log the provider's error object — don't swallow it. Add an idempotency/dedupe header (e.g. the order ID) so a retry doesn't double-send.
- 6
Make the send non-blocking
Don't await the email inside the request that signs a user up or charges a card. Enqueue it (Inngest, Trigger.dev, a DB-backed queue, or your platform's background function) so the user's action completes instantly and the send retries on transient failure.
- 7
Handle bounces and complaints
Wire up the provider's webhook for bounces and spam complaints. Suppress hard-bounced addresses and stop mailing anyone who marks you as spam — ignoring this is how providers suspend your account and how your reputation tanks.
- 8
Test in real inboxes
Send to a real Gmail and a real Outlook address, and run one through mail-tester.com to catch auth or content problems before your users do. Check that it renders, that links work, and that it's not in the Promotions/Spam tab.
Starter snippet
// app/api/receipt/route.ts — Next.js App Router
import { Resend } from 'resend';
import { ReceiptEmail } from '@/emails/receipt';
const resend = new Resend(process.env.RESEND_API_KEY); // server-side only
export async function POST(req: Request) {
const { email, orderId, amount } = await req.json();
const { data, error } = await resend.emails.send({
from: 'Acme <receipts@mail.acme.com>', // verified subdomain
to: email,
replyTo: 'support@acme.com',
subject: `Your receipt for order ${orderId}`,
react: ReceiptEmail({ orderId, amount }), // React Email template
headers: { 'X-Entity-Ref-ID': orderId }, // dedupe on retries
});
if (error) {
console.error('resend send failed', error);
return Response.json({ error: 'send_failed' }, { status: 502 });
}
return Response.json({ id: data?.id });
}✕ Watch out for
- Sending from client-side code, which leaks your API key — let anyone read the bundle and they can send mail as your domain. Always send from a server route/action.
- Skipping SPF/DKIM/DMARC and mailing from an unverified or bare-root domain. This is the #1 reason transactional mail lands in spam.
- Awaiting the email inside the signup or checkout request. A slow provider then makes account creation hang or fail — enqueue the send instead.
- Reusing the same domain/stream for marketing and transactional mail. Marketing spam complaints will drag down deliverability of your password resets.
- Never handling bounces and complaints. Repeatedly mailing dead addresses or spam-reporters gets your provider account throttled or suspended.
✓ Pro tips
- When prompting an AI agent, be explicit: 'send via the Resend SDK from a server route, read the key from process.env, never expose it client-side, and enqueue the send so it doesn't block the request.' Then check the diff yourself — confirm the key isn't in a NEXT_PUBLIC_ var or any file that ships to the browser.
- Ask the agent to build the template with React Email and include a plain-text fallback and a real reply-to address — don't accept a raw HTML string blob.
- Before going live, send one real message to Gmail and Outlook and run it through mail-tester.com. A green send status from the API does not mean it reached the inbox.
- Tell the agent to add an idempotency header (like the order or user ID) and wire the bounce/complaint webhook, so retries don't double-send and dead addresses get suppressed.