# Webhook 傳遞機制 (/guides/webhooks/delivery)





Webhook 傳遞機制 [#webhook-傳遞機制]

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

傳遞流程 [#傳遞流程]

當事件發生時，Recur 會：

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

請求格式 [#請求格式]

HTTP Headers [#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 [#request-body]

```json
{
  "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",
    "metadata": null,
    "created_at": "2024-01-15T10:30:00.000Z",
    "updated_at": "2024-01-15T10:30:00.000Z"
  }
}
```

簽章驗證 [#簽章驗證]

<Callout type="error">
  **重要**：務必驗證每個 Webhook 請求的簽章，確保請求確實來自 Recur。
</Callout>

簽章演算法 [#簽章演算法]

Recur 使用 HMAC-SHA256 演算法產生簽章：

1. 將請求 Body（JSON 字串）作為訊息
2. 使用您的 Webhook Secret 作為密鑰
3. 計算 HMAC-SHA256 雜湊
4. 將結果進行 Base64 編碼

驗證範例 [#驗證範例]

<Tabs items="['Node.js', 'Python', 'PHP', 'Go']">
  <Tab value="Node.js">
    ```typescript
    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 });
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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
    ```
  </Tab>

  <Tab value="PHP">
    ```php
    <?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]);
    ```
  </Tab>

  <Tab value="Go">
    ```go
    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})
    }
    ```
  </Tab>
</Tabs>

重試機制 [#重試機制]

Recur 內建可靠的 Webhook 傳遞機制，支援自動重試。

自動重試策略 [#自動重試策略]

當 Webhook 傳遞失敗時，系統會自動使用**指數退避**策略重試：

| 重試次數  | 延遲時間   | 累計時間  |
| ----- | ------ | ----- |
| 第 1 次 | 1 分鐘後  | 1 分鐘  |
| 第 2 次 | 2 分鐘後  | 3 分鐘  |
| 第 3 次 | 4 分鐘後  | 7 分鐘  |
| 第 4 次 | 8 分鐘後  | 15 分鐘 |
| 第 5 次 | 16 分鐘後 | 31 分鐘 |

總共最多自動重試 **5 次**，橫跨約 **31 分鐘**。

<Callout type="info">
  指數退避策略能避免在目標伺服器暫時故障時造成過多負載，同時確保在問題解決後快速恢復傳遞。
</Callout>

手動重試 [#手動重試]

如果自動重試後仍然失敗，您可以在後台手動觸發重試：

1. 前往「開發者」→「Webhooks」
2. 選擇目標 Webhook 端點
3. 在「事件歷史」中找到失敗的事件
4. 點擊「重試」按鈕

失敗條件 [#失敗條件]

以下情況視為傳遞失敗，會觸發自動重試：

* HTTP 狀態碼非 2xx（如 4xx、5xx）
* 請求逾時（超過 30 秒無回應）
* 連線失敗（無法建立連線）
* SSL/TLS 驗證失敗
* 網路錯誤

成功回應 [#成功回應]

請確保您的端點：

1. 在 **30 秒內**回傳回應（建議盡快處理，僅回傳 2xx，耗時任務放到背景執行）
2. 回傳 **2xx 狀態碼**（200, 201, 202, 204 等）
3. 可選：回傳 JSON 確認

```json
{
  "received": true
}
```

<Callout type="warning">
  即使處理邏輯需要較長時間，也應該先回傳 2xx 回應，再進行非同步處理。這能確保 Webhook 不會被誤判為失敗而觸發重試。
</Callout>

冪等處理 [#冪等處理]

<Callout type="warning">
  同一事件可能因重試而被傳遞多次，請確保您的處理邏輯是冪等的。
</Callout>

使用 `event.id` 來避免重複處理：

```typescript
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' };
}
```

非同步處理 [#非同步處理]

對於耗時操作，建議使用佇列進行非同步處理：

```typescript
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 [#403-forbidden]

您的端點可能有認證中介軟體阻擋請求。確保 Webhook 路由**不需要認證**：

```typescript
// Next.js 範例 - 排除 webhook 路由
export const config = {
  matcher: ['/((?!api/webhooks).*)'],
};
```

404 Not Found [#404-not-found]

確認：

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

簽章驗證失敗 [#簽章驗證失敗]

確認：

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

請求逾時 [#請求逾時]

優化端點效能：

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

下一步 [#下一步]

* [事件類型](/guides/webhooks/events) - 查看所有事件的 Payload 格式
* [設定端點](/guides/webhooks/endpoints) - 建立和管理 Webhook 端點
