---
title: "Webhook 设置指南 | Tajo"
description: "配置 Brevo webhook，让 Tajo 集成接收 Brevo 邮件和联系人事件的指南"
url: https://tajo.io/zh-cn/docs/webhook-configuration/setup-guide/
markdown: https://tajo.io/zh-cn/docs/webhook-configuration/setup-guide.md
lang: zh-CN
author: "Set Noa"
author_url: https://tajo.io/zh-cn/set-noa/
robots: noindex
---

申请抢先体验

请输入您的名字和公司电子邮箱地址。我们会发送确认 Tajo 访问申请的链接。

# Webhook 设置指南

[Set Noa](https://tajo.io/zh-cn/set-noa/)

Brevo webhook 会在邮件和联系人事件发生时，把事件发送到你选择的端点，例如一个 Tajo 集成。本指南带你走完整个设置流程。

## 概览

Webhook 让 Brevo 在特定事件发生时自动通知你的 Tajo 应用，例如：

- **邮件事件**：已投递、已打开、已点击、退信
- **短信事件**：已发送、已投递、发送失败、已回复
- **联系人事件**：已创建、已更新、已退订
- **营销活动事件**：已开始、已完成、已暂停

## 前置条件

设置 webhook 之前，请确认你已具备：

- 用于接收 webhook 的 **HTTPS 端点**（必须启用 SSL）
- 用于校验签名的 **webhook 密钥**
- 能够处理 HTTP POST 请求的**服务器环境**
- 拥有 webhook 访问权限的 **Brevo 账户**

## 第 1 步：准备 webhook 端点

### 创建 webhook 处理器

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

### 邮件事件处理器

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

### 短信事件处理器

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

## 第 2 步：在 Brevo 中配置 webhook

### 通过 Brevo 仪表板

1. **登录 Brevo 账户**
2. **进入 Developers > Webhooks**
3. **点击 “Add a new webhook”**
4. **配置 webhook 设置：**

```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"
  ]
}
```

### 通过 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;
  }
}
```

## 第 3 步：安全实现

### 环境变量

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

### 速率限制

```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 白名单（可选）

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

## 第 4 步：测试 webhook

### 测试用 webhook 处理器

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

### 手动测试

```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 校验

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

## 第 5 步：监控与日志

### Webhook 监控

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

### 健康检查端点

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

## 第 6 步：错误处理与恢复

### 重试逻辑

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

### 死信队列

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

## 常见问题排查

### 1. 签名校验失败

```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. 事件缺失

检查 webhook 配置：

```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. 延迟过高

优化 webhook 处理流程：

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

## 下一步

- **Webhook 安全指南**，进阶安全实践
- **[事件类型参考](https://tajo.io/zh-cn/docs/webhooks-events/event-types)**，完整的事件文档
- **[测试 Webhook](https://tajo.io/zh-cn/docs/testing-debug/)**，测试与调试指南
- **Webhook 分析**，监控 webhook 表现
