์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
: ๊ฐ๋ฐ์๋ฅผ ์ํ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ฐ์ด๋
์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
: ๊ฐ๋ฐ์๋ฅผ ์ํ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ฐ์ด๋ Leo Founder, BillionVerify
Dec 14, 2025
๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํ ์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
๊ตฌํ ๋ฐฉ๋ฒ. ์นํ
์ค์ , ๋ณด์, ์ค๋ฅ ์ฒ๋ฆฌ, ๋๋ ๊ฒ์ฆ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ๋ค๋ฃน๋๋ค. ํ๊ตญ์ด โข
์์ฒ ๋๋ ์๋ฐฑ๋ง ๊ฐ์ ์ด๋ฉ์ผ ์ฃผ์๋ฅผ ๊ฒ์ฆํ ๋, ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ๋๊ธฐ์ ์ผ๋ก ๊ธฐ๋ค๋ฆฌ๋ ๊ฒ์ ํ์ค์ ์ด์ง ์์ต๋๋ค. ์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
์ ๊ฒ์ฆ ์์
์ด ์๋ฃ๋๋ฉด ์ ํ๋ฆฌ์ผ์ด์
์ ์๋ฆผ์ ๋ณด๋ด๋ ๋ฐฉ์์ผ๋ก ์ฐ์ํ ์๋ฃจ์
์ ์ ๊ณตํ๋ฉฐ, ์ง์์ ์ธ ํด๋ง์ ํ์์ฑ์ ์ ๊ฑฐํ๊ณ ํจ์จ์ ์ธ ๋น๋๊ธฐ ์ํฌํ๋ก์ฐ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. ์ด ํฌ๊ด์ ์ธ ๊ฐ์ด๋๋ ๊ธฐ๋ณธ ์ค์ ๋ถํฐ ๋๊ท๋ชจ ๊ฒ์ฆ ์์
์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ๊ณ ๊ธ ํจํด๊น์ง, ์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
๊ตฌํ์ ๋ํด ๊ฐ๋ฐ์๊ฐ ์์์ผ ํ ๋ชจ๋ ๊ฒ์ ๋ค๋ฃน๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
์ดํดํ๊ธฐ ์นํ
์ ํน์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ 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}`);
}
}
Leo Founder, BillionVerify
์ด๋ฉ์ผ ๊ฒ์ฆ ์ธ์ฌ์ดํธ
์ค๋ ๊ฒ์ฆ์ ์์ํ์ธ์ BillionVerify๋ก ์ค๋๋ถํฐ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์์ํ์ธ์. ๊ฐ์
ํ๋ฉด ๋ฌด๋ฃ 100 ํฌ๋ ๋ง์ ๋ฐ์ผ์ธ์ - ์ ์ฉ์นด๋ ๋ถํ์. ์ ํํ ์ด๋ฉ์ผ ๊ฒ์ฆ์ผ๋ก ์ด๋ฉ์ผ ๋ง์ผํ
ROI๋ฅผ ๊ฐ์ ํ๋ ์์ฒ ๊ฐ ๊ธฐ์
๊ณผ ํจ๊ปํ์ธ์.
์ ์ฉ์นด๋ ๋ถํ์ ยท ๋งค์ผ 100+ ๋ฌด๋ฃ ํฌ๋ ๋ง ยท 30์ด ์์ ์์
์นํ
์๋ต ๋ชจ๋ฒ ์ฌ๋ก ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋น์ค๋ ์นํ
์๋ํฌ์ธํธ๋ก๋ถํฐ ๋น ๋ฅธ ์๋ต์ ๊ธฐ๋ํฉ๋๋ค. ์๋ํฌ์ธํธ๊ฐ ์๋ตํ๋ ๋ฐ ๋๋ฌด ์ค๋ ๊ฑธ๋ฆฌ๋ฉด, ์๋น์ค๋ ์ ๋ฌ์ด ์คํจํ๋ค๊ณ ๊ฐ์ ํ๊ณ ์ฌ์๋ํ ์ ์์ต๋๋ค.
์นํ
์์ ์ ์ฆ์ ํ์ธํ ๋ค์ ํ์ด๋ก๋๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค:
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.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
);
์นํ
์๋ํฌ์ธํธ ๋ณด์ ์นํ
์๋ํฌ์ธํธ๋ ๊ณต๊ฐ์ ์ผ๋ก ์ ๊ทผ ๊ฐ๋ฅํ๋ฏ๋ก ๋ณด์์ด ํ์์ ์
๋๋ค. ์ ์ ํ ๊ฒ์ฆ ์์ด๋ ๊ณต๊ฒฉ์๊ฐ ๊ฐ์ง ์นํ
ํ์ด๋ก๋๋ฅผ ๋ณด๋ด ์ ํ๋ฆฌ์ผ์ด์
์ ์กฐ์ํ ์ ์์ต๋๋ค.
์๋ช
๊ฒ์ฆ ๋๋ถ๋ถ์ ์ด๋ฉ์ผ ๊ฒ์ฆ ์๋น์ค๋ ๊ณต์ ๋น๋ฐ์ ์ฌ์ฉํ์ฌ 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.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`);
}
์งํ๋ฅ ์
๋ฐ์ดํธ ์ฒ๋ฆฌ ์งํ๋ฅ ์นํ
์ ์ฅ๊ธฐ ์คํ ์์
์ ์ถ์ ํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค:
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/emailverify',
['bulk.completed', 'bulk.failed', 'bulk.progress'],
process.env.EMAILVERIFY_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/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');
});
๊ฒฐ๋ก ์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
์ ํจ์จ์ ์ด๊ณ ํ์ฅ ๊ฐ๋ฅํ๋ฉฐ ์ ๋ขฐํ ์ ์๋ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํจ์ผ๋ก์จ ์ ํ๋ฆฌ์ผ์ด์
์ด ๋๋ ๊ฒ์ฆ์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ๋ณํ์ํต๋๋ค. ๋ณด์ ์กฐ์น, ์ค๋ฅ ์ฒ๋ฆฌ ๋ฐ ๋ชจ๋ํฐ๋ง์ ๊ฐ์ถ ์ ์ ํ ์นํ
์ฒ๋ฆฌ๋ฅผ ๊ตฌํํจ์ผ๋ก์จ ์ ํ๋ฆฌ์ผ์ด์
์ ์๊ตฌ ์ฌํญ์ ๋ง๊ฒ ํ์ฅ๋๋ ๊ฐ๋ ฅํ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ํฌํ๋ก์ฐ๋ฅผ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
์ด๋ฉ์ผ ๊ฒ์ฆ ์นํ
๊ตฌํ์ ์ํ ์ฃผ์ ์ฌํญ:
๋น ๋ฅด๊ฒ ์๋ต ํ๊ณ ํ์ด๋ก๋๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค์๋ช
์ ๊ฒ์ฆ ํ์ฌ ์นํ
์ด ์ ๋นํ ์ถ์ฒ์์ ์๋์ง ํ์ธํฉ๋๋ค๋ฉฑ๋ฑ์ฑ์ ๊ตฌํ ํ์ฌ ์ค๋ณต ์ ๋ฌ์ ์์ ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค๋ฉ์์ง ํ๋ฅผ ์ฌ์ฉ ํ์ฌ ๋๊ท๋ชจ๋ก ์ ๋ขฐํ ์ ์๋ ์ฒ๋ฆฌ๋ฅผ ์ํํฉ๋๋ค์นํ
์ํ๋ฅผ ๋ชจ๋ํฐ๋ง ํ๊ณ ๋ฉํธ๋ฆญ๊ณผ ์๋ฆผ์ ํตํด ๊ด๋ฆฌํฉ๋๋ค์์ฒ ๋๋ ์๋ฐฑ๋ง ๊ฐ์ ์ด๋ฉ์ผ ๊ฒ์ฆ์ ์ฒ๋ฆฌํ๋ , ์นํ
์ ํจ์จ์ ์ธ ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํ ๊ธฐ๋ฐ์ ์ ๊ณตํฉ๋๋ค. BillionVerify์ ํฌ๊ด์ ์ธ ์นํ
์ง์์ผ๋ก ์ค๋ ์นํ
๊ตฌํ์ ์์ํ๊ณ ์ด๋ฉ์ผ ๊ฒ์ฆ ์ํฌํ๋ก์ฐ๋ฅผ ํ ๋จ๊ณ ์
๊ทธ๋ ์ด๋ํ์ธ์.
Instantly ๋๋ Smartlead ๋ฅผ ์ฌ์ฉํ๋ ํ์ ์บ ํ์ธ ์ ์ BillionVerify๋ก ๋ชฉ๋ก์ ์ ๋ฆฌํ์ฌ ์ ๋ฌ์ฑ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค.
์ธ์ฆ ์ ๊ณต์
์ฒด๋ฅผ ์ ํํ๊ธฐ ์ ์ ์ ํ๋์ ์๋ ๋ฉด์์ BillionVerify์ ZeroBounce ๋ฅผ ๋น๊ตํด ๋ณด์ธ์.