Email Verification API Guide for Developers 2026 - Integration, Rate Limits, and Best Practices | BounceZero
| Developer | 11 min read | | 613 views

Email Verification API Guide for Developers 2026 - Integration, Rate Limits, and Best Practices

Everything a developer needs to integrate email verification in 2026: real-time vs bulk endpoints, Bearer auth, webhook callbacks, 429 backoff patterns, result-field interpretation, and production-ready code in Python, JavaScript, and cURL.

Every fake, mistyped, or disposable email address that enters your system costs you twice: once when the welcome email bounces, and again every time your marketing stack tries to reach an inbox that never existed. Email industry data puts the average signup-form typo rate at 5-8% of all submissions, and BounceZero's verification data across 50M+ emails shows that roughly 1 in 12 addresses submitted to registration forms is undeliverable at the moment of entry. If you're not verifying at the API layer, you're storing that garbage permanently.

The fix is architecturally simple but full of implementation details that trip up even experienced teams: do you verify synchronously in the request path or asynchronously via webhook? How do you handle a 429 without blocking your signup flow? Should you cache results, and for how long? What does a `catch_all: true` response actually mean for your business logic? Get these wrong and you'll either add seconds of latency to your signup form or silently accept addresses you should have rejected.

This guide walks through the full integration lifecycle using the BounceZero API as the reference implementation - from your first authenticated request to production-grade error handling, caching, and bulk processing of 500K-row lists. Every pattern here is transport-agnostic: even if you're evaluating multiple providers, the architecture applies.

BounceZero returns a verdict with **up to 99.8% accuracy in internal testing on SMTP-verifiable addresses** (against an industry benchmark of roughly 95%) with timing that varies by provider and verification path, which is fast enough to sit inline in a registration form. You can test everything in this guide with the **100 free verifications per month** that come with every account - no credit card required.

Real-Time API vs Bulk API: Choosing the Right Integration Pattern

The first architectural decision is not which provider to use - it's which *mode* of verification fits each touchpoint in your system. Almost every production integration ends up using both.

Real-time (synchronous) verification belongs anywhere an email address enters your system one at a time and you can act on the result immediately. The canonical use cases: registration and signup forms, checkout flows, lead-capture forms, CRM record creation, and API-driven enrichment where a sales rep adds a contact and you want the deliverability verdict before the record saves. The constraint is latency - your verification call sits in the user's critical path, so the provider's response time is *your* response time. BounceZero's provider-dependent API response time time means you can verify inline without a perceptible delay; anything over ~1 second and you should move the check to an async pattern with optimistic UI.

Bulk (asynchronous) verification is for lists: cleaning an existing database before a migration, validating a CSV import from a trade show, pre-flighting a newsletter list before a send, or scheduled hygiene jobs that re-verify aging segments. Here throughput matters more than latency. BounceZero's [bulk email verification](/bulk-email-validation) accepts up to 2,000,000 rows per API CSV upload (200 MB maximum) and typically completes in 5-10 minutes, returning results via file download or webhook.

A useful decision heuristic from BounceZero's verification data across 50M+ emails:

  • 1 email, user waiting > real-time endpoint, inline in the request path
  • 1-100 emails, no user waiting (e.g., CRM sync job) > real-time endpoint, called from a background worker with concurrency of 5-10
  • 100-500,000 emails > bulk endpoint, always
  • Recurring hygiene > bulk endpoint on a cron, re-verifying any record older than 90 days

The most common integration mistake we see is teams looping the single-check endpoint over a 200K-row list. It works, but it's strictly worse than bulk: you pay the per-request HTTP overhead 200,000 times, you have to build your own retry and progress tracking, and you'll hit rate limits that the bulk pipeline is explicitly designed to avoid. The bulk processor deduplicates, batches SMTP probes by domain (a 40-60% efficiency gain on typical B2C lists where Gmail, Outlook, and Yahoo dominate), and handles greylisting retries internally.

Pricing is identical across both modes - $3 per 1,000 verifications - so the choice is purely architectural, never financial. Use the [ROI calculator](/roi-calculator) to model the cost against your bounce-related losses; for most senders, verification pays for itself if it prevents even 2-3% of a list from bouncing.

Authentication: Bearer Tokens Done Right

BounceZero uses standard Bearer token authentication over HTTPS. You generate an API key from the dashboard after [registering](https://app.bouncezero.io/register), and pass it in the Authorization header on every request:

``

Authorization: Bearer bz_live_xxxxxxxxxxxxxxxxxxxx

`

Three operational rules that apply to any API key, and that we still see violated in a surprising number of integrations:

1. Never ship the key to the client. An email verification key in frontend JavaScript is a gift to abusers - they'll burn your quota validating their own spam lists. All verification calls must originate server-side. For signup forms, the correct pattern is: browser POSTs the email to *your* backend > your backend calls the verification API > your backend returns an accept/reject/soft-warn decision to the browser. This also lets you keep your rejection logic private, so bots can't reverse-engineer which addresses pass.

2. Use environment-scoped keys. Keep separate keys for production, staging, and CI, and store them in your secrets manager (Vault, AWS Secrets Manager, Doppler) or at minimum environment variables - never in the repository. Email industry data on credential leaks consistently shows API keys committed to public GitHub repos are scraped and exploited within a median of under 60 seconds.

3. Rotate on a schedule and on staff departure. Key rotation should be a config change, not a code change. Structure your client so the key is read once at boot:

`python

import os

import httpx

class BounceZeroClient:

BASE_URL = "https://api.bouncezero.io"

def __init__(self):

self.api_key = os.environ["BOUNCEZERO_API_KEY"]

self.client = httpx.Client(

base_url=self.BASE_URL,

headers={"Authorization": f"Bearer {self.api_key}"},

timeout=10.0,

)

`

A failed or missing token returns 401 Unauthorized; a valid token without sufficient credit returns 402 Payment Required. Handle both explicitly - a 401 is a configuration bug that should page someone, while a 402 is a billing state your application should degrade around gracefully (more on graceful degradation in the error-handling section below). Do not treat them as generic failures and retry: retrying a 401` fifty times doesn't fix a bad key, it just fills your logs.

Every account includes 100 free verifications per month with no credit card, which is enough to build and integration-test the entire flow described in this guide before you spend anything.

Single Email Verification: GET /v1/verify

The workhorse endpoint for real-time checks is a simple GET:

``

GET https://api.bouncezero.io/v1/[email protected]

Authorization: Bearer bz_live_xxxx

`

With cURL:

`bash

curl -s "https://api.bouncezero.io/v1/[email protected]" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY"

`

A successful response returns 200 OK with a JSON body:

`json

{

"email": "[email protected]",

"status": "valid",

"score": 0.98,

"catch_all": false,

"disposable": false,

"role_based": false,

"mx_valid": true,

"smtp_valid": true,

"spam_trap": false,

"processing_ms": 312

}

`

Under the hood, this single call runs BounceZero's six verification checks: mailbox existence (a live SMTP handshake against the recipient server), catch-all detection using a 3-probe method that distinguishes true accept-all domains from selectively-accepting ones, role-account detection (info@, sales@, admin@), disposable-domain matching against a continuously updated corpus, MX record validation, and spam-trap likelihood scoring. All six execute in parallel where possible, which is how the average round-trip stays at provider-dependent timing - DNS and SMTP stages that would take 1-2 seconds sequentially are pipelined.

The URL-encoding detail that bites people: email addresses can legally contain +, and + in a query string decodes to a space. [email protected] sent raw becomes jane [email protected] and fails syntax validation. Always URL-encode the email parameter - encodeURIComponent() in JavaScript, urllib.parse.quote() in Python, --data-urlencode in cURL:

`bash

curl -s -G "https://api.bouncezero.io/v1/verify" \

--data-urlencode "[email protected]" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY"

`

Set a client-side timeout of 5-8 seconds, not the default infinite. The provider-dependent timing has a long tail: greylisting mail servers deliberately delay first-contact SMTP probes, and a small fraction of verifications (under 2% in BounceZero's data across 50M+ emails) take 3+ seconds while the pipeline retries. Your signup form should treat a timeout as unknown` and proceed - never block a legitimate user because a mail server in the verification path was slow. Full parameter reference and response schemas are in the [API documentation](/api-email-validation).

Bulk Verification: POST /v1/bulk and the Async Lifecycle

Bulk verification is a three-step lifecycle: submit > poll or wait for webhook > fetch results.

Step 1 - Submit. POST your list as either a JSON array or a multipart CSV upload:

``bash

curl -s -X POST "https://api.bouncezero.io/v1/bulk" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY" \

-H "Content-Type: application/json" \

-d '{

"emails": ["[email protected]", "[email protected]"],

"webhook_url": "https://yourapp.com/hooks/bouncezero"

}'

`

The response is immediate - the API accepts the job and returns a job identifier:

`json

{

"job_id": "blk_9f2c81d4a7",

"status": "queued",

"email_count": 48212,

"estimated_seconds": 420

}

`

BounceZero accepts up to 2,000,000 rows per API CSV upload (200 MB maximum), and a full 1,000,000-address dashboard job typically completes in 5-10 minutes. Duplicates within a batch are deduplicated automatically and you're only charged for unique addresses - on typical CRM exports, that alone saves 3-7% of the verification cost.

Step 2 - Wait. You have two options. Polling: GET /v1/bulk/{job_id} returns the current status (queued > processing > completed) plus a progress percentage. If you poll, do it at a sane interval - every 15-30 seconds, not every 500ms; the job isn't going to finish faster because you asked more often. The better option for production systems is the webhook, covered in depth in the next section: pass webhook_url at submission and your endpoint gets called once, when the job finishes.

Step 3 - Fetch. On completion, download results as JSON or CSV:

`bash

curl -s "https://api.bouncezero.io/v1/bulk/blk_9f2c81d4a7/results?format=json" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY" -o results.json

`

Each row carries the same field set as the single-verify endpoint, so you can share one parsing/decision layer between your real-time and bulk code paths - a pattern worth designing for deliberately, since it means your "what do we do with a risky address" business logic lives in exactly one place.

Two production tips from large-batch integrations. First, chunk at the source, not the API: if your dataset is 2M rows, submit two 1,000,000-row jobs sequentially rather than trying to parallelize - you'll get cleaner failure isolation and simpler resume logic if anything goes wrong on your side. Second, persist the job_id` immediately, before doing anything else. If your process crashes between submission and result-fetch, the job_id is your only handle on work you've already paid for. Write it to your database in the same transaction that marks the import as "verification pending."

Webhooks: Getting Async Results Without Polling

Polling wastes requests and adds latency at the exact moment you don't want it - your job finished 25 seconds ago but your next poll is 5 seconds out. Webhooks invert the flow: BounceZero calls *you* when results are ready.

Pass a webhook_url when creating a bulk job (or configure a default in the dashboard). On completion, BounceZero POSTs a payload to your endpoint:

``json

{

"event": "bulk.completed",

"job_id": "blk_9f2c81d4a7",

"email_count": 48212,

"summary": {

"valid": 41780,

"invalid": 3901,

"risky": 1855,

"catch_all": 676

},

"results_url": "https://api.bouncezero.io/v1/bulk/blk_9f2c81d4a7/results"

}

`

Five rules for a production-grade webhook receiver - these apply to any webhook, but verification webhooks specifically tend to carry business-critical follow-on actions (suppressing invalid addresses before a send), so failures are expensive:

1. Verify the signature. Every BounceZero webhook includes an HMAC-SHA256 signature header computed over the raw body with your webhook secret. Validate it before parsing; reject anything that fails. Without this, anyone who discovers your endpoint URL can inject fake "completed" events.

2. Return 200 fast, process later. Your handler should validate the signature, enqueue the payload to your own job queue, and return 200 in under a second. Do the actual results download and database updates in a background worker. Webhook senders time out slow receivers and retry, and if your handler takes 30 seconds to process 48K rows inline, you'll receive duplicate deliveries of an event you already handled.

3. Be idempotent. Related to the above: webhook delivery is at-least-once, never exactly-once. Key your processing on job_id and skip events you've already processed. A simple processed_webhooks table with a unique constraint on the event identifier is enough.

4. Don't trust the payload for data - trust it as a signal. Fetch the authoritative results from results_url with your authenticated API key rather than acting on summary numbers alone. This means a spoofed webhook (if your signature check somehow failed) can at worst trigger a harmless authenticated fetch.

5. Have a reconciliation fallback. Webhooks fail for boring reasons: your endpoint was deploying, a proxy dropped the request, DNS hiccupped. BounceZero retries failed deliveries with exponential backoff, but you should also run a cheap cron that lists your jobs in completed` status without a corresponding processed-webhook record and fetches them. In practice this catches the ~0.1% of deliveries that slip through any retry scheme.

For local development, tunnel tools (ngrok, Cloudflare Tunnel) let you receive real webhook deliveries against localhost - far better than mocking, because you'll catch signature and encoding issues before production.

Interpreting Result Fields: What Each Verdict Actually Means

The single most consequential part of your integration is not the HTTP plumbing - it's the decision logic you build on top of the response. Here is what each field means and, more importantly, what you should *do* with it.

status: "valid" - The mailbox exists and accepted the SMTP probe. Across BounceZero's verification data on 50M+ emails, addresses returned as valid bounce at under 0.2% on subsequent real sends (that's the up to 99.8% accuracy in internal testing on SMTP-verifiable addresses figure in operational terms). Action: accept, send, store.

status: "invalid" - The address failed a hard check: syntax error, no MX records, or the mail server returned a definitive 550 mailbox does not exist. These bounce essentially 100% of the time. Action: reject at the form (with a gentle "did you mean gmail.com?" style prompt for common typo domains), suppress from lists.

status: "risky" - Deliverability is genuinely uncertain: the domain is catch-all, the server greylisted every probe, or scoring signals conflict. Action: this is a *business* decision, not a technical one. For a free-trial signup, accept risky addresses - the cost of a false rejection (a lost user) exceeds the cost of one bounce. For a cold outreach list where sender reputation is on the line, exclude them or route them to a low-volume warm-up segment.

catch_all: true - The domain accepts mail for *any* local part, so mailbox existence can't be proven by SMTP alone. Naive verifiers mark every catch-all as unknown; BounceZero's 3-probe catch-all detection sends differentiated probes to distinguish true accept-alls from servers that accept-then-bounce, resolving a meaningful share of catch-alls to a confident verdict. Note that roughly 20-25% of B2B addresses live on catch-all domains in industry data, so a strategy of "reject all catch-alls" silently discards a fifth of your B2B pipeline. Don't do that.

disposable: true - The domain is a temporary/burner service (mailinator-class). These users will never open email two. Action for SaaS: block at signup, or allow but exclude from trial-extension and lifecycle campaigns.

role_based: true - Addresses like info@, support@, billing@ that route to teams, not people. They're often deliverable but convert poorly and carry elevated complaint risk on marketing sends (a shared inbox means any one of five readers can hit "spam"). Action: fine for transactional mail, exclude from marketing.

mx_valid / smtp_valid - The component-level booleans behind the verdict. mx_valid: false means the domain can't receive mail at all; smtp_valid reflects the live mailbox probe. Log these for debugging, but drive decisions off status - the composite verdict already weighs them correctly.

spam_trap - A likelihood flag that the address is a recycled or pristine trap. Even a handful of trap hits can land your sending IP on a blocklist, so treat any flagged address as radioactive: never send, and audit how it entered your database.

Encode all of this in one function - decide(verification_result, context) - shared by every code path. Scattered if-statements across your codebase are how risky addresses end up handled three different ways.

Rate Limits and the 429 Backoff Pattern

Every serious API enforces rate limits, and your integration's behavior under a 429 Too Many Requests response is the difference between a system that degrades gracefully and one that melts down at the worst possible moment - usually during your biggest signup spike of the year.

BounceZero returns standard rate-limit headers on every response:

``

X-RateLimit-Limit: 300

X-RateLimit-Remaining: 287

X-RateLimit-Reset: 1751884800

Retry-After: 12

`

The correct client behavior is exponential backoff with jitter, capped retries, honoring Retry-After:

`python

import random

import time

import httpx

def verify_with_backoff(client, email, max_retries=4):

for attempt in range(max_retries + 1):

resp = client.get("/v1/verify", params={"email": email})

if resp.status_code == 429:

retry_after = float(resp.headers.get("Retry-After", 0))

backoff = max(retry_after, min(2 attempt, 30))

time.sleep(backoff + random.uniform(0, 1)) # jitter

continue

resp.raise_for_status()

return resp.json()

return None # exhausted - treat as 'unknown', do not block the user

`

Three details matter here. First, the jitter. If fifty of your workers all hit the limit simultaneously and all sleep exactly 2 seconds, they'll all retry simultaneously and get limited again - the classic thundering herd. The random.uniform(0, 1) desynchronizes them. Second, honoring Retry-After over your own schedule. The server is telling you exactly when capacity frees up; guessing is worse. Third, the cap and the fallback. After four attempts (~30-45 seconds of total waiting), stop. In a real-time flow you should have given up far earlier anyway - the right pattern for signup forms is a *single* attempt with a short timeout, treating any failure as unknown and letting the user through.

Equally important is not hitting the limit in the first place. For background jobs looping the single-verify endpoint, add a client-side concurrency cap (5-10 parallel requests) and a token-bucket throttle set slightly below your account's limit. Watching X-RateLimit-Remaining` proactively - slowing down when it drops below 20% - is strictly better than reacting to 429s.

And the structural fix, worth repeating: if you're generating enough requests to hit rate limits, you're almost certainly using the wrong endpoint. The [bulk API](/bulk-email-validation) exists precisely so that list-scale workloads (up to 1,000,000 addresses per dashboard job, processed in 5-10 minutes**) never contend with your real-time traffic. Teams that route batch work through bulk and reserve the single endpoint for genuinely interactive checks essentially never see a 429 in production.

Caching Strategy: Verify Once, Reuse Intelligently

Every verification you can serve from cache is money saved and provider-dependent timing recovered - but email deliverability is a time-varying fact, so cache lifetimes must reflect how fast each verdict goes stale.

The evidence-based TTL table, drawn from BounceZero's re-verification data across 50M+ emails:

  • valid > cache 30 days. Valid addresses decay slowly: email industry data puts natural list decay at roughly 2-3% per month (people change jobs, abandon accounts). A 30-day TTL means your worst-case staleness contributes well under 1% incremental bounce risk - acceptable for almost every use case. For high-stakes sends (deliverability-sensitive cold outreach), tighten to 7-14 days.
  • invalid > cache 7 days, not forever. This surprises people. Some invalids resurrect: a corporate mailbox mid-migration, a full inbox that gets cleaned out, a domain whose MX was briefly misconfigured. Roughly 1-2% of invalid verdicts flip to valid within a month. Seven days captures most of the cost savings while allowing recovery.
  • risky and catch_all > cache 24 hours or not at all. These are exactly the addresses where fresh information has the most value; a greylisting-induced risky today is often a clean valid tomorrow, because the retry probe succeeds once the server has seen your prober before.
  • disposable > cache the domain, not the address, for 24 hours. Disposable domains churn constantly as providers rotate; a long TTL here means missing newly-listed burner domains.

Implementation is a straightforward read-through cache. Redis with per-status TTLs:

``javascript

const TTL = { valid: 2592000, invalid: 604800, risky: 86400 };

async function verifyEmail(email) {

const key = bz:${email.toLowerCase().trim()};

const cached = await redis.get(key);

if (cached) return JSON.parse(cached);

const result = await bounceZero.verify(email);

const ttl = TTL[result.status] ?? 86400;

await redis.set(key, JSON.stringify(result), { EX: ttl });

return result;

}

`

Note the normalization in the cache key: lowercase and trim, or [email protected] and [email protected] become two cache entries and two billed verifications. Do *not* strip Gmail-style +tags or dots for verification purposes - [email protected] and [email protected] are the same mailbox at Gmail, but tag-stripping is provider-specific behavior and stripping it universally will corrupt results for domains where + addresses are distinct mailboxes.

Also record verified_at` in your own database alongside the verdict. The cache handles hot-path economics; the timestamp powers your hygiene layer - a scheduled job that re-runs anything older than 90 days through [bulk verification](/bulk-email-validation) before major sends. At $3 per 1,000 verifications, re-verifying a 100K list costs $300; email industry data prices the deliverability damage of a 5%+ bounce spike at far more than that, in throttled sends and weeks of reputation repair.

Error Handling: Designing for Every Failure Mode

A verification API integration has a small, fully enumerable set of failure modes. Handle each one explicitly and your system becomes boringly reliable; handle them with a generic catch block and you'll ship one of the two classic bugs - blocking real users when verification hiccups, or silently skipping verification and letting garbage in.

The response taxonomy and the correct reaction to each:

  • 200 - Success. Note that 200 includes verdicts of invalid and risky; an undeliverable email is a successful verification, not an error. Don't conflate HTTP failure with negative verdicts.
  • 400 Bad Request - Malformed input, usually a missing or syntactically hopeless email parameter. Never retry; log it, because a 400 in production means a bug in *your* input handling (often the URL-encoding issue from earlier).
  • 401 Unauthorized - Bad or revoked key. Never retry. Alert immediately - this is a config/deploy failure, and every verification is failing.
  • 402 Payment Required - Out of credit. Never retry. Alert billing, and fall back to your degraded mode (below).
  • 429 - Rate limited. Retry with the backoff pattern from the previous section.
  • 5xx - Server-side error. Retry once or twice with a short delay, then degrade.
  • Timeout / connection error - Network path failure. Same as 5xx: brief retry, then degrade.

The key design decision is your degraded mode - what happens when verification is unavailable? The answer should be fail-open with a flag:

``javascript

async function verifyOrDegrade(email) {

try {

return await verifyEmail(email); // cached, backoff-wrapped

} catch (err) {

metrics.increment("verification.degraded");

return { email, status: "unknown", degraded: true };

}

}

`

Accept the signup, tag the record verification_pending, and let a background reconciliation job re-verify flagged records once the path recovers. Rejecting users because a third-party call failed is the worst outcome available; a few hours of unverified signups is recoverable, lost customers are not. The one exception: flows where a bad address has severe immediate cost (e.g., triggering an expensive downstream provisioning action) may justify fail-closed - but make that an explicit, documented choice.

Finally, instrument the integration. Four metrics cover it: request count by response code, p50/p95/p99 latency, cache hit rate, and verdict distribution over time. The last one is the sleeper - if your invalid` rate on signups jumps from 6% to 25% overnight, you're not seeing a verification problem, you're seeing a bot attack, and your verification layer just became your earliest alarm. Teams running this dashboard typically detect list-bombing and credential-stuffing campaigns hours before their fraud tooling does.

Complete Code Examples: Python, JavaScript, and cURL

Everything above, condensed into minimal production-shaped clients. Full request/response schemas live in the [API documentation](/api-email-validation).

Python (httpx) - real-time verify with caching hook and backoff:

``python

import os, time, random, httpx

class BounceZero:

def __init__(self):

self.http = httpx.Client(

base_url="https://api.bouncezero.io",

headers={"Authorization": f"Bearer {os.environ['BOUNCEZERO_API_KEY']}"},

timeout=8.0,

)

def verify(self, email: str, retries: int = 3) -> dict:

email = email.strip().lower()

for attempt in range(retries + 1):

try:

r = self.http.get("/v1/verify", params={"email": email})

except httpx.TimeoutException:

return {"email": email, "status": "unknown", "degraded": True}

if r.status_code == 429:

wait = float(r.headers.get("Retry-After", 2 attempt))

time.sleep(wait + random.random())

continue

if r.status_code >= 500 and attempt < retries:

time.sleep(1)

continue

r.raise_for_status()

return r.json()

return {"email": email, "status": "unknown", "degraded": True}

bz = BounceZero()

result = bz.verify("[email protected]")

if result["status"] == "invalid":

raise ValueError("Please check your email address")

`

JavaScript (Node 18+, native fetch) - signup-form backend handler:

`javascript

const BASE = "https://api.bouncezero.io";

async function verifyEmail(email) {

const url = ${BASE}/v1/verify?email=${encodeURIComponent(email.trim().toLowerCase())};

const res = await fetch(url, {

headers: { Authorization: Bearer ${process.env.BOUNCEZERO_API_KEY} },

signal: AbortSignal.timeout(8000),

});

if (!res.ok) throw new Error(BounceZero ${res.status});

return res.json();

}

app.post("/api/signup", async (req, res) => {

let verdict = { status: "unknown", degraded: true };

try { verdict = await verifyEmail(req.body.email); } catch (_) {}

if (verdict.status === "invalid" || verdict.disposable) {

return res.status(422).json({ error: "Please use a valid, permanent email address." });

}

const user = await createUser({ ...req.body, email_verdict: verdict.status });

return res.status(201).json({ id: user.id });

});

`

cURL - bulk job lifecycle:**

`bash

# 1. Submit a CSV of 250K emails

curl -s -X POST "https://api.bouncezero.io/v1/bulk" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY" \

-F "[email protected]" \

-F "webhook_url=https://yourapp.com/hooks/bouncezero"

# > {"job_id":"blk_9f2c81d4a7","status":"queued","email_count":250000}

# 2. (If not using webhooks) poll status

curl -s "https://api.bouncezero.io/v1/bulk/blk_9f2c81d4a7" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY"

# 3. Download results as CSV

curl -s "https://api.bouncezero.io/v1/bulk/blk_9f2c81d4a7/results?format=csv" \

-H "Authorization: Bearer $BOUNCEZERO_API_KEY" -o verified.csv

`

Notice the shared shape across all three: normalize the input, bound the wait, treat verdicts as data and transport failures as unknown`. Any language, same architecture.

Production Checklist and Latency Budget

Before you flip verification on in production, walk this checklist. Each item corresponds to an incident we've seen a real team have.

Latency budget. For an inline signup check, budget end-to-end: provider-dependent API response time + your network overhead + your own processing. Keep total added latency under 500ms at p50 and set a hard 3-5 second ceiling at which you fail open. Email industry data on form abandonment shows conversion drops measurably beyond ~1 second of added submit latency, so if your provider can't consistently respond sub-second, verification must move out of the critical path. This is exactly why response time is a first-order selection criterion for verification APIs, not a nice-to-have - a 99% accurate verdict delivered in 4 seconds costs you more signups than it saves bounces.

The checklist:

  • ✓ API key server-side only, in a secrets manager, with separate staging/production keys
  • ✓ Email parameter URL-encoded; input normalized (trim + lowercase) before caching and sending
  • ✓ Client timeout set (5-8s); no infinite waits in any code path
  • ✓ 429 handled with jittered exponential backoff honoring Retry-After; 401/402 alert instead of retrying
  • ✓ Fail-open degraded mode: transport failures produce status: "unknown" + a flag, never a blocked user
  • ✓ Read-through cache with per-status TTLs (valid: 30d, invalid: 7d, risky: 24h)
  • ✓ One shared decision function mapping verdicts > business actions across all code paths
  • ✓ Bulk endpoint for anything over ~100 addresses; job_id persisted at submission time
  • ✓ Webhook receiver: signature-verified, fast-ACK, idempotent, with a reconciliation cron
  • ✓ Dashboards: response codes, latency percentiles, cache hit rate, verdict distribution
  • ✓ 90-day re-verification cron for stored addresses, routed through bulk

Cost model, for the finance conversation. At $3 per 1,000 verifications, a product with 50,000 signups/month spends $150/month on real-time checks - before cache hits, which typically cut billable volume 20-40% for products with repeat email entry. Against that, BounceZero's data across 50M+ emails shows unverified signup flows carrying 8-12% undeliverable addresses; at 50K signups that's ~5,000 dead records per month polluting your CRM, inflating your ESP's per-contact billing tier, and dragging your sender reputation toward the promotions tab. Run your own numbers in the [ROI calculator](/roi-calculator) - for most teams the integration pays for itself within the first bounce-related deliverability incident it prevents.

Testing. Use your 100 free monthly verifications to integration-test the full matrix before launch: a known-good address, a nonexistent mailbox on a real domain, a disposable domain, a role account, and a catch-all domain. Assert on your *decision function's output* for each, not just on the raw API response - the decisions are what your users experience.

Frequently Asked Questions

How fast is the BounceZero verification API, and is it fast enough to use in a signup form?

BounceZero's average API response time is provider-dependent timing, which is comfortably within the latency budget of an inline signup check - users don't perceive sub-500ms additions to a form submit. A small tail of verifications (under 2%) takes longer because the target mail server greylists or delays SMTP probes, which is why we recommend a 5-8 second client timeout that fails open: if the check doesn't complete in time, treat the address as unknown and let the user proceed. For flows where even that tail is unacceptable, verify asynchronously after signup and gate downstream actions (like list enrollment) on the verdict instead.

Should I use the real-time endpoint or the bulk endpoint?

Use the real-time GET /v1/verify endpoint whenever a single address enters your system and something is waiting on the answer - signup forms, checkout, CRM record creation. Use the bulk POST /v1/bulk endpoint for anything list-shaped: imports, migrations, pre-send hygiene, or scheduled re-verification. The practical threshold is about 100 addresses; above that, bulk is faster, immune to rate limits, deduplicates automatically, and handles retries internally. BounceZero's bulk pipeline accepts up to 1,000,000 addresses per dashboard job and completes in 5-10 minutes. Pricing is identical for both modes at $3 per 1,000 verifications, so the choice is purely architectural.

How should my code handle a 429 rate-limit response?

Retry with exponential backoff plus random jitter, honoring the Retry-After header when present, and cap total retries at three or four attempts. The jitter is essential in multi-worker systems - without it, all workers retry simultaneously and get limited again. In interactive flows, don't retry at all beyond one attempt: treat the failure as an unknown verdict and let the user through. The deeper fix is structural - sustained 429s almost always mean batch work is being pushed through the single-verify endpoint, and moving it to the bulk API eliminates the contention entirely.

How long can I safely cache verification results?

Cache by verdict, not uniformly. Valid results are safe for 30 days - natural list decay runs about 2-3% per month, so 30-day staleness contributes well under 1% incremental bounce risk. Cache invalid results for about 7 days, since 1-2% of invalid addresses recover within a month (mailbox migrations, cleaned-out full inboxes). Cache risky and catch-all verdicts for 24 hours at most, because those are precisely the cases where a fresh probe often resolves to a confident answer. Always normalize the address (trim and lowercase) before using it as a cache key, and store a verified_at timestamp so a scheduled job can re-verify anything older than 90 days via bulk.

What does a catch_all: true result mean, and should I reject those addresses?

A catch-all domain accepts SMTP delivery for any local part, so a standard mailbox probe can't prove the specific address exists. You should not blanket-reject them - industry data shows 20-25% of B2B addresses sit on catch-all domains, so rejecting them all discards a large slice of legitimate contacts. BounceZero's 3-probe catch-all detection sends differentiated probes to separate true accept-all servers from ones that accept and later bounce, resolving many catch-alls to a confident valid or invalid verdict. For the remainder, make a context-dependent call: accept them for product signups, and route them to low-volume or warmed-up segments for deliverability-sensitive campaigns.

How do I test the integration without spending money?

Every BounceZero account includes 100 free verifications per month with no credit card required, which covers full integration testing. Build a test matrix of five address types - a known-good mailbox, a nonexistent user on a real domain, a disposable-domain address, a role account like info@, and an address on a catch-all domain - and assert on your decision function's output for each, not just the raw API fields. Test your failure paths too: point the client at an unroutable host to exercise your timeout and fail-open logic, and use a tunnel tool like ngrok to receive real webhook deliveries against your local machine before deploying the receiver.

Ship Verification in an Afternoon

up to 99.8% accuracy in internal testing on SMTP-verifiable addresses, provider-dependent response time, $3 per 1,000 verifications - with 100 free checks every month to build against. No credit card required.

Learn More
email verification API developer guide API integration email validation BounceZero API

Continue with related resources

Move from this article to the most relevant guide, tool, or evidence page.

AL

Written by

Ayoub Lebda

Founder, BounceZero - Email-infrastructure engineer

Ayoub built BounceZero's 5-stage validation pipeline, its dedicated BGP-announced IP infrastructure, and the Patroni HA PostgreSQL cluster behind every verification. Previously built high-volume email delivery infrastructure. Trained at 1337 Benguerir (École 42 network, 2019). Open-source: bgp_analyzer.