Email validation · Integrations · Clerk

Integration guide

Email validation in Clerk with Trueguard.

To add email validation in Clerk, send the user.created webhook to a route that calls the Trueguard email validation API and bans the user when the address is disposable, or invalid and unverified. Clerk's docs list no synchronous before-signup hook (read 2026-11-09), so this runs after the account exists. If the check fails, the account stays.

In this guide

Endpoint1 POST
Median responseSub-1s
Free tier100 / month
Standard plan$49.99 / 50,000

How do I validate emails in Clerk?

Clerk sends this route a signed event for each new user. It uses the Clerk SDK already in your app and axios, which you add with npm install axios.

app/api/webhooks/clerk/route.ts

import axios from "axios"; import { clerkClient } from "@clerk/nextjs/server"; import { verifyWebhook } from "@clerk/nextjs/webhooks"; import { NextRequest } from "next/server"; const API = "https://api.trueguard.io/v2/email/validation"; type Validation = { deliverability: { status: "safe" | "risky" | "invalid" | "unknown" }; quality: { isDisposable: boolean }; }; // Returns null when Trueguard gives no verdict, so the account stays. const validate = async (email: string): Promise<Validation | null> => { try { const { data } = await axios.post<Validation>( API, { email }, { headers: { "X-API-KEY": process.env.TRUEGUARD_KEY! }, signal: AbortSignal.timeout(3000), }, ); return data; } catch (err) { // Log the message only, because the full error holds your API key. console.error("Trueguard call failed", (err as Error).message); return null; } }; // A disposable inbox can receive Clerk's code, so verified does not clear it. // An invalid result on an address that Clerk has verified keeps the account. const shouldBan = ({ deliverability, quality }: Validation, verified: boolean): boolean => quality.isDisposable || (deliverability.status === "invalid" && !verified); export async function POST(req: NextRequest) { const evt = await verifyWebhook(req).catch((err) => { console.error("Webhook verification failed", err); return null; }); // Not proven to be from Clerk, so nothing is checked or billed. if (!evt) { return new Response("Invalid signature", { status: 400 }); } if (evt.type !== "user.created") { return new Response("Ignored", { status: 200 }); } const { id, email_addresses, primary_email_address_id } = evt.data; const primary = email_addresses.find((e) => e.id === primary_email_address_id); // No primary email to check, for example a phone-only user. if (!primary) { return new Response("No email", { status: 200 }); } const verified = primary.verification?.status === "verified"; const result = await validate(primary.email_address); if (result && shouldBan(result, verified)) { try { const client = await clerkClient(); await client.users.banUser(id); } catch (err) { // A 500 makes Clerk redeliver, which runs one more billed check. console.error("Ban failed", id, err); return new Response("Ban failed", { status: 500 }); } } return new Response("OK", { status: 200 }); }

Add your key and the next new user is checked. 100 validations a month are free, no card.

Get your free API key

One header, one body field

Set X-API-KEY to your key and POST a JSON body with a single email field. HTTPS only. The response carries the seven signals plus one deliverability status.

Response fields you will branch on

deliverability.status

safe, risky, invalid or unknown. This is the field to branch on.

syntax.isValid

RFC 5322 format check. Deterministic, so a false here is always a typo or a malformed address.

quality.isDisposable

True for throwaway providers. Usually a hard block at signup.

deliverability.isCatchall

True when the domain accepts any address. The status is then risky, unless a second check confirmed or ruled out the mailbox.

quality.isRole

info@, support@, sales@. Route these differently rather than rejecting them.

Run it once before you write any code.

The same endpoint the snippet above calls. 10 free checks a day, no key needed.

Or try

Where does the email check go in Clerk?

Clerk's docs list no synchronous hook that can reject a signup server-side (read 2026-11-09). Webhooks are asynchronous, so the check goes in a user.created webhook, and it runs after the account exists.

A check in your signup form

Form code runs in the browser, so treat any check there as advisory. Your Trueguard key is a secret and cannot ship with it. Users created through the Backend API never touch the form.

A user.created webhook

Clerk documents this event for users who register in your app and users created in the Dashboard or Backend API. A session can already be active when it arrives, so the handler bans the user, and a ban revokes all their sessions.

What the endpoint returns

{ "email": "founder@yourcompany.com", "rawEmail": "founder@yourcompany.com", "syntax": { "isValid": true }, "deliverability": { "status": "safe", "isSmtpValid": true, "isMxValid": true, "isCatchall": false, "isInboxFull": false, "isDeliverable": true, "isDisabled": false, "mxRecords": ["mx1.yourcompany.com", "mx2.yourcompany.com"] }, "domain": { "name": "yourcompany.com", "age": 2841, "isLive": true, "isRisky": false }, "quality": { "isDisposable": false, "isFree": false, "isRole": false, "isSubaddress": false } }

How do I connect the webhook in Clerk?

Four steps. Until the signing secret is set, verifyWebhook throws and the route returns 400.

  1. 01

    Add the route.

    Run npm install axios and paste the handler above. /api/webhooks(.*) must stay public. By default clerkMiddleware() protects no routes, so only exclude it if you added auth checks. See Clerk's syncing guide.
  2. 02

    Create the endpoint.

    In Dashboard > Webhooks, point an endpoint at https://your-domain.com/api/webhooks/clerk, subscribe it to user.created only, and copy the signing secret.
  3. 03

    Set three secrets.

    Your Trueguard key comes from the Trueguard dashboard.
    # .env.local, keep it out of git CLERK_WEBHOOK_SIGNING_SECRET="whsec_..." CLERK_SECRET_KEY="your_clerk_secret_key" TRUEGUARD_KEY="your_api_key"
  4. 04

    Test both paths.

    Sign up with a real address and confirm the user stays active. Sign up with a disposable address and confirm the user is banned. If every delivery returns 400, check the signing secret first.

What does Clerk's email code prove, and who should the webhook ban?

Clerk's email code shows that someone read that inbox while the code was valid, which is 10 minutes. A temporary inbox can do that too, so disposable addresses are the webhook's main catch. A sign-up that requires verification is not complete until the address is verified, so an address that cannot receive mail should stop there. It can still reach your user table when you create the user through the Backend API or Dashboard. That is our reading of Clerk's docs on 2026-11-09. They do not state when user.created fires relative to verification, so the code reads the address's own verification status and is correct either way.

SignalHook actionWhy
quality.isDisposable (any status)BanA temporary inbox. It can receive Clerk's code, so verification does not stop it.
invalid, address not verifiedBanTrueguard says it cannot receive mail and Clerk holds no proof that it can. Malformed addresses land here too.
invalid, address verifiedAllowClerk's verification status takes priority over a single API result.
safeAllowThe mailbox can receive mail.
safe with isCatchall: trueAllowTrueguard's advanced catch-all detection validated the mailbox on a catch-all domain.
risky with isCatchall: trueAllowThe domain accepts all mail and the mailbox was not validated. Real people use these domains.
risky, high-risk TLDAllowA weak reason to ban an account on its own.
unknownAllowThe mail server gave no clear answer.
quality.isRoleAllowNever changes the status. team@ can be a paying customer. See role addresses.
quality.isFreeAllowNever changes the status. Personal addresses are normal signups.
quality.isSubaddressAllowAn alias on a mailbox.
Trueguard error or no answer in 3 sAllow and logYour quota or an outage should not cost you an account.
No primary emailAllow, no API callNo validation used.
Event other than user.createdIgnore, 200Subscribe to this one event and it should not occur.
Signature check failsReject, 400Not proven to come from Clerk. Nothing billed.
banUser throwsReturn 500Clerk redelivers, and the redelivery can finish the ban.

How do I avoid blocking real users?

Fail open on anything that is not a verdict.

A timeout, 401, 403, 429 or 5xx keeps the account. On the free plan, signups past 100 a month pass unchecked, so read your logs.

Ban two cases only.

Disposable, and invalid without verification. Unknown, role, free and an unvalidated catch-all all pass.

Run imports before you connect this.

Clerk documents user.created for Backend API creates. Each imported user would cost one check and could be banned.

Put the message at the form.

The webhook has no way to answer the signup form. For an inline error in a custom flow, call your own server route before signUp and keep the key on the server. Treat it as a usability step, because the webhook does the enforcement.

What should a Clerk webhook return?

Clerk retries until your endpoint returns a 2xx or the retries run out, and an event can reach you more than once. Read on 2026-11-09. Each delivery of a user.created event for an address is one more Trueguard check.

// Bad signature: no Trueguard call, nothing billed. return new Response("Invalid signature", { status: 400 }); // Address passed, or Trueguard failed: Clerk stops retrying. return new Response("OK", { status: 200 }); // banUser threw: Clerk retries until a 2xx or the retries run out. return new Response("Ban failed", { status: 500 });

Status

What Clerk does

200

Clerk stops retrying. Sent when the ban succeeds, the address passes, or Trueguard fails.

400

Bad or missing signature. Nothing is checked or billed. Clerk retries 4xx responses too.

500

banUser threw. Clerk retries, and each retry runs one more billed check.

None

No status code counts as a failure and is retried.

What does Clerk block on its own?

From Clerk's restriction, disposable, subaddress and Protect docs and its pricing page, read on 2026-11-09.

Disposable: every planSubaddress: every planSuspicious sign-ups: every planBlocklist: paid in productionRisky domains: Business

Disposable protection.

Off by default, no settings, and it uses Clerk's list. Trueguard returns quality.isDisposable to your code beside a deliverability status.

Subaddress protection.

Clerk blocks a second variation of an address and allows the first. Trueguard's quality.isSubaddress flags a + address on first sight.

Risky domains.

Clerk's rule requires the Business plan, at $300 a month or $250 billed annually. Trueguard returns domain.isRisky on every plan. It is a separate signal and may not match Clerk's rule.

Users you create yourself.

Clerk's Protect does not check Backend API, Dashboard or imported users. The webhook does receive Backend API and Dashboard creates.

What does each Clerk signup cost?

Trueguard checks run on user.created, so logins cost nothing. The free plan covers 100 signups a month. Standard is $49.99 for 50,000, which is $0.001 per signup at full use. At 2,000 signups a month that is $49.99 / 2,000 = about $0.025 each. Past 50,000 it is $0.001 per check, so 80,000 signups cost $49.99 + 30,000 × $0.001 = $79.99. Every check counts, banned addresses included, and each redelivered event is one more check. A 20,000 user import is 20,000 checks if the webhook is live.

Free

$0 forever

100 validations / month

Overage: n/a

Get started free

Standard

Popular

$49.99 / month

50,000 validations / month

Overage: $0.001 per validation

Get your API key

Custom

Contact us

500,000+ validations / month

Overage: Negotiated

Talk to us

Frequently asked questions

The full field reference lives in the API documentation.

How do I block disposable emails in Clerk?

Use Trueguard in a user.created webhook and ban when quality.isDisposable is true. The route takes one Next.js file, and 100 checks a month are free. Clerk also has a disposable setting on every plan, off by default, read 2026-11-09.

Does Clerk have a before-signup hook?

Clerk's docs list no synchronous hook that can reject a signup server-side, read 2026-11-09. Webhooks are asynchronous.

Does Clerk's email code make this check unnecessary?

The code shows someone read that inbox. A temporary inbox can receive one, so the disposable check still has work to do. Timing is our reading of the docs.

Should I ban or delete the user?

Ban. Clerk documents that a ban revokes all sessions, and its delete page does not mention sessions. Clerk bills retained users, never total users in the table.

Can a banned user keep using the app for a moment?

A session can be active until the handler finishes. Sessions are then revoked. Clerk's session token lasts 60 seconds, so on our reading an issued token may validate until it expires. Clerk does not describe this case.

What happens if Trueguard is slow or down?

The account stays. The handler waits 3 seconds, logs the error and returns 200, so Clerk does not redeliver.

Should I ban risky or catch-all addresses?

Only when disposable. Trueguard's advanced catch-all detection can validate a mailbox on a catch-all domain: validated is safe with isCatchall true, not validated is risky. The handler allows both.

Does every Clerk signup method fire user.created?

Clerk documents app registration, Dashboard and Backend API creates. Its docs do not list OAuth, magic link or import method by method. Test the methods you use.

Can I show an error on my signup form?

In a custom flow you can call your own route before signUp, keeping the key on the server. That is a usability step, the webhook stays the enforcement, and it uses a second check per signup. Clerk's disposable setting also shows an error at signup that the address is not allowed.

Does every login use a validation?

No. The route acts only on user.created. No primary email means no call. Banned addresses count, as does each redelivery.

Check every new Clerk signup with Trueguard.

100 validations a month, free, no card. Add the route, set your key, and the next new user is checked.

Get your free API key

No credit card required.

trueguard-logo© 2026 Trueguardinfo@trueguard.io