Sendkit TeamHow to validate email addresses before sending
Learn to validate email addresses with syntax checks, MX lookups, disposable detection, and SMTP verification to cut bounces and protect sender reputation.

Every bounced email chips away at your sender reputation. Send enough of them and mailbox providers start throttling you, routing your messages to spam, or blocking you outright. The fix is straightforward: validate email addresses before you send anything.
This guide breaks down each layer of email validation, shows you how to implement them, and walks through real code using Sendkit's email validation API.
Why validation matters
Three things happen when you send to bad addresses.
Your bounce rate climbs. ISPs track your bounce ratio. Anything above 2% is a red flag. Hit 5% consistently and you are in serious trouble. Hard bounces (permanent failures) hurt the most because they signal you are not maintaining your list.
Your sender reputation drops. Mailbox providers like Gmail and Outlook assign reputation scores to sending domains and IPs. High bounce rates, spam complaints, and hits to spam traps all drag that score down. Once it tanks, even your legitimate emails land in spam.
You waste money. Every ESP charges per email sent. Sending to addresses that will never open, click, or convert is throwing money away. If you are on a transactional email plan, bad addresses eat into your quota for no return.
Validation is not optional hygiene. It is infrastructure.
The layers of email validation
A single regex is not enough. Proper validation is a stack of checks, each catching problems the previous layer misses.
Syntax validation
The most basic check: does the address follow the rules defined in RFC 5321 and RFC 5322? You need a local part, an @ symbol, and a domain.
Most developers reach for regex here. That works for catching obvious garbage like not-an-email or user@@domain.com. But RFC-compliant email addresses are surprisingly complex. Quoted strings, comments, and internationalized domain names all make a complete regex nearly impossible to get right.
A good syntax check catches the obvious mistakes. Do not try to make it catch everything.
const isValidSyntax = (email) => {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(email);
};
// catches the basics
isValidSyntax("[email protected]"); // true
isValidSyntax("broken@@example.com"); // false
isValidSyntax("no-domain@"); // falseThis is your first filter, not your last.
DNS and MX record checks
An address can look perfectly valid syntactically and still be undeliverable. The domain might not exist, or it might not have mail exchange (MX) records configured.
An MX lookup queries DNS for the mail servers responsible for a domain. No MX records means nobody is accepting email there.
import { promises as dns } from "dns";
const hasMxRecords = async (email) => {
const domain = email.split("@")[1];
try {
const records = await dns.resolveMx(domain);
return records.length > 0;
} catch {
return false;
}
};
await hasMxRecords("[email protected]"); // true (assuming MX exists)
await hasMxRecords("[email protected]"); // falseThis catches typos in domains (gmial.com instead of gmail.com) and entirely made-up domains.
Disposable email detection
Disposable email services like Mailinator, Guerrilla Mail, and Temp Mail give users throwaway addresses. People use them to bypass signup forms, grab free trials, or avoid marketing emails.
These addresses work temporarily but become dead within hours or days. Sending to them wastes resources and skews your engagement metrics.
Detection relies on maintaining a list of known disposable email providers. There are thousands of them. Keeping that list current is a maintenance burden you probably do not want to own. This is where an API service earns its keep.
Mailbox verification (SMTP check)
This is the deepest check. An SMTP verification connects to the recipient's mail server and walks through the SMTP handshake up to the RCPT TO command without actually sending a message. If the server rejects the recipient, the mailbox does not exist.
There are caveats. Some servers accept all addresses during the handshake and bounce later (catch-all domains). Others rate-limit or block verification attempts. You should not run SMTP checks from your own infrastructure because it can get your IP blacklisted.
This check is best handled by a service that maintains a pool of IPs and understands the quirks of different mail servers.
Role-based address detection
Addresses like admin@, info@, support@, and sales@ are role-based. They typically forward to a group of people or a shared inbox. They are not inherently invalid, but they carry higher risk:
- Higher complaint rates because multiple people read the inbox
- Lower engagement because nobody feels personal ownership
- Some are used as spam traps by organizations testing sender behavior
Flagging role-based addresses lets you decide how to handle them rather than treating them like personal addresses.

Validating with Sendkit's API
You can stack all these checks yourself, or you can make one API call. Sendkit's email validation endpoint runs syntax, DNS, disposable, SMTP, and role-based checks in a single request and returns a structured result.
Single email validation
const validateEmail = async (email) => {
const response = await fetch("https://api.sendkit.com/v1/email/validate", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
return response.json();
};
const result = await validateEmail("[email protected]");
console.log(result);
// {
// "valid": true,
// "disposable": false,
// "mx_found": true,
// "role_based": false,
// "reason": "accepted_email"
// }The response tells you everything you need to make a decision. valid is the overall verdict. The other fields give you granular control over how strict you want to be.
Batch validation
Cleaning an existing list means validating thousands or millions of addresses. Doing that one at a time is slow. Batch processing lets you submit a list and get results back in bulk.
const validateBatch = async (emails) => {
const results = await Promise.allSettled(
emails.map((email) =>
fetch("https://api.sendkit.com/v1/email/validate", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
}).then((res) => res.json())
)
);
return results.map((r, i) => ({
email: emails[i],
...(r.status === "fulfilled" ? r.value : { valid: false, reason: "request_failed" }),
}));
};
const emails = [
"[email protected]",
"[email protected]",
"[email protected]",
];
const results = await validateBatch(emails);For large lists, add concurrency control. Hammering the API with 100,000 simultaneous requests is not going to end well. A simple concurrency limiter keeps things smooth:
const validateWithConcurrency = async (emails, concurrency = 10) => {
const results = [];
const queue = [...emails];
const worker = async () => {
while (queue.length > 0) {
const email = queue.shift();
const result = await validateEmail(email);
results.push({ email, ...result });
}
};
const workers = Array.from({ length: concurrency }, () => worker());
await Promise.all(workers);
return results;
};Integration in a signup form
The highest-value place to validate is at the point of collection. Catch bad addresses before they ever enter your database.
Here is a practical example for a Node.js backend handling form submissions:
import express from "express";
const app = express();
app.use(express.json());
const validateEmail = async (email) => {
const response = await fetch("https://api.sendkit.com/v1/email/validate", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SENDKIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
return response.json();
};
app.post("/signup", async (req, res) => {
const { email, name } = req.body;
const validation = await validateEmail(email);
if (!validation.valid) {
return res.status(400).json({
error: "Invalid email address",
reason: validation.reason,
});
}
if (validation.disposable) {
return res.status(400).json({
error: "Disposable email addresses are not allowed",
});
}
if (validation.role_based) {
// allow but flag for review
console.warn(`Role-based email signup: ${email}`);
}
// proceed with account creation
// ...
return res.status(201).json({ message: "Account created" });
});This catches bad addresses at the door. Users get immediate feedback. Your database stays clean.
What to do with validation results
Not every result is a binary accept/reject. Build a decision matrix:
Accept when valid is true, disposable is false, and role_based is false. This is a clean personal address. Send freely.
Reject when valid is false. The address is undeliverable. Do not send. If this is a signup form, ask the user to try a different address.
Reject when disposable is true. These addresses are temporary. For transactional apps it might not matter, but for anything requiring ongoing communication, block them.
Flag for review when role_based is true. The address works, but it carries risk. You might accept it for transactional email but exclude it from marketing campaigns.
const categorizeResult = (result) => {
if (!result.valid) return "reject";
if (result.disposable) return "reject";
if (result.role_based) return "review";
return "accept";
};Keep it simple. Three buckets. Automate the first two, manually review the third.

Real-time validation vs batch cleaning
These are not competing approaches. You need both.
Real-time validation at the point of entry (signup forms, checkout pages, contact forms) prevents bad data from entering your system. It is proactive. The latency cost is minimal since a single API call adds maybe 200-500ms, which is invisible to users when you validate on blur rather than on submit.
Batch cleaning handles the addresses already in your database. Lists decay at roughly 2-3% per month. People change jobs, abandon inboxes, and domains expire. Run batch validation quarterly at minimum, monthly if you are sending high volume.
A solid approach looks like this:
- Validate every address at collection in real time
- Re-validate your full list on a monthly schedule
- Suppress any address that fails re-validation
- Track bounce rates after sends and feed that data back into your suppression list
The two approaches reinforce each other. Real-time keeps the front door clean. Batch cleaning handles the inevitable decay.
Wrapping up
Email validation is not a nice-to-have. Every address in your list that cannot receive mail is actively damaging your deliverability. The fix is layered validation: syntax, DNS, disposable detection, SMTP verification, and role-based checks.
You can build parts of this yourself, but the SMTP verification and disposable detection layers are genuinely hard to maintain. An API handles the complexity and stays current as disposable providers multiply and mail server behaviors change.
Validate at the point of collection. Clean your existing lists regularly. Act on the results with clear rules. Your bounce rates will drop, your sender reputation will hold, and your emails will actually reach the people you are trying to reach.
Share this article