Skip to content

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:

  1. On blur / debounced input: call your own endpoint, which calls the verifier, and render the result inline. Best UX, more requests.
  2. 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.