Replacing A Third-party Email Verification API With Something I Actually Own


For a while now I’ve been using a third party service to validate email addresses on my store’s checkout. It works, or it did, but I started getting annoyed at the dependency. Paying a monthly fee for an API call that checks whether an email looks real felt like something I could just handle myself. So I went down that rabbit hole.

The Problem Worth Solving

Bad emails at checkout are more of a problem than they seem. Someone puts in a typo, you send them a receipt they never get, and suddenly you’ve got a support ticket about a missing order. Worse are the disposable inboxes, addresses that exist for five minutes and then vanish. And then there’s the role addresses, things like info@ or noreply@, which are technically valid but almost never belong to a real customer.

The service being used catches a lot of this but it’s a black box. I couldn’t fully tweak how it scores things, I couldn’t always see why it was rejecting something, and I had no control over what happened if it went down.

Building It

The core of the API is a pipeline where each check only runs if the previous one passed.

First it validates the format with a regex. Nothing fancy, just enough to catch the obvious stuff. Then it does an MX lookup to confirm the domain actually has mail servers set up. If there are no MX records, there’s nowhere to deliver to, so we stop there.

If the MX lookup passes, it does an SMTP ping. It connects to the mail server, says hello, and asks “does this address exist?” via a RCPT TO command. The server either accepts it, rejects it, or gives a temporary response (greylisting). The temporary ones get retried a couple of times before being marked inconclusive.

On top of that it checks the domain against a public disposable email blocklist, flags role-based local parts, and runs a catch-all probe to detect domains that just accept everything regardless of whether the mailbox actually exists.

Everything rolls up into a confidence score between 0.0 and 1.0.

The Bits That Were Interesting

A couple of things caught me off guard while building this.

Null MX records. Some domains have an MX record that points to . (this is RFC 7505, a way of explicitly saying “this domain doesn’t accept mail”). The DNS lookup was returning a result, so mx_found was coming back true, but the hostname was an empty string. When the SMTP code then tried to connect to that empty string it got a Name or service not known error and marked the result as inconclusive. That’s wrong. The domain deliberately doesn’t receive mail. The fix was just filtering out empty hostnames after stripping the trailing dot.

Sender callout verification. The original code sent MAIL FROM: probe@[domain] when doing the SMTP check. A bunch of servers, mostly cPanel/Exim setups, don’t just accept that. They do a reverse check: they call back to [domain]'s MX and ask “does probe@[domain] actually exist?” It doesn’t, so they reject the whole session with a 550. The address being checked might be perfectly valid but it comes back as rejected because of the fake sender. Switching to an empty MAIL FROM:<> fixed it. Empty senders are used for bounce messages and servers can’t callout-verify them.

The scoring didn’t add up to 1.0. The original weights were 20% format, 30% MX, 40% SMTP, which only adds to 0.9. A perfect address would never actually score 1.0. I bumped SMTP accepted up to 50% and adjusted inconclusive to 25%, so a clean result now correctly hits 1.0. I also added a small penalty for explicit SMTP rejections, which previously scored the same as a result where SMTP was simply skipped.

Where It’s At

The API runs on FastAPI, handles concurrent requests without any issues, and auto-refreshes the disposable domain blocklist every 24 hours in the background. The /health endpoint now returns actual useful information: DNS check, blocklist staleness, uptime, whether the API key is configured. Easy to wire into an uptime monitor.

Email health

It fails open if the API is unreachable so an outage doesn’t block purchases.

It’s not a particularly complicated project but it was a good one. I learned a few things about SMTP I didn’t know before and I’ve got something I can actually trust and adjust. Sometimes that’s reason enough to build it yourself.