Integration guide · Clerk

How to block disposable emails in Clerk

Clerk already ships a disposable-email protection on every plan. Turn it on first. Then, if you run a custom sign-up flow, add one server check for the cases the toggle was not built to catch.

By the Vetto team · Updated

Quick answer

In the Clerk Dashboard, open Restrictions and turn on Block disposable email addresses. It is included on every plan and off by default. If you also want domains that cannot receive mail rejected (including typo domains like gmal.com), or your own allow and block rules, check the address in a Server Action before signUp.create() in a custom sign-up flow.

1. Turn on Clerk’s built-in protection

This is the right first step for every Clerk app, and it costs nothing. According to Clerk’s docs, the protection:

  • blocks addresses whose domain is known to be disposable, on sign-up and when a user adds an email to an existing account;
  • does not affect sign-ins, so existing users keep access;
  • checks the domain and each parent domain, so mail.example.com is caught if example.com is listed;
  • has no settings. If a domain your users rely on is blocked by mistake, you contact Clerk support.

While you are there, turn on Block email subaddresses too. It stops variations of an address that already has an account, which is the cheapest way one person farms several free trials.

2. What the toggle does not cover

CaseClerk toggleServer check below
Known throwaway domain (mailinator.com)BlockedBlocked
Typo domain with no mail server (gmal.com)Not its jobRejected, with a “check for a typo” message
Domain that cannot receive mailNot its jobRejected
Your own allow or block rulesSupport ticket / paid blocklistYour code
Domains Clerk rates as riskyBusiness planNot covered

If none of the middle rows matter to you, stop here: the toggle is enough. The rest of this page is for apps where a typo or a dead domain means a user who never gets the verification code and never comes back.

3. Add a server check to a custom flow

The check has to run between the user pressing submit and signUp.create(), so it needs a custom sign-up form rather than the prebuilt <SignUp /> component. It calls Vetto’s keyless domain lookup, which needs no account, and it fails open: if the lookup is slow or down, the signup goes ahead.

lib/vetto.ts
// lib/vetto.ts (server only, keyless, fails open)
import 'server-only';

const DEAD = new Set(['nxdomain', 'no_mx_a', 'null_mx', 'no_mail']);

export async function emailProblem(email: string): Promise<string | null> {
  const domain = email.split('@').pop()?.trim().toLowerCase();
  if (!domain) return null;
  try {
    const res = await fetch('https://usevetto.com/v1/audit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ domains: [domain] }),
      signal: AbortSignal.timeout(2500),
      cache: 'no-store',
    });
    const r = (await res.json()).results?.[domain];
    if (r?.disposable) return 'Please use a permanent email address.';
    if (r && DEAD.has(r.status)) return 'This email domain cannot receive mail. Check for a typo.';
  } catch {
    // timeout or network error: let the signup through
  }
  return null;
}
app/sign-up/actions.ts
// app/sign-up/actions.ts
'use server';
import { emailProblem } from '@/lib/vetto';

export async function checkSignupEmail(email: string) {
  return emailProblem(email); // null means "go ahead"
}
app/sign-up/form.tsx
// app/sign-up/form.tsx (custom flow: you own the form)
'use client';
import { useSignUp } from '@clerk/nextjs';
import { checkSignupEmail } from './actions';

export function SignUpForm() {
  const { isLoaded, signUp } = useSignUp();
  // ...email/password state, error state

  async function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    if (!isLoaded) return;

    const problem = await checkSignupEmail(email);
    if (problem) return setError(problem); // show it next to the field

    await signUp.create({ emailAddress: email, password });
    // ...continue with Clerk's email verification step
  }
  // ...
}

Privacy relays: do not over-block

Apple Hide My Email, Firefox Relay and DuckDuckGo addresses forward to an inbox the person really reads. Many careful, paying users sign up with them. If you add your own block rules, leave relay domains out. Is privaterelay.appleid.com disposable? explains why.

Related: Better Auth · Auth.js / NextAuth · Next.js · Supabase · free trial abuse.

Frequently asked questions

Does Clerk block disposable emails?

Yes. Clerk has a "Block disposable email addresses" protection, included on every plan and off by default. Turn it on in the Clerk Dashboard. It blocks known disposable domains on sign-up and when a user adds an email, but not on sign-in.

Can I add my own domains to Clerk’s disposable list?

Not to that protection: Clerk documents it as having no settings, and asks you to contact support if a legitimate domain is blocked by mistake. Clerk’s general blocklist is a separate, paid feature. A server-side check like the one on this page gives you your own logic without a plan change.

What does Clerk’s protection not catch?

It only blocks domains already known to be disposable. It does not reject a typo domain such as gmal.com, or any other domain that simply has no mail server. Blocking domains Clerk rates as risky is a separate protection on the Business plan.

Does this work with Clerk’s prebuilt <SignUp /> component?

Turn on Clerk’s toggle for the prebuilt component. The extra server check on this page needs a custom sign-up flow, because you have to run code between the user pressing submit and signUp.create().

What about one person opening many free trials?

Turn on Clerk’s "Block email subaddresses" protection, which blocks variations of an address that already has an account. A disposable-domain list cannot see that pattern on its own.

Give your signup form sharp eyes.

Free on one site. One line of HTML. Remove it any time and your form works exactly as before.

Founding offer: Pro for life for $69 once, first 100 buyers, until Nov 27.