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 erfassen
app.use('/webhooks/brevo', express.raw({
type: 'application/json',
limit: '10mb'
}));
// Haupt-Webhook-Handler
app.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üfung
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) {
// 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 verarbeiten
async 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 verarbeiten
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 zugestellt an ${customerPhone}: ${messageId}`);
}
// SMS-Fehler verarbeiten
async 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

  1. In das Brevo-Konto einloggen
  2. Zu Entwickler > Webhooks navigieren
  3. Auf „Neuen Webhook hinzufügen” klicken
  4. 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

.env-Datei
BREVO_WEBHOOK_SECRET=your-super-secure-webhook-secret-here
BREVO_API_KEY=xkeysib-your-api-key-here
WEBHOOK_RATE_LIMIT=1000
WEBHOOK_TIMEOUT=5000

Rate 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-Verifizierung
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 empfangen:', testEvent);
res.status(200).json({
success: true,
message: 'Test-Webhook erfolgreich verarbeitet',
receivedAt: new Date().toISOString()
});
});

Manuelles Testen

Terminal window
# Webhook-Endpoint mit curl testen
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
}'

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ügen
app.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 debuggen
function 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 verarbeiten
app.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

AI-Assistent

Hallo! Fragen Sie mich alles über die Dokumentation.

Kostenlos mit Brevo starten