Email verification APIs evolved from synchronous-only (you POST, you wait, you get a verdict) to offering both REST and webhook patterns. The two are not just different transports - they fit fundamentally different workloads, and choosing wrong makes your integration either slow, fragile, or both.
This post explains when each pattern is the right answer, the architectural tradeoffs, and how to combine both for production systems that need to handle real-time signup verification AND nightly batch hygiene runs without one workload starving the other.
The Two Patterns Side-by-Side
Synchronous REST:
``
Client > POST /v1/verify { email: [email protected] }
↓ (200-1500ms)
Client 200 OK { result: "verified", score: 0.97 }
`
Client blocks waiting for the verdict. Single request, single response, hand-off complete.
Asynchronous Webhook:
`
Client > POST /v1/bulk-with-callback { emails: [...], callback_url: "https://you.com/cb" }
Client 202 Accepted { job_id: "abc123" }
[verification runs server-side over seconds to minutes]
Server > POST https://you.com/cb { job_id: "abc123", status: "completed", results_url: "..." }
Client 200 OK
``
Client submits, gets an immediate acknowledgement, then waits passively for a server-initiated callback. Three messages, multi-step, eventually consistent.
When REST is the Right Answer
Use synchronous REST when:
1. User is waiting in real-time. Signup forms, account creation flows, anything where a human is staring at a loading spinner. You need the verdict in under 2 seconds and you need it inline.
2. The decision is part of a synchronous flow. Login authentication, password-reset email validity check, MFA delivery verification. The next step in your flow depends on the answer.
3. Single addresses or small batches (under ~50). Synchronous overhead is amortized fine across 1-50 calls; bulk-with-callback machinery is overkill for small volumes.
4. Your infrastructure can't receive inbound webhooks. Some environments (corporate firewalls, serverless without public endpoints, mobile clients) genuinely can't receive callbacks. REST works from anywhere with outbound HTTPS.
5. You want simple control flow. Single request-response is the easiest mental model. No state machine, no callback handler, no idempotency keys to manage.
When Webhooks Are the Right Answer
Use webhook async when:
1. Bulk batch processing. Verifying 10K, 100K, or 1M addresses. Synchronous polling burns quota; webhook callback is a single notification when done.
2. You can't afford to block. Background workers, queue consumers, scheduled jobs - anywhere blocking on 1-second-per-call would destroy throughput.
3. Verification is long-running. Catch-all detection, deep SMTP probing, captcha-solving can take 10-60+ seconds per address. Holding open connections for that long is wasteful.
4. You want to decouple producer and consumer. Submitting machine isn't the same as the processing machine. Webhook lets you fan-out submission while concentrating processing.
5. Cost matters at scale. Async submission lets you batch and pipeline; sync forces 1:1. At high volume, async pays for itself in compute savings.
The Architectural Tradeoffs
Different reliability characteristics:
| Concern | REST | Webhook |
|---|---|---|
| Latency to verdict | Low (sub-second to 2s) | High (seconds to minutes for bulk) |
| Throughput | Limited by serial requests + rate limits | Very high - batch + async |
| Failure mode | Visible immediately (network error, 5xx) | Silent unless you set up callback monitoring |
| Retry complexity | Easy - just retry the call | Harder - need idempotency on callback receipt |
| State management | Stateless per call | Need to track job_id + handle out-of-order callbacks |
| Firewall/network | Outbound HTTPS only - works anywhere | Need public inbound endpoint |
| Cost per check | Higher (more overhead per call) | Lower (batching efficiency) |
| Operational complexity | Low | Medium-high |
The risk profile inverts: REST fails LOUDLY (your code immediately sees the error); webhooks fail SILENTLY (your callback handler never fires, you don't know why). Webhook integrations need monitoring of expected-callback-rate vs actual-callback-rate, and timeout handlers for jobs that don't call back.
Hybrid: Use Both
The right production pattern for many SaaS products is HYBRID: REST for the real-time path, webhook for batch.
Example architecture for a CRM with both signup forms AND nightly hygiene:
``
┌─────────────────────────────────┐
│ Signup form (real-time) │
│ > REST verify on submit │
│ > block invalid synchronously │
│ > accept everything else │
└──────────────┬──────────────────┘
▼
┌─────────────────────────────────┐
│ PostgreSQL: contacts table │
└──────────────┬──────────────────┘
▼ (nightly cron)
┌─────────────────────────────────┐
│ Bulk export of stale contacts │
│ > Webhook bulk verify │
│ > Wait for callback │
│ > Update contacts with results │
│ > Suppress invalid for next campaign │
└─────────────────────────────────┘
``
The two paths share an API key but operate independently. Real-time stays fast; batch happens at scale without interfering.
Implementing the Webhook Receiver
Receiving webhook callbacks correctly is non-trivial. The minimum-viable handler:
``python
from flask import Flask, request, abort
import hmac
import hashlib
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['BOUNCEZERO_WEBHOOK_SECRET']
@app.route('/webhooks/verification', methods=['POST'])
def handle_verification_callback():
# 1. Verify signature (prevents forgery)
signature_header = request.headers.get('X-Signature', '')
body = request.get_data()
expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature_header, expected):
abort(401)
# 2. Parse payload
data = request.get_json()
job_id = data['job_id']
status = data['status']
# 3. Idempotency - check if we already processed this job_id
if already_processed(job_id):
return ('OK', 200)
# 4. Fetch results (callback usually contains just a URL, not the full payload)
if status == 'completed':
results = fetch_results(data['results_url'])
process_results(job_id, results)
mark_processed(job_id)
elif status == 'failed':
log_failure(job_id, data.get('error'))
# 5. ACK fast - webhook senders retry on 5xx or timeout
return ('OK', 200)
`
Critical rules:
Webhook Retry Behavior
Webhook senders typically retry failed deliveries - but the retry policy varies. Standard patterns:
- Immediate retry on connection failure or 5xx
- Exponential backoff for subsequent retries (1m, 5m, 30m, 2h, 6h, 24h)
- Give up after N attempts (typically 5-10)
- Dead-letter queue for permanently failed callbacks
- Return 200 quickly for any valid callback (within 5 seconds). Slow handlers trigger retries even when processing succeeded.
- Return 4xx for invalid payloads (bad signature, malformed JSON). 4xx tells the sender 'this will never succeed, don't retry'.
- Return 5xx for transient errors (database unavailable). 5xx tells the sender 'try again later'.
What your handler must do:
If your callback URL goes down for 6 hours, the sender will keep retrying. Make sure your handler is at least as available as the rest of your API surface. A dedicated lightweight receiver that just queues messages for processing is the right architecture if your main app has unpredictable availability.
Polling: The Middle Ground
Some teams resist webhook setup (firewall constraints, lack of public endpoint, simpler ops). Polling is the compromise: submit a bulk job, then periodically check status until done.
``python
def submit_and_wait(emails, poll_interval=10, max_wait=3600):
job_id = submit_bulk(emails)
elapsed = 0
while elapsed < max_wait:
status = get_job_status(job_id)
if status['status'] == 'completed':
return status
if status['status'] == 'failed':
raise RuntimeError(status.get('error'))
time.sleep(poll_interval)
elapsed += poll_interval
raise TimeoutError(f'Job {job_id} did not complete in {max_wait}s')
``
When polling is acceptable:
- Batch sizes under ~10K
- You're already running a long-lived process anyway
- You don't have a public webhook endpoint and can't add one
- High volume (you waste quota on polls)
- Short-running serverless functions (Lambda 15-min limit)
- Anything where you want to scale verification without blocking workers
When polling is wrong:
Polling interval matters: don't poll faster than 5-10 seconds for bulk jobs. The verdict won't change faster than the underlying probes complete, and aggressive polling wastes both your quota and the server's resources.
The Decision Framework
A quick decision tree:
Is a human waiting for the result in real-time?
> Yes > REST
> No > continue
How many addresses?
> Under 100 > REST (in a loop is fine)
> Over 100 > continue
Can you receive inbound webhooks?
> Yes > Webhook async
> No > continue
Is your worker process long-lived (not serverless)?
> Yes > Polling is OK
> No > Webhook async (or break into chunks and use REST)
Do you need to start the next stage as soon as verification finishes?
> Yes > Webhook async (no polling lag)
> No > Polling is fine
For most SaaS products serving both signup-flow + batch hygiene needs, the right answer is BOTH: REST for the form, webhook async for the batch. Same API key, two different integration paths.
Frequently Asked Questions
Can I use REST for everything if I keep batch sizes small?
Yes for under ~100 addresses, with the caveat that you must handle rate limits properly. REST in a loop with proper backoff works fine for small batches and avoids webhook setup overhead. Above 100, the synchronous overhead and rate-limit interaction make webhook/polling the better choice.
What's the latency difference between REST and webhook for bulk jobs?
For a 10K-address bulk job: webhook callback typically fires 1-5 minutes after submission (depending on backlog and catch-all detection complexity). Polling at 10-second intervals adds 0-10 seconds vs webhook. Synchronous REST in a loop with rate limits could take 30-90 minutes for the same 10K - single-call latency × bulk count plus throttle waits.
Do I need to verify webhook signatures?
Yes, always. Without signature verification, anyone who guesses your webhook URL can POST forged 'completion' payloads. They might tell you a job 'succeeded' with fake results, causing you to suppress legitimate addresses. Signature verification (HMAC-SHA256 of the body using a shared secret) is non-negotiable.
What if my webhook receiver is down when callbacks fire?
Senders retry with exponential backoff (typically 5-10 attempts over 24 hours). If your receiver is down for hours, you may eventually exhaust retries - those callbacks are permanently lost unless you can re-fetch results via the bulk-job GET endpoint. For critical workloads, monitor expected-vs-actual callback rates and have a fallback that polls jobs whose callbacks didn't arrive within an expected window.
REST and Webhook - Same API
BounceZero supports both REST and webhook async patterns. One API key, two integration paths. 100 free credits to test both.
Get Free Credits