Webhook
接收支付事件的实时通知。
概述
Webhook 允许你的应用在支付事件发生时自动接收更新,无需轮询。
配置 Webhook
逐笔支付 Webhook
在创建支付时设置 Webhook URL:
const payment = await pelago.payments.create({
amount: 100,
currency: 'USD',
webhookUrl: 'https://mystore.com/api/webhooks/pelago',
// ...
});
全局 Webhook(控制台)
为所有支付配置默认 Webhook:
- 前往 控制台 → 设置 → Webhook
- 点击 添加端点
- 输入 URL 并选择事件
- 保存并复制签名密钥
Webhook 事件
| 事件 | 描述 |
|---|---|
payment.completed | 支付成功 |
payment.failed | 支付失败 |
payment.expired | 支付超时 |
payment.refunded | 退款完成 |
settlement.completed | 资金已结算至钱包 |
settlement.failed | 结算失败 |
Webhook 负载
结构
{
"id": "evt_abc123",
"object": "event",
"type": "payment.completed",
"created": "2025-02-08T22:35:00Z",
"data": {
"paymentId": "pay_7xKp9mNq2vT",
"status": "completed",
"amount": 100.00,
"currency": "USD",
"cryptoAmount": "100.000000",
"cryptocurrency": "USDC",
"network": "stellar",
"merchantWallet": "GXXXXX...",
"customerWallet": "GCUST...",
"transactionHash": "abc123...def456",
"metadata": {
"orderId": "ORD-12345",
"customerId": "cust_abc123"
}
}
}
事件类型
payment.completed
{
"type": "payment.completed",
"data": {
"paymentId": "pay_xxx",
"transactionHash": "abc...",
"customerWallet": "GCUST..."
}
}
payment.failed
{
"type": "payment.failed",
"data": {
"paymentId": "pay_xxx",
"failureReason": "insufficient_funds",
"failureMessage": "Customer wallet has insufficient USDC"
}
}
payment.expired
{
"type": "payment.expired",
"data": {
"paymentId": "pay_xxx",
"expiredAt": "2025-02-08T23:00:00Z"
}
}
签名验证
务必验证 Webhook 签名以确保真实性。
JavaScript
import crypto from 'crypto';
import express from 'express';
const app = express();
// 使用原始请求体进行签名验证
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-pelago-signature'];
const timestamp = req.headers['x-pelago-timestamp'];
const body = req.body.toString();
// 验证签名
const payload = timestamp + '.' + body;
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 验证时间戳(防止重放攻击)
const eventTime = parseInt(timestamp);
const now = Date.now();
if (Math.abs(now - eventTime) > 300000) { // 5 分钟
return res.status(401).json({ error: 'Stale timestamp' });
}
// 处理事件
const event = JSON.parse(body);
handleEvent(event);
res.status(200).json({ received: true });
}
);
async function handleEvent(event) {
switch (event.type) {
case 'payment.completed':
await fulfillOrder(event.data.metadata.orderId);
break;
case 'payment.failed':
await notifyCustomer(event.data.metadata.customerId);
break;
}
}
Python
import hmac
import hashlib
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Pelago-Signature')
timestamp = request.headers.get('X-Pelago-Timestamp')
body = request.get_data(as_text=True)
# 验证签名
payload = timestamp + '.' + body
expected = hmac.new(
os.environ['WEBHOOK_SECRET'].encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({'error': 'Invalid signature'}), 401
# 验证时间戳
event_time = int(timestamp)
now = int(time.time() * 1000)
if abs(now - event_time) > 300000:
return jsonify({'error': 'Stale timestamp'}), 401
# 处理事件
event = request.get_json()
handle_event(event)
return jsonify({'received': True}), 200
使用 SDK
// 通过 SDK 简化验证
app.post('/webhook', express.json(), (req, res) => {
const signature = req.headers['x-pelago-signature'];
const timestamp = req.headers['x-pelago-timestamp'];
const isValid = pelago.webhooks.verify(
req.body,
signature,
timestamp,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid');
}
// 处理...
});
重试策略
失败的 Webhook 将按指数退避策略重试:
| 尝试次数 | 延迟 |
|---|---|
| 1 | 立即 |
| 2 | 5 分钟 |
| 3 | 30 分钟 |
| 4 | 2 小时 |
| 5 | 6 小时 |
| 6-10 | 12 小时 |
10 次失败后,Webhook 将被标记为失败。请在控制台查看失败的 Webhook。
最佳实践
1. 快速响应
立即返回 HTTP 200,然后异步处理:
app.post('/webhook', async (req, res) => {
// 先验证签名
if (!verifySignature(req)) {
return res.status(401).send('Invalid');
}
// 立即确认
res.status(200).json({ received: true });
// 异步处理
processEventAsync(req.body);
});
async function processEventAsync(event: any) {
// 在此处理耗时任务
await fulfillOrder(event.data.metadata.orderId);
await sendConfirmationEmail(event.data.metadata.email);
}
2. 处理重复事件
实现幂等性以应对重试场景:
const processedEvents = new Set(); // 生产环境请使用 Redis
app.post('/webhook', (req, res) => {
const eventId = req.body.id;
if (processedEvents.has(eventId)) {
console.log('重复事件,跳过:', eventId);
return res.status(200).json({ received: true });
}
processedEvents.add(eventId);
handleEvent(req.body);
res.status(200).json({ received: true });
});
3. 记录所有日志
app.post('/webhook', (req, res) => {
console.log('收到 Webhook:', {
eventId: req.body.id,
type: req.body.type,
paymentId: req.body.data.paymentId,
timestamp: new Date().toISOString()
});
// ...
});
测试 Webhook
本地开发
使用 ngrok 暴露本地服务器:
ngrok http 3000
# 输出: https://abc123.ngrok.io
将此 URL 设为你的 Webhook 端点。
Webhook 测试器
从控制台发送测试 Webhook:
- 前往 Webhook → 你的端点
- 点击 发送测试事件
- 选择事件类型
- 点击 发送
重放 Webhook
从控制台重放失败的 Webhook:
- 前往 Webhook → 最近的投递记录
- 找到失败的投递
- 点击 重放