Webhook 安全最佳实践
使用独立于 API Key 的 Ed25519 v2 签名验证 PolyPay Webhook。
Webhook 鉴权:Ed25519 v2
v2 使用 PolyPay 平台签名密钥,不需要选择某个商户 API Key。验签时必须校验 X-Webhook-Signature-Version、X-Webhook-Key-Id、X-Webhook-Merchant-Id、X-Webhook-Environment、X-Timestamp、X-Nonce 和 X-Webhook-Signature-V2,并从 /api/v1/pay/public/webhook-jwks 获取公钥。签名原文为 polypay-webhook-v2、kid、timestamp、nonce、merchant ID、environment 与原始请求体,以换行符连接。
上线前检查清单
- 仅允许 HTTPS 回调地址
- 校验时间戳并限制重放窗口
- 验证 Ed25519 v2 签名与商户、环境受众
- 每个事件实现幂等处理
- 异常回调写入审计日志
密钥轮换策略
PolyPay 通过 JWKS 发布当前和轮换保留的 Ed25519 公钥。按 X-Webhook-Key-Id 选择公钥并短时缓存 JWKS;未知 kid 时刷新一次,仍不存在则拒绝请求。
幂等处理
对 `event_id` 或链上 `hash` 建立唯一处理记录。重复事件直接返回 200,避免重复发货或重复记账。
金额与币种字段
`fiat_amount` 是原始 USD 订单总额,`amount` 是加密币数量,`exchange_rate` 表示 1 个加密币对应的锁定 USD 价值,`currency` 是 USDT、USDC 等加密支付资产,不是账单法币。`order_no` 是商户订单号,`event_id` 是幂等键;`hash` 是链上交易哈希,但管理员手动标记支付(status=7)时可以为空。
排查接收端拒绝
HTTP 409 表示请求已经到达,但接收端业务状态或数据校验拒绝了事件。金额不匹配时应对比本地 USD 总额与 fiat_amount;资产不匹配时应对比预期加密资产与 currency,再对比代币数量与 amount。修正字段映射或订单数据后再重发。接收端应尽量返回具体 field、expected、actual 和 event_id,避免只返回笼统的“金额或币种不匹配”。
失败重试
Webhook 默认失败重试次数为 0,即每个事件只自动投递一次。你可以在 Dashboard 的 Webhooks 页面调整失败自动重试次数;手动重发不受该配置限制。
WHMCS 等在每笔订单中传入 notify_url 的接入方式,可以保留默认 Webhook URL 为空,并单独设置 0–10 次失败重试。未配置默认地址时,通知使用订单回调地址。
验签示例
<?php
use PolyPay\PolyPay;
use PolyPay\WebhookHandler;
use PolyPay\Exception\SignatureException;
$polypay = new PolyPay(getenv('POLYPAY_API_KEY') ?: '');
try {
$event = $polypay
->webhookV2('MCH_YOUR_ID', 'production')
->handle();
$status = WebhookHandler::resolveStatus($event);
if ($status === 'paid') {
handleOrderPaidIdempotently($event);
}
http_response_code(200);
echo 'OK';
} catch (SignatureException $e) {
http_response_code($e->getHttpStatus());
echo 'Unauthorized';
}限流与安全重试
请求超过当前策略额度时,PolyPay 返回真实 HTTP 429。应用响应使用错误码 40001;CDN/WAF 也可能返回 HTML 或纯文本,因此客户端应先检查 HTTP 状态,再按 Content-Type 解析响应体。
HTTP/1.1 429 Too Many Requests
Retry-After: 10
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 10
{"code":40001,"message":"RateLimitExceeded","data":{"retry_after":10}}优先读取 Retry-After(可能是秒数或 HTTP-date);缺失时使用带抖动的指数退避。只自动重试可安全重放或具备幂等保护的请求,并设置最大重试次数。
Dashboard 中的 Webhook 测试与手动重发可能被限流。接收端应尽快完成验签、持久化后返回 2xx,再异步处理业务;返回 429 会被视为本次投递失败。