Next.js

Send transactional emails from your Next.js application using Server Actions, Route Handlers, and the Veil Mail SDK.

Prerequisites

1. Install the SDK

npm install @resonia/veilmail-sdk

2. Configure Environment

Add your API key to .env.local:

.env.local
VEILMAIL_API_KEY=veil_live_xxxxx

Create a shared client instance:

lib/veilmail.ts
import { VeilMail } from '@resonia/veilmail-sdk';

export const veilmail = new VeilMail({
  apiKey: process.env.VEILMAIL_API_KEY!,
});

3. Send Email from a Server Action

Server Actions run on the server, so your API key stays secure. This is the recommended approach for form submissions and user-triggered emails.

app/actions/email.ts
'use server';

import { veilmail } from '@/lib/veilmail';

export async function sendWelcomeEmail(formData: FormData) {
  const email = formData.get('email') as string;
  const name = formData.get('name') as string;

  await veilmail.emails.send({
    from: 'hello@yourdomain.com',
    to: email,
    subject: `Welcome, ${name}!`,
    html: `<h1>Welcome to our platform</h1><p>Hi ${name}, thanks for signing up.</p>`,
  });

  return { success: true };
}
app/signup/page.tsx
'use client';

import { sendWelcomeEmail } from '@/app/actions/email';

export default function SignupForm() {
  return (
    <form action={sendWelcomeEmail}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Sign Up</button>
    </form>
  );
}

4. Send Email from a Route Handler

Use Route Handlers when you need an API endpoint, for example to trigger emails from external services or cron jobs.

app/api/send-email/route.ts
import { NextResponse } from 'next/server';

import { veilmail } from '@/lib/veilmail';

export async function POST(request: Request) {
  const { to, orderId } = await request.json();

  const email = await veilmail.emails.send({
    from: 'orders@yourdomain.com',
    to,
    subject: `Order #${orderId} confirmed`,
    html: `<p>Your order #${orderId} has been confirmed.</p>`,
    tags: [{ name: 'type', value: 'order-confirmation' }],
  });

  return NextResponse.json({ emailId: email.id });
}

5. Handle Webhooks

Create a Route Handler to receive webhook events. Always verify the signature using the X-VeilMail-Signature header.

.env.local
VEILMAIL_WEBHOOK_SECRET=whsec_xxxxx
app/api/webhooks/veilmail/route.ts
import crypto from 'crypto';
import { NextResponse } from 'next/server';

function verifySignature(payload: string, signature: string, secret: string) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

export async function POST(request: Request) {
  const signature = request.headers.get('x-veilmail-signature');
  const body = await request.text();

  if (!signature || !verifySignature(body, signature, process.env.VEILMAIL_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.type) {
    case 'email.delivered':
      // Update delivery status in your database
      console.log('Delivered:', event.data.emailId);
      break;
    case 'email.bounced':
      // Handle bounce — remove from mailing list
      console.log('Bounced:', event.data.emailId);
      break;
    case 'email.complained':
      // Handle spam complaint
      console.log('Complaint:', event.data.emailId);
      break;
  }

  return NextResponse.json({ received: true });
}

Important: Use request.text() instead of request.json() to get the raw body for signature verification. Parsing JSON first changes the string representation.

6. Use Templates

Use Veil Mail templates to keep your email content separate from your code.

lib/send-report.ts
import { veilmail } from '@/lib/veilmail';

await veilmail.emails.send({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Your monthly report',
  templateId: 'template_xxxxx',
  templateData: {
    userName: 'Alice',
    reportMonth: 'January 2025',
    totalEmails: '1,234',
  },
});