Webhook 概述
A55 在收款、订阅和支付状态变更时向您的服务器发送 HTTP POST Webhook(网络钩子)。Webhook 支持实时响应,无需轮询。
为什么使用 Webhook
| 方式 | 延迟 | 可靠性 | A55 负载 |
|---|---|---|---|
| Webhook(推送) | 秒级 | 至少一次投递 + 重试 | 极低 |
| 轮询(拉取) | 取决于间隔 | 不会遗漏(可追赶) | 较高 |
以 Webhook 作为主要通知渠道,轮询作为对账兜底。
配置方式
在收款请求中设置 webhook_url,或在 A55 控制台配置默认 URL。
| 参数 | 范围 | 说明 |
|---|---|---|
收款请求中的 webhook_url | 单笔收款 | 仅接收该笔收款的事件 |
| 控制台中的默认 URL | 账户级别 | 当未提供 webhook_url 时使用 |
投递详情
| 属性 | 值 |
|---|---|
| 方法 | POST |
| Content-Type | application/json |
| 超时 | 30 秒 |
| TLS | 仅 HTTPS(拒绝 HTTP) |
HMAC 签名验证
A55 使用 HMAC(基于哈希的消息认证码)-SHA256 对每个 Webhook 签名。处理前务必验证。
| 请求头 | 内容 |
|---|---|
X-Webhook-Signature | sha256={hex_signature} |
X-Webhook-Timestamp | Unix 时间戳(秒) |
验证步骤
- 提取
X-Webhook-Signature和X-Webhook-Timestamp请求头 - 如果时间戳超过 5 分钟则拒绝(防重放)
- 构建签名载荷:
{timestamp}.{raw_body} - 使用您的 Webhook 密钥计算 HMAC-SHA256
- 使用时间安全比较进行校验
- 仅在签名匹配时处理事件
- Python
- JavaScript
import hmac, hashlib, time
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
"""验证 Webhook HMAC-SHA256 签名。"""
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.".encode() + payload
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature.removeprefix("sha256="))
const crypto = require('crypto');
function verifyWebhook(payload, signature, timestamp, secret) {
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature.replace('sha256=', ''))
);
}
安全警告
切勿在未验证 HMAC 签名的情况下处理 Webhook。攻击者可能伪造未经验证的 Webhook。
重试策略
投递失败后将在 24 小时内以指数退避方式重试:
| 尝试次数 | 延迟 | 累计时间 |
|---|---|---|
| 1 | 立即 | T+0 |
| 2 | 1 分钟 | T+1m |
| 3 | 5 分钟 | T+6m |
| 4 | 30 分钟 | T+36m |
| 5 | 2 小时 | T+2h 36m |
| 6 | 6 小时 | T+8h 36m |
| 7 | 24 小时 | T+32h 36m |
经过 7 次失败尝试后,事件在控制台中标记为失败。
触发重试的条件
| 响应 | 是否重试? | 原因 |
|---|---|---|
| 200–299 | 否 | 投递成功 |
| 400–499 | 否 | 客户端错误——重试无效 |
| 500–599 | 是 | 服务端错误——暂时性 |
| 超时(>30 秒) | 是 | 端点响应过慢 |
| 连接被拒绝 | 是 | 端点不可用 |
幂等处理
Webhook 使用至少一次投递。同一事件可能多次到达。您的处理器必须保证幂等。
def handle_webhook(event):
"""幂等处理 Webhook 事件。"""
event_id = event["id"]
if db.webhook_events.find_one({"event_id": event_id, "status": "completed"}):
return Response(status=200)
db.webhook_events.upsert({"event_id": event_id, "status": "processing"})
process_event(event)
db.webhook_events.update({"event_id": event_id, "status": "completed"})
return Response(status=200)
快速响应
收到事件后立即返回 HTTP 200。将耗时逻辑放到后台任务中处理,避免超时。
事件类型
| 领域 | 事件 |
|---|---|
| 收款 | charge.authorized、charge.captured、charge.failed、charge.refunded、charge.cancelled、charge.chargeback |
| PIX | pix.received、pix.expired、pix.refunded |
| Boleto | boleto.paid、boleto.expired、boleto.cancelled |
| 订阅 | subscription.created、subscription.renewed、subscription.failed、subscription.cancelled |
详见收款 Webhook 了解载荷详情。
测试
| 工具 | 适用场景 |
|---|---|
| webhook.site | 可视化查看载荷 |
| ngrok | 转发到本地开发环境 |
| A55 沙箱 | 生产级测试事件 |