12 min read

Clerk Auth Emails with React Email: Replace Default Templates with Custom Designs

Replace Clerk's default auth emails with branded React Email templates. Full webhook setup, Svix verification, and production-ready code for verification, password reset, and magic link emails.

R

React Emails Pro

March 27, 2026

Clerk handles auth for thousands of Next.js apps. Sign-up, password reset, email verification, magic links - it all works out of the box. But open one of those default emails and you will see a generic white template with Clerk's styling, a tiny logo slot, and copy that reads like it was written for every company at once. Because it was.

For early-stage products, this is fine. Nobody churns because your verification email looks generic. But the moment you start charging money or onboarding enterprise users, those emails become a trust signal. A password reset that looks like a phishing attempt generates support tickets. A welcome email with no brand identity sets the wrong tone for the entire relationship.

Your auth emails are the first and most frequent touchpoint with new users. They set the expectation for everything that follows. A generic template tells users you did not care enough to customize the basics.

Guillermo Rauch

CEO, Vercel


What Clerk gives you by default

Clerk's built-in email system handles six email types through their dashboard editor:

  • Email verification (sign-up confirmation)
  • Password reset
  • Magic link sign-in
  • Invitation emails
  • Organization invitations
  • Two-factor authentication codes

The dashboard lets you edit subject lines, swap in a logo, and tweak copy. But the layout, typography, spacing, and overall design are locked. You cannot add sections, change the email structure, or make it match your product's design system. The WYSIWYG editor is a wrapper around a fixed template, not a design tool.

Clerk's newer webhook-based approach lets you fully replace their email sending. Instead of customizing their templates, you intercept the auth events and send your own emails through your own ESP. This gives you complete control over design, copy, and delivery infrastructure.

How the webhook approach works

The architecture is similar to how Supabase's Send Email hook works. You tell Clerk to fire a webhook instead of sending its own email, then your Next.js API route handles the rendering and sending.

1. User action

A user signs up, requests a password reset, or clicks a magic link button in your app
2. Clerk generates token

Clerk creates the verification token or OTP code and fires a webhook to your endpoint instead of sending its own email
3. Your API route

Your Next.js API route receives the webhook payload with the user data, token, and email type
4. React Email renders

You render the appropriate React Email template with your brand styling, copy, and layout
5. ESP delivers

Resend (or your ESP of choice) sends the email from your verified domain with proper DKIM/SPF alignment

Setting up the webhook in Clerk

1. Configure Clerk to use webhooks

In your Clerk dashboard, navigate to Configure → Emails and disable the built-in email sending for the email types you want to customize. Then set up a webhook endpoint under Configure → Webhooks.

.env.local
# Your Clerk webhook secret (from the Clerk dashboard)
CLERK_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Resend API key for sending
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Your app URL
NEXT_PUBLIC_APP_URL=https://yourapp.com

2. Create the webhook API route

Install the Svix package for webhook verification (Clerk uses Svix under the hood):

Terminal
npm install svix
app/api/clerk-webhooks/route.ts
import { Webhook } from "svix";
import { headers } from "next/headers";
import { Resend } from "resend";
import { renderAsync } from "@react-email/render";
import { WelcomeEmail } from "@/emails/welcome";
import { PasswordResetEmail } from "@/emails/password-reset";
import { VerificationEmail } from "@/emails/verification";
import { MagicLinkEmail } from "@/emails/magic-link";

const resend = new Resend(process.env.RESEND_API_KEY);
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!;

type EmailType =
  | "email.created"

interface ClerkWebhookPayload {
  type: string;
  data: {
    // email.created event data
    from_email_name?: string;
    to_email_address?: string;
    subject?: string;
    body?: string;
    slug?: string; // "verification_code", "reset_password", etc.
    data?: {
      otp?: string;
      token?: string;
      url?: string;
      first_name?: string;
    };
  };
}

export async function POST(req: Request) {
  const headerPayload = await headers();
  const svixId = headerPayload.get("svix-id");
  const svixTimestamp = headerPayload.get("svix-timestamp");
  const svixSignature = headerPayload.get("svix-signature");

  if (!svixId || !svixTimestamp || !svixSignature) {
    return new Response("Missing svix headers", { status: 400 });
  }

  const payload = await req.json();
  const body = JSON.stringify(payload);

  const wh = new Webhook(webhookSecret);
  let event: ClerkWebhookPayload;

  try {
    event = wh.verify(body, {
      "svix-id": svixId,
      "svix-timestamp": svixTimestamp,
      "svix-signature": svixSignature,
    }) as ClerkWebhookPayload;
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  await handleEmailEvent(event);

  return new Response("OK", { status: 200 });
}

async function handleEmailEvent(event: ClerkWebhookPayload) {
  const { data } = event;
  const to = data.to_email_address;
  if (!to) return;

  const emailMap: Record<string, () => Promise<void>> = {
    verification_code: async () => {
      const html = await renderAsync(
        VerificationEmail({
          code: data.data?.otp ?? "",
          name: data.data?.first_name ?? "",
        })
      );
      await resend.emails.send({
        from: "YourApp <auth@yourapp.com>",
        to,
        subject: "Verify your email address",
        html,
      });
    },
    reset_password_code: async () => {
      const html = await renderAsync(
        PasswordResetEmail({
          code: data.data?.otp ?? "",
          name: data.data?.first_name ?? "",
        })
      );
      await resend.emails.send({
        from: "YourApp <auth@yourapp.com>",
        to,
        subject: "Reset your password",
        html,
      });
    },
    magic_link: async () => {
      const html = await renderAsync(
        MagicLinkEmail({
          url: data.data?.url ?? "",
          name: data.data?.first_name ?? "",
        })
      );
      await resend.emails.send({
        from: "YourApp <auth@yourapp.com>",
        to,
        subject: "Sign in to YourApp",
        html,
      });
    },
  };

  const handler = emailMap[data.slug ?? ""];
  if (handler) await handler();
}
Always verify the webhook signature before processing. Without verification, anyone could POST to your endpoint and trigger emails to arbitrary addresses. The Svix verification is not optional.

3. Build your React Email templates

With the webhook handler in place, you need the actual email templates. Here is a verification email that matches your brand instead of Clerk's defaults:

emails/verification.tsx
import {
  Html,
  Head,
  Body,
  Container,
  Section,
  Img,
  Text,
  Hr,
} from "@react-email/components";

interface VerificationEmailProps {
  code: string;
  name: string;
}

export function VerificationEmail({ code, name }: VerificationEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: "#f9fafb", fontFamily: "sans-serif" }}>
        <Container
          style={{
            maxWidth: "480px",
            margin: "40px auto",
            backgroundColor: "#ffffff",
            borderRadius: "8px",
            padding: "40px 32px",
          }}
        >
          <Img
            src="https://yourapp.com/logo.png"
            alt="YourApp"
            width={120}
            height={36}
          />
          <Text style={{ fontSize: "20px", fontWeight: 600, marginTop: "24px" }}>
            Verify your email
          </Text>
          <Text style={{ color: "#6b7280", lineHeight: "1.6" }}>
            {name ? `Hey ${name}, enter` : "Enter"} this code to verify your
            email address:
          </Text>
          <Section
            style={{
              backgroundColor: "#f3f4f6",
              borderRadius: "8px",
              padding: "20px",
              textAlign: "center" as const,
              margin: "24px 0",
            }}
          >
            <Text
              style={{
                fontSize: "32px",
                fontWeight: 700,
                letterSpacing: "0.15em",
                margin: 0,
              }}
            >
              {code}
            </Text>
          </Section>
          <Text style={{ color: "#9ca3af", fontSize: "14px" }}>
            This code expires in 10 minutes. If you did not request this,
            you can safely ignore this email.
          </Text>
          <Hr style={{ borderColor: "#e5e7eb", margin: "24px 0" }} />
          <Text style={{ color: "#9ca3af", fontSize: "12px" }}>
            YourApp Inc. - Sent from auth@yourapp.com
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Handling every Clerk email type

Clerk fires different webhook events depending on the auth flow. Here is how each one maps to your templates:

  • verification_code - sent on sign-up. Render your verification template with the OTP code
  • reset_password_code - sent on password reset request. Render your password reset template with the OTP code
  • magic_link - sent for passwordless sign-in. Render your magic link template with the sign-in URL
  • invitation - sent when inviting a user to your app. Render your invitation template with the accept URL
  • organization_invitation - sent when inviting a user to an organization. Include org name and inviter details
Start by replacing just the verification and password reset emails. These are the highest-volume auth emails and the ones users scrutinize most. Add magic link and invitation templates once the core flow is working.

Testing the integration

Clerk provides a webhook testing tool in the dashboard, but for local development you will need to expose your local server. Use ngrokor Clerk's built-in tunnel:

Terminal
# Option 1: ngrok
ngrok http 3000

# Then add the ngrok URL as your webhook endpoint in Clerk:
# https://abc123.ngrok.io/api/clerk-webhooks

# Option 2: Use Clerk's development instance
# Dev instances deliver webhooks to localhost automatically

For email preview during development, render your templates directly without the webhook:

app/api/preview-email/route.ts
import { renderAsync } from "@react-email/render";
import { VerificationEmail } from "@/emails/verification";

export async function GET() {
  const html = await renderAsync(
    VerificationEmail({
      code: "482901",
      name: "Alex",
    })
  );

  return new Response(html, {
    headers: { "Content-Type": "text/html" },
  });
}
Visit /api/preview-email in your browser to see the rendered template. Update the props to test different states: long names, missing names, expired codes.

Edge cases to handle

Rate limiting

Clerk may fire multiple webhook events in quick succession during sign-up flows (verification + welcome). Your webhook handler should be idempotent - processing the same event twice should not send duplicate emails. Use the svix-id header as a deduplication key.

Webhook failures

If your endpoint returns a non-2xx response, Clerk (via Svix) will retry with exponential backoff. This means a temporary Resend outage will not permanently lose emails. However, you should still log failures and monitor your webhook endpoint health.

Fallback behavior

Consider keeping Clerk's built-in emails enabled as a fallback during initial rollout. If your webhook endpoint goes down, Clerk will still send its default templates. Once you are confident in your webhook reliability, disable the built-in emails to avoid duplicates.


Why custom templates matter for auth emails

Auth emails have a unique constraint: they ask users to take security-sensitive actions. Click this link. Enter this code. Reset this password. Every element of the email either builds or erodes the trust needed for users to follow through.

Generic templates create friction at the worst possible moment. A user who just signed up for your $49/month SaaS product receives a verification email that looks nothing like the product they just saw. The visual disconnect triggers a subconscious question: is this legitimate? That hesitation costs you activations.


Key takeaway
Clerk's webhook system gives you full control over auth emails without leaving the Clerk ecosystem. The setup requires a webhook endpoint, Svix signature verification, and your React Email templates. Start with verification and password reset (highest volume), then expand to magic links and invitations. The key advantage over the dashboard editor: you own the templates in your codebase, version them with git, and render them with the same component library as the rest of your email system.
R

React Emails Pro

Team

Building production-ready email templates with React Email. Writing about transactional email best practices, deliverability, and developer tooling.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates