Webhook Verifikasi Email: Panduan Asinkron

Leo
LeoFounder, BillionVerify

Cara implementasi webhook verifikasi email untuk pemrosesan asinkron. Konfigurasi, keamanan, dan error handling.

Cover Image for Webhook Verifikasi Email: Panduan Asinkron

Ketika Anda memverifikasi ribuan atau jutaan alamat email, menunggu secara sinkron untuk setiap hasil tidaklah praktis. Webhook verifikasi email menyediakan solusi elegan dengan memberi notifikasi kepada aplikasi Anda ketika tugas verifikasi selesai, menghilangkan kebutuhan untuk polling konstan dan memungkinkan alur kerja asinkron yang efisien. Panduan komprehensif ini mengeksplorasi segala yang perlu diketahui developer tentang mengimplementasikan webhook verifikasi email, dari konfigurasi dasar hingga pola lanjutan untuk menangani operasi verifikasi skala besar.

Memahami Webhook Verifikasi Email

Webhook adalah HTTP callback yang mengirimkan data ke aplikasi Anda ketika event tertentu terjadi. Dalam konteks verifikasi email, webhook memberi notifikasi kepada sistem Anda ketika pekerjaan verifikasi massal selesai, ketika verifikasi email individual selesai dalam mode async, atau ketika event signifikan lainnya terjadi selama proses verifikasi.

Mengapa Menggunakan Webhook untuk Verifikasi Email?

Pola request-response tradisional bekerja baik untuk verifikasi email tunggal, tetapi operasi massal menghadirkan tantangan. Memverifikasi 100.000 email mungkin memakan waktu berjam-jam, dan menjaga koneksi HTTP terbuka selama itu tidak layak. Polling untuk pembaruan status membuang resource dan menciptakan beban API yang tidak perlu.

Menghilangkan Overhead Polling

Tanpa webhook, Anda perlu berulang kali melakukan query ke API untuk memeriksa apakah pekerjaan massal telah selesai. Ini menciptakan trafik jaringan yang tidak perlu, mengonsumsi batas rate limit API, dan menambah kompleksitas pada aplikasi Anda. Webhook mendorong notifikasi kepada Anda tepat ketika dibutuhkan.

Pemrosesan Real-Time

Webhook memungkinkan tindakan segera ketika verifikasi selesai. Aplikasi Anda dapat memproses hasil, memperbarui database, dan memicu tindakan lanjutan tanpa penundaan yang diperkenalkan oleh interval polling.

Arsitektur yang Scalable

Arsitektur berbasis webhook dapat scale secara natural. Baik Anda memproses satu pekerjaan massal atau ratusan secara bersamaan, endpoint webhook Anda menerima notifikasi saat mereka tiba, dan Anda dapat memprosesnya secara asinkron menggunakan queue atau worker.

Efisiensi Resource

Alih-alih mempertahankan koneksi atau menjalankan loop polling, aplikasi Anda tetap idle sampai webhook tiba. Ini mengurangi biaya komputasi dan menyederhanakan persyaratan infrastruktur.

Event Webhook dalam Verifikasi Email

Layanan verifikasi email biasanya memicu webhook untuk beberapa jenis event:

Penyelesaian Pekerjaan Massal

Event webhook yang paling umum dipicu ketika pekerjaan verifikasi massal selesai diproses. Payload mencakup status pekerjaan, statistik ringkasan, dan informasi tentang mengunduh hasil.

Progress Pekerjaan Massal

Beberapa layanan mengirim webhook progress pada interval selama pemrosesan massal, memungkinkan Anda melacak kemajuan verifikasi dan memperkirakan waktu penyelesaian.

Kegagalan Pekerjaan Massal

Ketika pekerjaan massal mengalami error yang mencegah penyelesaian, webhook kegagalan memberikan detail tentang apa yang salah dan apakah hasil parsial tersedia.

Verifikasi Email Tunggal (Mode Async)

Untuk skenario verifikasi real-time volume tinggi, verifikasi email tunggal async mengirim hasil melalui webhook alih-alih menunggu respons sinkron.

Menyiapkan Endpoint Webhook

Mengimplementasikan webhook memerlukan pembuatan endpoint dalam aplikasi Anda yang dapat menerima dan memproses payload webhook.

Struktur Endpoint Dasar

Endpoint webhook hanyalah endpoint HTTP POST yang menerima payload JSON:

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/email-verification', async (req, res) => {
  const { event_type, job_id, status, data } = req.body;

  console.log(`Received webhook: ${event_type} for job ${job_id}`);

  // Process the webhook
  try {
    await handleWebhookEvent(req.body);

    // Always respond quickly to acknowledge receipt
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);

    // Still acknowledge receipt to prevent retries
    res.status(200).json({ received: true, error: error.message });
  }
});

async function handleWebhookEvent(payload) {
  switch (payload.event_type) {
    case 'bulk.completed':
      await handleBulkCompleted(payload);
      break;
    case 'bulk.failed':
      await handleBulkFailed(payload);
      break;
    case 'bulk.progress':
      await handleBulkProgress(payload);
      break;
    default:
      console.log(`Unknown event type: ${payload.event_type}`);
  }
}

Praktik Terbaik Respons Webhook

Layanan verifikasi email mengharapkan respons cepat dari endpoint webhook. Jika endpoint Anda memakan waktu terlalu lama untuk merespons, layanan mungkin menganggap pengiriman gagal dan mencoba lagi.

Respons Segera

Akui penerimaan webhook segera, lalu proses payload secara asinkron:

app.post('/webhooks/email-verification', async (req, res) => {
  // Immediately acknowledge receipt
  res.status(200).json({ received: true });

  // Process asynchronously
  setImmediate(async () => {
    try {
      await handleWebhookEvent(req.body);
    } catch (error) {
      console.error('Async webhook processing error:', error);
      // Log for retry or manual processing
      await logFailedWebhook(req.body, error);
    }
  });
});

Gunakan Message Queue untuk Pemrosesan Berat

Untuk sistem production, queue payload webhook untuk diproses oleh proses worker:

const Queue = require('bull');
const webhookQueue = new Queue('email-verification-webhooks');

app.post('/webhooks/email-verification', async (req, res) => {
  // Queue the webhook for processing
  await webhookQueue.add('process-webhook', req.body, {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 1000
    }
  });

  res.status(200).json({ received: true });
});

// Worker process
webhookQueue.process('process-webhook', async (job) => {
  const payload = job.data;
  await handleWebhookEvent(payload);
});

Mengonfigurasi Webhook dengan API

Daftarkan endpoint webhook Anda dengan layanan verifikasi email:

async function registerWebhook(webhookUrl, events, secret) {
  const response = await fetch('https://api.billionverify.com/v1/webhooks', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.BV_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: webhookUrl,
      events: events,
      secret: secret
    })
  });

  const result = await response.json();

  if (!response.ok) {
    throw new Error(`Failed to register webhook: ${result.error}`);
  }

  console.log(`Webhook registered: ${result.webhook_id}`);
  return result;
}

// Register for bulk job events
await registerWebhook(
  'https://yourapp.com/webhooks/email-verification',
  ['bulk.completed', 'bulk.failed', 'bulk.progress'],
  process.env.WEBHOOK_SECRET
);

Mengamankan Endpoint Webhook

Endpoint webhook dapat diakses publik, membuat keamanan menjadi esensial. Tanpa verifikasi yang tepat, penyerang dapat mengirim payload webhook palsu untuk memanipulasi aplikasi Anda.

Verifikasi Signature

Sebagian besar layanan verifikasi email menandatangani payload webhook menggunakan HMAC-SHA256 dengan shared secret. Verifikasi signature sebelum memproses:

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhooks/email-verification', async (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  const isValid = verifyWebhookSignature(
    req.body,
    signature,
    process.env.WEBHOOK_SECRET
  );

  if (!isValid) {
    console.warn('Invalid webhook signature received');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid, process webhook
  await handleWebhookEvent(req.body);
  res.status(200).json({ received: true });
});

Validasi Timestamp

Cegah replay attack dengan memvalidasi timestamp webhook:

function isTimestampValid(timestamp, toleranceSeconds = 300) {
  const webhookTime = new Date(timestamp).getTime();
  const currentTime = Date.now();
  const difference = Math.abs(currentTime - webhookTime);

  return difference <= toleranceSeconds * 1000;
}

app.post('/webhooks/email-verification', async (req, res) => {
  const { timestamp } = req.body;

  if (!isTimestampValid(timestamp)) {
    console.warn('Webhook timestamp outside acceptable range');
    return res.status(400).json({ error: 'Invalid timestamp' });
  }

  // Continue with signature verification and processing
});

IP Allowlisting

Untuk keamanan tambahan, batasi akses webhook ke alamat IP yang dikenal:

const allowedIPs = [
  '203.0.113.0/24',  // BillionVerify webhook servers
  '198.51.100.0/24'
];

function isIPAllowed(clientIP) {
  // Implement CIDR range checking
  return allowedIPs.some(range => isIPInRange(clientIP, range));
}

app.post('/webhooks/email-verification', async (req, res) => {
  const clientIP = req.ip || req.connection.remoteAddress;

  if (!isIPAllowed(clientIP)) {
    console.warn(`Webhook from unauthorized IP: ${clientIP}`);
    return res.status(403).json({ error: 'Forbidden' });
  }

  // Continue with processing
});

Penanganan Idempotency

Webhook mungkin dikirim beberapa kali karena masalah jaringan atau retry. Implementasikan idempotency untuk menangani duplikat dengan aman:

const processedWebhooks = new Set(); // Use Redis in production

async function handleWebhookIdempotent(payload) {
  const webhookId = payload.webhook_id || payload.event_id;

  // Check if already processed
  if (processedWebhooks.has(webhookId)) {
    console.log(`Duplicate webhook ignored: ${webhookId}`);
    return;
  }

  // Mark as processing
  processedWebhooks.add(webhookId);

  try {
    await handleWebhookEvent(payload);
  } catch (error) {
    // Remove from processed set to allow retry
    processedWebhooks.delete(webhookId);
    throw error;
  }
}

Untuk sistem production, gunakan Redis untuk idempotency terdistribusi:

const Redis = require('ioredis');
const redis = new Redis();

async function isWebhookProcessed(webhookId) {
  const key = `webhook:processed:${webhookId}`;
  const result = await redis.set(key, '1', 'NX', 'EX', 86400); // 24 hour expiry
  return result === null; // Already exists
}

app.post('/webhooks/email-verification', async (req, res) => {
  const webhookId = req.body.webhook_id;

  if (await isWebhookProcessed(webhookId)) {
    console.log(`Duplicate webhook: ${webhookId}`);
    return res.status(200).json({ received: true, duplicate: true });
  }

  await handleWebhookEvent(req.body);
  res.status(200).json({ received: true });
});

Memproses Payload Webhook

Event webhook yang berbeda memerlukan logika penanganan yang berbeda. Mari kita eksplorasi pola umum untuk memproses webhook verifikasi email.

Menangani Penyelesaian Pekerjaan Massal

Ketika pekerjaan verifikasi massal selesai, unduh dan proses hasilnya:

async function handleBulkCompleted(payload) {
  const { job_id, status, summary, download_url } = payload;

  console.log(`Bulk job ${job_id} completed with status: ${status}`);
  console.log(`Summary: ${summary.valid} valid, ${summary.invalid} invalid`);

  // Download results
  const results = await downloadResults(download_url);

  // Process results
  await processVerificationResults(job_id, results);

  // Update job status in database
  await updateJobStatus(job_id, 'completed', summary);

  // Notify relevant parties
  await sendCompletionNotification(job_id, summary);
}

async function downloadResults(url) {
  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${process.env.BV_API_KEY}`
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to download results: ${response.status}`);
  }

  return await response.json();
}

async function processVerificationResults(jobId, results) {
  // Batch update contacts in database
  const validEmails = results.filter(r => r.is_valid);
  const invalidEmails = results.filter(r => !r.is_valid);

  await db.transaction(async (trx) => {
    // Update valid emails
    for (const batch of chunkArray(validEmails, 1000)) {
      await trx('contacts')
        .whereIn('email', batch.map(r => r.email))
        .update({
          email_verified: true,
          verification_date: new Date(),
          verification_job_id: jobId
        });
    }

    // Handle invalid emails
    for (const batch of chunkArray(invalidEmails, 1000)) {
      await trx('contacts')
        .whereIn('email', batch.map(r => r.email))
        .update({
          email_verified: false,
          email_invalid_reason: trx.raw('CASE email ' +
            batch.map(r => `WHEN '${r.email}' THEN '${r.reason}'`).join(' ') +
            ' END'),
          verification_date: new Date(),
          verification_job_id: jobId
        });
    }
  });
}

Menangani Kegagalan Pekerjaan Massal

Ketika pekerjaan gagal, tangkap informasi error dan tentukan apakah pemulihan dimungkinkan:

async function handleBulkFailed(payload) {
  const { job_id, error_code, error_message, partial_results_available } = payload;

  console.error(`Bulk job ${job_id} failed: ${error_message}`);

  // Update job status
  await updateJobStatus(job_id, 'failed', {
    error_code,
    error_message
  });

  // Try to retrieve partial results if available
  if (partial_results_available) {
    console.log('Attempting to retrieve partial results...');
    try {
      const partialResults = await downloadPartialResults(job_id);
      await processVerificationResults(job_id, partialResults);

      // Identify unprocessed emails for retry
      const processedEmails = new Set(partialResults.map(r => r.email));
      const originalEmails = await getOriginalJobEmails(job_id);
      const unprocessedEmails = originalEmails.filter(e => !processedEmails.has(e));

      if (unprocessedEmails.length > 0) {
        // Schedule retry for unprocessed emails
        await scheduleRetryJob(job_id, unprocessedEmails);
      }
    } catch (error) {
      console.error('Failed to retrieve partial results:', error);
    }
  }

  // Notify about failure
  await sendFailureNotification(job_id, error_message);
}

async function scheduleRetryJob(originalJobId, emails) {
  // Create new job for remaining emails
  const response = await fetch('https://api.billionverify.com/v1/bulk/verify', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.BV_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      emails,
      metadata: {
        retry_of: originalJobId
      }
    })
  });

  const { job_id: newJobId } = await response.json();
  console.log(`Scheduled retry job ${newJobId} for ${emails.length} emails`);
}

Menangani Pembaruan Progress

Webhook progress membantu melacak pekerjaan yang berjalan lama:

async function handleBulkProgress(payload) {
  const { job_id, processed_count, total_count, estimated_completion } = payload;

  const percentComplete = Math.round((processed_count / total_count) * 100);
  console.log(`Job ${job_id}: ${percentComplete}% complete (${processed_count}/${total_count})`);

  // Update progress in database
  await updateJobProgress(job_id, {
    processed_count,
    total_count,
    percent_complete: percentComplete,
    estimated_completion: new Date(estimated_completion)
  });

  // Optionally notify users of progress
  if (percentComplete % 25 === 0) {
    await sendProgressNotification(job_id, percentComplete);
  }
}

Pola Webhook Lanjutan

Sistem production mendapat manfaat dari pola lanjutan yang meningkatkan keandalan dan maintainability.

Dead Letter Queue untuk Webhook yang Gagal

Ketika pemrosesan webhook gagal berulang kali, pindahkan payload ke dead letter queue untuk review manual:

const webhookQueue = new Queue('email-verification-webhooks');
const deadLetterQueue = new Queue('webhook-dead-letters');

webhookQueue.process('process-webhook', async (job) => {
  try {
    await handleWebhookEvent(job.data);
  } catch (error) {
    // Check if this is the final retry
    if (job.attemptsMade >= job.opts.attempts - 1) {
      // Move to dead letter queue
      await deadLetterQueue.add('failed-webhook', {
        original_payload: job.data,
        error: error.message,
        failed_at: new Date().toISOString(),
        attempts: job.attemptsMade + 1
      });
    }
    throw error; // Re-throw to trigger retry
  }
});

// Process dead letters manually or with alerts
deadLetterQueue.on('completed', async (job) => {
  await sendAlert({
    type: 'webhook_dead_letter',
    job_id: job.data.original_payload.job_id,
    error: job.data.error
  });
});

Event Sourcing Webhook

Simpan semua event webhook untuk audit trail dan kemampuan replay:

async function handleWebhookWithEventSourcing(payload) {
  // Store raw event
  const eventId = await storeWebhookEvent(payload);

  try {
    // Process event
    await handleWebhookEvent(payload);

    // Mark as processed
    await markEventProcessed(eventId);
  } catch (error) {
    // Mark as failed
    await markEventFailed(eventId, error);
    throw error;
  }
}

async function storeWebhookEvent(payload) {
  const result = await db('webhook_events').insert({
    event_type: payload.event_type,
    job_id: payload.job_id,
    payload: JSON.stringify(payload),
    received_at: new Date(),
    status: 'pending'
  });

  return result[0];
}

// Replay failed events
async function replayFailedEvents() {
  const failedEvents = await db('webhook_events')
    .where('status', 'failed')
    .where('retry_count', '<', 3);

  for (const event of failedEvents) {
    try {
      await handleWebhookEvent(JSON.parse(event.payload));
      await markEventProcessed(event.id);
    } catch (error) {
      await incrementRetryCount(event.id);
    }
  }
}

Routing Webhook Multi-Tenant

Untuk aplikasi SaaS, route webhook ke handler spesifik tenant:

async function handleMultiTenantWebhook(payload) {
  const { tenant_id, event_type, data } = payload;

  // Get tenant configuration
  const tenant = await getTenantConfig(tenant_id);

  if (!tenant) {
    console.error(`Unknown tenant: ${tenant_id}`);
    return;
  }

  // Route to tenant-specific handler
  switch (event_type) {
    case 'bulk.completed':
      await handleTenantBulkCompleted(tenant, data);
      break;
    case 'bulk.failed':
      await handleTenantBulkFailed(tenant, data);
      break;
  }

  // Forward to tenant webhook if configured
  if (tenant.webhook_url) {
    await forwardToTenant(tenant.webhook_url, tenant.webhook_secret, payload);
  }
}

async function forwardToTenant(url, secret, payload) {
  const signature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature
    },
    body: JSON.stringify(payload)
  });
}

Penanganan Error dan Keandalan

Implementasi webhook yang robust menangani kegagalan dengan baik dan memastikan tidak ada data yang hilang.

Strategi Retry

Implementasikan exponential backoff untuk kegagalan sementara:

async function processWebhookWithRetry(payload, maxRetries = 5) {
  const delays = [1000, 5000, 30000, 120000, 300000]; // 1s, 5s, 30s, 2m, 5m

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      await handleWebhookEvent(payload);
      return; // Success
    } catch (error) {
      const isRetryable = isRetryableError(error);

      if (!isRetryable || attempt === maxRetries - 1) {
        // Log to dead letter queue
        await logFailedWebhook(payload, error, attempt + 1);
        throw error;
      }

      console.log(`Retry ${attempt + 1}/${maxRetries} after ${delays[attempt]}ms`);
      await sleep(delays[attempt]);
    }
  }
}

function isRetryableError(error) {
  // Network errors, timeouts, and 5xx responses are retryable
  const retryableCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
  return retryableCodes.includes(error.code) ||
         (error.status && error.status >= 500);
}

Pola Circuit Breaker

Cegah cascade failure ketika layanan downstream tidak tersedia:

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.state = 'CLOSED';
    this.failures = 0;
    this.lastFailure = null;
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    this.lastFailure = Date.now();

    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      console.warn('Circuit breaker opened due to failures');
    }
  }
}

const databaseCircuitBreaker = new CircuitBreaker();

async function handleBulkCompletedSafely(payload) {
  await databaseCircuitBreaker.execute(async () => {
    await processVerificationResults(payload.job_id, payload.results);
  });
}

Monitoring dan Alerting

Lacak metrik kesehatan webhook:

const metrics = {
  received: 0,
  processed: 0,
  failed: 0,
  latency: []
};

app.post('/webhooks/email-verification', async (req, res) => {
  const startTime = Date.now();
  metrics.received++;

  try {
    await handleWebhookEvent(req.body);
    metrics.processed++;
  } catch (error) {
    metrics.failed++;
    throw error;
  } finally {
    metrics.latency.push(Date.now() - startTime);

    // Keep only last 1000 measurements
    if (metrics.latency.length > 1000) {
      metrics.latency.shift();
    }
  }

  res.status(200).json({ received: true });
});

// Expose metrics endpoint
app.get('/metrics/webhooks', (req, res) => {
  const avgLatency = metrics.latency.reduce((a, b) => a + b, 0) / metrics.latency.length;

  res.json({
    received: metrics.received,
    processed: metrics.processed,
    failed: metrics.failed,
    success_rate: (metrics.processed / metrics.received * 100).toFixed(2) + '%',
    avg_latency_ms: Math.round(avgLatency)
  });
});

// Alert on high failure rate
setInterval(() => {
  const failureRate = metrics.failed / metrics.received;

  if (failureRate > 0.1) { // More than 10% failures
    sendAlert({
      type: 'high_webhook_failure_rate',
      failure_rate: failureRate,
      total_received: metrics.received,
      total_failed: metrics.failed
    });
  }
}, 60000);

Testing Implementasi Webhook

Testing menyeluruh memastikan webhook handler bekerja dengan benar di production.

Testing Lokal dengan ngrok

Gunakan ngrok untuk mengekspos endpoint lokal untuk testing webhook:

# Start your local server
node server.js

# In another terminal, expose it via ngrok
ngrok http 3000

Daftarkan URL ngrok sebagai endpoint webhook Anda selama development.

Mock Payload Webhook

Buat fixture test untuk jenis event yang berbeda:

const mockPayloads = {
  bulkCompleted: {
    event_type: 'bulk.completed',
    job_id: 'job_123456',
    status: 'completed',
    timestamp: new Date().toISOString(),
    summary: {
      total: 1000,
      valid: 850,
      invalid: 120,
      risky: 30
    },
    download_url: 'https://api.billionverify.com/v1/bulk/download/job_123456'
  },

  bulkFailed: {
    event_type: 'bulk.failed',
    job_id: 'job_789012',
    error_code: 'PROCESSING_ERROR',
    error_message: 'Internal processing error',
    partial_results_available: true
  },

  bulkProgress: {
    event_type: 'bulk.progress',
    job_id: 'job_345678',
    processed_count: 5000,
    total_count: 10000,
    estimated_completion: new Date(Date.now() + 3600000).toISOString()
  }
};

// Test endpoint
describe('Webhook Handler', () => {
  it('should process bulk.completed event', async () => {
    const response = await request(app)
      .post('/webhooks/email-verification')
      .set('X-Webhook-Signature', generateSignature(mockPayloads.bulkCompleted))
      .send(mockPayloads.bulkCompleted);

    expect(response.status).toBe(200);
    expect(response.body.received).toBe(true);

    // Verify side effects
    const job = await db('verification_jobs').where('job_id', 'job_123456').first();
    expect(job.status).toBe('completed');
  });
});

Integration Testing

Test alur webhook lengkap termasuk verifikasi signature:

describe('Webhook Security', () => {
  it('should reject requests without signature', async () => {
    const response = await request(app)
      .post('/webhooks/email-verification')
      .send(mockPayloads.bulkCompleted);

    expect(response.status).toBe(401);
  });

  it('should reject requests with invalid signature', async () => {
    const response = await request(app)
      .post('/webhooks/email-verification')
      .set('X-Webhook-Signature', 'invalid_signature')
      .send(mockPayloads.bulkCompleted);

    expect(response.status).toBe(401);
  });

  it('should accept requests with valid signature', async () => {
    const signature = generateSignature(mockPayloads.bulkCompleted);

    const response = await request(app)
      .post('/webhooks/email-verification')
      .set('X-Webhook-Signature', signature)
      .send(mockPayloads.bulkCompleted);

    expect(response.status).toBe(200);
  });
});

Integrasi Webhook BillionVerify

BillionVerify menyediakan dukungan webhook komprehensif untuk event verifikasi email, memudahkan membangun alur kerja verifikasi asinkron.

Mengonfigurasi Webhook

Siapkan webhook melalui dashboard BillionVerify atau API:

// Register webhook via API
async function setupBillionVerifyWebhooks() {
  const webhook = await registerWebhook(
    'https://yourapp.com/webhooks/emailverify',
    ['bulk.completed', 'bulk.failed', 'bulk.progress'],
    process.env.EMAILVERIFY_WEBHOOK_SECRET
  );

  console.log('Webhook configured:', webhook);
}

Format Payload Webhook

Webhook BillionVerify mencakup informasi komprehensif tentang event verifikasi:

{
  "event_type": "bulk.completed",
  "webhook_id": "wh_abc123",
  "job_id": "job_xyz789",
  "timestamp": "2025-01-15T10:30:00Z",
  "status": "completed",
  "summary": {
    "total": 10000,
    "valid": 8500,
    "invalid": 1200,
    "risky": 300,
    "disposable": 150,
    "catch_all": 200
  },
  "processing_time_ms": 45000,
  "download_url": "https://api.billionverify.com/v1/bulk/download/job_xyz789"
}

Contoh Integrasi Lengkap

const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// Webhook endpoint for BillionVerify
app.post('/webhooks/emailverify', async (req, res) => {
  // Verify signature
  const signature = req.headers['x-emailverify-signature'];
  const isValid = verifySignature(req.body, signature);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Acknowledge immediately
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookAsync(req.body);
});

async function processWebhookAsync(payload) {
  try {
    switch (payload.event_type) {
      case 'bulk.completed':
        await handleBulkCompleted(payload);
        break;
      case 'bulk.failed':
        await handleBulkFailed(payload);
        break;
      case 'bulk.progress':
        await handleBulkProgress(payload);
        break;
    }
  } catch (error) {
    console.error('Webhook processing error:', error);
    await logFailedWebhook(payload, error);
  }
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

Kesimpulan

Webhook verifikasi email mentransformasi cara aplikasi menangani verifikasi massal dengan memungkinkan pemrosesan asinkron yang efisien, scalable, dan andal. Dengan mengimplementasikan penanganan webhook yang tepat dengan tindakan keamanan, penanganan error, dan monitoring, Anda dapat membangun alur kerja verifikasi email yang robust yang scale sesuai kebutuhan aplikasi Anda.

Poin penting untuk mengimplementasikan webhook verifikasi email:

  1. Respons cepat terhadap request webhook dan proses payload secara asinkron
  2. Verifikasi signature untuk memastikan webhook berasal dari sumber yang sah
  3. Implementasikan idempotency untuk menangani pengiriman duplikat dengan aman
  4. Gunakan message queue untuk pemrosesan yang andal dalam skala besar
  5. Monitor kesehatan webhook dengan metrik dan alerting

Baik Anda memproses ribuan atau jutaan verifikasi email, webhook menyediakan fondasi untuk pemrosesan async yang efisien. Mulai implementasikan webhook hari ini dengan dukungan webhook komprehensif BillionVerify dan tingkatkan alur kerja verifikasi email Anda ke level berikutnya.

Tim yang menggunakan Instantly atau Smartlead meningkatkan deliverabilitas dengan membersihkan daftar melalui BillionVerify sebelum setiap kampanye.

Bandingkan BillionVerify dengan ZeroBounce dalam hal akurasi dan kecepatan sebelum memilih penyedia verifikasi.

Leo
LeoFounder, BillionVerify
Wawasan Verifikasi Email

Mulai Verifikasi Hari Ini

Mulai verifikasi email dengan BillionVerify hari ini. Dapatkan 100 kredit gratis saat mendaftar - tanpa memerlukan kartu kredit. Bergabunglah dengan ribuan bisnis yang meningkatkan ROI pemasaran email mereka dengan verifikasi email yang akurat.

Tanpa memerlukan kartu kredit · 100+ kredit gratis per hari · Mulai dalam 30 detik

99.9%
Akurasi
Real-time
Kecepatan API
$0.00014
Per Email
100/day
Gratis Selamanya