Skip to content

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_at is in the response). Re-verify valid addresses after a month or two, not on every CRM update.
  • Don't cache unknown for 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 risky and unknown.
  • 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.