Skip to content
Docs

Tutorial

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.

Updated Oct 1, 2026

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

Terminal
npm install @emailit/node server-only

server-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:

.env.local
EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••
EMAILIT_WEBHOOK_SECRET=whsec_••••••••

Create one client in a server-only module:

lib/emailit.ts
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:

types/emailit.d.ts
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:

app/api/contact/route.ts
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:

app/signup/actions.ts
'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 },
  });
}
app/signup/page.tsx
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:

pages/api/contact.ts
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):

lib/mailer.ts
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:

app/api/webhooks/emailit/route.ts
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-only enforces this at build time.
  • Never let the client choose the sender. Hard-code from, and only accept to from 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.

Next steps

Error handling and Nodemailer in more detail.
Attachments, scheduling and tracking.
Design emails once, send them by alias.
Scopes, domain restrictions and rotation.

Was this page helpful?

Thanks for the feedback.

Thanks, we read every message.