수천 또는 수백만 개의 이메일 주소를 검증할 때, 각 결과를 동기적으로 기다리는 것은 현실적이지 않습니다. 이메일 검증 웹훅은 검증 작업이 완료되면 애플리케이션에 알림을 보내는 방식으로 우아한 솔루션을 제공하며, 지속적인 폴링의 필요성을 제거하고 효율적인 비동기 워크플로우를 가능하게 합니다. 이 포괄적인 가이드는 기본 설정부터 대규모 검증 작업을 처리하기 위한 고급 패턴까지, 이메일 검증 웹훅 구현에 대해 개발자가 알아야 할 모든 것을 다룹니다.
이메일 검증 웹훅 이해하기
웹훅은 특정 이벤트가 발생할 때 애플리케이션에 데이터를 전달하는 HTTP 콜백입니다. 이메일 검증 컨텍스트에서 웹훅은 대량 검증 작업이 완료되거나, 개별 이메일 검증이 비동기 모드에서 완료되거나, 검증 프로세스 중에 다른 중요한 이벤트가 발생할 때 시스템에 알립니다.
이메일 검증에 웹훅을 사용하는 이유는?
전통적인 요청-응답 패턴은 단일 이메일 검증에는 잘 작동하지만, 대량 작업은 문제를 일으킵니다. 100,000개의 이메일을 검증하는 데 몇 시간이 걸릴 수 있으며, HTTP 연결을 그렇게 오래 열어두는 것은 불가능합니다. 상태 업데이트를 위한 폴링은 리소스를 낭비하고 불필요한 API 부하를 생성합니다.
폴링 오버헤드 제거
웹훅이 없으면 대량 작업이 완료되었는지 확인하기 위해 API를 반복적으로 쿼리해야 합니다. 이는 불필요한 네트워크 트래픽을 생성하고, API 속도 제한을 소비하며, 애플리케이션에 복잡성을 추가합니다. 웹훅은 필요한 순간에 정확히 알림을 푸시합니다.
실시간 처리
웹훅은 검증이 완료되면 즉각적인 조치를 가능하게 합니다. 애플리케이션은 폴링 간격으로 인한 지연 없이 결과를 처리하고, 데이터베이스를 업데이트하며, 후속 조치를 트리거할 수 있습니다.
확장 가능한 아키텍처
웹훅 기반 아키텍처는 자연스럽게 확장됩니다. 하나의 대량 작업을 처리하든 수백 개를 동시에 처리하든, 웹훅 엔드포인트는 도착하는 대로 알림을 받으며, 큐나 워커를 사용하여 비동기적으로 처리할 수 있습니다.
리소스 효율성
연결을 유지하거나 폴링 루프를 실행하는 대신, 애플리케이션은 웹훅이 도착할 때까지 유휴 상태로 유지됩니다. 이는 컴퓨팅 비용을 줄이고 인프라 요구 사항을 단순화합니다.
이메일 검증의 웹훅 이벤트
이메일 검증 서비스는 일반적으로 여러 이벤트 유형에 대해 웹훅을 트리거합니다:
대량 작업 완료
가장 일반적인 웹훅 이벤트는 대량 검증 작업이 처리를 완료할 때 발생합니다. 페이로드에는 작업 상태, 요약 통계 및 결과 다운로드에 대한 정보가 포함됩니다.
대량 작업 진행률
일부 서비스는 대량 처리 중에 간격을 두고 진행률 웹훅을 전송하여 검증 진행률을 추적하고 완료 시간을 추정할 수 있게 합니다.
대량 작업 실패
대량 작업이 완료를 방해하는 오류를 만났을 때, 실패 웹훅은 무엇이 잘못되었는지, 부분 결과를 사용할 수 있는지에 대한 세부 정보를 제공합니다.
단일 이메일 검증 (비동기 모드)
대량의 실시간 검증 시나리오의 경우, 비동기 단일 이메일 검증은 동기 응답을 기다리는 대신 웹훅을 통해 결과를 전송합니다.
웹훅 엔드포인트 설정하기
웹훅을 구현하려면 웹훅 페이로드를 수신하고 처리할 수 있는 애플리케이션 내 엔드포인트를 만들어야 합니다.
기본 엔드포인트 구조
웹훅 엔드포인트는 단순히 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}`);
}
}
웹훅 응답 모범 사례
이메일 검증 서비스는 웹훅 엔드포인트로부터 빠른 응답을 기대합니다. 엔드포인트가 응답하는 데 너무 오래 걸리면, 서비스는 전달이 실패했다고 가정하고 재시도할 수 있습니다.
즉시 응답하기
웹훅 수신을 즉시 확인한 다음 페이로드를 비동기적으로 처리합니다:
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);
}
});
});
무거운 처리에는 메시지 큐 사용하기
프로덕션 시스템의 경우, 워커 프로세스가 처리할 수 있도록 웹훅 페이로드를 큐에 넣습니다:
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로 웹훅 구성하기
이메일 검증 서비스에 웹훅 엔드포인트를 등록합니다:
async function registerWebhook(webhookUrl, events, secret) {
const response = await fetch('https://api.billionverify.com/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BILLIONVERIFY_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
);
웹훅 엔드포인트 보안
웹훅 엔드포인트는 공개적으로 접근 가능하므로 보안이 필수적입니다. 적절한 검증 없이는 공격자가 가짜 웹훅 페이로드를 보내 애플리케이션을 조작할 수 있습니다.
서명 검증
대부분의 이메일 검증 서비스는 공유 비밀을 사용하여 HMAC-SHA256으로 웹훅 페이로드에 서명합니다. 처리하기 전에 서명을 검증합니다:
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 });
});
타임스탬프 검증
웹훅 타임스탬프를 검증하여 재생 공격을 방지합니다:
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 허용 목록
추가 보안을 위해 웹훅 액세스를 알려진 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
});
멱등성 처리
네트워크 문제나 재시도로 인해 웹훅이 여러 번 전달될 수 있습니다. 중복을 안전하게 처리하기 위해 멱등성을 구현합니다:
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 });
});
웹훅 페이로드 처리하기
각 웹훅 이벤트는 서로 다른 처리 로직이 필요합니다. 이메일 검증 웹훅을 처리하기 위한 일반적인 패턴을 살펴보겠습니다.
대량 작업 완료 처리
대량 검증 작업이 완료되면, 결과를 다운로드하고 처리합니다:
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.BILLIONVERIFY_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.BILLIONVERIFY_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`);
}
진행률 업데이트 처리
진행률 웹훅은 장기 실행 작업을 추적하는 데 도움이 됩니다:
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);
}
}
고급 웹훅 패턴
프로덕션 시스템은 신뢰성과 유지 관리성을 향상시키는 고급 패턴의 이점을 얻습니다.
실패한 웹훅을 위한 데드 레터 큐
웹훅 처리가 반복적으로 실패할 때, 수동 검토를 위해 페이로드를 데드 레터 큐로 이동합니다:
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
});
});
웹훅 이벤트 소싱
감사 추적 및 재생 기능을 위해 모든 웹훅 이벤트를 저장합니다:
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);
}
}
}
멀티 테넌트 웹훅 라우팅
SaaS 애플리케이션의 경우, 웹훅을 테넌트별 핸들러로 라우팅합니다:
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)
});
}
오류 처리 및 신뢰성
강력한 웹훅 구현은 실패를 우아하게 처리하고 데이터 손실이 없도록 보장합니다.
재시도 전략
일시적 실패에 대해 지수 백오프를 구현합니다:
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);
});
}
모니터링 및 알림
웹훅 상태 메트릭을 추적합니다:
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);
웹훅 구현 테스트
철저한 테스트는 웹훅 핸들러가 프로덕션에서 올바르게 작동하도록 보장합니다.
ngrok으로 로컬 테스트
웹훅 테스트를 위해 로컬 엔드포인트를 노출하려면 ngrok을 사용합니다:
# Start your local server node server.js # In another terminal, expose it via ngrok ngrok http 3000
개발 중에 ngrok URL을 웹훅 엔드포인트로 등록합니다.
모의 웹훅 페이로드
다양한 이벤트 유형에 대한 테스트 픽스처를 생성합니다:
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');
});
});
통합 테스트
서명 검증을 포함한 전체 웹훅 플로우를 테스트합니다:
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 웹훅 통합
BillionVerify는 이메일 검증 이벤트에 대한 포괄적인 웹훅 지원을 제공하여 비동기 검증 워크플로우를 쉽게 구축할 수 있습니다.
웹훅 구성
BillionVerify 대시보드 또는 API를 통해 웹훅을 설정합니다:
// Register webhook via API
async function setupBillionVerifyWebhooks() {
const webhook = await registerWebhook(
'https://yourapp.com/webhooks/billionverify',
['bulk.completed', 'bulk.failed', 'bulk.progress'],
process.env.BILLIONVERIFY_WEBHOOK_SECRET
);
console.log('Webhook configured:', webhook);
}
웹훅 페이로드 형식
BillionVerify 웹훅에는 검증 이벤트에 대한 포괄적인 정보가 포함됩니다:
{
"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/billionverify', async (req, res) => {
// Verify signature
const signature = req.headers['x-billionverify-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');
});
결론
이메일 검증 웹훅은 효율적이고 확장 가능하며 신뢰할 수 있는 비동기 처리를 가능하게 함으로써 애플리케이션이 대량 검증을 처리하는 방식을 변화시킵니다. 보안 조치, 오류 처리 및 모니터링을 갖춘 적절한 웹훅 처리를 구현함으로써 애플리케이션의 요구 사항에 맞게 확장되는 강력한 이메일 검증 워크플로우를 구축할 수 있습니다.
이메일 검증 웹훅 구현을 위한 주요 사항:
- 빠르게 응답하고 페이로드를 비동기적으로 처리합니다
- 서명을 검증하여 웹훅이 정당한 출처에서 왔는지 확인합니다
- 멱등성을 구현하여 중복 전달을 안전하게 처리합니다
- 메시지 큐를 사용하여 대규모로 신뢰할 수 있는 처리를 수행합니다
- 웹훅 상태를 모니터링하고 메트릭과 알림을 통해 관리합니다
수천 또는 수백만 개의 이메일 검증을 처리하든, 웹훅은 효율적인 비동기 처리를 위한 기반을 제공합니다. BillionVerify의 포괄적인 웹훅 지원으로 오늘 웹훅 구현을 시작하고 이메일 검증 워크플로우를 한 단계 업그레이드하세요.