Email verification API integration: a checklist for production
· 4 min read · Email validation
A production email verification API integration needs five things beyond the happy path: a client timeout longer than the slowest mail server, retries with backoff for rate limits and server errors only, a cache on your side so you don't pay twice for the same address, a defined policy for unknown and risky results, and a fallback that never blocks a real user when the API is slow. The checklist below uses pingutil's /v1/verify as the example, but every item applies to any verifier.
1. Set a timeout that matches SMTP reality
Email verification is slow compared with most APIs, because the verifier has to hold a conversation with the recipient's mail server. pingutil documents that most checks take one to five seconds and a slow mail server can take up to about 25. Our post on SMTP verification limits explains why.
- Background jobs (CRM sync, list cleaning): set the client timeout around 30 seconds.
- Interactive paths (signup form): don't wait 25 seconds. Run syntax and typo feedback inline, and do the full check after the form submits, in a background job.
2. Retry the right errors, with backoff and a cap
A verification call has no side effects (pingutil runs RCPT TO and then QUIT, so no message is sent), which makes it safe to retry. Still, retry only what can succeed next time:
| Response | Retry? |
|---|---|
200 |
No. Even unknown is a final answer for this request. |
400 invalid_input |
No. Fix the input. |
401 unauthorized |
No. Fix the key. |
429 rate_limited |
Yes, after retry_after_seconds. |
429 quota_exceeded |
Not today. The daily budget resets at 00:00 UTC. |
5xx / network timeout |
Yes, with exponential backoff and jitter, at most 2 to 3 times. |
AWS's Timeouts, retries, and backoff with jitter is the standard reference for why the jitter and the cap matter.
async function verify(email: string, attempt = 0): Promise<any | null> {
const res = await fetch('https://pingutil.com/v1/verify', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.PINGUTIL_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
}).catch(() => null);
if (res?.ok) return res.json(); // { status, reason, score, did_you_mean, checks, ... }
const body = res ? await res.json().catch(() => ({})) : {};
if (body.error === 'quota_exceeded' || (res && res.status < 500 && res.status !== 429)) return null;
if (attempt >= 2) return null;
const base = body.retry_after_seconds ? body.retry_after_seconds * 1000 : 1000 * 2 ** attempt;
await new Promise((r) => setTimeout(r, base + Math.random() * 1000));
return verify(email, attempt + 1);
}
null means "no answer": keep the contact as unverified and move on.
3. Respect the rate limit up front
Retrying 429s works, but it's better not to cause them. pingutil allows 60 requests a minute per account, shared by all keys, and 300 verified addresses a day, API and dashboard CSV uploads combined. POST /v1/verify/batch takes up to 50 addresses in one request and returns { "results": [...] } in the same order, so a batch counts as one request against the per-minute limit but 50 against the daily budget.
For bulk work, put a queue in front of the API with a fixed concurrency rather than firing everything at once. For bursty triggers (a CRM import that fires 2,000 webhooks), the queue is mandatory.
4. Cache on your side and deduplicate
Every address you send counts toward the daily budget, even when we answer from our 30-day cache. So:
- Normalize before you deduplicate. Lowercase the address and trim whitespace. Folding Gmail dots and plus tags is a separate decision; see Gmail dots and plus addressing.
- Store the result with a timestamp (
checked_atis in the response). Re-verifyvalidaddresses after a month or two, not on every CRM update. - Don't cache
unknownfor long. It usually means greylisting, a temporary block or a timeout. Re-check in an hour or so. We cover why in greylisting and email verification.
The general caching trade-offs are in caching domain lookups.
5. Make idempotency your job
pingutil doesn't take an idempotency key, and it doesn't need one because nothing changes when you verify. The idempotency problem is on your side: a retried job that writes the result twice, or a webhook that triggers a second verification for the same contact. Key your writes on the contact ID and the normalized address, and skip the call if a fresh result already exists.
6. Decide what each status does before you ship
Write this down with sales and marketing, not just engineering:
| Status | Signup | CRM / outbound |
|---|---|---|
valid |
Accept | Sequence |
risky / accept_all |
Accept, confirm by email | Separate low-volume segment |
risky / role_based |
Accept or ask for a personal address | Usually skip for one-to-one outreach |
invalid |
Reject with a helpful message; show did_you_mean if set |
Suppress |
unknown |
Accept, confirm by email | Hold, re-check later |
The full reason list is in the verification status reference.
7. Log enough to debug
Store status, reason, score, mx_host and checked_at with the contact. When a "valid" address bounces, mx_host and reason tell you whether it was a catch-all, a gateway that accepts then bounces, or a mailbox that was deleted since.
Before go-live
- Timeout set per path (interactive vs background).
- Retries only on 429 and 5xx, with jitter and a cap.
- Queue with fixed concurrency for bulk.
- Local cache and dedupe on normalized address.
- Written policy for
riskyandunknown. - Fail open on signup.
Try a few of your own addresses in the verification demo to see each status before you write the routing rules.
Questions people ask
- Should a signup form fail if the email verification API is down?
- No. Fail open: accept the signup, mark the address as unverified, and check it later or rely on a confirmation email. Blocking real users because a third-party call timed out costs more than letting one bad address through.
- Is it safe to retry an email verification request?
- Yes. A verification call reads state and doesn't send email or change anything on the recipient side, so retrying it is safe. Retry only on timeouts, 5xx errors and rate limits, with backoff, and cap the number of attempts.