Some of the worst signups aren't fake at all. They're real people who typed jane@gmial.com, clicked "Create account", and never heard from you again. Your welcome email bounced, the verification link never arrived, and a week later they either gave up or opened a support ticket titled "can't log in".
Typo'd addresses are one of the few email problems where the fix is almost entirely a UX problem. You don't need to block anyone. You need to notice the likely mistake and ask the user, while they're still on the page.
Why a typo costs more than one signup
A mistyped address is a quiet failure. Nothing errors, the account gets created, and every downstream step breaks:
- The user is lost. They can't confirm their account, reset their password or receive receipts. Many won't come back to try again.
- You get a bounce. Every message you send to
gmial.comoryahooo.comeither bounces or goes nowhere. Hard bounces count against your sender reputation, and a steady trickle of them from signups adds up. - Support gets the ticket. "I never got the email" and "can't log in" tickets are slow to resolve, because the first step is figuring out the address is wrong.
- Your data is dirty. The account record now holds an address that will never work, and it sits in your list until someone cleans it.
A regex doesn't help here. jane@gmial.com is perfectly valid syntax. The problem is that the domain is almost certainly not what the user meant.
What a typo looks like
Most typos land on the domain, and on a handful of very popular providers. A few patterns I see over and over:
| Typed | Probably meant | Kind of typo |
|---|---|---|
gmial.com |
gmail.com |
Swapped letters |
gmai.com |
gmail.com |
Missing letter |
hotmial.com |
hotmail.com |
Swapped letters |
yahooo.com |
yahoo.com |
Extra letter |
outlok.com |
outlook.com |
Missing letter |
gmail.con |
gmail.com |
Wrong TLD |
Some of these domains exist and some don't. Either way, a person who types gmial.com almost never has a mailbox there. That's why the right check is "does this look like a near-miss of a well-known provider?", not just "does this domain exist?".
Detecting typos with fast mode
MailRambo has a free fast mode built for signup forms. It never contacts the mailbox, so it's quick (well under a second) and free on every plan. It checks syntax, domain typos, MX records, disposable providers and role addresses.
Call it with ?mode=fast:
curl -X POST "https://www.mailrambo.com/v1/verify?mode=fast" \
-H "Authorization: Bearer mr_live_..." \
-H "Content-Type: application/json" \
-d '{"email": "jane@gmial.com"}'
For a likely typo, you get reason: "possible_typo" and a top-level suggestion with the corrected address:
{
"email": "jane@gmial.com",
"mode": "fast",
"deliverable": false,
"reason": "possible_typo",
"suggestion": "jane@gmail.com",
"checks": { "mx": true, "disposable": true, "role_account": false, "free_email": true },
"credits_remaining": null
}
Note that suggestion is the full address, not just the domain, so you can show it as-is. Fast mode returns deliverable: false when it finds a problem (invalid_syntax, possible_typo, disposable, no_mx) and null with reason: "fast_check_passed" otherwise. It never returns true, because it doesn't check the mailbox.
The "did you mean" UX
This is the part that matters most. A few rules I'd follow:
Suggest, don't block
A possible_typo result is a guess. Some people really do use unusual domains. So show the suggestion as a question, and let the user continue with what they typed if they insist:
Did you mean jane@gmail.com? [Use this] ยท [Keep jane@gmial.com]
Never auto-correct silently
It's tempting to just replace gmial.com with gmail.com on the server. Don't. If the guess is wrong, you've created an account for an address the user never typed, and they have no idea why their emails go to someone else. Any change to the address should be a click the user makes.
Check on blur, not on every keystroke
Run the check when the field loses focus (or when the user submits). Checking on every keystroke flashes errors at people halfway through typing. And since it's a network call, debounce it.
Keep the key on the server
The browser should call your own endpoint, which calls MailRambo. An API key in client-side code is visible to anyone.
Fail open
If the check times out or errors, let the user through. A typo check is a convenience; it should never be the reason a real signup fails.
Code: a small server endpoint
Here's a Next.js route handler that returns only what the form needs:
// app/api/check-email/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { email } = await req.json();
try {
const res = await fetch("https://www.mailrambo.com/v1/verify?mode=fast", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILRAMBO_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
if (!res.ok) return NextResponse.json({ ok: true, skipped: true });
const { deliverable, reason, suggestion } = await res.json();
return NextResponse.json({ ok: deliverable !== false, reason, suggestion: suggestion ?? null });
} catch {
return NextResponse.json({ ok: true, skipped: true }); // fail open
}
}
And the client side, checked on blur:
const input = document.querySelector("#email");
const hint = document.querySelector("#email-hint");
input.addEventListener("blur", async () => {
hint.textContent = "";
const value = input.value.trim();
if (!value) return;
const res = await fetch("/api/check-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: value }),
}).then((r) => r.json());
if (res.reason === "possible_typo" && res.suggestion) {
hint.innerHTML = `Did you mean <button type="button" id="use-suggestion"></button>?`;
const btn = hint.querySelector("#use-suggestion");
btn.textContent = res.suggestion; // textContent, not HTML: never inject user input
btn.addEventListener("click", () => {
input.value = res.suggestion;
hint.textContent = "";
});
} else if (res.reason === "disposable") {
hint.textContent = "Please use a permanent email address.";
} else if (res.reason === "no_mx" || res.reason === "invalid_syntax") {
hint.textContent = "That address can't receive email. Check for typos?";
}
});
For a typo, the form suggests; it doesn't refuse. For disposable, no_mx and invalid_syntax, you have firmer grounds to block on submit, because those can't be a real inbox the user owns (or, for disposable, one you'll be able to reach later).
Messages for each fast-mode reason
reason |
deliverable |
Suggested message | Block submit? |
|---|---|---|---|
possible_typo |
false | "Did you mean {suggestion}?" | No, ask |
invalid_syntax |
false | "Please enter a valid email address." | Yes |
no_mx |
false | "That domain can't receive email. Check for typos?" | Yes |
disposable |
false | "Please use a permanent email address." | Your call |
fast_check_passed |
null | (nothing) | No |
role_account doesn't appear as a fast-mode failure; it shows up as a flag in checks.role_account, which you can ignore for most signup forms.
After signup: the full check
Fast mode tells you nothing is obviously wrong. It doesn't confirm the mailbox exists. If you want that, run a full check (1 credit) after the account is created, in a background job, before your first marketing send. Repeat checks of the same address within 24 hours are free, so re-checking on a retry costs nothing.
For the wider picture of form validation (disposable blocking, timing, what to do when the API is down), see real-time email validation on signup forms. If you want to try an address by hand, the email syntax checker is free and needs no account.
Summary
- Typos are valid syntax, so regex can't catch them; you need a domain-aware check.
- Use fast mode's
possible_typoreason and showsuggestionas "Did you mean...?". - Ask, don't auto-correct. Check on blur, keep the key server-side, and fail open.
Fast mode is free on every plan, including the Free plan with 100 full checks a month. The request and response details are on the developers page.