跳到主要内容

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:

  1. 前往 控制台设置Webhook
  2. 点击 添加端点
  3. 输入 URL 并选择事件
  4. 保存并复制签名密钥

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立即
25 分钟
330 分钟
42 小时
56 小时
6-1012 小时

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:

  1. 前往 Webhook你的端点
  2. 点击 发送测试事件
  3. 选择事件类型
  4. 点击 发送

重放 Webhook

从控制台重放失败的 Webhook:

  1. 前往 Webhook最近的投递记录
  2. 找到失败的投递
  3. 点击 重放

下一步