Clerk · TypeScript

Block disposable email in Clerk

Use a Clerk webhook on user.created or validate in your custom signup API before calling Clerk. Two patterns depending on when you want to block.

The code

// app/api/webhooks/clerk/route.ts
import { Webhook } from "svix";
import { headers } from "next/headers";
import { NextResponse } from "next/server";

const PIISEND_URL = "https://api.piisend.com/api/v1/intelligence/email";

export async function POST(req: Request) {
  const payload = await req.text();
  const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
  const evt = wh.verify(payload, Object.fromEntries(await headers())) as any;

  if (evt.type === "user.created" && evt.data.email_addresses?.[0]?.email_address) {
    const email = evt.data.email_addresses[0].email_address;
    const r = await fetch(PIISEND_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.PIISEND_API_KEY!}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
    });
    if (r.ok && (await r.json()).signals?.disposable) {
      // Delete user or flag — prefer blocking before create when possible
      await fetch(`https://api.clerk.com/v1/users/${evt.data.id}`, {
        method: "DELETE",
        headers: { Authorization: `Bearer ${process.env.CLERK_SECRET_KEY!}` },
      });
    }
  }

  return NextResponse.json({ ok: true });
}

API key

Use separate secrets for Clerk webhook verification and Piisend API key.

Fail-open vs fail-closed

Webhook pattern is async — user may exist briefly. Prefer server-side check before `clerkClient.users.createUser` when you control signup.

Pair with verification

For custom flows, call Piisend in your signup Route Handler before any Clerk API call.

Send verification email with Piisend

After blocking disposable addresses, send OTP and verification mail through the same Piisend account. One API key for intelligence checks and transactional sends.