# 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.

Source: https://isbusinessemail.com/docs/signup-form-guide

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](https://isbusinessemail.com/docs/authentication#publishable-keys) restricted to your signup page's origin. Check on `blur` (or debounced input), not on every keystroke.

```html
<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>
```

```javascript
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.

```javascript
// 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 };
  }
}
```

```javascript
// 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](https://isbusinessemail.com/docs/categories-and-policies) · [Block free emails at signup](https://isbusinessemail.com/use-cases/block-free-emails-at-signup) · [Should B2B SaaS block free email signups?](https://isbusinessemail.com/blog/should-b2b-saas-block-free-email-signups)
