Throwaway accounts cost more than the row they occupy. They inflate the numbers you make decisions on, they burn your free tier, they poison your email deliverability when the address bounces, and a handful of them are the reconnaissance step before something worse.
Most of them are stopped by two calls at registration, and neither one adds a step for a real person. This guide covers which signals are worth a hard rejection, which are only worth a score, and where each check belongs in the flow.
The shape of the check
Three positions, and picking the right one matters more than picking the right endpoint:
| Where | What belongs there |
|---|---|
| As the user types | Typo correction. Advisory only, never blocking |
| On submit, before the account row is written | The address check. This is the one that rejects |
| After the account exists | The origin check. Scored, reviewed, never a hard block |
The reason for the split is that the two checks have different error costs. An email address that is provably disposable is a fact about the address, and rejecting on it turns away almost nobody real. An IP address that resolves to a VPN is a fact about a connection, and rejecting on it turns away every privacy-conscious user you have.
Check the address
Email validator is one call and answers the whole question:
curl 'https://api.apiverve.com/v1/emailvalidator?email=ada%40example.com' \
-H 'x-api-key: YOUR_API_KEY'{
"email": "[email protected]",
"domain": "example.com",
"isValid": true,
"isRegexValid": true,
"isMxValid": true,
"isDisposable": false,
"isFreeEmail": false,
"isCompanyEmail": true,
"isRoleAccount": false,
"hasTypo": false,
"suggestedCorrection": null
}Not every field deserves the same treatment. This is the part people get wrong:
| Field | What to do with it |
|---|---|
isDisposable | Hard reject. A burner domain is not a judgement call |
isMxValid | Hard reject. The domain cannot receive mail, so the account can never be verified |
isRegexValid | Hard reject. Malformed, and your own client should have caught it |
hasTypo | Suggest, never block. Offer suggestedCorrection and let them decide |
isRoleAccount | Score. support@ is a real address; it is just rarely one person |
isFreeEmail | Score, weakly. Gmail is most of the internet |
isValid is the composite. It is a good default for a lightweight check, but at registration you
usually want the individual fields, because "reject" and "ask them to confirm" are different
outcomes and the composite cannot tell you which one you are in.
Run this check before the account exists. Deleting a bad signup afterwards leaves the audit trail, the welcome email and often a Stripe customer behind it — and if your verification email is already in flight to a domain with no MX record, you have paid a bounce against your sending reputation for nothing.
suggestedCorrection and the riskScore/riskLevel pair are premium fields, so they are absent
rather than null on plans that do not include them. Check for presence, not for null — see
premium fields.
Check the origin
The IP a signup arrives from is worth knowing and rarely worth blocking on. Three endpoints answer different questions about it:
| Answers | |
|---|---|
| VPN detector | is_vpn, and is_datacenter — a signup from a server, not a laptop |
| IP blacklist lookup | isIPBlacklisted — this address has a history |
| Tor detector | isTor — an exit node |
Pick one rather than running all three on every signup; each is a credit, and their answers
overlap heavily. VPN detector is the usual choice, because is_datacenter is the single most
useful bit here: a genuine person signing up from a hosting range is unusual in a way that a
genuine person on a commercial VPN is not.
curl 'https://api.apiverve.com/v1/vpndetector?ip=203.0.113.10' \
-H 'x-api-key: YOUR_API_KEY'Score these, do not block on them. A VPN is what a careful person uses on hotel wifi. The signal is real, but it belongs in a score that decides how much friction to add — email verification before first use, a lower initial rate limit, a review queue — not in a rejection that a real customer will never write in to complain about, because they will simply leave.
The exception is is_datacenter combined with something else: a datacenter IP and a disposable
address and a signup thirty seconds after the last one is not a privacy-conscious user.
Putting it together
async function screen({ email, ip }) {
const v = await verve('emailvalidator', { email });
// Facts about the address — reject before the account exists.
if (!v.isRegexValid) return { allow: false, reason: 'invalid' };
if (!v.isMxValid) return { allow: false, reason: 'undeliverable' };
if (v.isDisposable) return { allow: false, reason: 'disposable' };
// Everything else is a score.
let risk = 0;
if (v.isRoleAccount) risk += 1;
if (v.hasTypo) risk += 1; // and surface v.suggestedCorrection in the UI
const o = await verve('vpndetector', { ip });
if (o.is_datacenter) risk += 3;
else if (o.is_vpn) risk += 1;
return { allow: true, risk, verifyFirst: risk >= 3, correction: v.suggestedCorrection };
}The structure is the point: two hard gates, then a number. The gates are things that are true about the address regardless of who is holding it. The number decides how much friction the account gets, and it is the part you tune with your own data over the following month.
Start the threshold high. A screen that blocks nothing on day one but records the score against every signup gives you a week of evidence about where your real cutoff is, and that is a far better input than a guess.
Signals for other fields
If your registration form takes more than an email address, the same shape applies to the rest of it:
- Phone numbers — phone validator returns
isValid,isVoipandisDisposableon a number. TreatisDisposablethe way you treat a disposable email; disposable phone check answers just that question for a credit less context. - Usernames — username profanity returns
isProfane, which is cheaper than maintaining your own word list and does not need updating when the internet invents a new one. - Free-text fields — a signup whose company name is keyboard mash is a signal; gibberish detector scores it.
Each is one call and each is optional. Add them when the data says you need them, not pre-emptively — every check is a credit and a few milliseconds on a path where the user is watching a spinner.
Costs and failure modes
Two calls per signup. At two credits a signup, screening is cheap next to the cost of the accounts it stops. If your form is being hammered, the rate limit and CAPTCHA in front of it are the fix, not a cheaper check — see rate limits.
Fail open, not closed. If the check errors or times out, let the signup through and flag it for review. An outage on a screening call must never become an outage on your registration form. Set a short timeout — a second or two — and treat a timeout as "unknown", not as "bad".
Cache the domain verdict. Disposable-domain status is a property of the domain, not the address, so one lookup covers every signup from that domain. An hour of caching removes most of the repeat calls on a busy form. See making requests.
Never call these from the browser. Screening from the page puts your API key in the page, and puts the decision somewhere the person being screened can change it. Both checks belong on your server. See security.
Log the verdict, not just the decision. Store the fields that produced the score. When a real customer writes in about being rejected, that record is the difference between fixing your threshold and guessing at it.
Next
Making requests covers the caching and timeout handling this leans on, and error handling covers failing open properly. Batch requests is the same screen run over a list you already have.