Tutorial
Send email with Next.js
Send email from Next.js route handlers and server actions with the Emailit Node.js SDK, keep the API key server-side and verify webhooks.
This guide shows how to send email from a Next.js app with the @emailit/node SDK, from both a route handler and a server action, and how to verify Emailit webhooks. The examples use the App Router; the Pages Router is covered at the end of the sending section.
Prerequisites
- Next.js 14 or later. The SDK needs Node.js 18 or later.
- A verified sending domain, for example
acme.com. - An API key. A Sending Only key restricted to your domain is enough.
- Until your workspace has production access, you can only send to the account emails of workspace members.
Install the SDK
npm install @emailit/node server-onlyserver-only makes the build fail if a client component imports the module that holds your key.
Configure your API key
Add the key to .env.local for development and to your host’s environment variables (for example, Vercel project settings) for production:
EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••
EMAILIT_WEBHOOK_SECRET=whsec_••••••••Create one client in a server-only module:
import 'server-only';
import { Emailit } from '@emailit/node';
export const emailit = new Emailit(process.env.EMAILIT_API_KEY!);The SDK has no TypeScript declarations yet. If the compiler complains, add a declaration file:
declare module '@emailit/node';Send from a route handler
A route handler is the right place for sends triggered by your own frontend or by other services. This contact form handler sends to a fixed internal address, so visitors can’t use it to email arbitrary people:
import { emailit } from '@/lib/emailit';
export async function POST(request: Request) {
const { email, message } = await request.json();
if (typeof email !== 'string' || typeof message !== 'string' || !email.includes('@')) {
return Response.json({ error: 'Invalid input' }, { status: 400 });
}
try {
const sent = await emailit.emails.send({
from: 'Acme website <website@acme.com>',
to: 'support@acme.com',
reply_to: email,
subject: 'New contact form message',
text: message,
});
return Response.json({ id: sent.id });
} catch (error) {
console.error(error);
return Response.json({ error: 'Could not send the message' }, { status: 502 });
}
}Call it from the browser with fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) }). The key never leaves the server.
Send from a server action
Server actions run on the server, so they can use the client directly:
'use server';
import { emailit } from '@/lib/emailit';
export async function signUp(formData: FormData) {
const email = formData.get('email');
if (typeof email !== 'string' || !email.includes('@')) return;
// Create the account here, then send the welcome email.
await emailit.emails.send({
from: 'Acme <hello@acme.com>',
to: email,
template: 'welcome',
variables: { email },
});
}import { signUp } from './actions';
export default function SignupPage() {
return (
<form action={signUp}>
<input type="email" name="email" required />
<button type="submit">Create account</button>
</form>
);
}template takes a template alias or tem_ ID; see Templates. Server actions are public endpoints, so validate input and add your usual bot and abuse protection before sending.
Pages Router
With the Pages Router, send from an API route instead:
import type { NextApiRequest, NextApiResponse } from 'next';
import { emailit } from '@/lib/emailit';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).end();
const sent = await emailit.emails.send({
from: 'Acme website <website@acme.com>',
to: 'support@acme.com',
subject: 'New contact form message',
text: String(req.body.message ?? ''),
});
res.status(200).json({ id: sent.id });
}Send with SMTP instead
If you already use Nodemailer, configure it with the Emailit relay inside a route handler or server action (SMTP needs the Node.js runtime, not the Edge runtime):
import 'server-only';
import nodemailer from 'nodemailer';
export const mailer = nodemailer.createTransport({
host: 'smtp.emailit.com',
port: 587,
secure: false,
requireTLS: true,
auth: { user: 'emailit', pass: process.env.EMAILIT_API_KEY },
});Then call await mailer.sendMail({ from, to, subject, html }). Serverless functions open a new SMTP connection on most invocations, so the API is usually faster on Vercel and similar platforms. See SMTP settings.
Receive webhooks
Create a webhook that points to https://your-app.com/api/webhooks/emailit. The handler must verify the signature against the raw body, so read it with request.text() before parsing:
import { createHmac, timingSafeEqual } from 'node:crypto';
export const runtime = 'nodejs';
type EmailitEvent = {
event_id: string;
type: string;
data: { object: Record<string, unknown> };
};
function isValid(rawBody: string, signature: string | null, timestamp: string | null) {
if (!signature || !timestamp) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected = createHmac('sha256', process.env.EMAILIT_WEBHOOK_SECRET!)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}
export async function POST(request: Request) {
const rawBody = await request.text();
const valid = isValid(
rawBody,
request.headers.get('x-emailit-signature'),
request.headers.get('x-emailit-timestamp'),
);
if (!valid) return new Response('Invalid signature', { status: 401 });
// Each request carries an array of up to 100 events.
const events = JSON.parse(rawBody) as EmailitEvent[];
for (const event of events) {
if (event.type === 'email.bounced') {
// Flag event.data.object.to in your database.
}
}
return new Response(null, { status: 200 });
}Return a 2xx within 30 seconds; other responses are retried. See Request signature and Event types.
Production tips
- Keep sends on the server. Only
lib/emailit.ts, route handlers and server actions should touch the key.server-onlyenforces this at build time. - Never let the client choose the sender. Hard-code
from, and only accepttofrom the browser when it’s the signed-in user’s own address. - Protect public endpoints. Rate-limit contact forms and sign-up actions, and add a CAPTCHA if bots find them. Every email costs credits and counts against your sending limits.
- Set variables per environment. Use separate keys for preview and production deployments so you can revoke one without affecting the other.