Webhooks API¶
实现状态
支付 webhooks 已完全实现。订单、产品和客户 webhooks 具有基本功能。高级 webhook 功能(如传递日志和重试配置)计划在未来版本(v0.2)中实现。
Webhooks API 允许您通过 HTTP 回调接收实时事件通知。
概览¶
Webhooks 使您的应用程序能够在 R Commerce 商店中发生事件时接收推送通知,而不是轮询更改。
基础 URL¶
认证¶
Webhook 管理需要密钥 API。
Webhook 对象¶
{
"id": "550e8400-e29b-41d4-a716-446655440300",
"url": "https://your-app.com/webhooks/rcommerce",
"topic": "order.created",
"include_fields": ["id", "order_number", "total_price", "customer"],
"metafield_namespaces": ["global"],
"secret": "whsec_...",
"api_version": "2024-01",
"is_active": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}
Webhook 字段¶
| 字段 | 类型 | 说明 |
|---|---|---|
id |
UUID | 唯一标识符 |
url |
string | HTTPS 端点 URL |
topic |
string | 订阅的事件主题 |
include_fields |
array | 要包含的特定字段(可选) |
metafield_namespaces |
array | 要包含的元字段命名空间 |
secret |
string | 用于验证的签名密钥 |
api_version |
string | 负载格式的 API 版本 |
is_active |
boolean | Webhook 是否活跃 |
created_at |
datetime | 创建时间戳 |
updated_at |
datetime | 最后修改时间 |
端点¶
列出 Webhooks¶
检索所有配置的 webhooks。
查询参数¶
| 参数 | 类型 | 说明 |
|---|---|---|
topic |
string | 按事件主题筛选 |
is_active |
boolean | 按活跃状态筛选 |
获取 Webhook¶
检索特定的 webhook 配置。
创建 Webhook¶
注册新的 webhook 端点。
请求体¶
{
"url": "https://your-app.com/webhooks/rcommerce",
"topic": "order.created",
"include_fields": ["id", "order_number", "total_price"],
"api_version": "2024-01"
}
必填字段¶
url- 可以接收 POST 请求的 HTTPS URLtopic- 要订阅的事件类型
更新 Webhook¶
更新 webhook 配置。
请求体¶
{
"url": "https://new-url.com/webhooks",
"is_active": true,
"include_fields": ["id", "order_number", "customer"]
}
删除 Webhook¶
移除 webhook 订阅。
测试 Webhook¶
向 webhook URL 发送测试事件。
请求体¶
Webhook 主题¶
订单¶
| 主题 | 说明 |
|---|---|
order.created |
新订单已下单 |
order.updated |
订单信息已更改 |
order.cancelled |
订单已取消 |
order.closed |
订单已关闭 |
order.reopened |
订单已重新打开 |
order.payment_received |
付款已捕获 |
order.fulfillment_created |
履行已创建 |
order.fulfillment_updated |
履行已更新 |
order.refund_created |
退款已处理 |
产品¶
| 主题 | 说明 |
|---|---|
product.created |
新产品已创建 |
product.updated |
产品信息已更改 |
product.deleted |
产品已删除 |
product.published |
产品已发布 |
product.unpublished |
产品已取消发布 |
product.inventory_changed |
库存数量已更新 |
product.low_stock |
库存低于阈值 |
product.out_of_stock |
库存已归零 |
客户¶
| 主题 | 说明 |
|---|---|
customer.created |
新客户账户已创建 |
customer.updated |
客户信息已更改 |
customer.deleted |
客户账户已删除 |
customer.address_created |
新地址已添加 |
支付¶
| 主题 | 说明 |
|---|---|
payment.created |
新支付已发起 |
payment.succeeded |
支付已完成 |
payment.failed |
支付失败 |
refund.created |
退款已发起 |
dispute.created |
争议已开启 |
购物车¶
| 主题 | 说明 |
|---|---|
cart.created |
新购物车已创建 |
cart.updated |
购物车已更新 |
cart.converted |
购物车已转换为订单 |
Webhook 负载¶
传递头¶
POST /webhooks/rcommerce HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-RCommerce-Topic: order.created
X-RCommerce-Webhook-Id: 550e8400-e29b-41d4-a716-446655440300
X-RCommerce-Event-Id: evt_550e8400e29b41d4a716446655440301
X-RCommerce-Signature: t=1705312800,v1=abc123...
User-Agent: R-Commerce-Webhook/1.0
负载格式¶
{
"id": "evt_550e8400e29b41d4a716446655440301",
"topic": "order.created",
"api_version": "2024-01",
"created_at": "2024-01-15T10:00:00Z",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440100",
"order_number": "1001",
"total_price": "59.49",
"currency": "USD",
"customer": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"email": "customer@example.com"
}
}
}
签名验证¶
验证 webhook 签名以确保负载来自 R Commerce。
签名格式¶
验证示例 (Node.js)¶
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const elements = signature.split(',');
const signatureHash = elements.find(e => e.startsWith('v1=')).split('v1=')[1];
const timestamp = elements.find(e => e.startsWith('t=')).split('t=')[1];
// 检查时间戳(防止重放攻击)
const now = Math.floor(Date.now() / 1000);
if (now - parseInt(timestamp) > 300) { // 5 分钟容差
throw new Error('Webhook 时间戳太旧');
}
// 计算预期签名
const signedPayload = timestamp + '.' + payload;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// 比较签名
return crypto.timingSafeEqual(
Buffer.from(signatureHash),
Buffer.from(expectedSignature)
);
}
// 使用
app.post('/webhooks/rcommerce', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-rcommerce-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('签名无效');
}
const event = JSON.parse(req.body);
// 处理事件...
res.status(200).send('OK');
});
验证示例 (Python)¶
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
elements = dict(e.split('=') for e in signature.split(','))
timestamp = elements['t']
signature_hash = elements['v1']
# 检查时间戳
now = int(time.time())
if now - int(timestamp) > 300:
raise ValueError('Webhook 时间戳太旧')
# 计算预期签名
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature_hash, expected)
验证示例 (Rust)¶
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
fn verify_webhook(payload: &[u8], signature: &str, secret: &str) -> Result<bool, Box<dyn std::error::Error>> {
let parts: std::collections::HashMap<_, _> = signature
.split(',')
.filter_map(|e| {
let mut kv = e.splitn(2, '=');
Some((kv.next()?, kv.next()?))
})
.collect();
let timestamp = parts.get("t").ok_or("缺少时间戳")?;
let signature_hash = parts.get("v1").ok_or("缺少签名")?;
// 检查时间戳
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
if now - timestamp.parse::<u64>()? > 300 {
return Err("Webhook 时间戳太旧".into());
}
// 计算签名
let signed_payload = format!("{}.{}", timestamp, String::from_utf8_lossy(payload));
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())?;
mac.update(signed_payload.as_bytes());
let expected = hex::encode(mac.finalize().into_bytes());
Ok(expected == *signature_hash)
}
传递行为¶
重试策略¶
失败的 webhook 传递使用指数退避重试:
| 尝试 | 延迟 |
|---|---|
| 1 | 立即 |
| 2 | 5 秒 |
| 3 | 25 秒 |
| 4 | 2 分钟 |
| 5 | 10 分钟 |
最多 5 次重试尝试,持续 15 分钟。
成功标准¶
如果您的端点返回以下内容,则认为 webhook 传递成功: - HTTP 200-299 状态码 - 30 秒内响应
失败的传递¶
所有重试用尽后: - Webhook 自动禁用 - 向商店所有者发送邮件通知 - 失败的传递记录保存 30 天
最佳实践¶
端点要求¶
- 仅 HTTPS - 必须使用有效的 SSL 证书
- 快速响应 - 在处理之前返回 200
- 幂等 - 优雅地处理重复事件
- 验证签名 - 始终验证真实性
处理事件¶
app.post('/webhooks/rcommerce', async (req, res) => {
// 1. 验证签名
if (!verifyWebhook(req.body, req.headers['x-rcommerce-signature'], secret)) {
return res.status(401).send('签名无效');
}
// 2. 立即确认
res.status(200).send('OK');
// 3. 异步处理
const event = JSON.parse(req.body);
try {
switch (event.topic) {
case 'order.created':
await handleOrderCreated(event.data);
break;
case 'order.cancelled':
await handleOrderCancelled(event.data);
break;
// ...
}
} catch (error) {
// 记录错误,提醒团队
console.error('Webhook 处理失败:', error);
}
});
幂等性¶
使用事件 ID 防止重复处理:
const processedEvents = new Set(); // 生产环境使用 Redis
async function handleWebhook(event) {
if (processedEvents.has(event.id)) {
return; // 已处理
}
// 处理事件...
processedEvents.add(event.id);
}
传递日志¶
列出传递尝试¶
查询参数¶
| 参数 | 类型 | 说明 |
|---|---|---|
status |
string | success、failed |
event_id |
string | 按事件筛选 |
传递对象¶
{
"id": "del_550e8400e29b41d4a716446655440302",
"event_id": "evt_550e8400e29b41d4a716446655440301",
"webhook_id": "550e8400-e29b-41d4-a716-446655440300",
"status": "success",
"http_status": 200,
"response_body": "OK",
"attempts": 1,
"created_at": "2024-01-15T10:00:00Z",
"completed_at": "2024-01-15T10:00:01Z"
}
错误代码¶
| 代码 | HTTP 状态 | 说明 |
|---|---|---|
WEBHOOK_NOT_FOUND |
404 | Webhook 不存在 |
INVALID_URL |
400 | URL 必须是有效的 HTTPS |
INVALID_TOPIC |
400 | 未知的事件主题 |
URL_UNREACHABLE |
400 | 测试传递失败 |
MAX_WEBHOOKS_EXCEEDED |
429 | 配置的 webhooks 过多 |
WEBHOOK_DISABLED |
400 | Webhook 未激活 |