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:
Syntax validation — Is the format valid? Does it conform to RFC 5321? This catches typos like
user@gmailcomoruser@@gmail.com.DNS MX record lookup — Does the domain have mail servers configured? If
example.xyzhas no MX records, no email is getting delivered there.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.
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.
Role-based detection — Addresses like
admin@,info@,support@are shared inboxes. They're valid but often have higher bounce rates and lower engagement.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_KEYContent-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 existsundeliverable— SMTP rejected the addresscatch-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 sendinvalid— Bad syntax, no MX, or the mailbox rejects — do not sendrisky— 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 confidentlyvalid+ score 70–89 → Send, but monitor bouncesrisky(catch-all, role-based, disposable, or a provider that blocks probing) → Include in campaigns with caution; exclude from cold outreachinvalid→ Remove immediatelyunknown→ Retry once; if still unknown, treat as riskydisposable: true→ Block on registration; irrelevant for list cleaning (they'll bounce anyway)role_based: true→ Don't block, but expect lower engagement
Next Steps
- Create your free account — 100 credits, no card required
- Create an API key under Developer in your account
- Make your first verification call
- 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 →