---
title: "Webhook Setup Guide"
description: "Complete guide to setting up and configuring Brevo webhooks for real-time Tajo loyalty platform integration"
url: https://tajo.io/docs/webhook-configuration/setup-guide/
markdown: https://tajo.io/docs/webhook-configuration/setup-guide.md
lang: en-US
---

# Webhook Setup Guide

Webhooks enable real-time communication between Brevo and your Tajo loyalty platform. This guide walks you through the complete setup process.

## Overview

Webhooks allow Brevo to automatically notify your Tajo application when specific events occur, such as:

- **Email events**: Delivered, opened, clicked, bounced
- **SMS events**: Sent, delivered, failed, replied
- **Contact events**: Created, updated, unsubscribed
- **Campaign events**: Started, completed, paused

## Prerequisites

Before setting up webhooks, ensure you have:

- **HTTPS endpoint** for receiving webhooks (SSL required)
- **Webhook secret** for signature verification
- **Server environment** capable of handling HTTP POST requests
- **Brevo account** with webhook access permissions

## Step 1: Prepare Your Webhook Endpoint

### Create Webhook Handler

```javascript
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);
  }
}
```

### Email Event Handlers

```javascript
// 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}`);
}
```

### SMS Event Handlers

```javascript
// 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}"`);
}
```

## Step 2: Configure Webhook in Brevo

### Via Brevo Dashboard

1. **Log into Brevo account**
2. **Navigate to Developers > Webhooks**
3. **Click “Add a new webhook”**
4. **Configure webhook settings:**

```json
{
  "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 API

```javascript
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;
  }
}
```

## Step 3: Security Implementation

### Environment Variables

```bash
# .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
```

### Rate Limiting

```javascript
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 (Optional)

```javascript
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();
});
```

## Step 4: Testing Webhooks

### Test Webhook Handler

```javascript
// Test endpoint for webhook verification
app.post('/webhooks/brevo/test', (req, res) => {
  const testEvent = {
    event: 'test',
    email: 'test@example.com',
    '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()
  });
});
```

### Manual Testing

```bash
# 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": "test@example.com",
    "message-id": "test-123",
    "ts": 1640995200
  }'
```

### Webhook Validation

```javascript
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);
  }
}
```

## Step 5: Monitoring and Logging

### Webhook Monitoring

```javascript
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

```javascript
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);
});
```

## Step 6: Error Handling and Recovery

### Retry Logic

```javascript
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

```javascript
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
  });
});
```

## Troubleshooting Common Issues

### 1. Signature Verification Fails

```javascript
// 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. Missing Events

Check webhook configuration:

```javascript
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. High Latency

Optimize webhook processing:

```javascript
// 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
    }
  });
});
```

## Next Steps

- **Webhook Security Guide** - Advanced security practices
- **[Event Types Reference](https://tajo.io/docs/webhooks-events/event-types)** - Complete event documentation
- **[Testing Webhooks](https://tajo.io/docs/testing-debug/)** - Testing and debugging guide
- **Webhook Analytics** - Monitor webhook performance
