開發者指南Webhooks
Webhook 傳遞機制
了解 Webhook 傳遞、重試策略和簽章驗證
Webhook 傳遞機制
本指南說明 Recur Webhook 的傳遞機制、重試策略,以及如何在您的伺服器上正確處理和驗證 Webhook。
傳遞流程
當事件發生時,Recur 會:
- 建立事件 Payload
- 使用您的 Webhook Secret 產生簽章
- 發送 POST 請求到您的端點
- 等待回應(最長 20 秒)
- 如果失敗,啟動重試機制
請求格式
HTTP Headers
每個 Webhook 請求包含以下標頭:
| Header | 說明 |
|---|---|
Content-Type | application/json |
X-Recur-Signature | HMAC-SHA256 簽章(Base64 編碼) |
X-Recur-Event-Id | 唯一事件 ID |
X-Recur-Event-Type | 事件類型(如 subscription.activated) |
X-Recur-Timestamp | 事件時間戳(Unix 毫秒) |
Request Body
{
"id": "evt_abc123def456",
"type": "subscription.activated",
"timestamp": "2024-01-15T10:30:00.000Z",
"data": {
"id": "sub_xyz789",
"customer_id": "cus_123",
"product_id": "prod_pro",
"status": "active",
"amount": 299,
"interval": "month",
"interval_count": 1,
"current_period_start": "2024-01-15T00:00:00.000Z",
"current_period_end": "2024-02-15T00:00:00.000Z",
"created_at": "2024-01-15T10:30:00.000Z",
"updated_at": "2024-01-15T10:30:00.000Z"
}
}簽章驗證
重要:務必驗證每個 Webhook 請求的簽章,確保請求確實來自 Recur。
簽章演算法
Recur 使用 HMAC-SHA256 演算法產生簽章:
- 將請求 Body(JSON 字串)作為訊息
- 使用您的 Webhook Secret 作為密鑰
- 計算 HMAC-SHA256 雜湊
- 將結果進行 Base64 編碼
驗證範例
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js 範例
app.post('/api/webhooks/recur', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-recur-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
// 處理事件...
console.log('Received event:', event.type);
res.status(200).json({ received: true });
});import hmac
import hashlib
import base64
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).digest()
expected_b64 = base64.b64encode(expected).decode('utf-8')
return hmac.compare_digest(signature, expected_b64)
@app.route('/api/webhooks/recur', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Recur-Signature')
payload = request.get_data()
if not verify_webhook_signature(payload, signature, os.environ['WEBHOOK_SECRET']):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
# 處理事件...
print(f"Received event: {event['type']}")
return jsonify({'received': True}), 200<?php
function verifyWebhookSignature(string $payload, string $signature, string $secret): bool {
$expected = base64_encode(hash_hmac('sha256', $payload, $secret, true));
return hash_equals($expected, $signature);
}
// 處理 Webhook
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_RECUR_SIGNATURE'] ?? '';
$secret = getenv('WEBHOOK_SECRET');
if (!verifyWebhookSignature($payload, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$event = json_decode($payload, true);
// 處理事件...
error_log("Received event: " . $event['type']);
http_response_code(200);
echo json_encode(['received' => true]);package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"os"
)
func verifyWebhookSignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Recur-Signature")
secret := os.Getenv("WEBHOOK_SECRET")
if !verifyWebhookSignature(payload, signature, secret) {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid signature"})
return
}
var event map[string]interface{}
json.Unmarshal(payload, &event)
// 處理事件...
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}重試機制
Recur 使用 Hookdeck 作為 Webhook 傳遞基礎設施,提供可靠的自動重試機制。
自動重試策略
當 Webhook 傳遞失敗時,系統會自動使用指數退避策略重試:
| 重試次數 | 延遲時間 | 累計時間 |
|---|---|---|
| 第 1 次 | 1 分鐘後 | 1 分鐘 |
| 第 2 次 | 2 分鐘後 | 3 分鐘 |
| 第 3 次 | 4 分鐘後 | 7 分鐘 |
| 第 4 次 | 8 分鐘後 | 15 分鐘 |
| 第 5 次 | 16 分鐘後 | 31 分鐘 |
總共最多自動重試 5 次,橫跨約 31 分鐘。
指數退避策略能避免在目標伺服器暫時故障時造成過多負載,同時確保在問題解決後快速恢復傳遞。
手動重試
如果自動重試後仍然失敗,您可以在後台手動觸發重試:
- 前往「開發者」→「Webhooks」
- 選擇目標 Webhook 端點
- 在「事件歷史」中找到失敗的事件
- 點擊「重試」按鈕
失敗條件
以下情況視為傳遞失敗,會觸發自動重試:
- HTTP 狀態碼非 2xx(如 4xx、5xx)
- 請求逾時(超過 60 秒無回應)
- 連線失敗(無法建立連線)
- SSL/TLS 驗證失敗
- 網路錯誤
成功回應
請確保您的端點:
- 在 60 秒內回傳回應(建議 30 秒內)
- 回傳 2xx 狀態碼(200, 201, 202, 204 等)
- 可選:回傳 JSON 確認
{
"received": true
}即使處理邏輯需要較長時間,也應該先回傳 2xx 回應,再進行非同步處理。這能確保 Webhook 不會被誤判為失敗而觸發重試。
冪等處理
同一事件可能因重試而被傳遞多次,請確保您的處理邏輯是冪等的。
使用 event.id 來避免重複處理:
import { prisma } from '@/lib/prisma';
async function handleWebhook(event: WebhookEvent) {
// 檢查是否已處理過
const existing = await prisma.processedWebhook.findUnique({
where: { eventId: event.id }
});
if (existing) {
console.log(`Event ${event.id} already processed, skipping`);
return { status: 'skipped' };
}
// 處理事件
await processEvent(event);
// 記錄已處理
await prisma.processedWebhook.create({
data: { eventId: event.id, processedAt: new Date() }
});
return { status: 'processed' };
}非同步處理
對於耗時操作,建議使用佇列進行非同步處理:
import { Queue } from 'bullmq';
const webhookQueue = new Queue('webhooks');
app.post('/api/webhooks/recur', async (req, res) => {
// 1. 驗證簽章
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. 快速回應
res.status(200).json({ received: true });
// 3. 加入佇列非同步處理
await webhookQueue.add('process', {
event: req.body,
receivedAt: new Date().toISOString()
});
});常見問題
403 Forbidden
您的端點可能有認證中介軟體阻擋請求。確保 Webhook 路由不需要認證:
// Next.js 範例 - 排除 webhook 路由
export const config = {
matcher: ['/((?!api/webhooks).*)'],
};404 Not Found
確認:
- URL 路徑正確
- 端點已部署
- 使用 POST 方法
簽章驗證失敗
確認:
- 使用正確的 Webhook Secret
- 使用原始 Request Body(未解析)
- Secret 正確編碼(不需要 Base64 解碼)
請求逾時
優化端點效能:
- 使用非同步處理
- 避免在回應前執行耗時操作
- 考慮增加伺服器資源