---
title: "Hướng dẫn thiết lập webhook | Tajo"
description: "Hướng dẫn thiết lập webhook của Brevo để một tích hợp Tajo nhận được sự kiện email và liên hệ từ Brevo"
url: https://tajo.io/vi/docs/webhook-configuration/setup-guide/
markdown: https://tajo.io/vi/docs/webhook-configuration/setup-guide.md
lang: vi-VN
author: "Set Noa"
author_url: https://tajo.io/vi/set-noa/
robots: noindex
---

Đăng ký quyền truy cập sớm

Nhập tên và địa chỉ email công việc của bạn. Chúng tôi sẽ gửi liên kết để xác nhận yêu cầu truy cập Tajo.

# Hướng dẫn thiết lập webhook

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

Webhook của Brevo gửi sự kiện email và liên hệ tới một endpoint bạn chọn, chẳng hạn một tích hợp Tajo, khi sự kiện xảy ra. Hướng dẫn này đưa bạn qua trọn vẹn quy trình thiết lập.

## Tổng quan

Webhook cho phép Brevo tự động thông báo cho ứng dụng Tajo của bạn khi có sự kiện cụ thể xảy ra, chẳng hạn:

- **Sự kiện email**: Đã gửi, đã mở, đã nhấp, bị trả lại
- **Sự kiện SMS**: Đã gửi đi, đã nhận, thất bại, có trả lời
- **Sự kiện liên hệ**: Đã tạo, đã cập nhật, đã hủy đăng ký
- **Sự kiện chiến dịch**: Đã bắt đầu, đã hoàn tất, đã tạm dừng

## Điều kiện tiên quyết

Trước khi thiết lập webhook, hãy bảo đảm bạn có:

- **Điểm cuối HTTPS** để nhận webhook (bắt buộc có SSL)
- **Khóa bí mật webhook** để xác minh chữ ký
- **Môi trường máy chủ** có khả năng xử lý yêu cầu HTTP POST
- **Tài khoản Brevo** có quyền truy cập webhook

## Bước 1: Chuẩn bị điểm cuối webhook của bạn

### Tạo trình xử lý 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);
  }
}
```

### Trình xử lý sự kiện email

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

### Trình xử lý sự kiện SMS

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

## Bước 2: Cấu hình webhook trong Brevo

### Qua bảng điều khiển Brevo

1. **Đăng nhập vào tài khoản Brevo**
2. **Đi tới Developers > Webhooks**
3. **Nhấp “Add a new webhook”**
4. **Cấu hình các thiết lập 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"
  ]
}
```

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

## Bước 3: Triển khai bảo mật

### Biến môi trường

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

### Giới hạn tần suất

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

### Danh sách IP cho phép (tùy chọn)

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

## Bước 4: Kiểm thử webhook

### Trình xử lý webhook kiểm thử

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

### Kiểm thử thủ công

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

### Xác thực 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);
  }
}
```

## Bước 5: Giám sát và ghi nhật ký

### Giám sát 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();
});
```

### Điểm cuối kiểm tra tình trạng

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

## Bước 6: Xử lý lỗi và khôi phục

### Logic thử lại

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

### Hàng đợi thư chết

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

## Khắc phục các sự cố thường gặp

### 1. Xác minh chữ ký thất bại

```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. Thiếu sự kiện

Kiểm tra cấu hình 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. Độ trễ cao

Tối ưu việc xử lý 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
    }
  });
});
```

## Bước tiếp theo

- **Hướng dẫn bảo mật webhook** - Các thực hành bảo mật nâng cao
- **[Tham chiếu loại sự kiện](https://tajo.io/vi/docs/webhooks-events/event-types)** - Tài liệu đầy đủ về sự kiện
- **[Kiểm thử webhook](https://tajo.io/vi/docs/testing-debug/)** - Hướng dẫn kiểm thử và gỡ lỗi
- **Phân tích webhook** - Giám sát hiệu năng webhook
