guides

Block personal emails at signup (without losing good users)

A practical pattern for B2B signup forms: inline work-email hints, typo suggestions with did_you_mean, server-side enforcement and failing open.

View as Markdown

Blocking @gmail.com at signup takes one if. Doing it without frustrating real customers takes a bit more care. This guide covers the pattern we recommend:

  1. A friendly inline hint while the user types (publishable key, browser).
  2. Typo suggestions from did_you_mean.
  3. Enforcement on the server at submit (secret key).
  4. Fail open when anything goes wrong.
  5. An escape hatch for the legitimate edge cases.

1. Inline hint in the browser

Create a publishable key restricted to your signup page’s origin. Check on blur (or debounced input), not on every keystroke.

<label for="email">Work email</label>
<input id="email" name="email" type="email" autocomplete="email" required
       aria-describedby="email-hint">
<p id="email-hint" role="status" aria-live="polite"></p>
const input = document.querySelector('#email');
const hint = document.querySelector('#email-hint');
const PUBLISHABLE_KEY = 'ibe_pub_…'; // restricted to https://app.example.com

input.addEventListener('blur', async () => {
  const email = input.value.trim();
  hint.textContent = '';
  if (!email.includes('@')) return;

  let r;
  try {
    const res = await fetch('https://api.isbusinessemail.com/v1/check', {
      method: 'POST',
      headers: { Authorization: `Bearer ${PUBLISHABLE_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(1500),
    });
    if (!res.ok) return;           // say nothing: the server decides at submit
    r = await res.json();
  } catch {
    return;                        // timeout or network error: stay quiet
  }

  if (r.did_you_mean) {
    hint.innerHTML = '';
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = r.did_you_mean;
    btn.onclick = () => { input.value = input.value.replace(/@.*/, '') + '@' + r.did_you_mean.replace(/^.*@/, ''); hint.textContent = ''; };
    hint.append('Did you mean ', btn, '?');
    return;
  }
  if (r.recommendation === 'block') {
    hint.textContent = messageFor(r.category);
  }
});

function messageFor(category) {
  switch (category) {
    case 'personal':   return 'Please use your work email so we can connect your company account.';
    case 'disposable': return 'Temporary inboxes can’t receive our setup emails. Please use your work email.';
    case 'relay':      return 'Relay addresses can’t be linked to your company. Please use your work email.';
    case 'invalid':    return 'This address can’t receive email. Please check it.';
    default:           return 'Please use your work email.';
  }
}

The suggestion handling above works whether did_you_mean holds a full address or just a domain.

Copy that works

  • Explain why, in terms of what the user gets: “so we can connect your Google Workspace”, “so your team can join automatically”.
  • Don’t accuse. “Please use your work email” beats “Personal emails are not allowed”.
  • Label the field “Work email” up front; many users switch before typing anything.
  • Keep the hint next to the field and announce it with aria-live for screen readers.

2. Enforce on the server

The browser hint is a courtesy: anyone can skip JavaScript. The decision happens on your server, with a secret key, when the form is submitted.

// server/check-email.js
export async function checkWorkEmail(email) {
  try {
    const res = await fetch('https://api.isbusinessemail.com/v1/check', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.IBE_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email, policy: 'b2b' }),
      signal: AbortSignal.timeout(2500),
    });
    if (!res.ok) {
      console.warn('isBusinessEmail returned', res.status); // 401/403: fix config; 429/5xx: transient
      return { recommendation: 'allow', failOpen: true };
    }
    return await res.json();
  } catch (err) {
    console.warn('isBusinessEmail unavailable', err.name);
    return { recommendation: 'allow', failOpen: true };
  }
}
// in your signup handler
const verdict = await checkWorkEmail(req.body.email);

if (verdict.recommendation === 'block') {
  return res.status(422).json({
    field: 'email',
    message: 'Please use your work email.',
    suggestion: verdict.did_you_mean ?? null,
  });
}

const user = await createUser({
  email: req.body.email,
  emailCategory: verdict.category ?? null,          // store the verdict, not a copy of our response
  needsReview: verdict.recommendation === 'review' || verdict.failOpen === true,
});

Fail open

Our API is fast and built to degrade gracefully, but your signup must never depend on a third party being up. Rules of thumb:

Situation Do
Timeout (> 2.5 s) or network error Allow, mark needsReview, re-check in a background job
429 rate_limited / quota_exceeded Allow, mark for re-check; alert if it keeps happening
5xx Allow, mark for re-check
401 / 403 Allow, alert your on-call: a key or config problem
degraded: true Trust block for list matches; treat review as allow and re-check later

Re-checking later is cheap: a background job that runs /v1/check on accounts with needsReview and acts on the result (for example, routes them to sales or limits the trial).

3. Handle review

review covers role accounts (info@, sales@) and domains with too little evidence (unknown). Don’t block them. Options:

  • Allow, and require email verification before the trial starts.
  • Allow with a shorter trial until someone looks.
  • Ask one extra question: company website, team size.

4. Give people an escape hatch

Some legitimate customers will be on a personal address: founders before they set up a domain, freelancers, researchers. Losing them silently is the real cost of blocking. Add a small link under the error:

Don’t have a work email? Request access and we’ll get back to you.

Route those requests to a human or a waitlist. You keep the lead, and abuse doesn’t scale through a manual form.

5. Measure

Store category (and workspace detection, if you use it) on the account. After a few weeks, compare activation and conversion by category. That tells you whether your policy is too strict or too loose for your product, which is better evidence than any general rule.

Checklist

  • Field labelled “Work email”
  • Inline hint with a publishable key, restricted to your origin
  • did_you_mean shown as a clickable suggestion, never auto-applied
  • Server-side check with a secret key, 2–3 s timeout
  • Fail open on timeouts, 429 and 5xx; alert on 401/403
  • review handled without blocking
  • Escape hatch for edge cases
  • Verdict stored on the account for analytics

See also: Categories and policies · Block free emails at signup · Should B2B SaaS block free email signups?