メール検証Webhook:開発者のための非同期処理ガイド

Leo
LeoFounder, BillionVerify

非同期処理のためのメール検証Webhookの実装方法を学びます。Webhookのセットアップ、セキュリティ、エラー処理、および大量メール検証結果を処理するためのベストプラクティスをカバーする完全ガイド。

Cover Image for メール検証Webhook:開発者のための非同期処理ガイド

数千、数百万のメールアドレスを検証する際、各結果を同期的に待つのは現実的ではありません。メール検証Webhookは、検証タスクが完了したときにアプリケーションに通知することで、エレガントなソリューションを提供し、定期的なポーリングの必要性を排除し、効率的な非同期ワークフローを可能にします。この包括的なガイドでは、基本的なセットアップから大規模な検証操作を処理するための高度なパターンまで、メール検証Webhookの実装について開発者が知る必要があるすべてを探ります。

メール検証Webhookの理解

WebhookはHTTPコールバックであり、特定のイベントが発生したときにアプリケーションにデータを配信します。メール検証のコンテキストでは、Webhookは一括検証ジョブが完了したとき、非同期モードで個々のメール検証が終了したとき、または検証プロセス中に他の重要なイベントが発生したときにシステムに通知します。

メール検証にWebhookを使用する理由

従来のリクエスト・レスポンスパターンは単一のメール検証には適していますが、一括操作には課題があります。10万通のメールを検証するには数時間かかる可能性があり、HTTP接続をそれほど長く開いたままにすることは現実的ではありません。ステータス更新のポーリングはリソースを浪費し、不必要なAPI負荷を生み出します。

ポーリングオーバーヘッドの排除

Webhookがなければ、一括ジョブが完了したかどうかを確認するために、APIに繰り返しクエリを実行する必要があります。これにより、不必要なネットワークトラフィックが発生し、APIレート制限が消費され、アプリケーションに複雑さが加わります。Webhookは、必要なときに正確に通知をプッシュします。

リアルタイム処理

Webhookは、検証が完了したときに即座にアクションを実行できるようにします。アプリケーションは、ポーリング間隔によって生じる遅延なしに、結果を処理し、データベースを更新し、フォローアップアクションをトリガーできます。

スケーラブルなアーキテクチャ

Webhookベースのアーキテクチャは自然にスケールします。1つの一括ジョブを処理している場合でも、同時に数百を処理している場合でも、Webhookエンドポイントは到着した通知を受信し、キューやワーカーを使用して非同期に処理できます。

リソース効率

接続を維持したりポーリングループを実行したりする代わりに、アプリケーションはWebhookが到着するまでアイドル状態のままです。これにより、計算コストが削減され、インフラストラクチャ要件が簡素化されます。

メール検証におけるWebhookイベント

メール検証サービスは、通常、いくつかのイベントタイプに対してWebhookをトリガーします。

一括ジョブ完了

最も一般的なWebhookイベントは、一括検証ジョブの処理が完了したときに発火します。ペイロードには、ジョブステータス、サマリー統計、および結果のダウンロードに関する情報が含まれます。

一括ジョブ進捗

一部のサービスは、一括処理中に間隔を置いて進捗Webhookを送信し、検証の進捗を追跡し、完了時間を推定できるようにします。

一括ジョブ失敗

一括ジョブが完了を妨げるエラーに遭遇した場合、失敗Webhookは何が問題だったか、および部分的な結果が利用可能かどうかの詳細を提供します。

単一メール検証(非同期モード)

大量のリアルタイム検証シナリオの場合、非同期単一メール検証は、同期レスポンスを待つ代わりに、Webhookを介して結果を送信します。

Webhookエンドポイントのセットアップ

Webhookを実装するには、Webhookペイロードを受信して処理できるエンドポイントをアプリケーションに作成する必要があります。

基本的なエンドポイント構造

Webhookエンドポイントは、単にJSONペイロードを受け入れるHTTP POSTエンドポイントです。

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}`);
  }
}

Webhookレスポンスのベストプラクティス

メール検証サービスは、Webhookエンドポイントからの迅速なレスポンスを期待しています。エンドポイントがレスポンスに時間がかかりすぎると、サービスは配信が失敗したと見なし、再試行する可能性があります。

即座にレスポンス

Webhookの受信を即座に確認してから、ペイロードを非同期に処理します。

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);
    }
  });
});

重い処理にはメッセージキューを使用

本番システムでは、Webhookペイロードをキューに入れてワーカープロセスで処理します。

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);
});

APIでWebhookを設定

メール検証サービスにWebhookエンドポイントを登録します。

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
);

Webhookエンドポイントのセキュリティ保護

Webhookエンドポイントは公開アクセス可能であるため、セキュリティが不可欠です。適切な検証がなければ、攻撃者がアプリケーションを操作するために偽のWebhookペイロードを送信する可能性があります。

署名検証

ほとんどのメール検証サービスは、共有シークレットを使用してHMAC-SHA256でWebhookペイロードに署名します。処理する前に署名を検証します。

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 });
});

タイムスタンプ検証

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許可リスト

追加のセキュリティのために、Webhookアクセスを既知のIPアドレスに制限します。

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
});

冪等性の処理

Webhookは、ネットワークの問題や再試行により、複数回配信される可能性があります。重複を安全に処理するために冪等性を実装します。

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;
  }
}

本番システムでは、分散冪等性にRedisを使用します。

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 });
});

Webhookペイロードの処理

異なるWebhookイベントには異なる処理ロジックが必要です。メール検証Webhookを処理するための一般的なパターンを探りましょう。

一括ジョブ完了の処理

一括検証ジョブが完了したら、結果をダウンロードして処理します。

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
        });
    }
  });
}

一括ジョブ失敗の処理

ジョブが失敗した場合、エラー情報を取得し、回復が可能かどうかを判断します。

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`);
}

進捗更新の処理

進捗Webhookは、長時間実行されるジョブの追跡に役立ちます。

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);
  }
}

高度なWebhookパターン

本番システムは、信頼性と保守性を向上させる高度なパターンから恩恵を受けます。

失敗したWebhookのデッドレターキュー

Webhookの処理が繰り返し失敗した場合、手動レビューのためにペイロードをデッドレターキューに移動します。

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
  });
});

Webhookイベントソーシング

監査証跡とリプレイ機能のために、すべてのWebhookイベントを保存します。

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);
    }
  }
}

マルチテナントWebhookルーティング

SaaSアプリケーションの場合、Webhookをテナント固有のハンドラにルーティングします。

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)
  });
}

エラー処理と信頼性

堅牢なWebhook実装は、障害を適切に処理し、データが失われないことを保証します。

再試行戦略

一時的な障害に対して指数バックオフを実装します。

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);
}

サーキットブレーカーパターン

ダウンストリームサービスが利用できない場合のカスケード障害を防ぎます。

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);
  });
}

モニタリングとアラート

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);

Webhook実装のテスト

徹底的なテストにより、Webhookハンドラが本番環境で正しく動作することを保証します。

ngrokを使用したローカルテスト

ngrokを使用して、Webhookテスト用にローカルエンドポイントを公開します。

# Start your local server
node server.js

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

開発中は、ngrok URLをWebhookエンドポイントとして登録します。

モックWebhookペイロード

異なるイベントタイプのテストフィクスチャを作成します。

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');
  });
});

統合テスト

署名検証を含む完全なWebhookフローをテストします。

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);
  });
});

BillionVerify Webhook統合

BillionVerifyは、メール検証イベント用の包括的なWebhookサポートを提供し、非同期検証ワークフローの構築を容易にします。

Webhookの設定

BillionVerifyダッシュボードまたはAPIを介してWebhookをセットアップします。

// 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);
}

Webhookペイロード形式

BillionVerify Webhookには、検証イベントに関する包括的な情報が含まれています。

{
  "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"
}

完全な統合例

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');
});

まとめ

メール検証Webhookは、効率的でスケーラブルかつ信頼性の高い非同期処理を可能にすることで、アプリケーションが一括検証を処理する方法を変革します。セキュリティ対策、エラー処理、モニタリングを備えた適切なWebhook処理を実装することで、アプリケーションのニーズに合わせてスケールする堅牢なメール検証ワークフローを構築できます。

メール検証Webhookを実装するための重要なポイント:

  1. 迅速にレスポンスし、ペイロードを非同期に処理する
  2. 署名を検証して、Webhookが正当なソースからのものであることを確認する
  3. 冪等性を実装して、重複配信を安全に処理する
  4. メッセージキューを使用して、大規模で信頼性の高い処理を実現する
  5. Webhookの健全性を監視し、メトリクスとアラートを使用する

数千または数百万のメール検証を処理する場合でも、Webhookは効率的な非同期処理の基盤を提供します。BillionVerifyの包括的なWebhookサポートで今すぐWebhookの実装を開始し、メール検証ワークフローを次のレベルに引き上げましょう。

InstantlySmartlead を使うチームは、キャンペーン前に BillionVerify でリストをクリーニングすることで到達率を大幅に改善できます。

認証プロバイダーを選ぶ前に、精度と速度の面で BillionVerify と ZeroBounce を比較してみてください。

Leo
LeoFounder, BillionVerify
メール検証のインサイト

今すぐ検証を開始

今日から BillionVerify でメール検証を開始しましょう。サインアップすると 100 個の無料クレジットが得られます。クレジットカード不要です。正確なメール検証で、メールマーケティングの ROI を向上させている何千もの企業に参加しましょう。

クレジットカード不要 · 毎日 100+ 無料クレジット · 30 秒で開始

99.9%
精度
Real-time
API 速度
$0.00014
1 通あたり
100/day
永久無料