Skip to content

Someone signs up as anna@gmial.com. The address looks fine, your welcome email bounces, and Anna never hears from you. One DNS lookup on the part after the @ catches that before you send anything. Here's how to do it with the DNS Lookup API, and the three answers that mean "don't send".

The lookup

Ask for the MX records of the domain:

curl -G "https://apixies.io/api/v1/dns-lookup" \
     -H "X-API-Key: YOUR_API_KEY" \
     --data-urlencode "domain=gmail.com" \
     --data-urlencode "type=MX"
{
  "status": "success",
  "data": {
    "domain": "gmail.com",
    "records": [
      { "type": "MX", "host": "gmail.com", "ttl": 2648, "priority": 5, "target": "gmail-smtp-in.l.google.com" },
      { "type": "MX", "host": "gmail.com", "ttl": 2648, "priority": 10, "target": "alt1.gmail-smtp-in.l.google.com" },
      { "type": "MX", "host": "gmail.com", "ttl": 2648, "priority": 20, "target": "alt2.gmail-smtp-in.l.google.com" },
      { "type": "MX", "host": "gmail.com", "ttl": 2648, "priority": 30, "target": "alt3.gmail-smtp-in.l.google.com" },
      { "type": "MX", "host": "gmail.com", "ttl": 2648, "priority": 40, "target": "alt4.gmail-smtp-in.l.google.com" }
    ],
    "record_count": 5
  }
}

Five mail servers. A sender tries the lowest priority first, so gmail-smtp-in.l.google.com (5) gets the mail and the alt hosts are backups. MX records come back in that order, lowest number first.

The target also tells you who runs the mail. outlook.com points at outlook-com.olc.protection.outlook.com, proton.me at mail.protonmail.ch. A company domain whose MX ends in google.com or outlook.com is on Google Workspace or Microsoft 365.

Three answers that mean "don't send"

No records. The typo from the top:

{ "domain": "gmial.com", "records": [], "record_count": 0 }

A domain that was never registered looks the same. It's still a 200 with "status": "success", because "nothing there" is a valid DNS answer. Check record_count, not the HTTP status.

A null MX. This one is easy to miss. Look up example.com:

{
  "domain": "example.com",
  "records": [
    { "type": "MX", "host": "example.com", "ttl": 300, "priority": 0, "target": "" }
  ],
  "record_count": 1
}

One record, so a check for "has at least one MX" says yes. But the target is empty. That's a null MX (RFC 7505), and it's the domain owner saying "we don't accept mail, don't try". Treat an empty target as no mail server.

An error. A 504 with code UPSTREAM_TIMEOUT means the resolver didn't answer in time. That says nothing about the domain. Don't reject a signup because of it. Let the person through and check again later.

What about domains with no MX at all?

Here it gets a bit grey. The mail standard (RFC 5321) says that if a domain has no MX record, the sender should fall back to its A record and try to deliver there. And gmial.com does have one:

{ "type": "A", "host": "gmial.com", "ttl": 1, "ip": "51.79.68.169" }

So a very patient mail server might try that address. In practice nobody runs real mail this way any more, and typo domains like this one are often parked. I treat "no MX" as "can't get mail" and ask the person to check their address. That's a nudge, not a block. Let them continue if they insist.

In code

Each version returns one of three words. ok means there's a real mail server. no_mail means there isn't. unknown means the check itself failed, and you should let the address pass.

JavaScript

async function mailStatus(email) {
  const domain = email.split("@").pop().trim().toLowerCase();
  if (!domain || !domain.includes(".")) return "no_mail";

  try {
    const params = new URLSearchParams({ domain, type: "MX" });
    const res = await fetch(`https://apixies.io/api/v1/dns-lookup?${params}`, {
      headers: { "X-API-Key": process.env.APIXIES_API_KEY },
    });
    const body = await res.json();

    if (res.status === 422) return "no_mail"; // not a valid domain name
    if (body.status !== "success") return "unknown";

    const servers = body.data.records.filter((r) => r.type === "MX" && r.target !== "");
    return servers.length > 0 ? "ok" : "no_mail";
  } catch {
    return "unknown";
  }
}

console.log(await mailStatus("anna@gmail.com"));   // ok
console.log(await mailStatus("anna@gmial.com"));   // no_mail
console.log(await mailStatus("anna@example.com")); // no_mail (null MX)

Python

import os
import requests

def mail_status(email):
    domain = email.rsplit("@", 1)[-1].strip().lower()
    if "." not in domain:
        return "no_mail"

    try:
        res = requests.get(
            "https://apixies.io/api/v1/dns-lookup",
            params={"domain": domain, "type": "MX"},
            headers={"X-API-Key": os.environ["APIXIES_API_KEY"]},
            timeout=15,
        )
        body = res.json()
    except (requests.RequestException, ValueError):
        return "unknown"

    if res.status_code == 422:
        return "no_mail"  # not a valid domain name
    if body.get("status") != "success":
        return "unknown"

    servers = [r for r in body["data"]["records"] if r["type"] == "MX" and r["target"]]
    return "ok" if servers else "no_mail"

PHP

function mailStatus(string $email): string
{
    $domain = strtolower(trim(substr(strrchr($email, '@') ?: '', 1)));
    if (! str_contains($domain, '.')) {
        return 'no_mail';
    }

    $context = stream_context_create(['http' => [
        'header' => 'X-API-Key: ' . getenv('APIXIES_API_KEY'),
        'ignore_errors' => true,
        'timeout' => 15,
    ]]);
    $query = http_build_query(['domain' => $domain, 'type' => 'MX']);
    $raw = @file_get_contents("https://apixies.io/api/v1/dns-lookup?$query", false, $context);
    $body = json_decode((string) $raw, true);

    if (($body['http_code'] ?? 0) === 422) {
        return 'no_mail'; // not a valid domain name
    }
    if (($body['status'] ?? '') !== 'success') {
        return 'unknown';
    }

    $servers = array_filter($body['data']['records'], fn ($r) => $r['type'] === 'MX' && $r['target'] !== '');

    return $servers ? 'ok' : 'no_mail';
}

Where to run it

Signup and contact forms. Check when the form is submitted, and show a "did you mean?" hint on no_mail. Keep the verdict per domain in a cache for a day. Most of your signups come from the same handful of mail providers, so after the first gmail.com you won't look it up again.

List imports. Pull out the distinct domains first. A list has far fewer domains than addresses, and the number of domains is the number of lookups you need.

The free tier is 75 requests a day. With a per-domain cache that goes a long way for a signup form. It won't clean a big list in one day, so spread an import over a few days or check only the domains you haven't seen before.

SPF and DMARC

MX says a domain can get mail. SPF and DMARC say how seriously it takes sending. SPF is a TXT record on the domain itself, so the same endpoint finds it:

curl -G "https://apixies.io/api/v1/dns-lookup" \
     -H "X-API-Key: YOUR_API_KEY" \
     --data-urlencode "domain=gmail.com" \
     --data-urlencode "type=TXT"
{ "type": "TXT", "host": "gmail.com", "ttl": 300, "txt": "v=spf1 redirect=_spf.google.com" }

DMARC lives at _dmarc.gmail.com. You can look that name up as a TXT record too, but then you're parsing policy strings yourself. The email authentication endpoint looks in the right places and reads the records for you:

curl -G "https://apixies.io/api/v1/email-auth" \
     -H "X-API-Key: YOUR_API_KEY" \
     --data-urlencode "domain=gmail.com"
"spf": { "found": true, "valid": true, "record": "v=spf1 redirect=_spf.google.com" },
"dmarc": {
  "found": true,
  "record": "v=DMARC1; p=none; sp=quarantine; rua=mailto:mailauth-reports@google.com",
  "parsed": { "v": "DMARC1", "p": "none", "sp": "quarantine" }
}

What this doesn't tell you

An MX record means the domain has a mail server. It doesn't mean anna has a mailbox on it. nobody-here-12345@gmail.com passes this check. To know if one address is real you have to send it a confirmation mail, and nothing replaces that.

For more signals on a single address (disposable providers, role addresses like info@, syntax), the Email Inspector does the MX check and those in one call. It also guesses the typo. For anna@gmial.com it returns "mx_records_found": false and "suggestion": "anna@gmail.com", which is the "did you mean?" hint ready to show.

Next steps

Try the DNS Lookup API

Free tier is for development & small projects. 75 requests/day with a registered account.

cookies

We use analytics cookies to see how the site gets used. Nothing loads until you accept. Privacy policy