Most email-validation examples finish with code like this:
if (result.valid) {
acceptEmail();
}
That is convenient—and usually too lossy.
Syntax, DNS, SMTP and risk checks produce different kinds of evidence. A receiving server may be healthy but refuse to reveal whether a mailbox exists. A catch-all domain may accept any recipient. A role account can be valid but unsuitable for one workflow and perfect for another.
In this tutorial, we will wrap an email validation API in a stable Node.js service that returns three decisions:
allowreviewblock
The result is easier to test, safer during outages and more honest about uncertainty.
1. Define an internal contract first
Do not let your application depend directly on every field returned by an external provider. Define the output your product needs.
/**
* @typedef {Object} EmailAssessment
* @property {"allow"|"review"|"block"} decision
* @property {string} reason
* @property {string} status
* @property {boolean} checked
* @property {string} checkedAt
* @property {object=} evidence
*/
The review state prevents two common bugs:
- treating an inconclusive SMTP result as proof that an address is bad;
- treating a catch-all domain as proof that a specific mailbox exists.
2. Call the API with a deadline
Node.js provides a global fetch() in current releases. Keep the RapidAPI key in an environment variable and copy the exact host from the marketplace snippet.
const RAPIDAPI_HOST = "email-verify-api1.p.rapidapi.com";
async function requestVerification(email, mode = "power") {
const response = await fetch(
`https://${RAPIDAPI_HOST}/api/v1/verify`,
{
method: "POST",
headers: {
"content-type": "application/json",
"x-rapidapi-host": RAPIDAPI_HOST,
"x-rapidapi-key": process.env.RAPIDAPI_KEY,
},
body: JSON.stringify({ email, mode }),
signal: AbortSignal.timeout(mode === "quick" ? 5_000 : 35_000),
},
);
const body = await response.json();
if (!response.ok) {
const error = new Error(body.error ?? "Email verification failed");
error.status = response.status;
throw error;
}
return body.data;
}
RapidAPI expects both authentication headers. The timeout is intentionally longer for a deep check because remote mail servers control part of its latency.
3. Normalize provider evidence
A Power-mode response can expose syntax, MX, SMTP and risk fields. We only select the fields our policy understands.
function normalizeEvidence(raw = {}) {
return {
status: String(raw.status ?? "unknown").toLowerCase(),
score: Number.isFinite(raw.overall_score)
? raw.overall_score
: null,
safeToSend: raw.is_safe_to_send === true,
syntaxValid:
raw.is_valid_syntax === true
? true
: raw.is_valid_syntax === false
? false
: null,
mxAcceptsMail: raw.mx_accepts_mail === true,
smtpConnected: raw.can_connect_smtp === true,
deliverable: raw.is_deliverable === true,
disposable: raw.is_disposable === true,
roleAccount: raw.is_role_account === true,
catchAll: raw.is_catch_all === true,
inboxFull: raw.has_inbox_full === true,
disabled: raw.is_disabled === true,
spamtrap: raw.is_spamtrap === true,
};
}
Notice that missing Boolean values do not automatically become positive evidence. An absent is_catch_all is different from a confirmed false in some integrations, so tighten this normalization if your application must preserve null explicitly.
4. Apply a policy—not a score threshold
Scores are helpful summaries, but hard-coding score > 70 hides the reason behind a decision. Start with explicit high-risk signals.
function decide(evidence) {
const blockingStatuses = new Set([
"invalid",
"disabled",
"disposable",
"spamtrap",
]);
if (
evidence.syntaxValid === false ||
evidence.disposable ||
evidence.disabled ||
evidence.spamtrap ||
blockingStatuses.has(evidence.status)
) {
return {
decision: "block",
reason: `high-risk result: ${evidence.status}`,
};
}
if (
evidence.status === "safe" &&
evidence.safeToSend &&
!evidence.catchAll
) {
return {
decision: "allow",
reason: "positive mailbox and risk signals",
};
}
return {
decision: "review",
reason: `inconclusive or contextual result: ${evidence.status}`,
};
}
Role accounts intentionally fall into context-dependent handling. A SaaS signup may allow them. A consumer rewards program may review them. A support-ticket system should probably expect them.
5. Fail into review during an outage
If the verification service times out, the address does not suddenly become invalid.
export async function assessEmail(email, { mode = "quick" } = {}) {
const checkedAt = new Date().toISOString();
try {
const raw = await requestVerification(email, mode);
const evidence = normalizeEvidence(raw);
return {
checked: true,
checkedAt,
status: evidence.status,
evidence,
...decide(evidence),
};
} catch (error) {
return {
checked: false,
checkedAt,
status: "unavailable",
decision: "review",
reason: "verification service unavailable",
};
}
}
Whether review means “allow signup but require confirmation” or “hold the CRM record” belongs to the calling application.
6. Test policy branches without calling the network
The most valuable tests exercise your decisions, not a provider’s live score.
import test from "node:test";
import assert from "node:assert/strict";
test("safe non-catch-all address is allowed", () => {
const evidence = normalizeEvidence({
status: "safe",
is_safe_to_send: true,
is_valid_syntax: true,
is_catch_all: false,
});
assert.equal(decide(evidence).decision, "allow");
});
test("unknown address is reviewed, not blocked", () => {
const evidence = normalizeEvidence({ status: "unknown" });
assert.equal(decide(evidence).decision, "review");
});
test("disposable address is blocked by this policy", () => {
const evidence = normalizeEvidence({
status: "disposable",
is_disposable: true,
});
assert.equal(decide(evidence).decision, "block");
});
Add cases for catch_all, role_account, inbox_full, provider timeouts and malformed input.
Quick versus power mode
Use quick mode for latency-sensitive checks. It can evaluate syntax, disposable status, domain/MX readiness and other fast signals, but it does not prove that an individual inbox exists.
Use power mode when deeper SMTP/mailbox evidence justifies a slower response. Even then, preserve unknown: providers may restrict address probing, defer connections or return insufficient evidence.
For signup, a good pattern is quick verification plus a confirmation email. For CRM cleanup, use deeper verification asynchronously.
Production guardrails
Before shipping, add:
- rate limits to prevent address enumeration;
- redacted or hashed email logs;
- explicit retry rules for
429and transient5xxresponses; - latency and status-distribution metrics;
- short, policy-appropriate cache windows;
- suppression-list checks that verification cannot override;
- a clear retention policy for personal data.
Remember: reachability is not consent, and verification is not an inbox-placement guarantee.
Try it with controlled addresses
The Email Verification API on RapidAPI exposes quick and power checks plus an asynchronous bulk workflow. The Basic plan includes 50 requests per month.
I also published a longer implementation guide on StadiaSoft covering bulk polling, failure handling and deliverability boundaries.
Test with addresses you control, inspect every field, and change the policy to match your use case before it touches production traffic.
Disclosure: I publish the RapidAPI product referenced in this tutorial. The code and decision model are provider-aware but intentionally preserve the limitations of SMTP verification.
Top comments (0)