REST vs Webhook Email Verification - When to Use Each Pattern | BounceZero
| API & Integration | 10 min read | | 73 views

REST vs Webhook Email Verification - When to Use Each Pattern

Modern email verification APIs expose two integration patterns: synchronous REST (you wait for the verdict) and asynchronous webhook (you submit and the API calls back when done). Each fits a different workload. This guide covers when to use which, the architectural tradeoffs, and how to combine both for resilient production systems.

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:

ConcernRESTWebhook
Latency to verdictLow (sub-second to 2s)High (seconds to minutes for bulk)
ThroughputLimited by serial requests + rate limitsVery high - batch + async
Failure modeVisible immediately (network error, 5xx)Silent unless you set up callback monitoring
Retry complexityEasy - just retry the callHarder - need idempotency on callback receipt
State managementStateless per callNeed to track job_id + handle out-of-order callbacks
Firewall/networkOutbound HTTPS only - works anywhereNeed public inbound endpoint
Cost per checkHigher (more overhead per call)Lower (batching efficiency)
Operational complexityLowMedium-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:

  • Verify the signature. Without it anyone can POST to your endpoint and you process forged data.
  • Idempotency. Webhook senders retry on timeout/5xx - you'll receive the same callback 2-3 times. Process the job_id exactly once.
  • ACK fast. Senders usually have a 5-10 second timeout. Don't do heavy processing in the handler; queue it for background processing and return 200 immediately.
  • Handle failures. Not every job completes successfully. Have a path for status: failed`.
  • Monitor expected-vs-actual callback rate. If you submitted 100 jobs today but only 80 callbacks fired, the 20 missing are silent failures.
  • 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
    • What your handler must do:

    • 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'.

    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
    • When polling is wrong:

    • 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

    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
    email verification api webhook rest api architecture integration patterns
    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.