# 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](/docs/domains/add-a-domain/), for example `acme.com`.
- An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough.
- Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members.

## Install the SDK

```bash
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:

```bash title=".env.local"
EMAILIT_API_KEY=secret_••••••••••••••••••••••••••••••••
EMAILIT_WEBHOOK_SECRET=whsec_••••••••
```

> **Never expose the key to the browser:** Don't prefix these variables with `NEXT_PUBLIC_`, and don't call Emailit from client components. Anything prefixed `NEXT_PUBLIC_` is bundled into JavaScript that every visitor can read.

Create one client in a server-only module:

```typescript title="lib/emailit.ts"

```

The SDK has no TypeScript declarations yet. If the compiler complains, add a declaration file:

```typescript title="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:

```typescript title="app/api/contact/route.ts"

  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:

```typescript title="app/signup/actions.ts"
'use server';

  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 },
  });
}
```

```tsx title="app/signup/page.tsx"

  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](/docs/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:

```typescript title="pages/api/contact.ts"

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

```typescript title="lib/mailer.ts"

  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](/docs/smtp/settings/).

## Receive webhooks

[Create a webhook](/docs/webhooks/set-up/) 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:

```typescript title="app/api/webhooks/emailit/route.ts"

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);
}

  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](/docs/webhooks/request-signature/) and [Event types](/docs/webhooks/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](/docs/limits/).
- **Set variables per environment.** Use separate keys for preview and production deployments so you can revoke one without affecting the other.

## Next steps

  - [Node.js guide](/docs/frameworks/nodejs/): Error handling and Nodemailer in more detail.
  - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling and tracking.
  - [Templates](/docs/templates/): Design emails once, send them by alias.
  - [API keys](/docs/developers/api-keys/): Scopes, domain restrictions and rotation.

---
Source: https://emailit.com/docs/frameworks/nextjs/
