Vraag vroegtijdige toegang aan

Vul je voornaam en een e-mailadres of telefoonnummer in. We nemen daarna contact met je op en leggen uit hoe je toegang krijgt tot Tajo.

Handleiding webhookinstallatie

Webhooks maken realtime communicatie mogelijk tussen Brevo en je Tajo-loyaliteitsplatform. Deze handleiding neemt je stap voor stap mee door het volledige installatieproces.

Overzicht

Met webhooks kan Brevo je Tajo-applicatie automatisch waarschuwen wanneer bepaalde gebeurtenissen plaatsvinden, zoals:

  • E-mailgebeurtenissen: afgeleverd, geopend, aangeklikt, gebounced
  • SMS-gebeurtenissen: verzonden, afgeleverd, mislukt, beantwoord
  • Contactgebeurtenissen: aangemaakt, bijgewerkt, uitgeschreven
  • Campagnegebeurtenissen: gestart, voltooid, gepauzeerd

Vereisten

Zorg dat je het volgende hebt voordat je webhooks instelt:

  • HTTPS-endpoint voor het ontvangen van webhooks (SSL verplicht)
  • Webhooksecret voor handtekeningverificatie
  • Serveromgeving die HTTP POST-verzoeken kan verwerken
  • Brevo-account met toegangsrechten voor webhooks

Stap 1: bereid je webhook-endpoint voor

Maak een webhookhandler

import express from 'express';
import crypto from 'crypto';
import { TajoLoyaltyService } from './loyalty-service.js';
const app = express();
const loyaltyService = new TajoLoyaltyService();
// Middleware to capture raw body for signature verification
app.use('/webhooks/brevo', express.raw({
type: 'application/json',
limit: '10mb'
}));
// Main webhook handler
app.post('/webhooks/brevo', async (req, res) => {
try {
// Verify webhook signature
const signature = req.headers['x-brevo-signature'];
if (!verifyWebhookSignature(req.body, signature)) {
console.warn('Invalid webhook signature received');
return res.status(401).json({ error: 'Unauthorized: Invalid signature' });
}
// Parse webhook payload
const event = JSON.parse(req.body.toString());
console.log('Received webhook event:', event.event, event.email);
// Route to appropriate handler
await handleWebhookEvent(event);
// Respond quickly (Brevo expects response within 5 seconds)
res.status(200).json({
success: true,
eventId: event['message-id'],
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({
error: 'Internal server error',
message: error.message
});
}
});
// Signature verification function
function verifyWebhookSignature(payload, signature) {
if (!process.env.BREVO_WEBHOOK_SECRET || !signature) {
return false;
}
const expectedSignature = crypto
.createHmac('sha256', process.env.BREVO_WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature.replace('sha256=', ''), 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
// Event router
async function handleWebhookEvent(event) {
switch (event.event) {
// Email events
case 'delivered':
await handleEmailDelivered(event);
break;
case 'opened':
await handleEmailOpened(event);
break;
case 'clicked':
await handleEmailClicked(event);
break;
case 'bounced':
case 'hard_bounced':
await handleEmailBounced(event);
break;
case 'spam':
await handleEmailSpam(event);
break;
case 'unsubscribed':
await handleEmailUnsubscribed(event);
break;
// SMS events
case 'sms_delivered':
await handleSMSDelivered(event);
break;
case 'sms_failed':
await handleSMSFailed(event);
break;
case 'sms_reply':
await handleSMSReply(event);
break;
// Contact events
case 'contact_created':
await handleContactCreated(event);
break;
case 'contact_updated':
await handleContactUpdated(event);
break;
case 'list_addition':
await handleListAddition(event);
break;
default:
console.warn('Unhandled webhook event:', event.event);
}
}

Handlers voor e-mailgebeurtenissen

// Handle email delivery confirmation
async function handleEmailDelivered(event) {
const customerEmail = event.email;
const messageId = event['message-id'];
await loyaltyService.updateCustomerEngagement(customerEmail, {
lastEmailDelivered: new Date(),
emailDeliveryRate: 'increment'
});
// Log for analytics
console.log(`Email delivered to ${customerEmail}: ${messageId}`);
}
// Handle email opens (key engagement metric)
async function handleEmailOpened(event) {
const customerEmail = event.email;
const subject = event.subject;
const timestamp = new Date(event.ts * 1000);
// Update customer engagement score
await loyaltyService.updateCustomerEngagement(customerEmail, {
lastEmailOpened: timestamp,
emailOpenRate: 'increment',
engagementScore: 'increase'
});
// Track loyalty campaign engagement
if (event.tag?.includes('loyalty')) {
await loyaltyService.trackLoyaltyEngagement(customerEmail, {
event: 'email_opened',
campaign: extractCampaignFromSubject(subject),
timestamp: timestamp
});
}
console.log(`Email opened by ${customerEmail}: "${subject}"`);
}
// Handle email clicks (high-value engagement)
async function handleEmailClicked(event) {
const customerEmail = event.email;
const clickedUrl = event.link;
const timestamp = new Date(event.ts * 1000);
// High-value engagement - boost customer score
await loyaltyService.updateCustomerEngagement(customerEmail, {
lastEmailClicked: timestamp,
emailClickRate: 'increment',
engagementScore: 'boost'
});
// Track reward page visits
if (clickedUrl.includes('/rewards') || clickedUrl.includes('/loyalty')) {
await loyaltyService.trackEvent(customerEmail, 'Rewards Page Visited', {
source: 'email',
referrer_url: clickedUrl,
timestamp: timestamp
});
}
console.log(`Email link clicked by ${customerEmail}: ${clickedUrl}`);
}
// Handle email bounces (delivery issues)
async function handleEmailBounced(event) {
const customerEmail = event.email;
const bounceReason = event.reason;
const bounceType = event.event; // 'bounced' or 'hard_bounced'
if (bounceType === 'hard_bounced') {
// Hard bounce - email address invalid
await loyaltyService.updateCustomerStatus(customerEmail, {
emailStatus: 'invalid',
emailBounced: true,
bounceReason: bounceReason,
lastBounce: new Date()
});
// Consider switching to SMS for critical notifications
await loyaltyService.suggestAlternativeChannel(customerEmail, 'sms');
} else {
// Soft bounce - temporary issue
await loyaltyService.updateCustomerEngagement(customerEmail, {
emailBounceCount: 'increment',
lastBounce: new Date()
});
}
console.warn(`Email bounced for ${customerEmail}: ${bounceReason}`);
}
// Handle spam reports (reputation management)
async function handleEmailSpam(event) {
const customerEmail = event.email;
// Mark customer as unengaged to prevent future spam reports
await loyaltyService.updateCustomerStatus(customerEmail, {
emailStatus: 'spam_reported',
marketingEnabled: false,
lastSpamReport: new Date()
});
// Alert marketing team for review
await loyaltyService.alertMarketing('spam_report', {
email: customerEmail,
campaign: event.tag
});
console.warn(`Spam reported by ${customerEmail}`);
}

Handlers voor SMS-gebeurtenissen

// Handle SMS delivery confirmation
async function handleSMSDelivered(event) {
const customerPhone = event.phone;
const messageId = event['message-id'];
await loyaltyService.updateCustomerEngagement(customerPhone, {
lastSMSDelivered: new Date(),
smsDeliveryRate: 'increment'
});
console.log(`SMS delivered to ${customerPhone}: ${messageId}`);
}
// Handle SMS failures
async function handleSMSFailed(event) {
const customerPhone = event.phone;
const failureReason = event.reason;
await loyaltyService.updateCustomerStatus(customerPhone, {
smsStatus: 'failed',
smsFailureReason: failureReason,
lastSMSFailure: new Date()
});
// If SMS fails, consider email as alternative
const customer = await loyaltyService.getCustomerByPhone(customerPhone);
if (customer?.email) {
await loyaltyService.suggestAlternativeChannel(customer.email, 'email');
}
console.warn(`SMS failed for ${customerPhone}: ${failureReason}`);
}
// Handle SMS replies (two-way communication)
async function handleSMSReply(event) {
const customerPhone = event.phone;
const replyText = event.text.toLowerCase().trim();
// Process common replies
if (replyText === 'stop' || replyText === 'unsubscribe') {
await loyaltyService.unsubscribeFromSMS(customerPhone);
} else if (replyText === 'help' || replyText === 'info') {
await loyaltyService.sendSMSHelp(customerPhone);
} else {
// Forward to customer service
await loyaltyService.forwardSMSToSupport(customerPhone, replyText);
}
console.log(`SMS reply from ${customerPhone}: "${replyText}"`);
}

Stap 2: configureer de webhook in Brevo

Via het Brevo-dashboard

  1. Log in op je Brevo-account
  2. Ga naar Developers > Webhooks
  3. Klik op “Add a new webhook”
  4. Configureer de webhookinstellingen:
{
"url": "https://your-tajo-domain.com/webhooks/brevo",
"description": "Tajo Loyalty Platform Integration",
"events": [
"delivered",
"opened",
"clicked",
"bounced",
"hard_bounced",
"spam",
"unsubscribed",
"sms_delivered",
"sms_failed",
"sms_reply",
"contact_created",
"contact_updated"
]
}

Via de API

import { WebhooksApi } from '@getbrevo/brevo';
async function createWebhook() {
const webhooksApi = new WebhooksApi();
const createWebhook = {
url: 'https://your-tajo-domain.com/webhooks/brevo',
description: 'Tajo Loyalty Platform Integration',
events: [
'delivered',
'opened',
'clicked',
'bounced',
'hard_bounced',
'spam',
'unsubscribed',
'sms_delivered',
'sms_failed',
'sms_reply',
'contact_created',
'contact_updated'
]
};
try {
const response = await webhooksApi.createWebhook(createWebhook);
console.log('Webhook created successfully:', response.id);
return response;
} catch (error) {
console.error('Error creating webhook:', error);
throw error;
}
}

Stap 3: beveiliging implementeren

Omgevingsvariabelen

Terminal window
# .env file
BREVO_WEBHOOK_SECRET=your-super-secure-webhook-secret-here
BREVO_API_KEY=xkeysib-your-api-key-here
WEBHOOK_RATE_LIMIT=1000
WEBHOOK_TIMEOUT=5000

Snelheidslimiet

import rateLimit from 'express-rate-limit';
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limit each IP to 1000 requests per windowMs
message: 'Too many webhook requests from this IP',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/webhooks/brevo', webhookLimiter);

IP-whitelist (optioneel)

const brevoIPs = [
'185.41.28.0/24',
'185.41.29.0/24',
'217.182.196.0/24'
];
function isBrevoIP(ip) {
// Implement IP range checking
return brevoIPs.some(range => ipInRange(ip, range));
}
app.use('/webhooks/brevo', (req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (process.env.NODE_ENV === 'production' && !isBrevoIP(clientIP)) {
return res.status(403).json({ error: 'Forbidden: Invalid source IP' });
}
next();
});

Stap 4: webhooks testen

Testhandler voor webhooks

// Test endpoint for webhook verification
app.post('/webhooks/brevo/test', (req, res) => {
const testEvent = {
event: 'test',
'message-id': 'test-message-id',
timestamp: Date.now() / 1000,
tags: ['test', 'loyalty']
};
console.log('Test webhook received:', testEvent);
res.status(200).json({
success: true,
message: 'Test webhook processed successfully',
receivedAt: new Date().toISOString()
});
});

Handmatig testen

Terminal window
# Test webhook endpoint with curl
curl -X POST https://your-domain.com/webhooks/brevo/test \
-H "Content-Type: application/json" \
-H "X-Brevo-Signature: sha256=test-signature" \
-d '{
"event": "delivered",
"email": "[email protected]",
"message-id": "test-123",
"ts": 1640995200
}'

Webhookvalidatie

class WebhookValidator {
static validateEvent(event) {
const required = ['event', 'email', 'message-id'];
const missing = required.filter(field => !event[field]);
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(', ')}`);
}
// Validate email format
if (!this.isValidEmail(event.email)) {
throw new Error('Invalid email format');
}
// Validate event type
const validEvents = [
'delivered', 'opened', 'clicked', 'bounced', 'hard_bounced',
'spam', 'unsubscribed', 'sms_delivered', 'sms_failed'
];
if (!validEvents.includes(event.event)) {
throw new Error(`Invalid event type: ${event.event}`);
}
return true;
}
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}

Stap 5: monitoring en logging

Webhookmonitoring

import winston from 'winston';
const webhookLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'webhook-error.log', level: 'error' }),
new winston.transports.File({ filename: 'webhook-combined.log' })
]
});
// Add monitoring middleware
app.use('/webhooks/brevo', (req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
webhookLogger.info('Webhook processed', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: duration,
userAgent: req.headers['user-agent'],
contentLength: req.headers['content-length']
});
});
next();
});

Health check-endpoint

app.get('/webhooks/brevo/health', async (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version,
environment: process.env.NODE_ENV,
checks: {
database: await checkDatabaseConnection(),
redis: await checkRedisConnection(),
brevoAPI: await checkBrevoAPIConnection()
}
};
const allHealthy = Object.values(health.checks).every(check => check.status === 'ok');
res.status(allHealthy ? 200 : 503).json(health);
});

Stap 6: foutafhandeling en herstel

Herhaallogica

class WebhookProcessor {
constructor() {
this.maxRetries = 3;
this.retryDelay = 1000; // 1 second
}
async processEvent(event, retries = 0) {
try {
await this.handleEvent(event);
} catch (error) {
if (retries < this.maxRetries && this.isRetryableError(error)) {
console.warn(`Webhook processing failed, retrying... (${retries + 1}/${this.maxRetries})`);
await this.delay(this.retryDelay * Math.pow(2, retries)); // Exponential backoff
return this.processEvent(event, retries + 1);
}
// Max retries reached or non-retryable error
await this.handleFailedEvent(event, error);
throw error;
}
}
isRetryableError(error) {
// Retry on temporary failures
return error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT' ||
(error.status >= 500 && error.status < 600);
}
async handleFailedEvent(event, error) {
// Store failed event for manual review
await loyaltyService.storeFailedEvent(event, error.message);
// Alert operations team for critical events
if (this.isCriticalEvent(event)) {
await loyaltyService.alertOps('webhook_failure', {
event: event.event,
email: event.email,
error: error.message
});
}
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

Dead letter queue

import Bull from 'bull';
const webhookQueue = new Bull('webhook processing', process.env.REDIS_URL);
const deadLetterQueue = new Bull('webhook failed', process.env.REDIS_URL);
webhookQueue.process(async (job) => {
const { event } = job.data;
await handleWebhookEvent(event);
});
webhookQueue.on('failed', async (job, err) => {
console.error(`Webhook job failed: ${err.message}`);
// Add to dead letter queue for manual processing
await deadLetterQueue.add('failed webhook', {
originalEvent: job.data.event,
error: err.message,
failedAt: new Date(),
attempts: job.attemptsMade
});
});

Veelvoorkomende problemen oplossen

1. Handtekeningverificatie mislukt

// Debug signature verification
function debugSignature(payload, receivedSignature) {
const expectedSignature = crypto
.createHmac('sha256', process.env.BREVO_WEBHOOK_SECRET)
.update(payload)
.digest('hex');
console.log('Received signature:', receivedSignature);
console.log('Expected signature:', expectedSignature);
console.log('Payload length:', payload.length);
console.log('First 100 chars:', payload.slice(0, 100));
return expectedSignature === receivedSignature.replace('sha256=', '');
}

2. Ontbrekende gebeurtenissen

Controleer de webhookconfiguratie:

async function auditWebhookConfig() {
const webhooksApi = new WebhooksApi();
try {
const webhooks = await webhooksApi.getWebhooks();
webhooks.webhooks.forEach(webhook => {
console.log('Webhook ID:', webhook.id);
console.log('URL:', webhook.url);
console.log('Events:', webhook.events);
console.log('Status:', webhook.is_enabled ? 'enabled' : 'disabled');
});
} catch (error) {
console.error('Error auditing webhooks:', error);
}
}

3. Hoge latentie

Optimaliseer de webhookverwerking:

// Process webhooks asynchronously
app.post('/webhooks/brevo', async (req, res) => {
// Verify signature quickly
if (!verifyWebhookSignature(req.body, req.headers['x-brevo-signature'])) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Respond immediately
res.status(200).json({ success: true });
// Process event asynchronously
const event = JSON.parse(req.body.toString());
webhookQueue.add('process event', { event }, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
});
});

Volgende stappen

  • Handleiding webhookbeveiliging - Geavanceerde beveiligingspraktijken
  • Naslagwerk gebeurtenistypen - Volledige documentatie over gebeurtenissen
  • Webhooks testen - Handleiding voor testen en debuggen
  • Webhookanalyse - Monitor de prestaties van je webhooks

Vraag vroegtijdige toegang aan

Vul je voornaam en een e-mailadres of telefoonnummer in. We nemen daarna contact met je op en leggen uit hoe je toegang krijgt tot Tajo.

automatische herkenning
AI-assistent

Hallo! Stel me vragen over de documentatie.