postkit
Integrations

Next.js

Send transactional emails from your Next.js application with Postkit

Integrate Postkit into your Next.js application using Server Actions, Route Handlers, or the API directly. This guide covers the recommended patterns for sending transactional emails from both server and client components.

Prerequisites:

  • A Postkit account with a verified domain
  • An API key (pk_live_ for production, pk_test_ for sandbox) -- see quickstart
  • Next.js 14+ with App Router

Setup

Set up environment variables

Create a .env.local file in your project root with your Postkit API key:

POSTKIT_API_KEY=pk_live_abc123...

Never expose your API key to the browser. All Postkit calls must happen server-side.

Create a server-side email helper

Create a reusable helper function that wraps the Postkit REST API. This keeps your API key secure and provides consistent error handling across your application.

// lib/postkit.ts

export async function sendEmail(options: {
  to: string[];
  subject: string;
  html: string;
  from?: string;
}) {
  const response = await fetch('https://api.postkit.eu/v1/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.POSTKIT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: options.from ?? 'Your App <noreply@yourdomain.eu>',
      ...options,
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message ?? 'Failed to send email');
  }

  return response.json();
}

Server Actions

Server Actions are the recommended pattern for sending emails from form submissions. They run server-side and can access environment variables directly.

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

import { sendEmail } from '@/lib/postkit';

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

  await sendEmail({
    to: ['support@yourdomain.eu'],
    subject: `Contact form: ${name}`,
    html: `<p>From: ${name} (${email})</p><p>${message}</p>`,
  });

  return { success: true };
}

Use the action in a client component form:

// app/contact/page.tsx
'use client';

import { submitContactForm } from '../actions';

export default function ContactPage() {
  return (
    <form action={submitContactForm}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button type="submit">Send</button>
    </form>
  );
}

Route Handlers

Use Route Handlers when you need an API-style endpoint that other services or client-side code can call.

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

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

  try {
    const result = await sendEmail({ to, subject, html });
    return NextResponse.json(result, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: { message: 'Failed to send email' } },
      { status: 500 },
    );
  }
}

Automated emails

For automated emails triggered by other actions (e.g., after signup), you can fire-and-forget from any server-side route.

// app/api/auth/signup/route.ts
import { NextResponse } from 'next/server';
import { sendEmail } from '@/lib/postkit';

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

  // ... create user in database ...

  // Send welcome email (fire-and-forget)
  sendEmail({
    to: [email],
    subject: 'Welcome!',
    html: `<h1>Welcome, ${name}!</h1>`,
  }).catch(console.error);

  return NextResponse.json({ success: true }, { status: 201 });
}

Webhook verification

Verify incoming Postkit webhooks using the Standard Webhooks HMAC-SHA256 signature. This ensures requests genuinely come from Postkit.

// app/api/webhooks/postkit/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('webhook-signature') ?? '';
  const msgId = request.headers.get('webhook-id') ?? '';
  const timestamp = request.headers.get('webhook-timestamp') ?? '';

  // Verify HMAC-SHA256 signature
  const secret = process.env.POSTKIT_WEBHOOK_SECRET!;
  const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64');
  const signedContent = `${msgId}.${timestamp}.${body}`;
  const expected = crypto
    .createHmac('sha256', secretBytes)
    .update(signedContent)
    .digest('base64');

  const signatures = signature.split(' ');
  const isValid = signatures.some((sig) => sig.replace('v1,', '') === expected);

  if (!isValid) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const event = JSON.parse(body);

  // Handle different event types
  switch (event.type) {
    case 'email.delivered':
      // Update delivery status in your database
      break;
    case 'email.bounced':
      // Handle bounce (remove address, notify user, etc.)
      break;
    case 'email.complained':
      // Handle spam complaint
      break;
  }

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

The signing secret starts with whsec_. Strip this prefix and base64-decode to get the HMAC key bytes. See the Webhooks guide for a detailed explanation of the verification algorithm.

Next steps