← Back to Blog

How to Verify Email Addresses with an API — Developer Guide

Sending emails to invalid addresses is expensive. Not in a vague, hand-wavy way — it directly damages your sender reputation, increases bounce rates, and can get your domain blacklisted by major email providers. Past a 2% bounce rate — the ceiling most email service providers hold senders to — your emails start landing in spam. Above 5%, you're looking at deliverability issues that take weeks to recover from.

Email verification solves this by checking whether an address is valid and deliverable before you send anything. This guide shows you how to integrate email verification into your application using an API.

What Email Verification Actually Checks

A proper verification goes through multiple layers:

  1. Syntax validation — Is the format valid? Does it conform to RFC 5321? This catches typos like user@gmailcom or user@@gmail.com.

  2. DNS MX record lookup — Does the domain have mail servers configured? If example.xyz has no MX records, no email is getting delivered there.

  3. SMTP handshake — Connect to the mail server, start a conversation, and ask "does this mailbox exist?" — all without actually sending an email. This is the most valuable check and the one most developers skip when building their own validation.

  4. Disposable email detection — Is this a throwaway address from services like Guerrilla Mail, Temp Mail, or Mailinator? These addresses work for 10 minutes and then disappear.

  5. Role-based detection — Addresses like admin@, info@, support@ are shared inboxes. They're valid but often have higher bounce rates and lower engagement.

  6. Catch-all detection — Some mail servers accept all addresses regardless of whether the mailbox exists. A catch-all server will say "yes" to anything@domain.com, making individual mailbox verification impossible.

Building all of this yourself means maintaining disposable domain lists, handling SMTP timeouts and greylisting, dealing with rate limiting from mail servers, and keeping up with constantly changing email infrastructure. Using an API handles all of that.

Getting Started with MailProbe

1. Create an Account

Sign up at mailprobe.dev/login. You get 100 free credits on signup — no credit card required. Each verification costs 1 credit.

2. Get Your API Key

After signing in, create a key under Developer in your account. You can hold several — one per environment or integration — and they all draw on the same credits. A key looks like mp_live_xxxxxxxxxxxxxxxxxxxx and is shown once, at creation: we only store its hash, so it cannot be displayed again. Copy it right away and keep it secure — it authenticates all your API requests. If you lose it, delete the key and create another.

3. Make Your First Request

Verify a single email — here, a Gmail mailbox that does not exist:

curl -X POST https://mailprobe.dev/api/v1/verify \
  -H "Authorization: Bearer mp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["zzq-no-such-mailbox-4821@gmail.com"]}'

The response is always an array, even for a single email:

[
  {
    "email": "zzq-no-such-mailbox-4821@gmail.com",
    "result": "undeliverable",
    "status": "invalid",
    "score": 0,
    "catch_all": false,
    "reason": null,
    "retry_after": null,
    "smtp_code": 550,
    "smtp_enhanced_code": "5.1.1",
    "syntax": true,
    "mx_found": true,
    "smtp_check": true,
    "disposable": false,
    "role_based": false,
    "free_provider": true,
    "did_you_mean": null,
    "mx_records": [
      "gmail-smtp-in.l.google.com",
      "alt1.gmail-smtp-in.l.google.com",
      "alt2.gmail-smtp-in.l.google.com",
      "alt3.gmail-smtp-in.l.google.com",
      "alt4.gmail-smtp-in.l.google.com"
    ]
  }
]

Every field is useful. result is the raw SMTP verdict; status is the actionable rollup you filter on; score is a 0–100 confidence value. The individual boolean fields let you build custom rules.

API Reference

Verify Emails

Endpoint: POST /api/v1/verify

Headers:

  • Authorization: Bearer YOUR_API_KEY
  • Content-Type: application/json

Body:

{
  "emails": ["user@example.com", "another@domain.com"]
}

You can send up to 20 emails per request. Each email costs 1 credit.

Response: An array of verification results, one per email, in the same order as the input.

Check Credits

Endpoint: GET /api/v1/credits

Headers:

  • Authorization: Bearer YOUR_API_KEY

Response:

{
  "credits": 847
}

Understanding the Response

The result Field

The raw technical verdict from the verification pipeline.

  • deliverable — SMTP confirmed the mailbox exists
  • undeliverable — SMTP rejected the address
  • catch-all — Server accepts all addresses (can't confirm individually)
  • unknown — Could not determine (timeout, connection refused, greylisting, sender rejected by policy)

The status Field

The actionable classification most integrations filter on.

  • valid — Deliverable and clean — safe to send
  • invalid — Bad syntax, no MX, or the mailbox rejects — do not send
  • risky — Accepted but uncertain: catch-all, role-based, disposable, or a provider that blocks probing (Microsoft consumer domains — outlook.com, hotmail.com, live.com, msn.com)
  • unknown — Could not be determined by our probe: either transient (greylisting, deferred, timeout, connection refused, unresolved MX — worth a retry) or a permanent refusal of our probe (policy_rejected, no_banner, ehlo_rejected, mail_from_rejected) that will recur on retry

Filtering out risky discards deliverable Microsoft mailboxes — score them instead.

The score Field

A deterministic 0–100 confidence value derived only from the signals already in the response — no black-box model, no stored data. Rough bands:

  • 90+ — deliverable and clean
  • ~65 — probe-blocking provider (well-formed address on a live Microsoft-consumer MX)
  • ~55 — catch-all
  • ~50 — greylisted at RCPT TO (the server evaluated the recipient before deferring)
  • ~30 — undetermined: timeout, unresolved MX, or a deferral received before we could ask about the mailbox (deferred)
  • ≤15 — disposable
  • 0 — invalid

Role-based and suspected-typo addresses are penalised. Threshold it to fit your risk tolerance per campaign.

Boolean Flags

Flag What it means
syntax The address is RFC-valid
mx_found The domain publishes usable MX records
smtp_check An SMTP conversation actually took place, not that the mailbox was accepted — it is true above even though the server answered 550
disposable Domain listed as disposable / throwaway
role_based Local part is a role alias (info, support, contact…) rather than a person
catch_all The domain accepts every address, so the mailbox cannot be confirmed individually
free_provider Domain belongs to a free consumer mailbox provider

Other Fields

Field What it means
reason Explains a non-deliverable / unknown result (e.g. no_mx, accept_all, greylisting, mailbox_full, provider_blocks_probe, mx_unresolved, policy_rejected), null otherwise. It is null above: when the server plainly rejects the mailbox, the SMTP codes are the explanation
retry_after Suggested wait in seconds whenever the server answered with a temporary (4xx) deferral — greylisting, a busy server, a temporary sender block — else null
smtp_code SMTP reply code observed on the RCPT probe, null when no handshake took place
smtp_enhanced_code The RFC 3463 status the server sent next to that reply code, or null if it sent none: 5.1.1 above says the mailbox does not exist, whereas 5.7.x would say the server refused our probe and told you nothing about the address
did_you_mean Suggests a likely typo fix (e.g. gmial.com → gmail.com) or null
mx_records The domain's MX hosts in priority order, capped at the first 10

Integration Patterns

Registration Form Validation

Verify emails when users sign up. Reject invalid ones before they enter your database:

# Check during registration
curl -X POST https://mailprobe.dev/api/v1/verify \
  -H "Authorization: Bearer mp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["newuser@signup.com"]}'

If the status is invalid, reject the registration with a "please enter a valid email" message. If it's risky, let it through but flag it for monitoring.

Bulk List Cleaning

Before sending a marketing campaign, verify your entire list. For a long list there is nothing to code: the batch screen in your account takes a pasted list (up to 500 addresses) or a CSV or TXT file (up to 10 MB and 250,000 addresses), runs it in the background and, from 25 addresses up, emails you when the CSV is ready to download.

From a backend, the API takes up to 20 addresses per request:

# Verify up to 20 emails at once
curl -X POST https://mailprobe.dev/api/v1/verify \
  -H "Authorization: Bearer mp_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["email1@a.com", "email2@b.com", "email3@c.com"]}'

Filter out anything with a status of invalid. Depending on your risk tolerance, also remove the lowest-scoring risky addresses — especially if you're sending from a new domain or one with a low sender reputation.

Background Processing

For large lists, don't verify synchronously during a user request. Queue the emails and verify in batches:

// Node.js example — batch processing
const BATCH_SIZE = 20; // the API accepts up to 20 emails per request

async function verifyList(emails) {
  const results = [];
  let done = 0;

  while (done < emails.length) {
    const batch = emails.slice(done, done + BATCH_SIZE);
    const res = await fetch('https://mailprobe.dev/api/v1/verify', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer mp_live_YOUR_API_KEY',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ emails: batch }),
    });

    if (res.status === 429) {
      // Rate limited: wait for the delay given in the Retry-After header, then send the same batch again
      const seconds = Number(res.headers.get('retry-after')) || 60;
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (!res.ok) {
      // For example 402 when you run out of credits. An error body is an object, not an
      // array, and a refused request is not charged: fix the cause, then resume from `done`
      console.error(`Stopped after ${done} of ${emails.length} addresses: ${res.status} ${await res.text()}`);
      break;
    }

    results.push(...await res.json());
    done += batch.length;
  }

  return results;
}

The loop waits when the API answers 429 and stops on any other error, keeping what is already verified: results then holds fewer entries than the list you passed in.

Handling Edge Cases

Catch-All Servers

When catch_all is true, the mail server accepts everything. You can't know for sure if the specific mailbox exists. In practice, most addresses on catch-all domains are real (they're usually corporate domains with configured mail servers). That is why their status is risky rather than invalid: don't reject them automatically.

Temporary Failures

If the status is unknown, it usually means the mail server was temporarily unreachable. Retry after a few minutes: when the server answered with a temporary deferral, retry_after gives the suggested wait in seconds. Don't retry more than twice — if it fails consistently, the server might be blocking verification attempts.

Rate Limiting

The API allows 60 requests per minute. Each request can contain up to 20 emails, so you can verify up to 1,200 emails per minute. If you hit the rate limit, you'll get a 429 response with a Retry-After header: wait that number of seconds, then send the request again.

Disposable Emails in Registration

If disposable is true, the user signed up with a throwaway address. Whether to block this depends on your product. For free trials, you might want to block it. For a newsletter signup, it probably doesn't matter.

Pricing

MailProbe uses a simple credit-based model:

Plan Price Credits Cost per check
Free $0 100 on signup —
Starter $5 1,000 $0.005
Pro $20 10,000 $0.002
Growth $90 50,000 $0.0018
Scale $150 100,000 $0.0015

No subscriptions. Buy credits when you need them. Credits are valid for 12 months from your most recent purchase. Sign up free and start with 100 credits — enough to test the API thoroughly and verify a small list.

Quick Decision Framework

Use this to decide what to do with each verification result:

  • valid + score ≥ 90 → Send confidently
  • valid + score 70–89 → Send, but monitor bounces
  • risky (catch-all, role-based, disposable, or a provider that blocks probing) → Include in campaigns with caution; exclude from cold outreach
  • invalid → Remove immediately
  • unknown → Retry once; if still unknown, treat as risky
  • disposable: true → Block on registration; irrelevant for list cleaning (they'll bounce anyway)
  • role_based: true → Don't block, but expect lower engagement

Next Steps

  1. Create your free account — 100 credits, no card required
  2. Create an API key under Developer in your account
  3. Make your first verification call
  4. Read the full API documentation for all available options

The entire integration takes about 15 minutes. Most of that is deciding what to do with each status — the API part is a single HTTP request.

Ready to verify your email list?

Paste a list, upload a CSV, or call the API. 100 free credits included, no card needed.

Get Started Free →