Mailbeam
Clerk + Next.jsIntermediate20 minutesUpdated January 2025

Email Verification with Clerk

Clerk supports webhooks for user lifecycle events including user.created. While Clerk doesn't have a native pre-creation block, you can combine a webhook with Clerk's user metadata to mark invalid signups for immediate deletion, or use Clerk's before-signup redirect pattern.

This tutorial uses the recommended pattern: verify the email via a server endpoint before calling Clerk's signUp.create.

Prerequisites

  • Clerk account + Next.js project with @clerk/nextjs
  • A Mailbeam API key (sign up free)

Setup

npm install @mailbeam/sdk svix  # svix is Clerk's webhook library

Server-side verification endpoint

// app/api/verify-email/route.ts
import { NextResponse } from "next/server";
import Mailbeam from "@mailbeam/sdk";

const mb = new Mailbeam({ apiKey: process.env.MAILBEAM_KEY! });

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

  if (!email) {
    return NextResponse.json({ error: "email required" }, { status: 400 });
  }

  try {
    const { valid, score, reason } = await mb.verify(email);

    return NextResponse.json({ valid, score, reason });
  } catch {
    // Fail open
    return NextResponse.json({ valid: true, score: 50, reason: null });
  }
}

Custom signup form

// app/signup/CustomSignupForm.tsx
"use client";

import { useSignUp } from "@clerk/nextjs";
import { useState } from "react";

export function CustomSignupForm() {
  const { signUp, setActive } = useSignUp();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError(null);

    // Step 1: Verify email server-side before calling Clerk
    const verifyRes = await fetch("/api/verify-email", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email }),
    });

    const { valid, score, reason } = await verifyRes.json();

    if (!valid || score < 60) {
      setError(
        reason === "disposable_domain"
          ? "Please use a permanent email address."
          : "Please provide a valid email address."
      );
      setLoading(false);
      return;
    }

    // Step 2: Proceed with Clerk signup
    try {
      const result = await signUp!.create({ emailAddress: email, password });

      if (result.status === "complete") {
        await setActive!({ session: result.createdSessionId });
        window.location.href = "/dashboard";
      }
    } catch (err: unknown) {
      setError((err as { errors?: { message: string }[] })?.errors?.[0]?.message ?? "Signup failed.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4 max-w-sm">
      {error && (
        <p role="alert" className="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
          {error}
        </p>
      )}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email address"
        className="w-full rounded-lg border border-border px-3 py-2 text-sm bg-background"
        required
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
        className="w-full rounded-lg border border-border px-3 py-2 text-sm bg-background"
        required
      />
      <button
        type="submit"
        disabled={loading}
        className="w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-60"
      >
        {loading ? "Creating account…" : "Create account"}
      </button>
    </form>
  );
}

Best practices

  • Verify before calling Clerk, not after — prevents orphaned unverified users
  • Keep the verify endpoint lightweight — it should only call Mailbeam and return
  • Use reason to give users specific error messages

Next steps