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.