Recur
Recur
Recur 文件中心
開發者指南
Webhooks 概覽設定 Webhook 端點Webhook 傳遞機制Webhook 事件類型
開發者指南Webhooks

Webhook 傳遞機制

了解 Webhook 傳遞、重試策略和簽章驗證

Webhook 傳遞機制

本指南說明 Recur Webhook 的傳遞機制、重試策略,以及如何在您的伺服器上正確處理和驗證 Webhook。

傳遞流程

當事件發生時,Recur 會:

  1. 建立事件 Payload
  2. 使用您的 Webhook Secret 產生簽章
  3. 發送 POST 請求到您的端點
  4. 等待回應(最長 20 秒)
  5. 如果失敗,啟動重試機制

請求格式

HTTP Headers

每個 Webhook 請求包含以下標頭:

Header說明
Content-Typeapplication/json
X-Recur-SignatureHMAC-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",
    "plan_id": "plan_pro",
    "status": "active",
    "amount": 299,
    "billing_period": "monthly",
    "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 演算法產生簽章:

  1. 將請求 Body(JSON 字串)作為訊息
  2. 使用您的 Webhook Secret 作為密鑰
  3. 計算 HMAC-SHA256 雜湊
  4. 將結果進行 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})
}

重試機制

重試策略

當 Webhook 傳遞失敗時,Recur 會自動重試:

重試次數延遲時間
第 1 次立即
第 2 次5 分鐘後
第 3 次30 分鐘後
第 4 次2 小時後
第 5 次5 小時後
第 6 次10 小時後
第 7-10 次每 12 小時

總共最多重試 10 次,橫跨約 3 天。

失敗條件

以下情況視為傳遞失敗:

  • HTTP 狀態碼非 2xx
  • 請求逾時(超過 20 秒)
  • 連線失敗(無法建立連線)
  • SSL/TLS 驗證失敗

成功回應

請確保您的端點:

  1. 在 20 秒內回傳回應
  2. 回傳 2xx 狀態碼(200, 201, 202, 204 等)
  3. 可選:回傳 JSON 確認
{
  "received": true
}

冪等處理

同一事件可能因重試而被傳遞多次,請確保您的處理邏輯是冪等的。

使用 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

確認:

  1. URL 路徑正確
  2. 端點已部署
  3. 使用 POST 方法

簽章驗證失敗

確認:

  1. 使用正確的 Webhook Secret
  2. 使用原始 Request Body(未解析)
  3. Secret 正確編碼(不需要 Base64 解碼)

請求逾時

優化端點效能:

  1. 使用非同步處理
  2. 避免在回應前執行耗時操作
  3. 考慮增加伺服器資源

下一步

  • 事件類型 - 查看所有事件的 Payload 格式
  • 設定端點 - 建立和管理 Webhook 端點

設定 Webhook 端點

在 Recur 後台建立和管理 Webhook 端點

Webhook 事件類型

所有 Webhook 事件的完整列表和 Payload 格式

On this page

Webhook 傳遞機制傳遞流程請求格式HTTP HeadersRequest Body簽章驗證簽章演算法驗證範例重試機制重試策略失敗條件成功回應冪等處理非同步處理常見問題403 Forbidden404 Not Found簽章驗證失敗請求逾時下一步