If you finished the Fundamentals level you have probably already built a signup gate that blocks an address when isValid is false or isDisposable is true. That gate is fine for a free tier. It is not enough for a sales form that routes leads to humans, a marketplace that pays sellers, or a newsletter whose sender reputation you care about. Those flows need to know why an address looks bad, because the right response differs: a typo deserves a hint, a role mailbox deserves a human, a domain that refuses mail deserves a hard stop.
This lesson takes the Email Validator response apart field by field, shows you real responses where the fields disagree with intuition, and ends with a decision function you can drop into any backend.
What one call actually returns
Start with an address that exercises most of the response: a Gmail address with dots, mixed case and a +tag.
curl "https://api.apiverve.com/v1/emailvalidator?email=Jane.Doe%2Bnews%40gmail.com" \
-H "x-api-key: YOUR_API_KEY"Look at isSubaddressed and normalizedEmail: the address you will store and the address you dedupe on are different strings.
The response comes back lowercased ([email protected]) and carries roughly twenty fields. They fall into four groups, and it helps to think of them that way rather than as a flat list.
| Group | Fields | Question it answers |
|---|---|---|
| Shape | isRegexValid, hasTypo, username, domain | Is this string an email address at all? |
| Deliverability | isMxValid, mxRecord, isNullMx, canReceiveMail | Can mail for this domain be delivered anywhere? |
| Domain posture | hasSpf, hasDmarc, isFreeEmail, isCompanyEmail, isDisposable | What kind of domain is it, and is it run properly? |
| Mailbox | isRoleAccount, isSubaddressed, normalizedEmail | What kind of mailbox is this, and have we seen it before? |
isValid is the roll-up. In every response used for this lesson it matched isRegexValid && isMxValid: the shape is acceptable and the domain has MX records. It does not consider disposability, role accounts or anything in the mailbox group. Treat it as a deliverability summary, not a verdict.
Three fields are premium: suggestedCorrection, riskScore and riskLevel. They are useful when your plan includes them, and your code must keep working when they are absent. Every other field in the table is on every plan. The call costs 20 credits, which matters later when you decide what to call and in what order.
Deliverability: MX, null MX and the A-record fallback
Mail for a domain is routed by its MX records. No MX records usually means mail cannot be delivered, but "usually" hides two special cases that the response separates for you.
Null MX. RFC 7505 lets a domain publish a single MX record pointing at . to declare "this domain accepts no mail, do not try." example.com does exactly that:
curl "https://api.apiverve.com/v1/emailvalidator?email=user%40example.com" \
-H "x-api-key: YOUR_API_KEY"isNullMx is true and canReceiveMail is false. This is the one deliverability answer that is a deliberate no, not a missing record.
A null MX is the strongest reject signal in the response. The domain owner has told the world nobody reads mail there. A person typing such an address is either testing your form or hiding.
The A-record fallback. RFC 5321 says that when a domain has no MX record at all, a sender may deliver to the domain's A record instead. isMxValid ignores that fallback; canReceiveMail follows it. The difference shows up on parked and misspelled domains. Validate [email protected] and you get isMxValid: false, mxRecord: null, and canReceiveMail: true, because the typo domain has a web server but no mail exchanger. Technically a mail server could try delivery there. In practice that address is a typo, and the person who owns gmial.com is not your customer.
The rule that falls out: isMxValid: false is a reject or review signal even when canReceiveMail is true. Use canReceiveMail only to separate "can never work" (null MX, nothing resolvable) from "probably misconfigured".
Looking at the records directly
When you need the actual records, for example to show a support agent which provider hosts a customer's mail, MX Lookup returns them for a bare domain at 10 credits a call.
curl "https://api.apiverve.com/v1/mxlookup?domain=gmail.com" \
-H "x-api-key: YOUR_API_KEY"The records arrive in no particular order. The lowest priority number is the preferred server.
Two details catch people. First, the mx array is not sorted: for gmail.com the priority-5 record is last. Sort it yourself before you call the first entry "primary". Second, a domain with no usable MX records does not return an empty array. mxlookup on example.com returns HTTP 404 with the error no mx records found for domain. That is the "no result for your input" kind of 404, not an unknown API, so your client must treat it as an answer rather than a failure to retry.
const BASE = 'https://api.apiverve.com/v1';
export async function mxRecords(domain) {
const res = await fetch(`${BASE}/mxlookup?${new URLSearchParams({ domain })}`, {
headers: { 'x-api-key': process.env.APIVERVE_API_KEY },
signal: AbortSignal.timeout(3000),
});
const body = await res.json().catch(() => null);
if (res.status === 404) return []; // no MX records: an answer, not an outage
if (!res.ok || body?.status !== 'ok') throw new Error(body?.error || `HTTP ${res.status}`);
return [...body.data.mx].sort((a, b) => a.priority - b.priority);
}You rarely need both mxlookup and emailvalidator for the same address, because the validator already reports the preferred exchanger as mxRecord. Reach for mxlookup when you are auditing domains rather than addresses, such as checking a list of B2B customer domains after a migration.
An address returns isMxValid false, mxRecord null, isNullMx false and canReceiveMail true. What is the most accurate reading?
Answer the check above to continue.
Turning signals into accept, review or reject
Now combine everything. The function below takes the data object and returns a decision plus reasons, because a decision without reasons is undebuggable the first time a customer complains. It treats premium fields as optional and null lookups as unknown.
export function decideEmail(d) {
const reasons = [];
if (d.suggestedCorrection) return { decision: 'review', reasons: ['typo'], hint: d.suggestedCorrection };
if (d.isNullMx) reasons.push('null_mx');
if (!d.isRegexValid) reasons.push('bad_format');
if (d.isDisposable) reasons.push('disposable');
if (!d.isMxValid) reasons.push('no_mx');
if (reasons.length) return { decision: 'reject', reasons };
if (d.isRoleAccount) reasons.push('role_account');
if (d.isCompanyEmail && d.hasSpf === false && d.hasDmarc === false) reasons.push('unconfigured_domain');
if (d.riskLevel === 'high') reasons.push('risk_high'); // premium, absent on some plans
return { decision: reasons.length ? 'review' : 'accept', reasons, normalized: d.normalizedEmail };
}def decide_email(d: dict) -> dict:
if d.get("suggestedCorrection"):
return {"decision": "review", "reasons": ["typo"], "hint": d["suggestedCorrection"]}
reasons = []
if d.get("isNullMx"):
reasons.append("null_mx")
if not d.get("isRegexValid"):
reasons.append("bad_format")
if d.get("isDisposable"):
reasons.append("disposable")
if not d.get("isMxValid"):
reasons.append("no_mx")
if reasons:
return {"decision": "reject", "reasons": reasons}
if d.get("isRoleAccount"):
reasons.append("role_account")
if d.get("isCompanyEmail") and d.get("hasSpf") is False and d.get("hasDmarc") is False:
reasons.append("unconfigured_domain")
if d.get("riskLevel") == "high": # premium, absent on some plans
reasons.append("risk_high")
return {"decision": "review" if reasons else "accept", "reasons": reasons,
"normalized": d.get("normalizedEmail")}Run the responses from this lesson through it and you get: [email protected] accept, normalised to [email protected]; [email protected] reject for null_mx and no_mx; [email protected] review for role_account; [email protected] review with a hint on premium plans, and reject for disposable and no_mx on plans without suggestedCorrection.
That last case is a real tradeoff. Without the premium hint, the typo is indistinguishable from a junk domain, and the person is rejected. If typo signups matter to you, either add your own short list of common misspellings of the top providers, or make "reject" show an inline message ("we can't deliver to gmial.com, check the spelling") rather than a dead end. A rejection message that names the domain fixes most typos for free.
Your plan does not include premium fields. A visitor types [email protected]. With the decide function above, what happens and what is the cheapest improvement?
Next
Email is one channel. The next lesson does the same for phone numbers, where the stakes are higher because every SMS you send costs money and some numbers exist only to receive verification codes. Continue to Validate phone numbers before you send an SMS. If you need to re-screen an existing user table, the same endpoint accepts up to 200 addresses per call through batch requests.