How to verify email at signup without adding friction
· 3 min read · Email validation
To verify email at signup without hurting conversion, run the check when the user leaves the email field or submits the form, and only block what you're certain is bad: broken syntax, domains that can't receive mail, disposable addresses and mailboxes the server says don't exist. Show a "did you mean" prompt for typos like gmial.com. Let everything uncertain through, then confirm the address with a link or code.
Most friction comes from treating every non-perfect answer as an error. A verifier gives you four kinds of answer, and only one of them justifies stopping someone.
Block, warn or allow
| Result | Example reasons | At signup |
|---|---|---|
invalid |
invalid_syntax, no_domain, no_mx, null_mx, mailbox_not_found, disposable_domain |
Block with a specific message |
Any non-valid result with a suggestion |
did_you_mean: "jane@gmail.com" |
Show the suggestion, one click to accept |
risky |
accept_all, role_based, mailbox_full, randomized_local_part |
Allow. Maybe flag for review. |
unknown |
greylisted, blocked, timeout |
Allow. Re-check later. |
valid |
mailbox_exists |
Allow |
Two judgment calls:
- Disposable domains. Blocking is reasonable for free trials and anything with credits, and a mistake for a newsletter. Block or flag disposable emails goes through the trade-off.
- Role addresses.
info@at a small business may be the only inbox they have. Allow it at signup; route it differently in sales. See role-based email addresses.
Typos are the biggest win
A user who mistypes their address never gets the confirmation email, never activates, and often never comes back. Catching jane@gmial.com on the form saves that signup outright.
Ask, don't correct. Silently rewriting an address is wrong sometimes, and the user won't know you did it. A line under the field works:
Did you mean jane@gmail.com? [Use this]
Amazon SES's guidance on reducing bounces suggests asking users to type their address twice and using double opt-in. A typo prompt gets most of the benefit of the double-entry field without the extra input.
Where to run the check
Run verification on your server, not from the browser, so your API key stays private. Two common shapes:
- On blur / debounced input: call your own endpoint, which calls the verifier, and render the result inline. Best UX, more requests.
- On submit: verify inside the signup handler. Fewer requests, and the only option if the form posts directly.
Either way, set a short timeout. An SMTP check talks to someone else's mail server, which may be slow or may greylist the first attempt. If it doesn't answer in a few seconds, let the user in and finish the check in the background.
A server-side example
This uses pingutil's /v1/verify from a Node.js signup handler (Node 18+ for global fetch):
async function checkSignupEmail(email) {
try {
const res = await fetch('https://pingutil.com/v1/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PINGUTIL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(4000),
});
if (!res.ok) return { action: 'allow' }; // 429 or 5xx: never block on our own limits
const r = await res.json();
if (r.did_you_mean) {
return { action: 'suggest', suggestion: r.did_you_mean };
}
if (r.status === 'invalid') {
return { action: 'block', reason: r.reason };
}
// valid, risky, unknown: let them in; keep the result for later decisions
return { action: 'allow', status: r.status, reason: r.reason, score: r.score };
} catch {
return { action: 'allow' }; // timeout or network error
}
}
Map reason to plain messages: no_mx becomes "This domain can't receive email", mailbox_not_found becomes "We couldn't find that mailbox. Check for a typo?". Specific messages get fixed; "Invalid email" gets retyped identically.
Keep checks.disposable, checks.free_provider and checks.role_based on the user record too. They're useful later for lead routing and abuse review even when they don't change the signup decision.
Limits to design around
pingutil accounts get 60 requests a minute and 300 verifications a day, which fits a product with a modest signup volume; the fallback-to-allow above means a busy day degrades to "verify later" instead of failed signups. Results are cached for 30 days under a keyed hash of the address, so a user retrying the form gets a fast cached answer (it still counts toward the daily budget).
Verification also has limits of its own. A valid result means the server accepted the address during the SMTP conversation, not that the person typing it owns it. Catch-all domains can't be confirmed at all, which is why they return risky / accept_all. The details are in why SMTP verification is unreliable.
The ship-it version: typo prompt, block on certain invalid, allow the rest, send a confirmation link. Try addresses in the email verification demo to see what each status looks like, and check the verify endpoint reference for every field.
Questions people ask
- Should I block signups with catch-all email addresses?
- No. A catch-all domain accepts every address during the SMTP check, so verification can't tell real from fake there. Plenty of legitimate companies run catch-all mail servers. Let them sign up and rely on a confirmation email.
- Do I still need a confirmation email if I verify at signup?
- Yes. Verification tells you the mail server would accept the address; it doesn't prove the person signing up owns it. A confirmation link or code is the only check that does.
- What if the verification API is slow or down during signup?
- Let the signup through. Set a short timeout on the call, treat a timeout or error as unknown, and verify the address again in the background. Never make your own signup depend on a third-party API being up.