How to Send Transactional Emails in Next.js
Learn how to integrate Sently into your Next.js App Router application in under 3 minutes. Clean code, native TypeScript types, and zero hassle.
💡 Summary: Sending Emails in Next.js (App Router)
To send transactional emails in Next.js, install sently-sdk, store your SENTLY_API_KEY in .env.local, and dispatch emails inside Route Handlers (app/api/send/route.ts) or Server Actions ('use server') using await sently.emails.send().
Transactional emails are the backbone of user interactions—account activations, password resets, order confirmations, and security alerts. Sently provides a developer-first platform designed to deliver your emails instantly with unmatched deliverability and predictable pricing.
In this guide, we will set up the official Sently TypeScript SDK in a modern Next.js project using the App Router.
1 Install the Sently SDK
Add the lightweight, zero-dependency sently-sdk package to your Next.js project:
npm install sently-sdk
# or
pnpm add sently-sdk
# or
yarn add sently-sdk2 Configure Environment Variables
Grab your API key from the Sently dashboard. In the root of your Next.js project, open or create your .env.local file:
# .env.local (Server-side only — never expose to client!)
SENTLY_API_KEY=snt_live_a1b2c3d4e5f6g7h8...
SENTLY_BASE_URL=https://sently.inSecurity Tip: Never prefix your API key with NEXT_PUBLIC_. Omitting the prefix guarantees that Next.js keeps your key secure on the server side and never leaks it into the client-side JavaScript bundle.
3 Initialize the Sently Client
Create a centralized helper module in lib/sently.ts to configure a singleton client instance. This keeps your configuration consistent across all API routes and Server Actions:
// lib/sently.ts
import { Sently } from 'sently-sdk';
if (!process.env.SENTLY_API_KEY) {
throw new Error('SENTLY_API_KEY is missing from environment variables');
}
export const sently = new Sently(process.env.SENTLY_API_KEY, {
baseUrl: process.env.SENTLY_BASE_URL || 'https://sently.in',
timeout: 10_000,
maxRetries: 2,
});4 Create an API Route Handler
Next.js App Router uses route.ts files to handle HTTP requests. Create an endpoint at app/api/send/route.ts that accepts a POST request, validates payload fields, and sends an email via Sently:
// app/api/send/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { sently } from '@/lib/sently';
import { SentlyError } from 'sently-sdk';
export async function POST(request: NextRequest) {
try {
const { to, subject, name, orderId } = await request.json();
if (!to || !subject) {
return NextResponse.json(
{ error: 'Fields "to" and "subject" are required.' },
{ status: 400 }
);
}
// Dispatch email through Sently
const result = await sently.emails.send({
from: 'Acme Orders <[email protected]>',
to,
subject,
html: `
<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 560px; margin: 0 auto; padding: 20px;">
<h2 style="color: #111;">Order Confirmation</h2>
<p>Hi ${name || 'there'},</p>
<p>We've received your order <strong>#${orderId}</strong> and are preparing it for shipment.</p>
<div style="margin: 24px 0; padding: 16px; background: #f9f9f9; border-radius: 6px;">
<p style="margin: 0; font-size: 14px; color: #555;">Reference ID: ${orderId}</p>
</div>
<p style="font-size: 13px; color: #888;">If you didn't place this order, please contact our support team immediately.</p>
</div>
`,
text: `Hi ${name || 'there'}, we received your order #${orderId}. We will notify you when it ships.`,
category: 'transactional',
metadata: {
orderId: String(orderId),
source: 'nextjs-app-router',
},
}, {
// Prevent duplicate sends if the user retries a network request
idempotencyKey: orderId ? `order-conf-${orderId}` : undefined,
});
return NextResponse.json({
success: true,
id: result.id,
status: result.status,
});
} catch (error) {
if (error instanceof SentlyError) {
console.error(`[Sently Error ${error.status}] ${error.code}: ${error.message}`);
return NextResponse.json(
{
error: error.message,
code: error.code,
type: error.type,
fields: error.fields,
},
{ status: error.status || 500 }
);
}
console.error('Internal server error:', error);
return NextResponse.json(
{ error: 'An unexpected internal error occurred' },
{ status: 500 }
);
}
}5 Optional: Using Server Actions
If you are building React Server Components with forms or mutations, you can invoke Sently directly inside a Server Action without exposing an external HTTP endpoint:
// app/actions/email.ts
'use server';
import { sently } from '@/lib/sently';
import { SentlyError } from 'sently-sdk';
export async function sendWelcomeEmail(formData: FormData) {
const email = formData.get('email') as string;
const name = formData.get('name') as string;
if (!email) {
return { error: 'Email address is required.' };
}
try {
const { id, status } = await sently.emails.send({
from: 'Acme Team <[email protected]>',
to: email,
subject: `Welcome to Acme, ${name || 'friend'}!`,
html: `<p>Hi ${name}, welcome aboard! We are thrilled to have you with us.</p>`,
category: 'transactional',
});
return { success: true, id, status };
} catch (err) {
if (err instanceof SentlyError) {
return { error: err.message, code: err.code };
}
return { error: 'Failed to dispatch email.' };
}
}6 Robust Error Handling & Retries
The Sently SDK throws instances of SentlyError containing detailed error metadata:
error.status: The HTTP status code returned by the API (e.g., 400 for bad parameters, 401 for unauthorized, 429 for rate limits).error.code: Machine-readable error code (e.g.,invalid_recipient,unverified_domain).error.fields: A map of specific form fields that failed validation rules.idempotencyKey: Passing a unique key (such as an order number or user ID) in the options ensures that accidental duplicate requests within 24 hours safely return the original email rather than sending twice.