How to send email from Next.js
Short answer: Send email from server code, never from the browser. Put your key in .env.local, POST from, to, subject and html to https://api.sendfox.com/transactional/emails from a server action or route handler, and you get back a message id. It's free for 3,000 emails a month.
This works with the App Router in Next.js 14 and 15. It uses plain fetch, so there's no package to install and it runs on any runtime.
Step 1: Get an API key
Sign up free and open Transactional. Your key is created on the first visit. Add it to .env.local, and to your host's environment variables when you deploy:
.env.local
SENDFOX_API_KEY=your_sendfox_key
Step 2: Write a sendEmail helper
One function the rest of your app calls. It throws with the API's own explanation when something's wrong, so a missing DNS record shows up as domain_not_verified: Verify the domain under Account > Domains... rather than a bare 403.
lib/email.ts
// Import this from server code only: server actions, route handlers, server components.
type Email = {
to: string | string[];
subject: string;
html: string;
text?: string;
tag?: string;
idempotencyKey?: string;
};
export async function sendEmail({ idempotencyKey, ...email }: Email): Promise<string> {
const res = await fetch('https://api.sendfox.com/transactional/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDFOX_API_KEY}`,
'Content-Type': 'application/json',
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: JSON.stringify({
from: 'Acme <[email protected]>',
...email,
to: Array.isArray(email.to) ? email.to : [email.to],
}),
});
const body = await res.json();
if (!res.ok) {
// Every error has a stable name and a fix, e.g. "domain_not_verified".
throw new Error(`${body.name}: ${body.fix}`);
}
return body.id; // "tm_..."
}
Step 3: Send from a server action
A welcome email when someone signs up:
app/signup/actions.ts
'use server';
import { sendEmail } from '@/lib/email';
export async function signUp(formData: FormData) {
const email = String(formData.get('email'));
// ...create the user...
await sendEmail({
to: email,
subject: 'Welcome to Acme',
html: '<p>Thanks for signing up. Here is how to get started.</p>',
tag: 'welcome',
});
}
Step 4: Send from a route handler
Receipts usually come from a webhook or a background job, which can retry. Pass an idempotency key and SendFox returns the original message for 24 hours instead of sending it again:
app/api/orders/[id]/receipt/route.ts
import { sendEmail } from '@/lib/email';
import { getOrder } from '@/lib/orders';
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const order = await getOrder(id);
const emailId = await sendEmail({
to: order.email,
subject: `Your receipt for order ${id}`,
html: `<p>Thanks! You paid ${order.total}.</p>`,
tag: 'receipt',
idempotencyKey: `receipt-${id}`, // a retried request never sends twice
});
return Response.json({ emailId });
}
Templates with React Email
If you build templates with React Email, render them to HTML and pass the string:
import { render } from '@react-email/render';
import WelcomeEmail from '@/emails/welcome';
const html = await render(<WelcomeEmail name={name} />);
await sendEmail({ to: email, subject: 'Welcome to Acme', html, tag: 'welcome' });
Step 5: Test, then verify your domain
Send to [email protected] first. Test addresses never reach a mailbox and never count against your quota, and [email protected] and [email protected] fake those outcomes so you can see your error handling and webhook fire. Until your domain is verified, send from [email protected], which delivers to the test addresses and your own account email.
Then add your domain under Account > Domains, paste the DNS records into your DNS provider and click verify. After that, send from any address on it.
Things to know
- Already using the Resend SDK? Keep it. Set
RESEND_BASE_URL=https://api.sendfox.com/transactional/resendand use your SendFox key. See how to switch from Resend. - Rate limit: 5 requests a second. For many emails at once, use the batch endpoint (up to 100 a call).
- Login codes and magic links: tag them
login_codeormagic_link. Click tracking can never be turned on for those tags, so mailbox scanners can't burn the link. - Attachments aren't supported yet. Link to the file from the email instead, for example a signed URL to the invoice PDF.
Full reference: Transactional API docs, or the same docs as Markdown to hand to your coding agent.
FAQ
Can I send email from a client component?
No. Your API key would ship to the browser. Send from a server action, a route handler or other server code, and have the client call that.
Does it work on the Edge runtime and on Vercel?
Yes. The helper only uses fetch, so it runs on the Node.js and Edge runtimes alike.
Do I need Nodemailer?
No. SendFox is an HTTPS API, so fetch is all you need. SendFox does not offer SMTP, so Nodemailer cannot send through it.
How much does it cost?
Free for 3,000 emails a month. $15 a month for 50,000.
Send your first email in two minutes.
Free for 3,000 emails a month. $15 a month for 50,000. Already on Resend? Change two environment variables.
Get your free API key