Skip to content
Next.js Tutorial

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.

Written by Ashish Sharda•✓ Verified for Next.js 15 App Router•4 min read

💡 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-sdk

2 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.in

Security 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.

Ready to send emails with Sently?

Get started with free sends every month. Set up your custom domain and start dispatching emails in minutes.