Guide

How to send transactional emails with Next.js (App Router)

Send transactional email from a Next.js App Router server action or route handler.

In short

Create a Sendpository account, verify your domain, put your API key in SENDPOSITORY_API_KEY, and send with the code below - one HTTPS request per email, from server-side code only.
Sending and receiving email events in a Next.js App Router appsubmitfetchlaterPOSTupdateBrowserform or buttonServer Action"use server"SENDPOSITORY_API_KEYSendpositoryPOST /v1/emailsRecipientinboxWebhook eventemail.bouncedRoute Handlerapi/webhooks/emailverifyWebhook()Your databaseusers, orders

Swipe the diagram to see the rest

Where email code belongs in a Next.js app. The API key is only read on the server, and delivery events come back to a route handler that verifies them.

1. Verify your sending domain

Add your domain in the dashboard and publish the three DNS records it shows - SPF and a bounce record on an sp. subdomain, and a DKIM key on your apex. Your existing records are never edited. Verification usually lands in a few minutes, and you can set up the code below while you wait.

2. Create a key

Create an API key with the sending permission. It is shown once, so put it straight into your secrets. Add SENDPOSITORY_API_KEY to .env.local. Never prefix it with NEXT_PUBLIC_ - it must stay on the server.

3. Install

Terminal
npm install sendpository

4. Send an email

Next.js
// app/actions.ts
"use server";

import { Sendpository } from "sendpository";

const sendpository = new Sendpository(process.env.SENDPOSITORY_API_KEY);

export async function sendWelcome(email: string) {
  const { id } = await sendpository.emails.send({
    from: "Your App <hello@mail.yourdomain.com>",
    to: [email],
    subject: "Welcome aboard",
    html: "<p>Thanks for signing up.</p>",
  });
  return id;
}

A success returns { "id": "..." } - the message’s id for looking it up later. A failure returns { "error": { "type", "message" } } with a 4xx or 5xx status.

Where the send belongs

In a Server Action or a Route Handler - never a Client Component, because the key would ship to the browser. Mark the module `server-only` so an accidental import from a client file fails the build instead of leaking at runtime.

If the user does not need the result, call `after()` from `next/server`. The response goes back as soon as the work is queued and the send finishes afterwards, so a slow upstream never shows up as a slow form submit.

Testing it

Things to know

Put email behind one server-only module

Keep the client and every email your app sends in one file that can only ever run on the server. The server-only package makes the build fail if a client component imports it, so the API key can't leak by accident.

lib/email.ts
import "server-only";
import { Sendpository } from "sendpository";

const sendpository = new Sendpository(process.env.SENDPOSITORY_API_KEY);
const FROM = "Acme <hello@mail.acme.com>";

export async function sendWelcome(user: { id: string; email: string; name: string }) {
  return sendpository.emails.send(
    {
      from: FROM,
      to: [user.email],
      subject: `Welcome to Acme, ${user.name}`,
      html: `<p>Hi ${user.name}, your account is ready.</p>`,
      text: `Hi ${user.name}, your account is ready.`,
      tags: { type: "welcome" },
    },
    // Retrying the job can never send two welcomes.
    { idempotencyKey: `welcome-${user.id}` },
  );
}

Send from a Server Action

A Server Action runs on the server and can be called straight from a form. Sending after the response with after() keeps the form fast; the email goes out a moment later.

app/signup/actions.ts
"use server";

import { after } from "next/server";
import { createUser } from "@/lib/users";
import { sendWelcome } from "@/lib/email";

export async function signUp(formData: FormData) {
  const user = await createUser(formData);
  after(() => sendWelcome(user)); // runs once the response has been sent
  return { ok: true };
}

Render templates with React Email

Sendpository sends whatever HTML you give it, so any template library works. With React Email, render the component to HTML and pass the result as html.

React Email
import { render } from "@react-email/render";
import WelcomeEmail from "@/emails/welcome";

const html = await render(<WelcomeEmail name={user.name} />);
const text = await render(<WelcomeEmail name={user.name} />, { plainText: true });

Receive delivery events in a Route Handler

Create an endpoint on the Webhooks page, store its secret as SENDPOSITORY_WEBHOOK_SECRET, and verify every request. Read the body with req.text() - the signature covers the raw bytes, so parsing first would break it.

app/api/webhooks/email/route.ts
import { verifyWebhook } from "sendpository";

export async function POST(req: Request) {
  let event;
  try {
    event = verifyWebhook({
      body: await req.text(),
      signature: req.headers.get("sendpository-signature") ?? "",
      secret: process.env.SENDPOSITORY_WEBHOOK_SECRET!,
    });
  } catch {
    return new Response("invalid signature", { status: 401 });
  }

  if (event.type === "email.bounced" || event.type === "email.complained") {
    await markUndeliverable(event.data.to);
  }
  return new Response("ok");
}

Deploying

  • Set SENDPOSITORY_API_KEY and SENDPOSITORY_WEBHOOK_SECRET in your host's environment variables (on Vercel: Project Settings → Environment Variables) - never with a NEXT_PUBLIC_ prefix.
  • Use a separate API key for preview deployments, so a test branch can be revoked without touching production.
  • Route handlers run on the Node.js runtime by default, which is what the SDK expects.

More on what happens after the send: how transactional email works and how email webhooks work.

Questions

Can I send email from a Client Component?

No - the key would be bundled into the browser. Put the send in a Server Action or Route Handler and mark the module `server-only`, so an import from a client file fails at build time rather than leaking a key at runtime.

How do I send email with Next.js?

Verify your sending domain, create an API key, put it in an environment variable, and POST the message as JSON to https://api.sendpository.com/v1/emails from server-side Next.js code. The example on this page runs as written once the domain is verified.

Can I send email with Next.js without SMTP?

Yes. This is plain HTTPS, so Next.js needs no mail library and no SMTP port - which matters on hosts that block outbound 25 and 587. Failures come back as a status code on the call instead of as a bounce message hours later.

More guides

Send your first email in minutes

6,000 emails a month (up to 200 a day), free forever. No card needed.