How to Block Disposable Emails at Signup (Server-Side)
by Francis Baker
Loading article…
Share this article
Enjoyed this article?
More notes on building products, infrastructure, and teams.
by Francis Baker
Loading article…
More notes on building products, infrastructure, and teams.
To block disposable emails at signup, validate the address on your server when the form is submitted, call an email validation API that returns a disposable flag, and reject the signup when that flag is true. Run the check after syntax validation and before you create the user record. Always enforce the rule server-side; a browser-only check can be bypassed.
This guide walks through a production-ready pattern using EmailGuard's detect API. The same structure applies if you use another provider, as long as the response exposes disposable status clearly.
By the end, you will:
Prerequisites: A signup endpoint you control, an API key with the email:detect scope, and the ability to deploy a small HTTP client call in your backend. Staging environment recommended before you enforce blocks in production.
The simplest approach is a GitHub blocklist imported into a hash set. That works until it does not.
Community-maintained lists cover a large share of known throwaway domains, but they are reactive. A domain must be discovered, reported, and merged before your app rejects it. Disposable providers rotate domains and subdomains to stay ahead of static lists. Tutorials from Fidro and Tickstem both note that lists decay within weeks if they are not updated continuously.
An API-backed checker maintains the list for you and can add signals beyond domain string matching, such as MX clustering and live DNS lookups for domains that never appeared on a public list. Prospeo's 2026 disposable detection overview estimates static lists catch roughly 60–80% of throwaway addresses in the wild, with the gap filled by infrastructure analysis.
For signup flows, that gap matters. One bad row in your users table can pollute analytics, waste verification emails, and invite abuse.
Run disposable detection on your server at the moment you handle signup, after you parse the email field and before you insert a user row.
Optional: add a lightweight check when the user leaves the email field (on blur) for faster feedback. That improves UX, but it is not security. Attackers can POST directly to your API or disable JavaScript. Treat blur validation as a hint; treat server validation as the rule.
Recommended order inside your handler:
EmailGuard exposes a single GET endpoint:
GET /api/v1/emails/[email protected]
Authenticate with a team API key that includes the email:detect scope. The response includes syntax validation, normalization, and boolean flags for disposable, role, and relay domains. See the full detect email characteristics reference for every field.
A successful response wraps the result in the standard API envelope. The fields you care about for signup blocking:
| Field | Use at signup |
|---|---|
syntax_validation | Reject malformed addresses before anything else |
disposable | Block when true |
disposable_provider | Log or display in admin tooling |
detection_source | precomputed (curated domain data) or live_dns (live DNS lookup) |
relay_domain | Usually flag, not block (see below) |
role_address | Optional: block, flag, or route to sales ops |
const EMAILGUARD_API = "https://emailguard.co/api/v1/emails/detect";
async function detectEmail(email, apiKey) {
const url = new URL(EMAILGUARD_API);
url.searchParams.set("email", email);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
throw new Error(`detect failed: ${res.status}`);
}
const body = await res.json();
return body.data; // EmailDetectResult
}
app.post("/signup", async (req, res) => {
const { email, password } = req.body;
let result;
try {
result = await detectEmail(email, process.env.EMAILGUARD_API_KEY);
} catch (err) {
// Fail open: log and allow signup (see Step 3)
console.error("email detect unavailable", err);
result = null;
}
if (result && !result.syntax_validation) {
return res.status(422).json({
error: "Enter a valid email address.",
});
}
if (result?.disposable) {
return res.status(422).json({
error: "Use a permanent email address. Temporary inboxes are not allowed.",
});
}
// create user...
});TypeScript SDK users can follow the SDK getting started guide instead of raw fetch.
type detectResponse struct {
Data struct {
SyntaxValidation bool `json:"syntax_validation"`
Disposable bool `json:"disposable"`
DetectionSource string `json:"detection_source"`
} `json:"data"`
}
func detectEmail(ctx context.Context, email, apiKey string) (*detectResponse, error) {
req, _ := http.NewRequestWithContext(ctx, "GET",
"https://emailguard.co/api/v1/emails/detect?email="+url.QueryEscape(email), nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out detectResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return &out, nil
}Keep the API key on the server. Never embed it in frontend bundles or mobile apps without a backend proxy.
Generic "invalid email" messages frustrate legitimate users and encourage retries with another throwaway domain.
When disposable is true, say what went wrong and what to do next:
When syntax fails, suggest fixing typos. If your provider returns a normalized form, you can offer "Did you mean …?" for common Gmail dot variants.
Log disposable_provider and detection_source server-side. If support tickets spike after a deploy, you can tell whether live DNS started flagging domains your curated data had not yet classified.
Validation adds a network hop. If EmailGuard or your network path is slow or unavailable, you have two choices:
Most B2B products fail open for detection timeouts and fail closed only when the API explicitly returns disposable: true. Document the choice for your team. If you process high-value payments at signup, lean fail closed; if you optimize for top-of-funnel volume, lean fail open with later cleanup.
Set a short timeout (200–500 ms) on the detect call so registration does not hang. Rate limits apply per API key; cache results by domain in Redis if the same domain appears in bursts.
Monitor detection_source in your logs after launch. A sudden rise in live_dns hits often means disposable operators rotated to domains EmailGuard has not yet ingested into curated data. That is expected behavior, not a bug in your integration.
Disposable detection is not the only useful signal in the same response.
Relay domains (relay_domain: true) include privacy forwarding services such as Apple Hide My Email. These are not throwaway inboxes. Hard-blocking them rejects privacy-conscious customers. Common policy: block disposable, flag relay_domain for sales scoring, and allow signup.
Role addresses (role_address: true) like info@ or support@ are valid mailboxes but poor fit for product trials. Block them for B2B SaaS trials; allow them for contact forms.
EmailGuard returns all flags in one request so you can branch without chaining three vendors. The features overview lists every signal the API checks.
Before shipping:
detection_source: live_dns versus precomputed to confirm both paths work.Add an integration test that mocks the detect response with disposable: true and expects HTTP 422.
Client-only validation. Browser checks improve UX; they do not stop abuse.
Blocking relay domains like disposables. You will lose valid users who use privacy relays.
Matching the full hostname only. Disposable domains often use subdomains. Match on the registrable domain (eTLD+1), not the full string after @. Your API provider should handle public suffix rules; if you roll your own list, use a proper suffix library.
Caching forever. Domain status changes. Cache per domain for minutes or hours, not weeks.
Silent failures. If detection fails open, log it. Otherwise abuse spikes with no obvious cause.
Both, if you can. Signup blocking stops bad data at the source. Periodic CRM sweeps catch addresses that slipped through before you added detection or during API outages.
Target under 200 ms added latency at p95. Users abandon slow registration flows. Use a timeout and fail open if you miss that budget.
You can for a prototype. For production, plan on continuous updates and DNS-level signals. Static lists alone leave a predictable gap that rotated disposable domains exploit.
No. EmailGuard analyzes syntax, domain characteristics, and disposable/relay/role signals. SMTP mailbox verification is a different product category. For signup abuse, disposable and role detection usually matter more than mailbox existence pings.
Any plan with API access and the email:detect scope works. See pricing for volume tiers. Start with a free account, create a key under Team settings, and test against staging before you enforce blocks in production.
Next steps: Create an API key, run a detect call against a throwaway domain, and wire the disposable branch into your signup handler. When you want volume without signup integration, use the bulk validation tools or raise limits on your pricing plan.