GetLaunchpad
Back to blog

Sending transactional emails from Next.js with Resend

Resend is the cleanest way to send transactional emails from Next.js — type-safe, domain-verified, and built for developers. Here's how to set up welcome emails, receipts, and cancellation notices that trigger from Stripe webhooks.

Resend is the cleanest way to send transactional emails from Next.js — type-safe, built for developers, and free for the first 3,000 emails per month. Here's how to install it, set up domain verification, write the three core transactional emails every SaaS needs, and trigger them from Stripe webhooks — using the same pattern deployed in GetLaunchpad.

Why Resend over SendGrid or Mailgun

The legacy email providers were designed for marketing teams, not developers. Resend was built API-first with TypeScript types, a clean REST API, and first-class support for React Email templates. You write email markup in the same TSX you use everywhere else, and the SDK enforces the shape of every call at compile time.

Install the package

npm install resend

Grab your API key from the Resend dashboard and add it to your environment:

RESEND_API_KEY=re_your_key_here

Create the client singleton

// lib/resend.ts
import "server-only";
import { Resend } from "resend";

export const resend = new Resend(process.env.RESEND_API_KEY);

The server-only import is essential — it causes a build error if this file is accidentally imported into a client component, keeping your API key out of the browser bundle. Always create the Resend instance once and export it rather than calling new Resend() in every route handler.

HTML templates vs React Email

You have two options for authoring email templates:

For the three templates below, plain HTML strings keep the example self-contained. Swap in React Email components when your template library grows.

Domain verification

Before sending from your own domain (hello@yourdomain.com instead of a Resend subdomain), you must add DNS records. In the Resend dashboard, go to Domains → Add Domain and enter your domain. Resend will give you three records to add:

DNS propagation typically takes a few minutes on modern providers like Cloudflare or Vercel DNS. Resend shows a green checkmark once all records verify. You cannot send from a custom domain until verification passes.

The three core transactional emails for SaaS

1. Welcome email on signup

Send this when a new user creates an account. The best trigger point is your POST /api/user upsert route — the same place you write the new user to Supabase.

// Inside POST /api/user, after the Supabase upsert:
await resend.emails.send({
  from: "GetLaunchpad <hello@getlaunchpad.net>",
  to: userEmail,
  subject: "Welcome to GetLaunchpad",
  html: `
    <p>Hi ${firstName},</p>
    <p>Thanks for signing up. Your account is ready.</p>
    <p><a href="https://getlaunchpad.net/dashboard">Open your dashboard</a></p>
  `,
});

2. Receipt on payment

Send this when a checkout.session.completed or invoice.payment_succeeded webhook fires. Pull the amount and next billing date from the Stripe event object.

// Inside your Stripe webhook handler, after upserting the subscription:
if (event.type === "checkout.session.completed") {
  const session = event.data.object;
  await resend.emails.send({
    from: "GetLaunchpad <hello@getlaunchpad.net>",
    to: session.customer_email!,
    subject: "Your payment was successful",
    html: `
      <p>Thanks for subscribing to GetLaunchpad Pro.</p>
      <p>Amount charged: ${(session.amount_total! / 100).toFixed(2)} USD</p>
      <p>Your subscription is now active.</p>
    `,
  });
}

3. Cancellation notice on subscription end

Send this when a customer.subscription.deleted event arrives. Keep the tone neutral — give the user a way to resubscribe.

if (event.type === "customer.subscription.deleted") {
  const subscription = event.data.object;
  // Look up the user email from your Supabase subscriptions table
  await resend.emails.send({
    from: "GetLaunchpad <hello@getlaunchpad.net>",
    to: userEmail,
    subject: "Your GetLaunchpad subscription has ended",
    html: `
      <p>Your Pro subscription has been cancelled.</p>
      <p>You can resubscribe at any time from your dashboard.</p>
      <p><a href="https://getlaunchpad.net/dashboard">Resubscribe</a></p>
    `,
  });
}

Triggering from Stripe webhooks vs user actions

There are two trigger points for transactional emails:

Never send a transactional email from both a webhook and a user action for the same event — the user will receive duplicates. Pick one source of truth per email type.

Testing with a real send

Resend has no sandbox mode — every call sends a real email. For local development, use a personal email address or a service like Mailtrap as the recipient. A minimal smoke test:

// One-off test script: npx tsx scripts/test-email.ts
import { resend } from "../lib/resend";

const { data, error } = await resend.emails.send({
  from: "onboarding@resend.dev", // Resend's shared domain — no DNS setup needed
  to: "you@example.com",
  subject: "Test email",
  html: "<p>Hello from Resend</p>",
});

console.log({ data, error });

Using onboarding@resend.dev as the sender lets you test without configuring DNS records first. Switch to your verified domain before going to production.

Monitoring delivery in the Resend dashboard

Every email sent through Resend appears in the Emails tab of the dashboard with its delivery status: delivered, bounced, complained, or clicked (if click tracking is enabled). This is your primary tool for diagnosing delivery issues — if a user says they did not receive a welcome email, check here first.

Resend also exposes webhook events for delivery status changes, so you can write bounced addresses to your database and suppress future sends automatically.


The complete Resend integration — client singleton, welcome email, payment receipt, and cancellation notice — is pre-wired in GetLaunchpad, a Next.js 16 SaaS boilerplate. Every Stripe webhook already triggers the right email on day one.

More articles