Webhook 处理
学习如何接收和处理 Payve 支付通知
7 分钟阅读
更新于 2025/8/13
Webhook通知API
Webhook 是 Payve 主动向您的服务器发送支付事件通知的机制。当支付状态发生变化时,Payve 会向您配置的 Webhook URL 发送 HTTP POST 请求。
工作原理
sequenceDiagram
participant User as 用户
participant Payve as Payve
participant Merchant as 商户服务器
User->>Payve: 完成支付
Payve->>Payve: 确认交易
Payve->>Merchant: 发送 Webhook 通知
Merchant->>Merchant: 验证签名
Merchant->>Merchant: 处理支付
Merchant-->>Payve: 返回 200 OK
Note over Payve,Merchant: 如失败,自动重试MERMAID
代码已折叠
共 12 行
配置 Webhook
1. 在商户后台配置
登录 Payve 商户后台,在"Webhook 设置"中配置:
- Webhook URL: 您的服务器接收通知的地址
- 签名密钥: 用于验证请求来源的密钥
- 事件类型: 选择要接收的事件类型
2. 通过 API 配置
async function configureWebhook(url, events = ['payment.succeeded']) {
const token = await generateToken();
const response = await fetch('https://api.payve.io/api/v1/webhooks/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
url: url,
events: events,
active: true
})
});
const config = await response.json();
console.log('Webhook 密钥:', config.secret);
return config;
}JAVASCRIPT
代码已折叠
共 20 行
接收 Webhook 通知
基础示例(Node.js + Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
// 配置 - 从环境变量或配置文件读取
const WEBHOOK_SECRET = process.env.PAYVE_WEBHOOK_SECRET;
// 中间件:保存原始请求体用于签名验证
app.use('/webhook/payve', express.raw({ type: 'application/json' }));
// Webhook 处理端点
app.post('/webhook/payve', async (req, res) => {
try {
// 1. 获取签名
const signature = req.headers['x-payve-signature'];
const eventId = req.headers['x-payve-event-id'];
const timestamp = req.headers['x-payve-timestamp'];
// 2. 验证签名
const isValid = verifyWebhookSignature(
req.body,
signature,
WEBHOOK_SECRET,
timestamp
);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// 3. 解析事件数据
const event = JSON.parse(req.body.toString());
// 4. 幂等性检查(防止重复处理)
if (await isEventProcessed(eventId)) {
console.log('Event already processed:', eventId);
return res.status(200).json({ message: 'Already processed' });
}
// 5. 处理不同类型的事件
switch (event.type) {
case 'payment.succeeded':
await handlePaymentSuccess(event.data);
break;
case 'payment.failed':
await handlePaymentFailure(event.data);
break;
case 'payment.expired':
await handlePaymentExpired(event.data);
break;
default:
console.log('Unknown event type:', event.type);
}
// 6. 标记事件已处理
await markEventAsProcessed(eventId);
// 7. 返回成功响应
res.status(200).json({ message: 'Webhook processed successfully' });
} catch (error) {
console.error('Webhook processing error:', error);
// 返回 500 会触发 Payve 重试
res.status(500).json({ error: 'Processing failed' });
}
});
// 验证签名函数
function verifyWebhookSignature(payload, signature, secret, timestamp) {
// 检查时间戳(防止重放攻击)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) { // 5分钟容差
return false;
}
// 构造签名字符串
const signaturePayload = `${timestamp}.${payload.toString()}`;
// 计算期望的签名
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signaturePayload)
.digest('hex');
// 使用时间恒定比较防止时序攻击
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// 处理支付成功
async function handlePaymentSuccess(payment) {
console.log('Payment succeeded:', payment.orderId);
// 更新订单状态
await updateOrderStatus(payment.metadata.orderId, 'PAID');
// 发送确认邮件
await sendPaymentConfirmationEmail(payment);
// 触发后续业务逻辑
await fulfillOrder(payment.metadata.orderId);
}
// 幂等性检查
const processedEvents = new Set(); // 实际应使用数据库
async function isEventProcessed(eventId) {
return processedEvents.has(eventId);
}
async function markEventAsProcessed(eventId) {
processedEvents.add(eventId);
}JAVASCRIPT
代码已折叠
共 119 行
Python Flask 示例
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import time
import os
app = Flask(__name__)
# 配置
WEBHOOK_SECRET = os.environ.get('PAYVE_WEBHOOK_SECRET')
@app.route('/webhook/payve', methods=['POST'])
def handle_webhook():
try:
# 1. 获取请求头
signature = request.headers.get('X-Payve-Signature')
event_id = request.headers.get('X-Payve-Event-Id')
timestamp = request.headers.get('X-Payve-Timestamp')
# 2. 获取原始请求体
payload = request.get_data()
# 3. 验证签名
if not verify_signature(payload, signature, timestamp):
return jsonify({'error': 'Invalid signature'}), 401
# 4. 解析事件
event = json.loads(payload)
# 5. 检查幂等性
if is_event_processed(event_id):
return jsonify({'message': 'Already processed'}), 200
# 6. 处理事件
if event['type'] == 'payment.succeeded':
handle_payment_success(event['data'])
elif event['type'] == 'payment.failed':
handle_payment_failure(event['data'])
elif event['type'] == 'payment.expired':
handle_payment_expired(event['data'])
# 7. 标记已处理
mark_event_processed(event_id)
return jsonify({'message': 'Success'}), 200
except Exception as e:
print(f'Webhook error: {e}')
return jsonify({'error': 'Processing failed'}), 500
def verify_signature(payload, signature, timestamp):
# 验证时间戳
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
return False
# 计算签名
signature_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
signature_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def handle_payment_success(payment):
print(f"Payment succeeded: {payment['orderId']}")
# 实现您的业务逻辑
update_order_status(payment['metadata']['orderId'], 'PAID')
send_confirmation_email(payment)PYTHON
代码已折叠
共 72 行
事件类型
Payve 支持以下 Webhook 事件类型:
payment.succeeded
支付成功完成
{
"type": "payment.succeeded",
"eventId": "evt_1234567890",
"timestamp": "2024-01-15T12:00:00Z",
"data": {
"orderId": "pay_abc123",
"status": "COMPLETED",
"amount": "100.00",
"currency": "USDT",
"network": "TRC20",
"txHash": "0x123...",
"confirmations": 6,
"paidAmount": "100.00",
"paidAt": "2024-01-15T11:58:00Z",
"metadata": {
"orderId": "ORDER-123"
}
}
}JSON
代码已折叠
共 19 行
payment.failed
支付失败
{
"type": "payment.failed",
"eventId": "evt_1234567891",
"timestamp": "2024-01-15T12:00:00Z",
"data": {
"orderId": "pay_abc124",
"status": "FAILED",
"amount": "100.00",
"currency": "USDT",
"failureReason": "INSUFFICIENT_PAYMENT",
"paidAmount": "50.00",
"metadata": {
"orderId": "ORDER-124"
}
}
}JSON
代码已折叠
共 16 行
payment.expired
支付订单过期
{
"type": "payment.expired",
"eventId": "evt_1234567892",
"timestamp": "2024-01-15T12:00:00Z",
"data": {
"orderId": "pay_abc125",
"status": "EXPIRED",
"amount": "100.00",
"currency": "USDT",
"expiresAt": "2024-01-15T12:00:00Z",
"metadata": {
"orderId": "ORDER-125"
}
}
}JSON
代码已折叠
共 15 行
payment.confirming
支付确认中(可选)
{
"type": "payment.confirming",
"eventId": "evt_1234567893",
"timestamp": "2024-01-15T11:55:00Z",
"data": {
"orderId": "pay_abc123",
"status": "CONFIRMING",
"amount": "100.00",
"currency": "USDT",
"txHash": "0x123...",
"confirmations": 3,
"requiredConfirmations": 6
}
}JSON
代码已折叠
共 14 行
重试机制
Payve 的 Webhook 重试策略:
- 初次发送: 立即发送
- 第1次重试: 1分钟后
- 第2次重试: 5分钟后
- 第3次重试: 15分钟后
- 第4次重试: 1小时后
- 第5次重试: 3小时后
- 最后重试: 24小时后
处理重试
// 确保幂等性处理
app.post('/webhook/payve', async (req, res) => {
const eventId = req.headers['x-payve-event-id'];
// 使用数据库或 Redis 存储已处理的事件
const db = await getDatabase();
// 检查是否已处理
const processed = await db.collection('webhook_events').findOne({
eventId: eventId
});
if (processed) {
// 已处理,直接返回成功
return res.status(200).json({
message: 'Already processed',
eventId: eventId
});
}
try {
// 处理事件
await processWebhookEvent(req.body);
// 记录已处理
await db.collection('webhook_events').insertOne({
eventId: eventId,
processedAt: new Date(),
data: req.body
});
res.status(200).json({ message: 'Success' });
} catch (error) {
// 返回 500 触发重试
res.status(500).json({ error: 'Processing failed' });
}
});JAVASCRIPT
代码已折叠
共 37 行
安全最佳实践
1. 验证签名
始终验证 Webhook 签名以确保请求来自 Payve:
function strictVerifySignature(req, secret) {
const signature = req.headers['x-payve-signature'];
const timestamp = req.headers['x-payve-timestamp'];
const body = req.rawBody;
// 1. 验证必需的头部
if (!signature || !timestamp) {
throw new Error('Missing required headers');
}
// 2. 验证时间戳防止重放攻击
const currentTime = Math.floor(Date.now() / 1000);
const webhookTime = parseInt(timestamp);
if (isNaN(webhookTime)) {
throw new Error('Invalid timestamp');
}
const timeDiff = Math.abs(currentTime - webhookTime);
if (timeDiff > 300) { // 5分钟容差
throw new Error('Timestamp too old');
}
// 3. 验证签名
const payload = `${timestamp}.${body}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// 4. 使用恒定时间比较
const signatureBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expectedSig);
if (signatureBuffer.length !== expectedBuffer.length) {
throw new Error('Invalid signature');
}
if (!crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
throw new Error('Invalid signature');
}
return true;
}JAVASCRIPT
代码已折叠
共 44 行
2. 使用 HTTPS
确保您的 Webhook 端点使用 HTTPS:
// 强制 HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.status(403).send('HTTPS required');
}
next();
});3. IP 白名单(可选)
限制只接受来自 Payve 服务器的请求:
const PAYVE_IPS = [
'203.0.113.0/24', // 示例 IP 范围
'198.51.100.0/24'
];
app.use('/webhook/payve', (req, res, next) => {
const clientIp = req.ip || req.connection.remoteAddress;
if (!isIpAllowed(clientIp, PAYVE_IPS)) {
return res.status(403).send('Forbidden');
}
next();
});JAVASCRIPT
代码已折叠
共 14 行
4. 限流
防止 Webhook 端点被滥用:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1分钟
max: 100, // 最多100个请求
message: 'Too many requests'
});
app.use('/webhook/payve', webhookLimiter);测试 Webhook
1. 使用测试工具
在商户后台使用测试工具发送测试通知:
// 商户后台提供的测试功能
async function sendTestWebhook() {
const response = await fetch('/api/v1/webhooks/test', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
type: 'payment.succeeded',
testMode: true
})
});
return await response.json();
}JAVASCRIPT
代码已折叠
共 15 行
2. 本地测试
使用 ngrok 等工具将本地服务暴露到公网:
# 安装 ngrok
npm install -g ngrok
# 暴露本地 3000 端口
ngrok http 3000
# 获得公网 URL,例如:https://abc123.ngrok.io
# 在 Payve 后台配置:https://abc123.ngrok.io/webhook/payve3. 模拟 Webhook
创建测试脚本模拟 Webhook 请求:
const crypto = require('crypto');
const axios = require('axios');
function sendMockWebhook(url, secret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const event = {
type: 'payment.succeeded',
eventId: 'evt_test_' + Date.now(),
timestamp: new Date().toISOString(),
data: {
orderId: 'pay_test_123',
status: 'COMPLETED',
amount: '100.00',
currency: 'USDT',
txHash: '0xtest123',
metadata: {
orderId: 'ORDER-TEST-123'
}
}
};
const payload = JSON.stringify(event);
const signaturePayload = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signaturePayload)
.digest('hex');
return axios.post(url, payload, {
headers: {
'Content-Type': 'application/json',
'X-Payve-Signature': signature,
'X-Payve-Event-Id': event.eventId,
'X-Payve-Timestamp': timestamp
}
});
}
// 使用
sendMockWebhook('http://localhost:3000/webhook/payve', 'your-webhook-secret')
.then(response => console.log('Success:', response.data))
.catch(error => console.error('Error:', error.response?.data));JAVASCRIPT
代码已折叠
共 42 行
故障排查
常见问题
-
未收到 Webhook
- 检查 Webhook URL 是否正确
- 确认服务器可以从公网访问
- 查看商户后台的 Webhook 日志
-
签名验证失败
- 确认使用正确的密钥
- 检查是否正确获取原始请求体
- 验证时间戳是否在容差范围内
-
重复处理事件
- 实现幂等性检查
- 使用 eventId 去重
- 正确返回 200 状态码
-
处理超时
- 异步处理耗时操作
- 先返回 200,后台处理
- 优化数据库查询
监控和日志
// 记录所有 Webhook 请求
app.post('/webhook/payve', async (req, res) => {
const startTime = Date.now();
const eventId = req.headers['x-payve-event-id'];
console.log('Webhook received:', {
eventId,
type: req.body.type,
timestamp: new Date().toISOString()
});
try {
await processWebhook(req);
const duration = Date.now() - startTime;
console.log('Webhook processed:', {
eventId,
duration,
status: 'success'
});
res.status(200).json({ success: true });
} catch (error) {
const duration = Date.now() - startTime;
console.error('Webhook failed:', {
eventId,
duration,
error: error.message,
stack: error.stack
});
res.status(500).json({ error: 'Processing failed' });
}
});JAVASCRIPT
代码已折叠
共 34 行
