Single-address verification is synchronous — you call POST /v1/verify and get an answer in one response. That doesn’t scale to tens of thousands of addresses: the request would take too long and time out. So bulk verification is a three-step asynchronous flow instead.
- Submit the list to
POST /v1/verify/batch. It returns immediately with a batchidand astatusofqueued. - Wait for completion — either poll
GET /v1/verify/batch/{id}untilstatusiscompleted, or receive a signedbatch.completedwebhook. - Fetch the results from
GET /v1/verify/batch/{id}/results, paging through them.
Everything below is about doing those three steps reliably at scale.
1. Dedupe and normalise before you submit (idempotency)
Every address in a batch costs a credit, so the cheapest optimisation is to never verify the same address twice. Before submitting, lowercase and trim each address, drop exact duplicates, and remove anything you’ve verified recently and already have a fresh result for. On a real-world list this routinely cuts 10–30% of the volume — and therefore the cost — before a single credit is spent.
Idempotency also matters for the submit call itself. Network hiccups happen: you send the batch, the connection drops before you read the response, and now you don’t know whether it was created. The safe pattern is to generate your own client-side batch key (store it before you send), pass a recognisable name, and if you must retry, first check whether a batch with that key already exists rather than blindly re-submitting. That prevents a dropped response from turning into two paid jobs. Treat the result the same way on the receiving end — key any downstream processing off the batch id so a re-delivered webhook or a repeated poll never double-processes.
2. Submit the batch
Send the addresses (and optionally a name and a webhookUrl). You get back { ok, batch: { id, status, total } } with a 202 Accepted. Very large lists are best split into chunks — submit several batches rather than one enormous request — which also keeps each job’s memory and retry surface manageable.
async function submitBatch(emails, name) {
// Dedupe + normalise first — you pay per address.
const clean = [...new Set(emails.map((e) => e.trim().toLowerCase()))];
const res = await fetch("https://api.verifyany.email/v1/verify/batch", {
method: "POST",
headers: {
Authorization: "Bearer vae_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ emails: clean, name }),
});
if (res.status === 402) throw new Error("Insufficient credits");
if (res.status === 413) throw new Error("Batch too large for your plan — split it");
if (!res.ok) throw new Error("Submit failed: " + res.status);
const { batch } = await res.json();
return batch; // { id, status: "queued", total }
}3a. Poll for completion
Polling is the simplest way to wait: call the status endpoint on an interval until status becomes completed (or failed). Two rules keep it well-behaved. First, back off — don’t poll every second for a job that takes minutes; a few seconds between polls, growing over time, is plenty and avoids rate limits. Second, set a ceiling so a stuck job doesn’t loop forever. The status response also carries a running processed count and a summary, so you can show progress.
async function waitForBatch(id, { intervalMs = 5000, maxMs = 30 * 60 * 1000 } = {}) {
const started = Date.now();
let wait = intervalMs;
while (true) {
const res = await fetchWithRetry(
"https://api.verifyany.email/v1/verify/batch/" + id
);
const { batch } = await res.json();
if (batch.status === "completed") return batch;
if (batch.status === "failed") throw new Error("Batch failed");
if (Date.now() - started > maxMs) throw new Error("Timed out waiting");
await new Promise((r) => setTimeout(r, wait));
wait = Math.min(wait * 1.5, 30000); // gentle backoff, capped at 30s
}
}3b. Or use a webhook instead of polling
For large lists, a webhook is more efficient than polling: pass a webhookUrl when you submit, and VerifyAnyEmail POSTs a signed batch.completed event the moment the job finishes — no repeated status calls, no wasted requests, and you react instantly. The trade-off is that you need a publicly reachable HTTPS endpoint, and you must verify the signature on every callback. Do not skip that step; an unverified webhook is an open door. The full verification code (raw body, HMAC-SHA256, constant-time compare) is in how to verify webhook signatures correctly.
A robust setup often uses both: rely on the webhook for prompt notification, but keep a slow safety-net poll (say, once a minute) in case a callback is ever missed. Because both paths key off the same batch id and your processing is idempotent, handling the completion twice is harmless.
4. Page through every result
Results are paginated. GET /v1/verify/batch/{id}/results?page=1&pageSize=1000 returns a page of results plus a total count. Loop, incrementing page, until you’ve collected total rows (or a page comes back empty). Use a large pageSize (up to 1000) to minimise round-trips.
async function fetchAllResults(id, pageSize = 1000) {
const all = [];
for (let page = 1; ; page++) {
const url =
"https://api.verifyany.email/v1/verify/batch/" + id +
"/results?page=" + page + "&pageSize=" + pageSize;
const res = await fetchWithRetry(url);
const { results, total } = await res.json();
all.push(...results);
if (all.length >= total || results.length === 0) break;
}
return all; // each: { email, normalized, status, subStatus, score, suggestion }
}Each result carries a status (deliverable, undeliverable, risky, unknown), a score, and sometimes a suggestion for likely typos. How to act on each status — what to keep, suppress, or segment — is covered in the results-handling guides; the short version is: mail the deliverables, drop the undeliverables, and segment risky and catch-all results separately.
5. Retry transient failures with backoff
At scale you will hit the occasional 429 (rate limit or concurrent-batch limit) or 5xx. These are transient — the correct response is to wait and retry, not to give up. Retry only those status codes, use exponential backoff with a little jitter so parallel workers don’t retry in lockstep, honour a Retry-After header if one is present, and cap the number of attempts. Never retry a 4xx other than 429 (a 400 or 402 won’t fix itself).
async function fetchWithRetry(url, opts = {}, tries = 5) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, {
...opts,
headers: { Authorization: "Bearer vae_your_api_key", ...(opts.headers || {}) },
});
if (res.ok) return res;
// Only 429 and 5xx are worth retrying.
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt >= tries - 1) {
throw new Error("Request failed: " + res.status);
}
const retryAfter = Number(res.headers.get("retry-after")) || 0;
const backoff = retryAfter * 1000 || Math.min(1000 * 2 ** attempt, 30000);
await new Promise((r) => setTimeout(r, backoff + Math.random() * 250));
}
}6. Stay credit-aware
Submitting a batch consumes credits up front, and the API returns 402 Insufficient credits if you don’t have enough — so check your balance before dispatching a huge job, and handle 402 rather than treating it as a generic error. Deduplicating first (step 1) is the single biggest lever on cost. If your plan caps batch size or concurrent batches, you’ll see 413 or 429 respectively; the fix is to split the list and limit how many batches you run at once.
Common mistakes
- Not deduping. You pay for every address, including the duplicates you could have removed for free.
- Tight polling loops. Polling every second wastes requests and invites
429s. Back off and cap the wait. - Retrying non-retryable errors. Retrying a
400or402just repeats a guaranteed failure. Retry only429and5xx. - Non-idempotent processing. If a re-delivered webhook or a repeated poll double-imports results, you corrupt your data. Key everything off the batch
id. - Stopping paging too early. Loop until you’ve collected
totalrows — don’t assume one page is the whole batch.
Where to go next
The exact request and response schemas for every endpoint are in the API docs. For the completion-callback security layer, see how to verify webhook signatures correctly, and keep the credential that authorises all of this safe with how to protect your API keys. To validate a single address while you’re building, the Email Verifier is the quickest way, and verifying email in Node.js & Express shows the surrounding application code.
Sources
The 429 Too Many Requests status and the Retry-After header are defined in RFC 6585 and RFC 9110. Batch limits, credit costs and plan-specific caps are described in the VerifyAnyEmail API docs.