PolyPay
高階6 分鐘閱讀

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 頁面調整失敗自動重試次數;手動重發不受該配置限制。

驗籤範例

<?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 會被視為本次投遞失敗。