Back to roadmaps resend Course

Implementing a Double Opt-In Subscription Flow

To ensure subscriber list quality and avoid spam honeypots, implement a Double Opt-In flow.


1. Double Opt-In Flow Architecture

graph TD
    A[Visitor submits email] --> B[Generate encrypted token link]
    B --> C[Send verification email via Resend]
    C --> D[Visitor clicks link in email]
    D --> E[Server verifies token signature]
    E --> F[API adds email to Resend Audience]

2. Part 1: Sending the Verification Link

When a visitor submits the subscription form, generate a verification token (save it temporarily with an expiration timestamp) and mail the link:

// app/api/subscribe/route.ts
import { resend } from "../../lib/resend";
import { prisma } from "../../lib/prisma";
import crypto from "crypto";

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

  if (!email) return new Response("Email required", { status: 400 });

  // 1. Generate a random secure token
  const token = crypto.randomBytes(32).toString("hex");
  const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hour expiry

  // 2. Save pending verification state in database
  await prisma.pendingSubscription.create({
    data: { email, token, expiresAt },
  });

  const confirmUrl = `https://mycompany.com/api/subscribe/confirm?token=${token}`;

  // 3. Send validation email
  await resend.emails.send({
    from: "Newsletter <newsletter@mycompany.com>",
    to: [email],
    subject: "Confirm your subscription",
    html: `<p>Please click the link below to confirm your subscription:</p><a href="${confirmUrl}">Confirm Subscription</a>`,
  });

  return new Response("Verification email sent", { status: 200 });
}

3. Part 2: Verifying the Token and Adding Contact

Create the confirmation endpoint handler that processes the token link:

// app/api/subscribe/confirm/route.ts
import { resend } from "../../../lib/resend";
import { prisma } from "../../../lib/prisma";

export async function GET(req: Request) {
  const url = new URL(req.url);
  const token = url.searchParams.get("token");

  if (!token) return new Response("Missing token", { status: 400 });

  // 1. Fetch pending token details
  const pending = await prisma.pendingSubscription.findUnique({
    where: { token },
  });

  if (!pending || pending.expiresAt < new Date()) {
    return new Response("Invalid or expired token", { status: 400 });
  }

  // 2. Add email to Resend Audience list
  const audienceId = process.env.RESEND_AUDIENCE_ID as string;
  await resend.contacts.create({
    audienceId,
    email: pending.email,
  });

  // 3. Clean up database entry
  await prisma.pendingSubscription.delete({
    where: { token },
  });

  // Redirect to success landing page
  return Response.redirect("https://mycompany.com/subscription-success");
}
Published on Last updated: