Webhook 集成指南


本指南将帮助您集成我们的 Webhook 系统,以接收来自 YCloud 的实时事件通知。

开始使用

什么是 Webhook?

Webhook 是一种 HTTP 回调,允许 YCloud 向您的服务器发送实时事件通知。当特定事件发生时(例如消息状态更新或入站消息),YCloud 将自动向您配置的 URL 发送包含事件数据的 HTTP POST 请求。

基本要求

在实现 Webhook 之前,您需要:

一个公开可访问的 HTTP 端点

  • 必须可从互联网访问
  • 强烈建议使用 HTTPS 以确保安全(仍支持 HTTP)
  • 不得使用内部/私有 IP 地址

流程概述

sequenceDiagram
    participant Event as 事件发生
    participant YCloud as YCloud Webhook 系统
    participant Server as 您的服务器
    
    Event->>YCloud: 1. 触发事件<br/>(消息状态更新/入站消息等)
    YCloud->>Server: 2. 发送 HTTP POST 请求<br/>(包含事件数据)
    Server->>Server: 3. 接收并处理事件
    Server-->>YCloud: 4. 返回 2xx 状态码
    
    alt 响应成功
        YCloud->>YCloud: 投递成功
    else 响应失败或超时
        YCloud->>YCloud: 自动重试
    end

流程说明:

  1. YCloud 上发生事件(例如,收到入站消息、状态更新)
  2. YCloud 向您的 Webhook URL 发送 HTTP POST 请求
  3. 您的服务器接收并处理事件
  4. 您的服务器响应 2xx 状态码
  5. 如果请求失败,YCloud 将自动重试

实现 Webhook

步骤 1:创建 Webhook 端点

使用我们的 API 创建 Webhook 端点:

请求示例:

curl -X POST "https://api.ycloud.com/v2/webhookEndpoints" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "url": "https://api.yourcompany.com/webhook",
    "description": "生产环境 Webhook 端点",
    "enabledEvents": [
      "whatsapp.message.updated"
    ],
    "status": "active"
  }'

响应:

{
  "id": "wep_1234567890abcdef",
  "url": "https://api.yourcompany.com/webhook",
  "description": "生产环境 Webhook 端点",
  "enabledEvents": [
    "whatsapp.message.updated"
  ],
  "status": "active",
  "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "createTime": "2025-11-03T10:00:00.000+08:00",
  "updateTime": "2025-11-03T10:00:00.000+08:00"
}

重要提示: 请安全地保存 secret 值。您需要使用它来验证 Webhook 签名。

有关端点 API 的详细信息,请参阅 Webhook 端点

端点限制:

  • 每个账户最多 20 个 Webhook 端点
  • URL 必须公开可访问(不能使用内部 IP)
  • URL 最大长度:500 个字符
  • 描述最大长度:400 个字符

步骤 2:处理 Webhook 请求

当事件发生时,YCloud 将向您的 Webhook URL 发送 POST 请求:

请求头:

Content-Type: application/json
YCloud-Signature: t=1762224357,s=a1b2c3d4e5f6...
X-Webhook-Endpoint-ID: 6c31924eae964fafbf0c19efc20d6f4d

请求体示例:

{
  "id": "evt_1234567890abcdef",
  "type": "whatsapp.message.updated",
  "apiVersion": "v2",
  "createTime": "2025-11-03T10:00:00.000+08:00",
  "whatsappMessage": {
    "id": "wamid.1234567890",
    "wabaId": "123456789",
    "from": "+8613800138000",
    "to": "+8613900139000",
    "type": "text",
    "status": "delivered",
    "text": {
      "body": "Hello, World!"
    },
    "createTime": "2025-11-03T09:59:00.000+08:00",
    "updateTime": "2025-11-03T10:00:00.000+08:00"
  }
}

事件通用结构:

  • id:唯一事件标识符
  • type:事件类型(例如 whatsapp.message.updated
  • apiVersion:API 版本(当前为 v2
  • createTime:事件创建时间戳
  • {eventType}:事件特定数据(例如 whatsappMessagesmsMessage

有关事件特定数据,请参阅事件类型和载荷

步骤 3:验证 Webhook 签名

始终验证签名以确保请求来自 YCloud 且未被篡改。

签名格式:

YCloud-Signature: t={timestamp},s={signature}

验证算法:

  1. 从请求头中提取时间戳(t)和签名(s)(时间戳是以秒为单位的 Unix 时间戳)
  2. 构造签名载荷:{timestamp}.{request_body}
  3. 计算 HMAC-SHA256:HMAC-SHA256(signed_payload, secret)
  4. 将计算出的签名与接收到的签名进行比较

示例(伪代码):

function verifySignature(payload, signatureHeader, secret) {
  // 解析请求头
  const parts = signatureHeader.split(',');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];
  
  // 构造签名载荷
  const signedPayload = `${timestamp}.${payload}`;
  
  // 计算预期签名
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // 比较签名(生产环境中使用恒定时间比较)
  return signature === expectedSignature;
}

步骤 4:响应 Webhook

响应要求:

  1. 返回 2xx 状态码(例如 200201204

    • 任何非 2xx 响应都会触发重试
  2. 快速响应(建议在 6 秒内)

    • 快速响应可提高您的 Webhook 优先级
    • 慢速响应(>10 秒)可能会被降低优先级
  3. 异步处理(推荐)

    • 立即返回 200 OK
    • 在后台作业/队列中处理事件

响应示例:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "received": true
}

注意: 响应体会被忽略。只有状态码重要。


实现示例

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// 中间件:捕获原始请求体以进行签名验证
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

// Webhook 端点
app.post('/webhook', (req, res) => {
  const signature = req.headers['ycloud-signature'];
  const payload = req.rawBody;
  
  // 验证签名
  if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
    console.error('签名无效');
    return res.status(401).send('未授权');
  }
  
  // 处理事件
  const event = req.body;
  console.log('收到事件:', event.type, event.id);
  
  // 处理不同的事件类型
  switch (event.type) {
    case 'whatsapp.message.updated':
      handleWhatsAppMessage(event.whatsappMessage);
      break;
    case 'whatsapp.inbound.message':
      handleInboundMessage(event.whatsappInboundMessage);
      break;
    default:
      console.log('未处理的事件类型:', event.type);
  }
  
  // 立即响应
  res.status(200).json({ received: true });
});

function verifySignature(payload, signatureHeader, secret) {
  if (!signatureHeader) return false;
  
  const parts = signatureHeader.split(',');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];
  
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // 使用恒定时间比较
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

function handleWhatsAppMessage(message) {
  // 异步处理消息
  console.log('WhatsApp 消息状态:', message.status);
  // 添加到队列、更新数据库等
}

function handleInboundMessage(message) {
  // 处理入站消息
  console.log('收到来自以下用户的消息:', message.from);
  // 添加到队列、触发自动回复等
}

app.listen(3000, () => {
  console.log('Webhook 服务器正在监听端口 3000');
});
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@RestController
public class WebhookController {
    
    private static final String WEBHOOK_SECRET = "<YOUR_WEBHOOK_SECRET>";
    
    @PostMapping("/webhook")
    public ResponseEntity<Map<String, Boolean>> handleWebhook(
            @RequestHeader("YCloud-Signature") String signature,
            @RequestBody String payload) {
        
        // 验证签名
        if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
            return ResponseEntity.status(401).build();
        }
        
        // 解析事件
        ObjectMapper mapper = new ObjectMapper();
        JsonNode event = mapper.readTree(payload);
        String eventType = event.get("type").asText();
        String eventId = event.get("id").asText();
        
        System.out.println("收到事件: " + eventType + " - " + eventId);
        
        // 处理不同的事件类型
        switch (eventType) {
            case "whatsapp.message.updated":
                handleWhatsAppMessage(event.get("whatsappMessage"));
                break;
            case "whatsapp.inbound.message":
                handleInboundMessage(event.get("whatsappInboundMessage"));
                break;
            default:
                System.out.println("未处理的事件类型: " + eventType);
        }
        
        // 立即响应
        return ResponseEntity.ok(Map.of("received", true));
    }
    
    private boolean verifySignature(String payload, String signatureHeader, String secret) {
        try {
            // 解析请求头
            String[] parts = signatureHeader.split(",");
            String timestamp = parts[0].split("=")[1];
            String signature = parts[1].split("=")[1];
            
            // 计算预期签名
            String signedPayload = timestamp + "." + payload;
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            byte[] hash = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
            
            String expectedSignature = bytesToHex(hash);
            
            // 恒定时间比较
            return MessageDigest.isEqual(
                signature.getBytes(StandardCharsets.UTF_8),
                expectedSignature.getBytes(StandardCharsets.UTF_8)
            );
        } catch (Exception e) {
            return false;
        }
    }
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
    
    private void handleWhatsAppMessage(JsonNode message) {
        // 异步处理消息
        System.out.println("WhatsApp 消息状态: " + message.get("status").asText());
        // 添加到队列、更新数据库等
    }
    
    private void handleInboundMessage(JsonNode message) {
        // 处理入站消息
        System.out.println("收到来自以下用户的消息: " + message.get("from").asText());
        // 添加到队列、触发自动回复等
    }
}

重要注意事项

最佳实践

  1. 验证签名 - 始终验证 YCloud-Signature 请求头以确保请求来自 YCloud

  2. 使用 HTTPS - 建议用于加密传输中的数据并防止攻击

  3. 快速响应 - 在 6 秒内返回 200 OK 以获得最佳投递优先级

  4. 处理幂等性 - 使用事件 ID 检测并跳过重复事件

  5. 异步处理 - 立即返回 200 OK,然后在后台工作进程中处理事件

幂等性示例:

const Redis = require('ioredis');
const redis = new Redis();

app.post('/webhook', async (req, res) => {
  const event = req.body;
  const eventId = event.id;
  
  // 检查是否已处理(TTL:7 天)
  const exists = await redis.get(`webhook:processed:${eventId}`);
  if (exists) {
    console.log('重复事件,跳过:', eventId);
    return res.status(200).json({ received: true });
  }
  
  // 标记为已处理
  await redis.setex(`webhook:processed:${eventId}`, 7 * 24 * 3600, '1');
  
  // 异步处理事件
  await queue.add('process-webhook', event);
  
  res.status(200).json({ received: true });
});
@Autowired
private RedisTemplate<String, String> redisTemplate;

@PostMapping("/webhook")
public ResponseEntity<Map<String, Boolean>> handleWebhook(@RequestBody String payload) {
    JsonNode event = objectMapper.readTree(payload);
    String eventId = event.get("id").asText();
    
    // 检查是否已处理
    String key = "webhook:processed:" + eventId;
    Boolean exists = redisTemplate.hasKey(key);
    if (Boolean.TRUE.equals(exists)) {
        logger.info("重复事件,跳过: {}", eventId);
        return ResponseEntity.ok(Map.of("received", true));
    }
    
    // 标记为已处理(TTL:7 天)
    redisTemplate.opsForValue().set(key, "1", 7, TimeUnit.DAYS);
    
    // 异步处理事件
    eventQueue.send(event);
    
    return ResponseEntity.ok(Map.of("received", true));
}

错误处理

重试机制:

如果您的服务器返回非 2xx 状态码或未响应,YCloud 将自动重试:

  • 重试计划: 10秒 → 30秒 → 5分钟 → 30分钟 → 1小时 → 2小时 → 2小时
  • 最大重试次数: 7 次
  • 7 次失败后: 该事件不再重试

URL 暂停:

为了保护系统资源,频繁失败的 URL 将被临时暂停:

  • 触发条件: 每分钟 200 次失败 或 每分钟累计失败时间 10 分钟
  • 暂停时长: 3 分钟
  • 暂停期间: 不会发送 Webhook 请求
  • 暂停后: 自动恢复

最佳实践:

  • 监控您的 Webhook 端点的正常运行时间
  • 为 Webhook 失败设置警报
  • 即使处理失败也返回 200 OK(在内部处理重试)

附加资源