рдЬрдм рдЖрдк рд╣рдЬрд╛рд░реЛрдВ рдпрд╛ рд▓рд╛рдЦреЛрдВ рдИрдореЗрд▓ рдкрддреЛрдВ рдХреЛ рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░ рд░рд╣реЗ рд╣реЛрдВ, рддреЛ рдкреНрд░рддреНрдпреЗрдХ рдкрд░рд┐рдгрд╛рдо рдХреЗ рд▓рд┐рдП рд╕рд┐рдВрдХреНрд░реЛрдирд╕ рд░реВрдк рд╕реЗ рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░рдирд╛ рд╡реНрдпрд╛рд╡рд╣рд╛рд░рд┐рдХ рдирд╣реАрдВ рд╣реИред рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди webhooks рдПрдХ рд╕реБрд░реБрдЪрд┐рдкреВрд░реНрдг рд╕рдорд╛рдзрд╛рди рдкреНрд░рджрд╛рди рдХрд░рддреЗ рд╣реИрдВ рдЬреЛ рдЖрдкрдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд╕реВрдЪрд┐рдд рдХрд░рддреЗ рд╣реИрдВ рдЬрдм рд╕рддреНрдпрд╛рдкрди рдХрд╛рд░реНрдп рдкреВрд░реНрдг рд╣реЛ рдЬрд╛рддреЗ рд╣реИрдВ, рдирд┐рд░рдВрддрд░ polling рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдХреЛ рд╕рдорд╛рдкреНрдд рдХрд░рддреЗ рд╣реИрдВ рдФрд░ рдХреБрд╢рд▓ рдЕрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рд╡рд░реНрдХрдлрд╝реНрд▓реЛ рдХреЛ рд╕рдХреНрд╖рдо рдХрд░рддреЗ рд╣реИрдВред рдпрд╣ рд╡реНрдпрд╛рдкрдХ рдЧрд╛рдЗрдб рдмреБрдирд┐рдпрд╛рджреА рд╕реЗрдЯрдЕрдк рд╕реЗ рд▓реЗрдХрд░ рдмрдбрд╝реЗ рдкреИрдорд╛рдиреЗ рдкрд░ рд╕рддреНрдпрд╛рдкрди рд╕рдВрдЪрд╛рд▓рди рдХреЛ рд╕рдВрднрд╛рд▓рдиреЗ рдХреЗ рд▓рд┐рдП рдЙрдиреНрдирдд рдкреИрдЯрд░реНрди рддрдХ, рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди webhooks рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдбреЗрд╡рд▓рдкрд░реНрд╕ рдХреЛ рдЬрд╛рдирдиреЗ рдХреЗ рд▓рд┐рдП рдЖрд╡рд╢реНрдпрдХ рд╕рдм рдХреБрдЫ рдкреНрд░рд╕реНрддреБрдд рдХрд░рддреА рд╣реИред
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди Webhooks рдХреЛ рд╕рдордЭрдирд╛
Webhooks HTTP callbacks рд╣реИрдВ рдЬреЛ рд╡рд┐рд╢рд┐рд╖реНрдЯ рдШрдЯрдирд╛рдУрдВ рдХреЗ рдШрдЯрд┐рдд рд╣реЛрдиреЗ рдкрд░ рдЖрдкрдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рдбреЗрдЯрд╛ рд╡рд┐рддрд░рд┐рдд рдХрд░рддреЗ рд╣реИрдВред рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рдХреЗ рд╕рдВрджрд░реНрдн рдореЗрдВ, webhooks рдЖрдкрдХреЗ рд╕рд┐рд╕реНрдЯрдо рдХреЛ рд╕реВрдЪрд┐рдд рдХрд░рддреЗ рд╣реИрдВ рдЬрдм рдмрд▓реНрдХ рд╕рддреНрдпрд╛рдкрди рдХрд╛рд░реНрдп рдкреВрд░реНрдг рд╣реЛрддреЗ рд╣реИрдВ, рдЬрдм async рдореЛрдб рдореЗрдВ рд╡реНрдпрдХреНрддрд┐рдЧрдд рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╕рдорд╛рдкреНрдд рд╣реЛрддрд╛ рд╣реИ, рдпрд╛ рдЬрдм рд╕рддреНрдпрд╛рдкрди рдкреНрд░рдХреНрд░рд┐рдпрд╛ рдХреЗ рджреМрд░рд╛рди рдЕрдиреНрдп рдорд╣рддреНрд╡рдкреВрд░реНрдг рдШрдЯрдирд╛рдПрдВ рдШрдЯрд┐рдд рд╣реЛрддреА рд╣реИрдВред
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рдХреЗ рд▓рд┐рдП Webhooks рдХрд╛ рдЙрдкрдпреЛрдЧ рдХреНрдпреЛрдВ рдХрд░реЗрдВ?
рдкрд╛рд░рдВрдкрд░рд┐рдХ request-response рдкреИрдЯрд░реНрди рдПрдХрд▓ рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рдХреЗ рд▓рд┐рдП рдЕрдЪреНрдЫреА рддрд░рд╣ рд╕реЗ рдХрд╛рдо рдХрд░рддреЗ рд╣реИрдВ, рд▓реЗрдХрд┐рди рдмрд▓реНрдХ рдСрдкрд░реЗрд╢рди рдЪреБрдиреМрддрд┐рдпрд╛рдВ рдкреНрд░рд╕реНрддреБрдд рдХрд░рддреЗ рд╣реИрдВред 100,000 рдИрдореЗрд▓ рдХреЛ рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░рдиреЗ рдореЗрдВ рдШрдВрдЯреЛрдВ рд▓рдЧ рд╕рдХрддреЗ рд╣реИрдВ, рдФрд░ рдПрдХ HTTP рдХрдиреЗрдХреНрд╢рди рдХреЛ рдЗрддрдиреЗ рд▓рдВрдмреЗ рд╕рдордп рддрдХ рдЦреБрд▓рд╛ рд░рдЦрдирд╛ рд╕рдВрднрд╡ рдирд╣реАрдВ рд╣реИред рд╕реНрдЯреЗрдЯрд╕ рдЕрдкрдбреЗрдЯ рдХреЗ рд▓рд┐рдП polling рдХрд░рдирд╛ рд╕рдВрд╕рд╛рдзрдиреЛрдВ рдХреЛ рдмрд░реНрдмрд╛рдж рдХрд░рддрд╛ рд╣реИ рдФрд░ рдЕрдирд╛рд╡рд╢реНрдпрдХ API рд▓реЛрдб рдмрдирд╛рддрд╛ рд╣реИред
Polling рдУрд╡рд░рд╣реЗрдб рдХреЛ рд╕рдорд╛рдкреНрдд рдХрд░рдирд╛
Webhooks рдХреЗ рдмрд┐рдирд╛, рдЖрдкрдХреЛ рдмрд▓реНрдХ рдХрд╛рд░реНрдп рдкреВрд░реНрдг рд╣реЛ рдЧрдП рд╣реИрдВ рдпрд╛ рдирд╣реАрдВ рдпрд╣ рдЬрд╛рдВрдЪрдиреЗ рдХреЗ рд▓рд┐рдП API рдХреЛ рдмрд╛рд░-рдмрд╛рд░ query рдХрд░рдирд╛ рд╣реЛрдЧрд╛ред рдпрд╣ рдЕрдирд╛рд╡рд╢реНрдпрдХ рдиреЗрдЯрд╡рд░реНрдХ рдЯреНрд░реИрдлрд╝рд┐рдХ рдмрдирд╛рддрд╛ рд╣реИ, API рджрд░ рд╕реАрдорд╛рдУрдВ рдХрд╛ рдЙрдкрднреЛрдЧ рдХрд░рддрд╛ рд╣реИ, рдФрд░ рдЖрдкрдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдЬрдЯрд┐рд▓рддрд╛ рдЬреЛрдбрд╝рддрд╛ рд╣реИред Webhooks рдЖрдкрдХреЛ рд╕реВрдЪрдирд╛рдПрдВ рддрднреА push рдХрд░рддреЗ рд╣реИрдВ рдЬрдм рдЙрдирдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛред
рд░реАрдпрд▓-рдЯрд╛рдЗрдо рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ
Webhooks рд╕рддреНрдпрд╛рдкрди рдкреВрд░реНрдг рд╣реЛрдиреЗ рдкрд░ рддреБрд░рдВрдд рдХрд╛рд░реНрд░рд╡рд╛рдИ рд╕рдХреНрд╖рдо рдХрд░рддреЗ рд╣реИрдВред рдЖрдкрдХрд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдкрд░рд┐рдгрд╛рдореЛрдВ рдХреЛ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░ рд╕рдХрддрд╛ рд╣реИ, рдбреЗрдЯрд╛рдмреЗрд╕ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд░ рд╕рдХрддрд╛ рд╣реИ, рдФрд░ polling рдЕрдВрддрд░рд╛рд▓ рджреНрд╡рд╛рд░рд╛ рдкреЗрд╢ рдХреА рдЧрдИ рджреЗрд░реА рдХреЗ рдмрд┐рдирд╛ рдЕрдиреБрд╡рд░реНрддреА рдХрд╛рд░реНрд░рд╡рд╛рдИ рдХрд░ рд╕рдХрддрд╛ рд╣реИред
рд╕реНрдХреЗрд▓реЗрдмрд▓ рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░
Webhook-рдЖрдзрд╛рд░рд┐рдд рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░ рд╕реНрд╡рд╛рднрд╛рд╡рд┐рдХ рд░реВрдк рд╕реЗ рд╕реНрдХреЗрд▓ рдХрд░рддреЗ рд╣реИрдВред рдЪрд╛рд╣реЗ рдЖрдк рдПрдХ рдмрд▓реНрдХ рдХрд╛рд░реНрдп рд╕рдВрд╕рд╛рдзрд┐рдд рдХрд░ рд░рд╣реЗ рд╣реЛрдВ рдпрд╛ рд╕реИрдХрдбрд╝реЛрдВ рдПрдХ рд╕рд╛рде, рдЖрдкрдХрд╛ webhook endpoint рд╕реВрдЪрдирд╛рдПрдВ рдкреНрд░рд╛рдкреНрдд рдХрд░рддрд╛ рд╣реИ рдЬреИрд╕реЗ рд╣реА рд╡реЗ рдЖрддреА рд╣реИрдВ, рдФрд░ рдЖрдк рдЙрдиреНрд╣реЗрдВ queues рдпрд╛ workers рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдЕрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рд░реВрдк рд╕реЗ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВред
рд╕рдВрд╕рд╛рдзрди рджрдХреНрд╖рддрд╛
рдХрдиреЗрдХреНрд╢рди рдмрдирд╛рдП рд░рдЦрдиреЗ рдпрд╛ polling loops рдЪрд▓рд╛рдиреЗ рдХреЗ рдмрдЬрд╛рдп, рдЖрдкрдХрд╛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдирд┐рд╖реНрдХреНрд░рд┐рдп рд░рд╣рддрд╛ рд╣реИ рдЬрдм рддрдХ webhooks рдирд╣реАрдВ рдЖрддреЗред рдпрд╣ compute рд▓рд╛рдЧрдд рдХреЛ рдХрдо рдХрд░рддрд╛ рд╣реИ рдФрд░ infrastructure рдЖрд╡рд╢реНрдпрдХрддрд╛рдУрдВ рдХреЛ рд╕рд░рд▓ рдмрдирд╛рддрд╛ рд╣реИред
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рдореЗрдВ Webhook рдШрдЯрдирд╛рдПрдВ
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╕реЗрд╡рд╛рдПрдВ рдЖрдорддреМрд░ рдкрд░ рдХрдИ рдШрдЯрдирд╛ рдкреНрд░рдХрд╛рд░реЛрдВ рдХреЗ рд▓рд┐рдП webhooks рдЯреНрд░рд┐рдЧрд░ рдХрд░рддреА рд╣реИрдВ:
рдмрд▓реНрдХ рдХрд╛рд░реНрдп рдкреВрд░реНрдгрддрд╛
рд╕рдмрд╕реЗ рд╕рд╛рдорд╛рдиреНрдп webhook рдШрдЯрдирд╛ рддрдм рд╕рдХреНрд░рд┐рдп рд╣реЛрддреА рд╣реИ рдЬрдм рдПрдХ рдмрд▓реНрдХ рд╕рддреНрдпрд╛рдкрди рдХрд╛рд░реНрдп рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рд╕рдорд╛рдкреНрдд рдХрд░ рд▓реЗрддрд╛ рд╣реИред Payload рдореЗрдВ рдХрд╛рд░реНрдп рд╕реНрдерд┐рддрд┐, рд╕рд╛рд░рд╛рдВрд╢ рд╕рд╛рдВрдЦреНрдпрд┐рдХреА, рдФрд░ рдкрд░рд┐рдгрд╛рдо рдбрд╛рдЙрдирд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рдЬрд╛рдирдХрд╛рд░реА рд╢рд╛рдорд┐рд▓ рд╣реЛрддреА рд╣реИред
рдмрд▓реНрдХ рдХрд╛рд░реНрдп рдкреНрд░рдЧрддрд┐
рдХреБрдЫ рд╕реЗрд╡рд╛рдПрдВ рдмрд▓реНрдХ рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдХреЗ рджреМрд░рд╛рди рдЕрдВрддрд░рд╛рд▓ рдкрд░ рдкреНрд░рдЧрддрд┐ webhooks рднреЗрдЬрддреА рд╣реИрдВ, рдЬрд┐рд╕рд╕реЗ рдЖрдк рд╕рддреНрдпрд╛рдкрди рдкреНрд░рдЧрддрд┐ рдХреЛ рдЯреНрд░реИрдХ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдФрд░ рдкреВрд░реНрдгрддрд╛ рд╕рдордп рдХрд╛ рдЕрдиреБрдорд╛рди рд▓рдЧрд╛ рд╕рдХрддреЗ рд╣реИрдВред
рдмрд▓реНрдХ рдХрд╛рд░реНрдп рд╡рд┐рдлрд▓рддрд╛
рдЬрдм рдПрдХ рдмрд▓реНрдХ рдХрд╛рд░реНрдп рддреНрд░реБрдЯрд┐рдпреЛрдВ рдХрд╛ рд╕рд╛рдордирд╛ рдХрд░рддрд╛ рд╣реИ рдЬреЛ рдкреВрд░реНрдгрддрд╛ рдХреЛ рд░реЛрдХрддреА рд╣реИрдВ, рд╡рд┐рдлрд▓рддрд╛ webhooks рд╡рд┐рд╡рд░рдг рдкреНрд░рджрд╛рди рдХрд░рддреЗ рд╣реИрдВ рдХрд┐ рдХреНрдпрд╛ рдЧрд▓рдд рд╣реБрдЖ рдФрд░ рдХреНрдпрд╛ рдЖрдВрд╢рд┐рдХ рдкрд░рд┐рдгрд╛рдо рдЙрдкрд▓рдмреНрдз рд╣реИрдВред
рдПрдХрд▓ рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди (Async рдореЛрдб)
рдЙрдЪреНрдЪ-рдорд╛рддреНрд░рд╛ рд░реАрдпрд▓-рдЯрд╛рдЗрдо рд╕рддреНрдпрд╛рдкрди рдкрд░рд┐рджреГрд╢реНрдпреЛрдВ рдХреЗ рд▓рд┐рдП, async рдПрдХрд▓-рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╕рд┐рдВрдХреНрд░реЛрдирд╕ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдХреА рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░рдиреЗ рдХреЗ рдмрдЬрд╛рдп webhook рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдкрд░рд┐рдгрд╛рдо рднреЗрдЬрддрд╛ рд╣реИред
Webhook Endpoints рд╕реЗрдЯрдЕрдк рдХрд░рдирд╛
Webhooks рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЖрдкрдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдореЗрдВ рдПрдХ endpoint рдмрдирд╛рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИ рдЬреЛ webhook payloads рдХреЛ рдкреНрд░рд╛рдкреНрдд рдФрд░ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░ рд╕рдХреЗред
рдмреБрдирд┐рдпрд╛рджреА Endpoint рд╕рдВрд░рдЪрдирд╛
рдПрдХ webhook endpoint рдХреЗрд╡рд▓ рдПрдХ HTTP POST endpoint рд╣реИ рдЬреЛ JSON payloads рд╕реНрд╡реАрдХрд╛рд░ рдХрд░рддрд╛ рд╣реИ:
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 endpoints рд╕реЗ рддреНрд╡рд░рд┐рдд рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдХреА рдЕрдкреЗрдХреНрд╖рд╛ рдХрд░рддреА рд╣реИрдВред рдпрджрд┐ рдЖрдкрдХрд╛ endpoint рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджреЗрдиреЗ рдореЗрдВ рдмрд╣реБрдд рдЕрдзрд┐рдХ рд╕рдордп рд▓реЗрддрд╛ рд╣реИ, рддреЛ рд╕реЗрд╡рд╛ рдпрд╣ рдорд╛рди рд╕рдХрддреА рд╣реИ рдХрд┐ рд╡рд┐рддрд░рдг рд╡рд┐рдлрд▓ рд╣реЛ рдЧрдпрд╛ рдФрд░ рдкреБрдирдГ рдкреНрд░рдпрд╛рд╕ рдХрд░ рд╕рдХрддреА рд╣реИред
рддреБрд░рдВрдд рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджреЗрдВ
Webhook рдкреНрд░рд╛рдкреНрддрд┐ рдХреЛ рддреБрд░рдВрдд рд╕реНрд╡реАрдХрд╛рд░ рдХрд░реЗрдВ, рдлрд┐рд░ payload рдХреЛ рдЕрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рд░реВрдк рд╕реЗ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░реЗрдВ:
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);
}
});
});
рднрд╛рд░реА рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдХреЗ рд▓рд┐рдП Message Queues рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ
рдкреНрд░реЛрдбрдХреНрд╢рди рд╕рд┐рд╕реНрдЯрдо рдХреЗ рд▓рд┐рдП, worker рдкреНрд░рдХреНрд░рд┐рдпрд╛рдУрдВ рджреНрд╡рд╛рд░рд╛ рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдХреЗ рд▓рд┐рдП webhook payloads рдХреЛ queue рдХрд░реЗрдВ:
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 рдХреЗ рд╕рд╛рде Webhooks рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░рдирд╛
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╕реЗрд╡рд╛ рдХреЗ рд╕рд╛рде рдЕрдкрдиреЗ webhook endpoint рдХреЛ рдкрдВрдЬреАрдХреГрдд рдХрд░реЗрдВ:
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
);
Webhook Endpoints рдХреЛ рд╕реБрд░рдХреНрд╖рд┐рдд рдХрд░рдирд╛
Webhook endpoints рд╕рд╛рд░реНрд╡рдЬрдирд┐рдХ рд░реВрдк рд╕реЗ рд╕реБрд▓рдн рд╣реИрдВ, рдЬрд┐рд╕рд╕реЗ рд╕реБрд░рдХреНрд╖рд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реЛ рдЬрд╛рддреА рд╣реИред рдЙрдЪрд┐рдд рд╕рддреНрдпрд╛рдкрди рдХреЗ рдмрд┐рдирд╛, рд╣рдорд▓рд╛рд╡рд░ рдЖрдкрдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд╣реЗрд░рдлреЗрд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдирдХрд▓реА webhook payloads рднреЗрдЬ рд╕рдХрддреЗ рд╣реИрдВред
рд╣рд╕реНрддрд╛рдХреНрд╖рд░ рд╕рддреНрдпрд╛рдкрди
рдЕрдзрд┐рдХрд╛рдВрд╢ рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╕реЗрд╡рд╛рдПрдВ рд╕рд╛рдЭрд╛ secret рдХреЗ рд╕рд╛рде HMAC-SHA256 рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ webhook payloads рдкрд░ рд╣рд╕реНрддрд╛рдХреНрд╖рд░ рдХрд░рддреА рд╣реИрдВред рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рд╕реЗ рдкрд╣рд▓реЗ рд╣рд╕реНрддрд╛рдХреНрд╖рд░ рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░реЗрдВ:
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 });
});
Timestamp рд╕рддреНрдпрд╛рдкрди
Webhook timestamps рдХреЛ рдорд╛рдиреНрдп рдХрд░рдХреЗ replay рд╣рдорд▓реЛрдВ рдХреЛ рд░реЛрдХреЗрдВ:
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
рдЕрддрд┐рд░рд┐рдХреНрдд рд╕реБрд░рдХреНрд╖рд╛ рдХреЗ рд▓рд┐рдП, 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
});
Idempotency рдкреНрд░рдмрдВрдзрди
рдиреЗрдЯрд╡рд░реНрдХ рд╕рдорд╕реНрдпрд╛рдУрдВ рдпрд╛ рдкреБрдирдГ рдкреНрд░рдпрд╛рд╕реЛрдВ рдХреЗ рдХрд╛рд░рдг Webhooks рдХрдИ рдмрд╛рд░ рд╡рд┐рддрд░рд┐рдд рдХрд┐рдП рдЬрд╛ рд╕рдХрддреЗ рд╣реИрдВред рдбреБрдкреНрд▓рд┐рдХреЗрдЯ рдХреЛ рд╕реБрд░рдХреНрд╖рд┐рдд рд░реВрдк рд╕реЗ рд╕рдВрднрд╛рд▓рдиреЗ рдХреЗ рд▓рд┐рдП idempotency рд▓рд╛рдЧреВ рдХрд░реЗрдВ:
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;
}
}
рдкреНрд░реЛрдбрдХреНрд╢рди рд╕рд┐рд╕реНрдЯрдо рдХреЗ рд▓рд┐рдП, рд╡рд┐рддрд░рд┐рдд idempotency рдХреЗ рд▓рд┐рдП 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 Payloads рдХреЛ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░рдирд╛
рд╡рд┐рднрд┐рдиреНрди webhook рдШрдЯрдирд╛рдУрдВ рдХреЛ рд╡рд┐рднрд┐рдиреНрди рдкреНрд░рдмрдВрдзрди рддрд░реНрдХ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИред рдЖрдЗрдП рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди webhooks рдХреЛ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕рд╛рдорд╛рдиреНрдп рдкреИрдЯрд░реНрди рджреЗрдЦреЗрдВред
рдмрд▓реНрдХ рдХрд╛рд░реНрдп рдкреВрд░реНрдгрддрд╛ рдХреЛ рд╕рдВрднрд╛рд▓рдирд╛
рдЬрдм рдПрдХ рдмрд▓реНрдХ рд╕рддреНрдпрд╛рдкрди рдХрд╛рд░реНрдп рдкреВрд░реНрдг рд╣реЛрддрд╛ рд╣реИ, рддреЛ рдкрд░рд┐рдгрд╛рдореЛрдВ рдХреЛ рдбрд╛рдЙрдирд▓реЛрдб рдФрд░ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░реЗрдВ:
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`);
}
рдкреНрд░рдЧрддрд┐ рдЕрдкрдбреЗрдЯ рдХреЛ рд╕рдВрднрд╛рд▓рдирд╛
рдкреНрд░рдЧрддрд┐ webhooks рд▓рдВрдмреЗ рд╕рдордп рддрдХ рдЪрд▓рдиреЗ рд╡рд╛рд▓реЗ рдХрд╛рд░реНрдпреЛрдВ рдХреЛ рдЯреНрд░реИрдХ рдХрд░рдиреЗ рдореЗрдВ рдорджрдж рдХрд░рддреЗ рд╣реИрдВ:
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 рдкреИрдЯрд░реНрди
рдкреНрд░реЛрдбрдХреНрд╢рди рд╕рд┐рд╕реНрдЯрдо рдЙрдиреНрдирдд рдкреИрдЯрд░реНрди рд╕реЗ рд▓рд╛рднрд╛рдиреНрд╡рд┐рдд рд╣реЛрддреЗ рд╣реИрдВ рдЬреЛ рд╡рд┐рд╢реНрд╡рд╕рдиреАрдпрддрд╛ рдФрд░ рд░рдЦрд░рдЦрд╛рд╡ рдпреЛрдЧреНрдпрддрд╛ рдореЗрдВ рд╕реБрдзрд╛рд░ рдХрд░рддреЗ рд╣реИрдВред
рд╡рд┐рдлрд▓ Webhooks рдХреЗ рд▓рд┐рдП Dead Letter Queue
рдЬрдм webhook рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдмрд╛рд░-рдмрд╛рд░ рд╡рд┐рдлрд▓ рд╣реЛрддреА рд╣реИ, рддреЛ рдореИрдиреБрдЕрд▓ рд╕рдореАрдХреНрд╖рд╛ рдХреЗ рд▓рд┐рдП payloads рдХреЛ dead letter queue рдореЗрдВ рд▓реЗ рдЬрд╛рдПрдВ:
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 Event Sourcing
рдСрдбрд┐рдЯ рдЯреНрд░реЗрд▓ рдФрд░ replay рдХреНрд╖рдорддрд╛ рдХреЗ рд▓рд┐рдП рд╕рднреА 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 рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд▓рд┐рдП, webhooks рдХреЛ tenant-рд╡рд┐рд╢рд┐рд╖реНрдЯ handlers рдореЗрдВ рд░реВрдЯ рдХрд░реЗрдВ:
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 рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рд╡рд┐рдлрд▓рддрд╛рдУрдВ рдХреЛ рд╕реБрдЪрд╛рд░реВ рд░реВрдк рд╕реЗ рд╕рдВрднрд╛рд▓рддреЗ рд╣реИрдВ рдФрд░ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░рддреЗ рд╣реИрдВ рдХрд┐ рдХреЛрдИ рдбреЗрдЯрд╛ рдЦреЛ рдирд╣реАрдВ рдЧрдпрд╛ рд╣реИред
рдкреБрдирдГ рдкреНрд░рдпрд╛рд╕ рд░рдгрдиреАрддрд┐рдпрд╛рдВ
рдХреНрд╖рдгрд┐рдХ рд╡рд┐рдлрд▓рддрд╛рдУрдВ рдХреЗ рд▓рд┐рдП exponential backoff рд▓рд╛рдЧреВ рдХрд░реЗрдВ:
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);
}
Circuit Breaker рдкреИрдЯрд░реНрди
рдЬрдм downstream рд╕реЗрд╡рд╛рдПрдВ рдЕрдиреБрдкрд▓рдмреНрдз рд╣реЛрдВ рддреЛ cascade рд╡рд┐рдлрд▓рддрд╛рдУрдВ рдХреЛ рд░реЛрдХреЗрдВ:
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 handlers рдкреНрд░реЛрдбрдХреНрд╢рди рдореЗрдВ рд╕рд╣реА рддрд░реАрдХреЗ рд╕реЗ рдХрд╛рдо рдХрд░рддреЗ рд╣реИрдВред
ngrok рдХреЗ рд╕рд╛рде рд╕реНрдерд╛рдиреАрдп рдкрд░реАрдХреНрд╖рдг
Webhook рдкрд░реАрдХреНрд╖рдг рдХреЗ рд▓рд┐рдП рд╕реНрдерд╛рдиреАрдп endpoints рдХреЛ expose рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП ngrok рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ:
# Start your local server node server.js # In another terminal, expose it via ngrok ngrok http 3000
рд╡рд┐рдХрд╛рд╕ рдХреЗ рджреМрд░рд╛рди рдЕрдкрдиреЗ webhook endpoint рдХреЗ рд░реВрдк рдореЗрдВ ngrok URL рдкрдВрдЬреАрдХреГрдд рдХрд░реЗрдВред
Mock Webhook Payloads
рд╡рд┐рднрд┐рдиреНрди рдШрдЯрдирд╛ рдкреНрд░рдХрд╛рд░реЛрдВ рдХреЗ рд▓рд┐рдП рдкрд░реАрдХреНрд╖рдг fixtures рдмрдирд╛рдПрдВ:
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 рд╕рдорд░реНрдерди рдкреНрд░рджрд╛рди рдХрд░рддрд╛ рд╣реИ, рдЬрд┐рд╕рд╕реЗ рдЕрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рд╕рддреНрдпрд╛рдкрди рд╡рд░реНрдХрдлрд╝реНрд▓реЛ рдмрдирд╛рдирд╛ рдЖрд╕рд╛рди рд╣реЛ рдЬрд╛рддрд╛ рд╣реИред
Webhooks рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░рдирд╛
BillionVerify dashboard рдпрд╛ API рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ webhooks рд╕реЗрдЯрдЕрдк рдХрд░реЗрдВ:
// 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);
}
Webhook Payload рдлреЙрд░реНрдореЗрдЯ
BillionVerify webhooks рд╕рддреНрдпрд╛рдкрди рдШрдЯрдирд╛рдУрдВ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ рд╡реНрдпрд╛рдкрдХ рдЬрд╛рдирдХрд╛рд░реА рд╢рд╛рдорд┐рд▓ рдХрд░рддреЗ рд╣реИрдВ:
{
"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');
});
рдирд┐рд╖реНрдХрд░реНрд╖
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди webhooks рдмрд▓реНрдХ рд╕рддреНрдпрд╛рдкрди рдХреЛ рд╕рдВрднрд╛рд▓рдиреЗ рдХреЗ рддрд░реАрдХреЗ рдХреЛ рд░реВрдкрд╛рдВрддрд░рд┐рдд рдХрд░рддреЗ рд╣реИрдВ, рдХреБрд╢рд▓, рд╕реНрдХреЗрд▓реЗрдмрд▓ рдФрд░ рд╡рд┐рд╢реНрд╡рд╕рдиреАрдп рдЕрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдХреЛ рд╕рдХреНрд╖рдо рдХрд░рдХреЗред рд╕реБрд░рдХреНрд╖рд╛ рдЙрдкрд╛рдпреЛрдВ, рддреНрд░реБрдЯрд┐ рдкреНрд░рдмрдВрдзрди рдФрд░ рдирд┐рдЧрд░рд╛рдиреА рдХреЗ рд╕рд╛рде рдЙрдЪрд┐рдд webhook рдкреНрд░рдмрдВрдзрди рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдХреЗ, рдЖрдк рдордЬрдмреВрдд рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╡рд░реНрдХрдлрд╝реНрд▓реЛ рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВ рдЬреЛ рдЖрдкрдХреЗ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреА рдЬрд░реВрд░рддреЛрдВ рдХреЗ рд╕рд╛рде рд╕реНрдХреЗрд▓ рдХрд░рддреЗ рд╣реИрдВред
рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди webhooks рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдореБрдЦреНрдп рдмрд┐рдВрджреБ:
- Webhook рдЕрдиреБрд░реЛрдзреЛрдВ рдкрд░ рддреНрд╡рд░рд┐рдд рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рджреЗрдВ рдФрд░ payloads рдХреЛ рдЕрд╕рд┐рдВрдХреНрд░реЛрдирд╕ рд░реВрдк рд╕реЗ рдкреНрд░реЛрд╕реЗрд╕ рдХрд░реЗрдВ
- рдпрд╣ рд╕реБрдирд┐рд╢реНрдЪрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╣рд╕реНрддрд╛рдХреНрд╖рд░ рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░реЗрдВ рдХрд┐ webhooks рд╡реИрдз рд╕реНрд░реЛрддреЛрдВ рд╕реЗ рдЖрддреЗ рд╣реИрдВ
- рдбреБрдкреНрд▓рд┐рдХреЗрдЯ рд╡рд┐рддрд░рдг рдХреЛ рд╕реБрд░рдХреНрд╖рд┐рдд рд░реВрдк рд╕реЗ рд╕рдВрднрд╛рд▓рдиреЗ рдХреЗ рд▓рд┐рдП idempotency рд▓рд╛рдЧреВ рдХрд░реЗрдВ
- рдкреИрдорд╛рдиреЗ рдкрд░ рд╡рд┐рд╢реНрд╡рд╕рдиреАрдп рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдХреЗ рд▓рд┐рдП message queues рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ
- рдореЗрдЯреНрд░рд┐рдХреНрд╕ рдФрд░ рдЪреЗрддрд╛рд╡рдиреА рдХреЗ рд╕рд╛рде webhook рд╕реНрд╡рд╛рд╕реНрдереНрдп рдХреА рдирд┐рдЧрд░рд╛рдиреА рдХрд░реЗрдВ
рдЪрд╛рд╣реЗ рдЖрдк рд╣рдЬрд╛рд░реЛрдВ рдпрд╛ рд▓рд╛рдЦреЛрдВ рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рдкреНрд░реЛрд╕реЗрд╕ рдХрд░ рд░рд╣реЗ рд╣реЛрдВ, webhooks рдХреБрд╢рд▓ async рдкреНрд░реЛрд╕реЗрд╕рд┐рдВрдЧ рдХреЗ рд▓рд┐рдП рдиреАрдВрд╡ рдкреНрд░рджрд╛рди рдХрд░рддреЗ рд╣реИрдВред BillionVerify рдХреЗ рд╡реНрдпрд╛рдкрдХ webhook рд╕рдорд░реНрдерди рдХреЗ рд╕рд╛рде рдЖрдЬ рд╣реА webhooks рдХреЛ рд▓рд╛рдЧреВ рдХрд░рдирд╛ рд╢реБрд░реВ рдХрд░реЗрдВ рдФрд░ рдЕрдкрдиреЗ рдИрдореЗрд▓ рд╕рддреНрдпрд╛рдкрди рд╡рд░реНрдХрдлрд╝реНрд▓реЛ рдХреЛ рдЕрдЧрд▓реЗ рд╕реНрддрд░ рдкрд░ рд▓реЗ рдЬрд╛рдПрдВред