Webhook-Setup-Leitfaden
Webhooks ermöglichen die Echtzeit-Kommunikation zwischen Brevo und deiner Tajo-Loyalty-Plattform. Dieser Leitfaden führt dich durch den kompletten Einrichtungsprozess.
Überblick
Webhooks erlauben es Brevo, deine Tajo-Anwendung automatisch zu benachrichtigen, wenn bestimmte Ereignisse eintreten, etwa:
- E-Mail-Ereignisse: Zugestellt, geöffnet, geklickt, zurückgewiesen
- SMS-Ereignisse: Gesendet, zugestellt, fehlgeschlagen, beantwortet
- Kontakt-Ereignisse: Angelegt, aktualisiert, abgemeldet
- Kampagnen-Ereignisse: Gestartet, abgeschlossen, pausiert
Voraussetzungen
Bevor du Webhooks einrichtest, stelle sicher, dass du Folgendes hast:
- HTTPS-Endpoint zum Empfangen der Webhooks (SSL erforderlich)
- Webhook-Secret zur Signaturprüfung
- Server-Umgebung, die HTTP-POST-Anfragen verarbeiten kann
- Brevo-Konto mit Webhook-Zugriffsberechtigungen
Schritt 1: Webhook-Endpoint vorbereiten
Webhook-Handler erstellen
import express from 'express';import crypto from 'crypto';import { TajoLoyaltyService } from './loyalty-service.js';
const app = express();const loyaltyService = new TajoLoyaltyService();
// Middleware, um den Raw-Body für die Signaturprüfung zu erfassenapp.use('/webhooks/brevo', express.raw({ type: 'application/json', limit: '10mb'}));
// Haupt-Webhook-Handlerapp.post('/webhooks/brevo', async (req, res) => { try { // Webhook-Signatur verifizieren const signature = req.headers['x-brevo-signature']; if (!verifyWebhookSignature(req.body, signature)) { console.warn('Ungültige Webhook-Signatur empfangen'); return res.status(401).json({ error: 'Unauthorized: Invalid signature' }); }
// Webhook-Payload parsen const event = JSON.parse(req.body.toString()); console.log('Webhook-Ereignis empfangen:', event.event, event.email);
// An passenden Handler weiterleiten await handleWebhookEvent(event);
// Schnell antworten (Brevo erwartet Antwort innerhalb von 5 Sekunden) res.status(200).json({ success: true, eventId: event['message-id'], timestamp: new Date().toISOString() });
} catch (error) { console.error('Fehler bei der Webhook-Verarbeitung:', error); res.status(500).json({ error: 'Internal server error', message: error.message }); }});
// Funktion zur Signaturprüfungfunction 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-Routerasync function handleWebhookEvent(event) { switch (event.event) { // E-Mail-Ereignisse 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-Ereignisse case 'sms_delivered': await handleSMSDelivered(event); break; case 'sms_failed': await handleSMSFailed(event); break; case 'sms_reply': await handleSMSReply(event); break;
// Kontakt-Ereignisse case 'contact_created': await handleContactCreated(event); break; case 'contact_updated': await handleContactUpdated(event); break; case 'list_addition': await handleListAddition(event); break;
default: console.warn('Nicht behandeltes Webhook-Ereignis:', event.event); }}Handler für E-Mail-Ereignisse
// Zustellbestätigung für E-Mails verarbeitenasync function handleEmailDelivered(event) { const customerEmail = event.email; const messageId = event['message-id'];
await loyaltyService.updateCustomerEngagement(customerEmail, { lastEmailDelivered: new Date(), emailDeliveryRate: 'increment' });
// Für Analytics protokollieren console.log(`E-Mail zugestellt an ${customerEmail}: ${messageId}`);}
// E-Mail-Öffnungen verarbeiten (zentrale Engagement-Metrik)async function handleEmailOpened(event) { const customerEmail = event.email; const subject = event.subject; const timestamp = new Date(event.ts * 1000);
// Engagement-Score der Kund:in aktualisieren await loyaltyService.updateCustomerEngagement(customerEmail, { lastEmailOpened: timestamp, emailOpenRate: 'increment', engagementScore: 'increase' });
// Loyalty-Kampagnen-Engagement tracken if (event.tag?.includes('loyalty')) { await loyaltyService.trackLoyaltyEngagement(customerEmail, { event: 'email_opened', campaign: extractCampaignFromSubject(subject), timestamp: timestamp }); }
console.log(`E-Mail geöffnet von ${customerEmail}: "${subject}"`);}
// E-Mail-Klicks verarbeiten (besonders wertvolles Engagement)async function handleEmailClicked(event) { const customerEmail = event.email; const clickedUrl = event.link; const timestamp = new Date(event.ts * 1000);
// Hohes Engagement – Kund:innen-Score steigern await loyaltyService.updateCustomerEngagement(customerEmail, { lastEmailClicked: timestamp, emailClickRate: 'increment', engagementScore: 'boost' });
// Besuche von Prämienseiten tracken if (clickedUrl.includes('/rewards') || clickedUrl.includes('/loyalty')) { await loyaltyService.trackEvent(customerEmail, 'Rewards Page Visited', { source: 'email', referrer_url: clickedUrl, timestamp: timestamp }); }
console.log(`E-Mail-Link geklickt von ${customerEmail}: ${clickedUrl}`);}
// E-Mail-Bounces verarbeiten (Zustellungsprobleme)async function handleEmailBounced(event) { const customerEmail = event.email; const bounceReason = event.reason; const bounceType = event.event; // 'bounced' oder 'hard_bounced'
if (bounceType === 'hard_bounced') { // Hard Bounce – E-Mail-Adresse ungültig await loyaltyService.updateCustomerStatus(customerEmail, { emailStatus: 'invalid', emailBounced: true, bounceReason: bounceReason, lastBounce: new Date() });
// Für kritische Benachrichtigungen auf SMS ausweichen await loyaltyService.suggestAlternativeChannel(customerEmail, 'sms'); } else { // Soft Bounce – vorübergehendes Problem await loyaltyService.updateCustomerEngagement(customerEmail, { emailBounceCount: 'increment', lastBounce: new Date() }); }
console.warn(`E-Mail-Bounce für ${customerEmail}: ${bounceReason}`);}
// Spam-Meldungen verarbeiten (Reputationsmanagement)async function handleEmailSpam(event) { const customerEmail = event.email;
// Kund:in als nicht engagiert markieren, um künftige Spam-Meldungen zu vermeiden await loyaltyService.updateCustomerStatus(customerEmail, { emailStatus: 'spam_reported', marketingEnabled: false, lastSpamReport: new Date() });
// Marketing-Team zur Prüfung alarmieren await loyaltyService.alertMarketing('spam_report', { email: customerEmail, campaign: event.tag });
console.warn(`Spam gemeldet von ${customerEmail}`);}Handler für SMS-Ereignisse
// SMS-Zustellbestätigung verarbeitenasync function handleSMSDelivered(event) { const customerPhone = event.phone; const messageId = event['message-id'];
await loyaltyService.updateCustomerEngagement(customerPhone, { lastSMSDelivered: new Date(), smsDeliveryRate: 'increment' });
console.log(`SMS zugestellt an ${customerPhone}: ${messageId}`);}
// SMS-Fehler verarbeitenasync function handleSMSFailed(event) { const customerPhone = event.phone; const failureReason = event.reason;
await loyaltyService.updateCustomerStatus(customerPhone, { smsStatus: 'failed', smsFailureReason: failureReason, lastSMSFailure: new Date() });
// Schlägt SMS fehl, E-Mail als Alternative anbieten const customer = await loyaltyService.getCustomerByPhone(customerPhone); if (customer?.email) { await loyaltyService.suggestAlternativeChannel(customer.email, 'email'); }
console.warn(`SMS fehlgeschlagen für ${customerPhone}: ${failureReason}`);}
// SMS-Antworten verarbeiten (Zwei-Wege-Kommunikation)async function handleSMSReply(event) { const customerPhone = event.phone; const replyText = event.text.toLowerCase().trim();
// Gängige Antworten verarbeiten if (replyText === 'stop' || replyText === 'unsubscribe') { await loyaltyService.unsubscribeFromSMS(customerPhone); } else if (replyText === 'help' || replyText === 'info') { await loyaltyService.sendSMSHelp(customerPhone); } else { // An Kundenservice weiterleiten await loyaltyService.forwardSMSToSupport(customerPhone, replyText); }
console.log(`SMS-Antwort von ${customerPhone}: "${replyText}"`);}Schritt 2: Webhook in Brevo konfigurieren
Über das Brevo-Dashboard
- In das Brevo-Konto einloggen
- Zu Entwickler > Webhooks navigieren
- Auf „Neuen Webhook hinzufügen” klicken
- Webhook-Einstellungen konfigurieren:
{ "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" ]}Über die API
import { WebhooksApi } from '@brevo/brevo-js';
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 erfolgreich erstellt:', response.id); return response; } catch (error) { console.error('Fehler beim Erstellen des Webhooks:', error); throw error; }}Schritt 3: Sicherheitsumsetzung
Umgebungsvariablen
BREVO_WEBHOOK_SECRET=your-super-secure-webhook-secret-hereBREVO_API_KEY=xkeysib-your-api-key-hereWEBHOOK_RATE_LIMIT=1000WEBHOOK_TIMEOUT=5000Rate Limiting
import rateLimit from 'express-rate-limit';
const webhookLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 Minuten max: 1000, // jede IP auf 1000 Anfragen pro windowMs begrenzen message: 'Too many webhook requests from this IP', standardHeaders: true, legacyHeaders: false,});
app.use('/webhooks/brevo', webhookLimiter);IP-Whitelist (optional)
const brevoIPs = [ '185.41.28.0/24', '185.41.29.0/24', '217.182.196.0/24'];
function isBrevoIP(ip) { // Prüfung von IP-Bereichen umsetzen 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();});Schritt 4: Webhooks testen
Test-Handler für Webhooks
// Test-Endpunkt zur Webhook-Verifizierungapp.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 empfangen:', testEvent);
res.status(200).json({ success: true, message: 'Test-Webhook erfolgreich verarbeitet', receivedAt: new Date().toISOString() });});Manuelles Testen
# Webhook-Endpoint mit curl testencurl -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 }'Webhook-Validierung
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(', ')}`); }
// E-Mail-Format prüfen if (!this.isValidEmail(event.email)) { throw new Error('Invalid email format'); }
// Event-Typ prüfen 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); }}Schritt 5: Monitoring und Logging
Webhook-Monitoring
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' }) ]});
// Monitoring-Middleware hinzufügenapp.use('/webhooks/brevo', (req, res, next) => { const startTime = Date.now();
res.on('finish', () => { const duration = Date.now() - startTime;
webhookLogger.info('Webhook verarbeitet', { method: req.method, url: req.url, statusCode: res.statusCode, duration: duration, userAgent: req.headers['user-agent'], contentLength: req.headers['content-length'] }); });
next();});Health-Check-Endpunkt
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);});Schritt 6: Fehlerbehandlung und Recovery
Retry-Logik
class WebhookProcessor { constructor() { this.maxRetries = 3; this.retryDelay = 1000; // 1 Sekunde }
async processEvent(event, retries = 0) { try { await this.handleEvent(event); } catch (error) { if (retries < this.maxRetries && this.isRetryableError(error)) { console.warn(`Webhook-Verarbeitung fehlgeschlagen, erneuter Versuch... (${retries + 1}/${this.maxRetries})`);
await this.delay(this.retryDelay * Math.pow(2, retries)); // Exponentielles Backoff return this.processEvent(event, retries + 1); }
// Maximale Wiederholungen erreicht oder nicht wiederholbarer Fehler await this.handleFailedEvent(event, error); throw error; } }
isRetryableError(error) { // Bei temporären Fehlern wiederholen return error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || (error.status >= 500 && error.status < 600); }
async handleFailedEvent(event, error) { // Fehlgeschlagenes Event für manuelle Prüfung ablegen await loyaltyService.storeFailed Event(event, error.message);
// Operations-Team bei kritischen Events alarmieren 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 fehlgeschlagen: ${err.message}`);
// An Dead Letter Queue zur manuellen Verarbeitung übergeben await deadLetterQueue.add('failed webhook', { originalEvent: job.data.event, error: err.message, failedAt: new Date(), attempts: job.attemptsMade });});Fehlersuche bei häufigen Problemen
1. Signaturprüfung schlägt fehl
// Signaturprüfung debuggenfunction debugSignature(payload, receivedSignature) { const expectedSignature = crypto .createHmac('sha256', process.env.BREVO_WEBHOOK_SECRET) .update(payload) .digest('hex');
console.log('Empfangene Signatur:', receivedSignature); console.log('Erwartete Signatur:', expectedSignature); console.log('Payload-Länge:', payload.length); console.log('Erste 100 Zeichen:', payload.slice(0, 100));
return expectedSignature === receivedSignature.replace('sha256=', '');}2. Fehlende Events
Webhook-Konfiguration prüfen:
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 ? 'aktiviert' : 'deaktiviert'); }); } catch (error) { console.error('Fehler beim Auditieren der Webhooks:', error); }}3. Hohe Latenz
Webhook-Verarbeitung optimieren:
// Webhooks asynchron verarbeitenapp.post('/webhooks/brevo', async (req, res) => { // Signatur schnell prüfen if (!verifyWebhookSignature(req.body, req.headers['x-brevo-signature'])) { return res.status(401).json({ error: 'Unauthorized' }); }
// Sofort antworten res.status(200).json({ success: true });
// Event asynchron verarbeiten const event = JSON.parse(req.body.toString()); webhookQueue.add('process event', { event }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });});Nächste Schritte
- Webhook-Sicherheitsleitfaden – Fortgeschrittene Sicherheitspraktiken
- Event-Typen-Referenz – Vollständige Event-Dokumentation
- Webhooks testen – Leitfaden zum Testen und Debuggen
- Webhook-Analytics – Webhook-Performance überwachen