---
title: Webhook 事件類型
description: 所有 Webhook 事件的完整列表和 Payload 格式
---

  **狀態值格式**：Webhook payload 中的 `status` 欄位使用大寫格式（如 `ACTIVE`、`PAST_DUE`、`CANCELED`），與 API 回傳一致。本頁範例中的小寫 status 值僅供示意。

## 事件觸發時序

### 初次訂閱（Checkout）

下圖說明首次訂閱結帳流程中各事件的觸發順序：

```mermaid
sequenceDiagram
    participant 顧客
    participant 結帳頁面
    participant Recur
    participant Webhook

    顧客->>結帳頁面: 進入結帳頁面
    結帳頁面->>Recur: 建立 Checkout Session
    Recur-->>Webhook: checkout.created
    Note right of Webhook: 結帳 Session 建立

    顧客->>結帳頁面: 填寫姓名、Email
    結帳頁面->>Recur: 建立訂閱記錄
    Recur-->>Webhook: subscription.created
    Note right of Webhook: 訂閱建立 (pending)

    顧客->>結帳頁面: 輸入信用卡並送出
    結帳頁面->>Recur: 執行付款

    alt 付款成功
        Recur-->>Webhook: order.paid
        Recur-->>Webhook: subscription.activated
        Note right of Webhook: 訂閱啟用 (active)
    else 付款失敗
        Recur-->>Webhook: order.payment_failed
    end

    Recur-->>Webhook: checkout.completed
    Note right of Webhook: 結帳流程完成
```

**checkout.created vs subscription.created 的差異**

- `checkout.created`：顧客**進入結帳頁面時**觸發，此時尚未填寫任何資訊
- `subscription.created`：顧客**提交基本資訊後**觸發，系統建立 pending 狀態的訂閱記錄

### 初次訂閱（含試用期）

當產品設有試用期時，結帳流程會以 NT$2 驗證扣款取代全額扣款：

```mermaid
sequenceDiagram
    participant 顧客
    participant 結帳頁面
    participant Recur
    participant 金流
    participant Webhook

    顧客->>結帳頁面: 進入結帳頁面
    結帳頁面->>Recur: 建立 Checkout Session
    Recur-->>Webhook: checkout.created
    Note right of Webhook: 結帳 Session 建立

    顧客->>結帳頁面: 填寫姓名、Email
    結帳頁面->>Recur: 建立訂閱記錄
    Recur-->>Webhook: subscription.created
    Note right of Webhook: 訂閱建立 (pending)

    顧客->>結帳頁面: 輸入信用卡並送出
    結帳頁面->>Recur: 執行 NT$2 驗證
    Recur->>金流: NT$2 授權交易
    金流-->>Recur: 授權結果

    alt 驗證成功
        Recur->>金流: 取消授權（不實際扣款）
        Recur-->>Webhook: order.paid
        Note right of Webhook: NT$2 驗證訂單
        Recur-->>Webhook: subscription.activated
        Note right of Webhook: 訂閱啟用 (trialing)
    else 驗證失敗
        Recur-->>Webhook: order.payment_failed
    end

    Recur-->>Webhook: checkout.completed
    Note right of Webhook: 結帳流程完成
```

**試用期 subscription.activated 的差異**

- 一般訂閱：`subscription.activated` 的 `status` 為 `active`
- 試用期訂閱：`subscription.activated` 的 `status` 為 `trialing`，且 `trial_ends_at` 有值

收到事件時可根據 `trial_ends_at` 是否有值來判斷。詳見[試用期文件](/features/payments/trials)。

### 訂單生命週期（付款與退款）

下圖說明訂單從建立到完成，以及退款的完整流程：

```mermaid
sequenceDiagram
    participant 顧客
    participant 結帳頁面
    participant Recur
    participant 金流
    participant Webhook

    顧客->>結帳頁面: 進入結帳頁面
    結帳頁面->>Recur: 建立訂單 (PENDING)

    顧客->>結帳頁面: 輸入信用卡並送出
    結帳頁面->>Recur: 執行付款
    Recur->>金流: 請求扣款
    金流-->>Recur: 回傳結果

    alt 付款成功
        Recur->>Recur: 訂單狀態 → COMPLETED
        Recur-->>Webhook: order.paid
        Note right of Webhook: 訂單付款成功
    else 付款失敗
        Recur->>Recur: 訂單狀態 → FAILED
        Recur-->>Webhook: order.payment_failed
        Note right of Webhook: 訂單付款失敗
    end
```

### 退款流程

當需要退款時，流程如下：

```mermaid
sequenceDiagram
    participant 商家後台
    participant Recur
    participant 金流
    participant Webhook

    商家後台->>Recur: 發起退款請求
    Recur->>Recur: 驗證退款資格
    Note right of Recur: 檢查 180 天限制<br/>檢查可退款金額
    Recur-->>Webhook: refund.created
    Note right of Webhook: 退款建立 (pending)

    Recur->>金流: 執行退款
    金流-->>Recur: 回傳結果

    alt 退款成功
        Recur->>Recur: 更新已退款金額
        Recur-->>Webhook: refund.succeeded
        Note right of Webhook: 退款成功

        alt 全額退款
            Recur->>Recur: 訂單狀態 → REFUNDED
        else 部分退款
            Recur->>Recur: 訂單狀態 → PARTIALLY_REFUNDED
        end
    else 退款失敗
        Recur-->>Webhook: refund.failed
        Note right of Webhook: 退款失敗（可重試）
    end
```

### 訂閱續費（Renewal）

下圖說明訂閱週期扣款流程中各事件的觸發順序：

```mermaid
sequenceDiagram
    participant 排程任務
    participant Recur
    participant 金流
    participant Webhook

    排程任務->>Recur: 觸發週期扣款
    Recur->>Recur: 建立帳單 Invoice
    Recur-->>Webhook: invoice.created
    Note right of Webhook: 帳單建立 (pending)

    Recur->>金流: 執行信用卡扣款
    金流-->>Recur: 回傳結果

    alt 扣款成功
        Recur-->>Webhook: invoice.paid
        Recur-->>Webhook: subscription.renewed
        Note right of Webhook: 訂閱續訂成功
    else 扣款失敗
        Recur-->>Webhook: invoice.payment_failed
        Recur-->>Webhook: subscription.past_due
        Note right of Webhook: 訂閱進入逾期狀態
    end
```

**逾期與重試機制**

當續費扣款失敗時，行為取決於產品的「付款寬限期」設定：

**寬限期啟用（預設）：** 訂閱進入 `past_due` 狀態，系統在 3 天寬限期內自動重試扣款（最多 3 次）：
- 重試成功：觸發 `invoice.paid` + `subscription.renewed`，訂閱恢復為 `active`
- 所有重試失敗：觸發 `subscription.revoked`，訂閱取消

**寬限期關閉：** 訂閱立即取消，觸發 `invoice.payment_failed` + `subscription.revoked`，不會觸發 `subscription.past_due`。

### 試用期轉正式（Trial Graduation）

試用期到期時，系統自動將訂閱從 `trialing` 轉為 `active` 並發起全額扣款：

```mermaid
sequenceDiagram
    participant 排程任務
    participant Recur
    participant 金流
    participant Webhook

    排程任務->>Recur: 偵測 trialEndsAt <= now
    Recur->>Recur: TRIAL → ACTIVE
    Recur->>Recur: 建立 Invoice
    Recur-->>Webhook: invoice.created
    Note right of Webhook: 帳單建立 (pending)

    Recur->>金流: 執行信用卡扣款（全額）
    金流-->>Recur: 回傳結果

    alt 扣款成功
        Recur-->>Webhook: invoice.paid
        Recur-->>Webhook: subscription.renewed
        Note right of Webhook: 訂閱轉正式 (active)
    else 扣款失敗
        Recur-->>Webhook: invoice.payment_failed
        Recur-->>Webhook: subscription.past_due
        Note right of Webhook: 訂閱進入逾期狀態
    end
```

**Trial Graduation 使用相同的續約機制**

試用期轉正式扣款與一般續約使用相同的排程任務和重試機制。扣款失敗時的重試策略也相同。詳見[試用期文件](/features/payments/trials)。

### 訂閱方案切換（Switching）

下圖說明訂閱方案切換流程中各事件的觸發順序：

```mermaid
sequenceDiagram
    participant 顧客
    participant 客戶門戶
    participant Recur
    participant Webhook

    顧客->>客戶門戶: 選擇變更方案

    alt 升級/月轉年（立即執行）
        客戶門戶->>Recur: 請求方案切換
        Recur->>Recur: 計算差額並收費
        Recur-->>Webhook: subscription.upgraded
        Note right of Webhook: 訂閱升級（含計費週期升級）
    else 降級/年轉月（排程執行）
        客戶門戶->>Recur: 請求方案切換
        Recur->>Recur: 建立排程變更
        Recur-->>Webhook: subscription.schedule_created
        Note right of Webhook: 排程變更建立

        Note over Recur: 等待當前週期結束...

        Recur->>Recur: 執行排程變更
        Recur-->>Webhook: subscription.downgraded
        Note right of Webhook: 訂閱降級
        Recur-->>Webhook: subscription.schedule_executed
        Note right of Webhook: 排程執行完成
    end
```

**方案切換邏輯**

- **立即執行**：升級（切換到更貴的方案）、平級轉換、月付→年付
- **排程執行**：降級（切換到更便宜的方案）、年付→月付

排程執行可確保用戶能享用完已付費的服務直到週期結束。

## 事件命名規則

所有事件遵循 `{resource}.{action}` 格式：

- **resource**：資源類型（checkout、order、subscription、customer）
- **action**：動作（created、activated、cancelled 等）

## 事件信封格式

所有事件都使用統一的信封格式：

```json
{
  "id": "evt_1234567890abcdef",
  "type": "subscription.activated",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    // 事件特定的 payload
  }
}
```

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 唯一事件 ID |
| `type` | string | 事件類型 |
| `timestamp` | string | ISO 8601 時間戳 |
| `data` | object | 事件 Payload |

---

## Checkout 事件

結帳流程相關事件。

### checkout.created

結帳 Session 建立時觸發。

```json
{
  "id": "evt_chk_created_001",
  "type": "checkout.created",
  "timestamp": "2024-01-15T10:00:00.000Z",
  "data": {
    "id": "chk_abc123def456",
    "status": "open",
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "product_id": "prod_pro_monthly",
    "customer": null,
    "customer_email": "user@example.com",
    "created_at": "2024-01-15T10:00:00.000Z",
    "completed_at": null
  }
}
```

### checkout.completed

結帳完成時觸發（付款成功或失敗後）。

```json
{
  "id": "evt_chk_completed_001",
  "type": "checkout.completed",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "chk_abc123def456",
    "status": "complete",
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "customer_email": "user@example.com",
    "created_at": "2024-01-15T10:00:00.000Z",
    "completed_at": "2024-01-15T10:05:00.000Z"
  }
}
```

**使用優惠碼完成結帳的範例：**

```json
{
  "id": "evt_chk_completed_002",
  "type": "checkout.completed",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "chk_abc123def456",
    "status": "complete",
    "subtotal": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "currency": "TWD",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "customer_email": "user@example.com",
    "created_at": "2024-01-15T10:00:00.000Z",
    "completed_at": "2024-01-15T10:05:00.000Z"
  }
}
```

---

## Order 事件

訂單付款相關事件。

### order.paid

訂單付款成功時觸發。

```json
{
  "id": "evt_order_paid_001",
  "type": "order.paid",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "ord_abc123",
    "order_id": "ord_abc123",
    "order_number": "ORD-20240115-ABC123",
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "status": "paid",
    "billing_reason": "subscription_create",
    "payment_method": "card",
    "paid_at": "2024-01-15T10:05:00.000Z",
    "created_at": "2024-01-15T10:04:00.000Z",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro_monthly",
    "checkout_id": "chk_abc123def456",
    "subscription_id": "sub_def456"
  }
}
```

**使用優惠碼付款的範例：**

```json
{
  "id": "evt_order_paid_002",
  "type": "order.paid",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "ord_abc123",
    "order_id": "ord_abc123",
    "order_number": "ORD-20240115-ABC123",
    "subtotal": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "currency": "TWD",
    "status": "paid",
    "billing_reason": "subscription_create",
    "payment_method": "card",
    "paid_at": "2024-01-15T10:05:00.000Z",
    "created_at": "2024-01-15T10:04:00.000Z",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro_monthly",
    "checkout_id": "chk_abc123def456",
    "subscription_id": "sub_def456"
  }
}
```

**訂單金額與折扣**

- `subtotal`：原價金額
- `discount`：折扣資訊（包含折扣金額、優惠碼、優惠券詳情）
- `amount`：實際付款金額（= subtotal - discount.discount_amount）

**billing_reason 可能的值：**

| 值 | 說明 |
|---|---|
| `purchase` | 一次性購買 |
| `subscription_create` | 訂閱首次付款 |
| `subscription_cycle` | 訂閱續訂付款 |
| `subscription_update` | 訂閱升降級差額 |

### order.payment_failed

訂單付款失敗時觸發。

```json
{
  "id": "evt_order_failed_001",
  "type": "order.payment_failed",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "ord_abc123",
    "order_id": "ord_abc123",
    "order_number": "ORD-20240115-ABC123",
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "status": "failed",
    "billing_reason": "subscription_cycle",
    "payment_method": "card",
    "paid_at": null,
    "created_at": "2024-01-15T10:04:00.000Z",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro_monthly",
    "checkout_id": null,
    "subscription_id": "sub_def456"
  }
}
```

---

## Subscription 事件

訂閱生命週期相關事件。

### subscription.created

訂閱建立時觸發（付款前）。

```json
{
  "id": "evt_sub_created_001",
  "type": "subscription.created",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "pending",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": null,
    "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:05:00.000Z",
    "updated_at": "2024-01-15T10:05:00.000Z"
  }
}
```

**使用優惠碼建立訂閱的範例：**

```json
{
  "id": "evt_sub_created_002",
  "type": "subscription.created",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "pending",
    "original_amount": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": null,
    "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:05:00.000Z",
    "updated_at": "2024-01-15T10:05:00.000Z"
  }
}
```

**含試用期的訂閱建立範例：**

```json
{
  "id": "evt_sub_created_003",
  "type": "subscription.created",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "sub_trial123",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "pending",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": "2024-01-29T00:00:00.000Z",
    "current_period_start": "2024-01-15T00:00:00.000Z",
    "current_period_end": "2024-01-29T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-01-15T10:05:00.000Z"
  }
}
```

**試用期訂閱的 current_period_end**

注意 `current_period_end` 為 `2024-01-29`（試用到期日），而非 `2024-02-15`（一個月後）。試用期間 `current_period_end` 等同 `trial_ends_at`。詳見[試用期文件](/features/payments/trials#試用期間的重要日期)。

### subscription.activated

訂閱啟用時觸發（首次付款成功）。

```json
{
  "id": "evt_sub_activated_001",
  "type": "subscription.activated",
  "timestamp": "2024-01-15T10:05:30.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "active",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-02-15T00:00:00.000Z",
    "trial_ends_at": null,
    "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:05:00.000Z",
    "updated_at": "2024-01-15T10:05:30.000Z"
  }
}
```

**使用優惠碼啟用訂閱的範例：**

```json
{
  "id": "evt_sub_activated_002",
  "type": "subscription.activated",
  "timestamp": "2024-01-15T10:05:30.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "active",
    "original_amount": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-02-15T00:00:00.000Z",
    "trial_ends_at": null,
    "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:05:00.000Z",
    "updated_at": "2024-01-15T10:05:30.000Z"
  }
}
```

**含試用期的訂閱啟用範例：**

```json
{
  "id": "evt_sub_activated_003",
  "type": "subscription.activated",
  "timestamp": "2024-01-15T10:05:30.000Z",
  "data": {
    "id": "sub_trial123",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "trialing",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-01-29T00:00:00.000Z",
    "trial_ends_at": "2024-01-29T00:00:00.000Z",
    "current_period_start": "2024-01-15T00:00:00.000Z",
    "current_period_end": "2024-01-29T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-01-15T10:05:30.000Z"
  }
}
```

**辨別試用期訂閱**

試用期啟用的 `status` 為 `trialing`（非 `active`），且 `trial_ends_at` 有值。`next_billing_date` 和 `current_period_end` 都等於 `trial_ends_at`，代表試用到期時才會進行首次全額扣款。

### subscription.renewed

訂閱續訂時觸發（週期性扣款成功）。

```json
{
  "id": "evt_sub_renewed_001",
  "type": "subscription.renewed",
  "timestamp": "2024-02-15T00:00:30.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "active",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-03-15T00:00:00.000Z",
    "trial_ends_at": null,
    "current_period_start": "2024-02-15T00:00:00.000Z",
    "current_period_end": "2024-03-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-15T00:00:30.000Z"
  }
}
```

**續訂時套用優惠折扣的範例：**

```json
{
  "id": "evt_sub_renewed_002",
  "type": "subscription.renewed",
  "timestamp": "2024-02-15T00:00:30.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "active",
    "original_amount": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-03-15T00:00:00.000Z",
    "trial_ends_at": null,
    "current_period_start": "2024-02-15T00:00:00.000Z",
    "current_period_end": "2024-03-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-15T00:00:30.000Z"
  }
}
```

**折扣期間結束**

當週期性折扣用盡後，`discount` 會變為 `null`，`amount` 恢復為 `original_amount`，表示訂閱已恢復原價計費。

### subscription.cancelled

顧客或管理員主動取消訂閱時觸發。因付款失敗導致的取消請參考 [`subscription.revoked`](#subscriptionrevoked)。

  取消後訂閱仍會持續到 `current_period_end`。期滿後會觸發 `subscription.expired`。

```json
{
  "id": "evt_sub_cancelled_001",
  "type": "subscription.cancelled",
  "timestamp": "2024-02-10T15:30:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "cancelled",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": null,
    "current_period_start": "2024-02-15T00:00:00.000Z",
    "current_period_end": "2024-03-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-10T15:30:00.000Z"
  }
}
```

**取消時有套用折扣的範例：**

```json
{
  "id": "evt_sub_cancelled_002",
  "type": "subscription.cancelled",
  "timestamp": "2024-02-10T15:30:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "cancelled",
    "original_amount": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": null,
    "current_period_start": "2024-02-15T00:00:00.000Z",
    "current_period_end": "2024-03-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-10T15:30:00.000Z"
  }
}
```

### subscription.revoked

因付款失敗導致訂閱被系統撤銷時觸發。當所有扣款重試失敗且寬限期結束後，系統會自動終止訂閱並觸發此事件。

  與 `subscription.cancelled` 的區別：`subscription.cancelled` 是顧客或管理員主動取消，`subscription.revoked` 是系統因付款失敗而強制終止。payload 中的 `cancellation_reason` 欄位會標示為 `payment_failed`。

```json
{
  "id": "evt_sub_revoked_001",
  "type": "subscription.revoked",
  "timestamp": "2024-02-18T00:00:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "canceled",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": null,
    "current_period_start": "2024-02-15T00:00:00.000Z",
    "current_period_end": "2024-03-15T00:00:00.000Z",
    "metadata": null,
    "cancellation_reason": "payment_failed",
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-18T00:00:00.000Z"
  }
}
```

### subscription.expired

訂閱過期時觸發（顧客主動取消後期滿）。

```json
{
  "id": "evt_sub_expired_001",
  "type": "subscription.expired",
  "timestamp": "2024-03-15T00:00:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "expired",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": null,
    "trial_ends_at": null,
    "current_period_start": "2024-02-15T00:00:00.000Z",
    "current_period_end": "2024-03-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-03-15T00:00:00.000Z"
  }
}
```

### subscription.trial_ending

試用期即將結束時觸發（提前 3 天通知）。

**即將推出**

此事件目前尚未上線，預計在未來版本中推出。目前建議在收到 `subscription.activated` 事件時，根據 `trial_ends_at` 欄位在您的系統中自行排程提醒通知。

詳見[試用期文件](/features/payments/trials#subscriptiontrial_ending-事件)。

```json
{
  "id": "evt_sub_trial_ending_001",
  "type": "subscription.trial_ending",
  "timestamp": "2024-01-12T00:00:00.000Z",
  "data": {
    "id": "sub_trial123",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "trialing",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-01-15T00:00:00.000Z",
    "trial_ends_at": "2024-01-15T00:00:00.000Z",
    "current_period_start": "2024-01-01T00:00:00.000Z",
    "current_period_end": "2024-01-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-01T10:00:00.000Z",
    "updated_at": "2024-01-12T00:00:00.000Z"
  }
}
```

**試用期結束後將套用折扣的範例：**

```json
{
  "id": "evt_sub_trial_ending_002",
  "type": "subscription.trial_ending",
  "timestamp": "2024-01-12T00:00:00.000Z",
  "data": {
    "id": "sub_trial123",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "trialing",
    "original_amount": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-01-15T00:00:00.000Z",
    "trial_ends_at": "2024-01-15T00:00:00.000Z",
    "current_period_start": "2024-01-01T00:00:00.000Z",
    "current_period_end": "2024-01-15T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-01T10:00:00.000Z",
    "updated_at": "2024-01-12T00:00:00.000Z"
  }
}
```

### subscription.upgraded

訂閱升級時觸發。包含以下情境：

- **方案升級**：切換到更高價格的方案（例如：Basic → Pro）
- **平級轉換**：切換到同價位的不同方案（例如：方案 A → 方案 B，價格相同）
- **計費週期升級**：切換到更長的計費週期（例如：月付 → 年付）

**計費週期變更**

當用戶將計費週期從月付切換到年付時，系統會立即執行變更並觸發 `subscription.upgraded`。
這是因為年付通常代表更長期的承諾，被視為「升級」行為。

Payload 包含完整的訂閱資訊（與 `subscription.created` 等事件相同的欄位），以及額外的切換專用欄位：

```json
{
  "id": "evt_sub_upgraded_001",
  "type": "subscription.upgraded",
  "timestamp": "2024-02-01T14:00:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_enterprise",
    "price_id": "price_enterprise_monthly",
    "status": "active",
    "original_amount": 999,
    "discount": null,
    "amount": 999,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-03-01T00:00:00.000Z",
    "trial_ends_at": null,
    "current_period_start": "2024-02-01T00:00:00.000Z",
    "current_period_end": "2024-03-01T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-01T14:00:00.000Z",
    "previous_product_id": "prod_basic",
    "previous_product_name": "Basic 方案",
    "new_product_id": "prod_enterprise",
    "new_product_name": "Enterprise 方案",
    "switch_type": "upgrade",
    "execution_mode": "immediate",
    "proration": {
      "credit_amount": 47,
      "charge_amount": 499,
      "net_amount": 452,
      "unused_days": 29
    },
    "effective_at": "2024-02-01T14:00:00.000Z",
    "was_scheduled": false
  }
}
```

| 切換專用欄位 | 說明 |
| --- | --- |
| `previous_product_id` | 切換前的方案 ID |
| `previous_product_name` | 切換前的方案名稱 |
| `new_product_id` | 切換後的方案 ID |
| `new_product_name` | 切換後的方案名稱 |
| `switch_type` | 切換類型：`upgrade`、`downgrade`、`period_change`、`crossgrade` |
| `execution_mode` | 執行模式：`immediate`（立即）或 `scheduled`（排程） |
| `proration` | 按比例計費明細（包含 `credit_amount`、`charge_amount`、`net_amount`、`unused_days`），無按比例計費時為 `null` |
| `effective_at` | 生效時間（ISO 8601） |
| `was_scheduled` | 是否由排程執行 |

### subscription.downgraded

訂閱降級時觸發。包含以下情境：

- **方案降級**：切換到更低價格的方案（例如：Pro → Basic）
- **計費週期降級**：切換到更短的計費週期（例如：年付 → 月付）

**降級的執行時機**

方案降級和計費週期降級通常會在當前計費週期結束時才生效。系統會先觸發 `subscription.schedule_created` 事件，
等到生效時再觸發 `subscription.downgraded`。這是為了讓用戶能繼續享用已付費的服務直到期滿。

Payload 格式與 `subscription.upgraded` 相同，包含完整訂閱資訊加上切換專用欄位：

```json
{
  "id": "evt_sub_downgraded_001",
  "type": "subscription.downgraded",
  "timestamp": "2024-02-01T14:00:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_basic",
    "price_id": "price_basic_monthly",
    "status": "active",
    "original_amount": 99,
    "discount": null,
    "amount": 99,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-03-01T00:00:00.000Z",
    "trial_ends_at": null,
    "current_period_start": "2024-02-01T00:00:00.000Z",
    "current_period_end": "2024-03-01T00:00:00.000Z",
    "metadata": null,
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-02-01T14:00:00.000Z",
    "previous_product_id": "prod_enterprise",
    "previous_product_name": "Enterprise 方案",
    "new_product_id": "prod_basic",
    "new_product_name": "Basic 方案",
    "switch_type": "downgrade",
    "execution_mode": "scheduled",
    "proration": null,
    "effective_at": "2024-02-01T14:00:00.000Z",
    "was_scheduled": true
  }
}
```

### subscription.schedule_created

當訂閱變更被排程時觸發（例如：降級或計費週期縮短）。排程的變更會在當前計費週期結束時自動執行。

**排程變更適用情境**

- 方案降級（切換到更便宜的方案）
- 計費週期縮短（年付 → 月付）

這些變更會先建立排程，等到當前週期結束時才真正執行，以確保用戶能享用完已付費的服務。

```json
{
  "id": "evt_sub_schedule_created_001",
  "type": "subscription.schedule_created",
  "timestamp": "2024-02-01T14:00:00.000Z",
  "data": {
    "id": "sch_abc123",
    "subscription_id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "target_product_id": "prod_basic",
    "target_product_name": "Basic 方案",
    "switch_type": "downgrade",
    "status": "pending",
    "effective_at": "2024-03-01T00:00:00.000Z",
    "executed_at": null,
    "cancelled_at": null,
    "created_at": "2024-02-01T14:00:00.000Z"
  }
}
```

| 欄位 | 說明 |
|---|---|
| `id` | 排程 ID |
| `subscription_id` | 訂閱 ID |
| `target_product_id` | 目標方案 ID |
| `target_product_name` | 目標方案名稱 |
| `switch_type` | `upgrade`、`downgrade`、`period_change`、`crossgrade` |
| `status` | `pending`（等待執行）、`executed`（已執行）、`cancelled`（已取消） |
| `effective_at` | 預定生效時間 |
| `executed_at` | 實際執行時間（僅 executed 狀態有值） |
| `cancelled_at` | 取消時間（僅 cancelled 狀態有值） |

### subscription.schedule_executed

當排程的訂閱變更被執行時觸發。此事件會在 `subscription.downgraded` 之後觸發。

```json
{
  "id": "evt_sub_schedule_executed_001",
  "type": "subscription.schedule_executed",
  "timestamp": "2024-03-01T00:00:00.000Z",
  "data": {
    "id": "sch_abc123",
    "subscription_id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "target_product_id": "prod_basic",
    "target_product_name": "Basic 方案",
    "switch_type": "downgrade",
    "status": "executed",
    "effective_at": "2024-03-01T00:00:00.000Z",
    "executed_at": "2024-03-01T00:00:05.000Z",
    "cancelled_at": null,
    "created_at": "2024-02-01T14:00:00.000Z"
  }
}
```

### subscription.schedule_cancelled

當排程的訂閱變更被取消時觸發。用戶可以在變更生效前取消排程。

```json
{
  "id": "evt_sub_schedule_cancelled_001",
  "type": "subscription.schedule_cancelled",
  "timestamp": "2024-02-15T10:00:00.000Z",
  "data": {
    "id": "sch_abc123",
    "subscription_id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "target_product_id": "prod_basic",
    "target_product_name": "Basic 方案",
    "switch_type": "downgrade",
    "status": "cancelled",
    "effective_at": "2024-03-01T00:00:00.000Z",
    "executed_at": null,
    "cancelled_at": "2024-02-15T10:00:00.000Z",
    "created_at": "2024-02-01T14:00:00.000Z"
  }
}
```

### subscription.past_due

訂閱付款失敗時觸發，訂閱進入逾期狀態。**僅在產品的「付款寬限期」啟用時觸發。**

  逾期狀態會持續最多 3 天（寬限期），期間系統會自動重試扣款。
  若所有重試都失敗，訂閱將被取消（觸發 `subscription.revoked`）。

  若產品關閉寬限期，此事件不會觸發，付款失敗時直接觸發 `subscription.revoked`。

```json
{
  "id": "evt_sub_past_due_001",
  "type": "subscription.past_due",
  "timestamp": "2024-02-15T00:05:00.000Z",
  "data": {
    "id": "sub_def456",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "price_pro_monthly",
    "status": "past_due",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-02-15T00:00:00.000Z",
    "trial_ends_at": null,
    "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:05:00.000Z",
    "updated_at": "2024-02-15T00:05:00.000Z"
  }
}
```

### subscription.payment_method_required

訂閱到期但客戶沒有綁定付款方式時觸發。常見於透過 API 匯入的訂閱（使用 `POST /subscriptions` 搭配 `status: ACTIVE` 建立，但客戶尚未綁卡）。**僅在產品的「付款寬限期」啟用時觸發。**

觸發此事件後，訂閱會轉為 `PAST_DUE` 狀態，進入 3 天寬限期。若產品關閉寬限期，此事件不會觸發，訂閱直接取消（觸發 `subscription.revoked`）。

```json
{
  "id": "evt_sub_pm_required_001",
  "type": "subscription.payment_method_required",
  "timestamp": "2024-02-15T00:00:00.000Z",
  "data": {
    "id": "sub_imported_001",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "product_id": "prod_pro",
    "price_id": "prod_pro",
    "status": "past_due",
    "original_amount": 299,
    "discount": null,
    "amount": 299,
    "interval": "month",
    "interval_count": 1,
    "next_billing_date": "2024-02-15T00:00:00.000Z",
    "trial_ends_at": null,
    "current_period_start": "2024-01-15T00:00:00.000Z",
    "current_period_end": "2024-02-15T00:00:00.000Z",
    "metadata": { "source": "stripe", "original_id": "sub_xxx" },
    "created_at": "2024-01-15T10:30:00.000Z",
    "updated_at": "2024-02-15T00:00:00.000Z"
  }
}
```

**建議處理方式**

收到此事件時，建議：
1. 通知客戶前往 Customer Portal 綁定付款方式
2. 在您的應用中顯示提示訊息
3. 如果寬限期結束仍未綁卡，訂閱將被取消（觸發 `subscription.revoked`）

---

## Invoice 事件

帳單（週期扣款）相關事件。每次訂閱續費都會建立一張帳單（Invoice）。

### invoice.created

帳單建立時觸發（扣款前）。

```json
{
  "id": "evt_inv_created_001",
  "type": "invoice.created",
  "timestamp": "2024-02-15T00:00:00.000Z",
  "data": {
    "id": "inv_abc123",
    "invoice_number": "INV-20240215-XYZ789",
    "subscription_id": "sub_def456",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "status": "pending",
    "billing_reason": "subscription_cycle",
    "period_start": "2024-02-15T00:00:00.000Z",
    "period_end": "2024-03-15T00:00:00.000Z",
    "paid_at": null,
    "created_at": "2024-02-15T00:00:00.000Z"
  }
}
```

**建立帳單時套用折扣的範例：**

```json
{
  "id": "evt_inv_created_002",
  "type": "invoice.created",
  "timestamp": "2024-02-15T00:00:00.000Z",
  "data": {
    "id": "inv_abc123",
    "invoice_number": "INV-20240215-XYZ789",
    "subscription_id": "sub_def456",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "subtotal": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "currency": "TWD",
    "status": "pending",
    "billing_reason": "subscription_cycle",
    "period_start": "2024-02-15T00:00:00.000Z",
    "period_end": "2024-03-15T00:00:00.000Z",
    "paid_at": null,
    "created_at": "2024-02-15T00:00:00.000Z"
  }
}
```

### invoice.paid

帳單付款成功時觸發。

```json
{
  "id": "evt_inv_paid_001",
  "type": "invoice.paid",
  "timestamp": "2024-02-15T00:00:30.000Z",
  "data": {
    "id": "inv_abc123",
    "invoice_number": "INV-20240215-XYZ789",
    "subscription_id": "sub_def456",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "status": "paid",
    "billing_reason": "subscription_cycle",
    "period_start": "2024-02-15T00:00:00.000Z",
    "period_end": "2024-03-15T00:00:00.000Z",
    "paid_at": "2024-02-15T00:00:30.000Z",
    "created_at": "2024-02-15T00:00:00.000Z"
  }
}
```

**續訂時套用週期性折扣的帳單範例：**

```json
{
  "id": "evt_inv_paid_002",
  "type": "invoice.paid",
  "timestamp": "2024-02-15T00:00:30.000Z",
  "data": {
    "id": "inv_abc123",
    "invoice_number": "INV-20240215-XYZ789",
    "subscription_id": "sub_def456",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "subtotal": 299,
    "discount": {
      "discount_amount": 60,
      "promotion_code_id": "promo_abc123",
      "promotion_code": "NEWYEAR2024",
      "coupon_id": "cpn_abc123",
      "coupon_name": "新年優惠 8 折"
    },
    "amount": 239,
    "currency": "TWD",
    "status": "paid",
    "billing_reason": "subscription_cycle",
    "period_start": "2024-02-15T00:00:00.000Z",
    "period_end": "2024-03-15T00:00:00.000Z",
    "paid_at": "2024-02-15T00:00:30.000Z",
    "created_at": "2024-02-15T00:00:00.000Z"
  }
}
```

**帳單金額與折扣**

- `subtotal`：原價金額
- `discount`：折扣資訊（包含折扣金額、優惠碼、優惠券詳情）
- `amount`：實際付款金額（= subtotal - discount.discount_amount）

### invoice.payment_failed

帳單付款失敗時觸發。

  此事件通常伴隨 `subscription.past_due` 一起觸發。
  使用此事件可以發送付款失敗通知給顧客。

```json
{
  "id": "evt_inv_failed_001",
  "type": "invoice.payment_failed",
  "timestamp": "2024-02-15T00:05:00.000Z",
  "data": {
    "id": "inv_abc123",
    "invoice_number": "INV-20240215-XYZ789",
    "subscription_id": "sub_def456",
    "product_id": "prod_pro_monthly",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "subtotal": 299,
    "discount": null,
    "amount": 299,
    "currency": "TWD",
    "status": "pending",
    "billing_reason": "subscription_cycle",
    "period_start": "2024-02-15T00:00:00.000Z",
    "period_end": "2024-03-15T00:00:00.000Z",
    "paid_at": null,
    "created_at": "2024-02-15T00:00:00.000Z"
  }
}
```

---

## Refund 事件

退款相關事件。當用戶申請退款時會觸發這些事件。退款流程請參考上方的[退款流程圖](#退款流程)。

**退款限制**

- 退款必須在付款後 180 天內申請
- 退款金額不能超過原始付款金額
- 支援全額退款和部分退款

### refund.created

退款申請建立時觸發。此時退款尚未處理，狀態為 `pending`。

```json
{
  "id": "evt_ref_created_001",
  "type": "refund.created",
  "timestamp": "2024-01-20T14:00:00.000Z",
  "data": {
    "id": "ref_abc123",
    "refund_number": "REF-20240120-XYZ789",
    "charge_id": "chg_def456",
    "charge_number": "CHG-20240115-ABC123",
    "order_id": "ord_xyz789",
    "invoice_id": null,
    "subscription_id": "sub_ghi012",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "amount": 299,
    "currency": "TWD",
    "status": "pending",
    "reason": "customer_request",
    "reason_detail": "用戶要求取消訂閱並退款",
    "original_amount": 299,
    "refunded_amount": 0,
    "created_at": "2024-01-20T14:00:00.000Z",
    "processed_at": null,
    "failed_at": null,
    "failure_code": null,
    "failure_message": null
  }
}
```

### refund.succeeded

退款處理成功時觸發。款項將退回到原付款方式。

```json
{
  "id": "evt_ref_succeeded_001",
  "type": "refund.succeeded",
  "timestamp": "2024-01-20T14:01:00.000Z",
  "data": {
    "id": "ref_abc123",
    "refund_number": "REF-20240120-XYZ789",
    "charge_id": "chg_def456",
    "charge_number": "CHG-20240115-ABC123",
    "order_id": "ord_xyz789",
    "invoice_id": null,
    "subscription_id": "sub_ghi012",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "amount": 299,
    "currency": "TWD",
    "status": "succeeded",
    "reason": "customer_request",
    "reason_detail": "用戶要求取消訂閱並退款",
    "original_amount": 299,
    "refunded_amount": 299,
    "created_at": "2024-01-20T14:00:00.000Z",
    "processed_at": "2024-01-20T14:01:00.000Z",
    "failed_at": null,
    "failure_code": null,
    "failure_message": null
  }
}
```

### refund.failed

退款處理失敗時觸發。可能需要重試或人工介入。

```json
{
  "id": "evt_ref_failed_001",
  "type": "refund.failed",
  "timestamp": "2024-01-20T14:01:00.000Z",
  "data": {
    "id": "ref_abc123",
    "refund_number": "REF-20240120-XYZ789",
    "charge_id": "chg_def456",
    "charge_number": "CHG-20240115-ABC123",
    "order_id": "ord_xyz789",
    "invoice_id": null,
    "subscription_id": "sub_ghi012",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "amount": 299,
    "currency": "TWD",
    "status": "failed",
    "reason": "customer_request",
    "reason_detail": "用戶要求取消訂閱並退款",
    "original_amount": 299,
    "refunded_amount": 0,
    "created_at": "2024-01-20T14:00:00.000Z",
    "processed_at": null,
    "failed_at": "2024-01-20T14:01:00.000Z",
    "failure_code": "REFUND_FAILED",
    "failure_message": "金流商退款處理失敗"
  }
}
```

**reason 可能的值：**

Webhook payload 中的 `reason` 欄位使用 snake_case 格式，對應到系統中的 RefundReason enum。

| Enum 值 | Payload 值 | 說明 |
|---|---|---|
| `REQUESTED_BY_CUSTOMER` | `customer_request` | 客戶主動要求退款 |
| `DUPLICATE_CHARGE` | `duplicate` | 重複付款 |
| `FRAUDULENT` | `fraudulent` | 詐騙或未授權交易 |
| `SUBSCRIPTION_CANCELED` | `subscription_cancelled` | 訂閱取消退款 |
| `PRODUCT_NOT_DELIVERED` | `product_not_delivered` | 商品/服務未提供 |
| `PRODUCT_UNSATISFACTORY` | `product_unsatisfactory` | 商品/服務不滿意 |
| `OTHER` | `other` | 其他原因（需填 reason_detail） |

---

## Customer 事件

客戶資料相關事件。

### customer.created

新客戶建立時觸發。

```json
{
  "id": "evt_cus_created_001",
  "type": "customer.created",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "cus_xyz789",
    "external_id": "my_user_456",
    "email": "user@example.com",
    "name": "王小明",
    "status": "active",
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-01-15T10:05:00.000Z"
  }
}
```

### customer.updated

客戶資料更新時觸發。當 `external_id` 變更時，payload 會包含 `previous_external_id` 欄位。

```json
{
  "id": "evt_cus_updated_001",
  "type": "customer.updated",
  "timestamp": "2024-01-20T09:00:00.000Z",
  "data": {
    "id": "cus_xyz789",
    "external_id": "new_user_789",
    "email": "user@example.com",
    "name": "王小明",
    "status": "active",
    "previous_external_id": "my_user_456",
    "created_at": "2024-01-15T10:05:00.000Z",
    "updated_at": "2024-01-20T09:00:00.000Z"
  }
}
```

---

## Payload 欄位說明

### discount 物件結構

所有包含折扣資訊的事件都使用統一的 `discount` 物件結構：

| 欄位 | 類型 | 說明 |
|------|------|------|
| `discount_amount` | number | 折扣金額（新台幣） |
| `promotion_code_id` | string \| null | 優惠碼 ID |
| `promotion_code` | string \| null | 優惠碼代碼（例如 "NEWYEAR2024"） |
| `coupon_id` | string \| null | 優惠券 ID |
| `coupon_name` | string \| null | 優惠券名稱（例如 "新年優惠 8 折"） |

**折扣資訊說明**

- 當沒有套用折扣時，`discount` 為 `null`
- 當有折扣時，`discount_amount` 為實際折扣金額
- `promotion_code` 是用戶輸入的優惠碼
- `coupon` 是優惠碼所對應的優惠券

### Subscription Payload

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 訂閱 ID |
| `customer` | object | 客戶物件（見下方說明） |
| `product_id` | string | 商品 ID |
| `price_id` | string | 價格 ID |
| `status` | string | 訂閱狀態 |
| `original_amount` | number | 原價金額（新台幣） |
| `discount` | object \| null | 折扣資訊（見上方 discount 物件結構） |
| `amount` | number | 實際金額（= original_amount - discount.discount_amount） |
| `interval` | string | 計費週期單位（day, week, month, year） |
| `interval_count` | number | 計費週期數量（例如：2 表示每 2 個週期） |
| `next_billing_date` | string \| null | 下次扣款日期 |
| `trial_ends_at` | string \| null | 試用結束日期 |
| `current_period_start` | string | 當前週期開始 |
| `current_period_end` | string | 當前週期結束 |
| `metadata` | object \| null | 自訂中繼資料 |
| `created_at` | string | 建立時間 |
| `updated_at` | string | 更新時間 |

**status 可能的值：**

| 狀態 | 說明 |
|------|------|
| `pending` | 待付款 |
| `trial` | 試用中 |
| `active` | 使用中 |
| `past_due` | 付款逾期 |
| `canceled` | 已取消（期滿前） |
| `expired` | 已過期 |

### Order Payload

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 訂單 ID |
| `order_id` | string | 訂單 ID（同 id） |
| `order_number` | string \| null | 訂單編號（ORD-XXXX，與後台顯示一致，可用於開立發票） |
| `subtotal` | number | 原價金額（新台幣） |
| `discount` | object \| null | 折扣資訊（見上方 discount 物件結構） |
| `amount` | number | 實際付款金額（= subtotal - discount.discount_amount） |
| `currency` | string | 幣別（TWD） |
| `status` | string | 訂單狀態 |
| `billing_reason` | string | 帳單原因 |
| `payment_method` | string | 付款方式 |
| `paid_at` | string \| null | 付款時間 |
| `created_at` | string | 建立時間 |
| `customer` | object | 客戶物件（見下方說明） |
| `product_id` | string \| null | 商品 ID |
| `checkout_id` | string \| null | 結帳 ID |
| `subscription_id` | string \| null | 訂閱 ID |

### Refund Payload

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 退款 ID |
| `refund_number` | string | 退款編號 |
| `charge_id` | string | 原付款 Charge ID |
| `charge_number` | string | 原付款編號 |
| `order_id` | string \| null | 訂單 ID（首次付款） |
| `invoice_id` | string \| null | 帳單 ID（續費付款） |
| `subscription_id` | string \| null | 關聯的訂閱 ID |
| `customer` | object \| null | 客戶物件（見下方說明） |
| `amount` | number | 退款金額 |
| `currency` | string | 幣別（TWD） |
| `status` | string | 退款狀態 |
| `reason` | string | 退款原因代碼 |
| `reason_detail` | string \| null | 退款原因詳情 |
| `original_amount` | number | 原付款金額 |
| `refunded_amount` | number | 已退款總額 |
| `created_at` | string | 建立時間 |
| `processed_at` | string \| null | 處理完成時間 |
| `failed_at` | string \| null | 失敗時間 |
| `failure_code` | string \| null | 失敗代碼 |
| `failure_message` | string \| null | 失敗訊息 |

**status 可能的值：**

| 狀態 | 說明 |
|------|------|
| `pending` | 待處理 |
| `processing` | 處理中 |
| `succeeded` | 退款成功 |
| `failed` | 退款失敗 |

### Invoice Payload

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 帳單 ID |
| `invoice_number` | string | 帳單編號 |
| `subscription_id` | string | 訂閱 ID |
| `product_id` | string \| null | 商品 ID（來自訂閱的方案，可用於開立發票品項） |
| `customer` | object | 客戶物件（見下方說明） |
| `subtotal` | number | 原價金額（新台幣） |
| `discount` | object \| null | 折扣資訊（見上方 discount 物件結構） |
| `amount` | number | 實際付款金額（= subtotal - discount.discount_amount） |
| `currency` | string | 幣別（TWD） |
| `status` | string | 帳單狀態 |
| `billing_reason` | string | 帳單原因 |
| `period_start` | string | 週期開始 |
| `period_end` | string | 週期結束 |
| `paid_at` | string \| null | 付款時間 |
| `created_at` | string | 建立時間 |

### Checkout Payload

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 結帳 Session ID |
| `status` | string | 結帳狀態：`open`（待付款）、`complete`（已完成）、`expired`（已過期） |
| `subtotal` | number | 原價金額（新台幣） |
| `discount` | object \| null | 折扣資訊（見上方 discount 物件結構） |
| `amount` | number | 實際付款金額（= subtotal - discount.discount_amount） |
| `currency` | string | 幣別（TWD） |
| `product_id` | string | 商品 ID |
| `customer` | object \| null | 客戶物件（完成後才有，見下方說明） |
| `customer_email` | string | 客戶 Email |
| `created_at` | string | 建立時間 |
| `completed_at` | string \| null | 完成時間 |

### Customer Payload

用於 `customer.created` 和 `customer.updated` 事件。

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 客戶 ID |
| `external_id` | string \| null | 外部 ID（開發者系統中的 ID） |
| `email` | string | 電子郵件 |
| `name` | string | 姓名 |
| `status` | string | 客戶狀態 |
| `previous_external_id` | string \| null | 僅在 `customer.updated` 且 `external_id` 變更時出現，表示變更前的值 |
| `created_at` | string | 建立時間 |
| `updated_at` | string | 更新時間 |

### Customer 物件（嵌套欄位）

在其他 Payload（如 Subscription、Order、Invoice 等）中的 `customer` 欄位使用此結構：

| 欄位 | 類型 | 說明 |
|------|------|------|
| `id` | string | 客戶 ID |
| `external_id` | string \| null | 外部 ID（開發者系統中的 ID，可用於與您的系統關聯） |
| `email` | string | 電子郵件 |
| `name` | string \| null | 姓名 |

**使用 external_id 關聯您的系統**

`external_id` 是您在建立客戶時傳入的 ID，通常對應您系統中的用戶 ID。收到 webhook 時，您可以透過 `data.customer.external_id` 快速找到對應的用戶記錄，而不需要維護額外的 ID 對照表。

---

## 電子發票事件（Einvoice）

組織[啟用電子發票整合](/guides/checkout/einvoice)後,發票的開立/作廢/折讓會發送以下事件。測試環境(SANDBOX)也會發送,方便在上線前驗證 handler;事件只會送到與交易相同環境的 webhook endpoint。

`data.order_id` 與 `data.invoice_id` 恰有一個非 null:首次付款的發票掛在 order、續費的發票掛在 invoice。`buyer` 為買家的發票選擇快照,格式同結帳 API 的 `einvoice` 欄位。

### einvoice.issued

發票開立成功時發送(含自動補開)。

```json
{
  "id": "evt_einvoice_issued_001",
  "type": "einvoice.issued",
  "timestamp": "2026-07-22T10:05:00.000Z",
  "data": {
    "order_id": "ord_abc123",
    "invoice_id": null,
    "customer": { "id": "cus_xyz", "external_id": "user_42", "email": "buyer@example.com", "name": "王小明" },
    "invoice_number": "ZA12345678",
    "random_code": "5417",
    "invoice_date": "2026-07-22",
    "total_amount": 800,
    "buyer": { "type": "ubn", "ubn": "04595252", "buyer_name": "恆吉股份有限公司" }
  }
}
```

### einvoice.issue_failed

**只在不可自動重試的最終失敗**時發送(如統編格式錯誤等驗證失敗、或自動重試次數用盡)。暫時性失敗(網路、字軌用罄)會自動重試,不發送事件。

```json
{
  "id": "evt_einvoice_failed_001",
  "type": "einvoice.issue_failed",
  "timestamp": "2026-07-22T10:05:00.000Z",
  "data": {
    "order_id": "ord_abc123",
    "invoice_id": null,
    "customer": { "id": "cus_xyz", "external_id": "user_42", "email": "buyer@example.com", "name": "王小明" },
    "total_amount": 800,
    "buyer": { "type": "ubn", "ubn": "04595252", "buyer_name": "恆吉股份有限公司" },
    "failure_message": "統一編號格式錯誤",
    "will_retry": false
  }
}
```

### einvoice.voided

發票作廢成功時發送(通常由全額退款觸發)。

```json
{
  "id": "evt_einvoice_voided_001",
  "type": "einvoice.voided",
  "timestamp": "2026-07-25T10:05:00.000Z",
  "data": {
    "order_id": "ord_abc123",
    "invoice_id": null,
    "customer": { "id": "cus_xyz", "external_id": "user_42", "email": "buyer@example.com", "name": "王小明" },
    "invoice_number": "ZA12345678",
    "total_amount": 800,
    "buyer": { "type": "personal" },
    "void_reason": "訂單退款"
  }
}
```

### einvoice.allowance_created

折讓開立成功時發送(部分退款、或發票已有折讓歷史無法作廢時的全額折讓)。

```json
{
  "id": "evt_einvoice_allowance_001",
  "type": "einvoice.allowance_created",
  "timestamp": "2026-07-25T10:05:00.000Z",
  "data": {
    "order_id": "ord_abc123",
    "invoice_id": null,
    "customer": { "id": "cus_xyz", "external_id": "user_42", "email": "buyer@example.com", "name": "王小明" },
    "invoice_number": "ZA12345678",
    "allowance_number": "AL123456",
    "allowance_date": "2026-07-25",
    "allowance_amount": 300,
    "refund_id": "ref_def456",
    "total_amount": 800,
    "buyer": { "type": "personal" }
  }
}
```

## 處理範例

### 依事件類型處理

```typescript
app.post('/api/webhooks/recur', async (req, res) => {
  const event = req.body;

  switch (event.type) {
    case 'subscription.activated':
      await handleSubscriptionActivated(event.data);
      break;

    case 'subscription.cancelled':
      await handleSubscriptionCancelled(event.data);
      break;

    case 'subscription.revoked':
      await handleSubscriptionRevoked(event.data);
      break;

    case 'order.paid':
      await handleOrderPaid(event.data);
      break;

    case 'customer.created':
      await handleCustomerCreated(event.data);
      break;

    // 退款事件處理
    case 'refund.created':
      await handleRefundCreated(event.data);
      break;

    case 'refund.succeeded':
      await handleRefundSucceeded(event.data);
      break;

    case 'refund.failed':
      await handleRefundFailed(event.data);
      break;

    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  res.status(200).json({ received: true });
});

async function handleSubscriptionActivated(data: SubscriptionPayload) {
  // 使用 external_id 找到您系統中的用戶
  const userId = data.customer.external_id;

  // 啟用用戶權限
  await enableUserAccess(userId, data.product_id);

  // 發送歡迎郵件
  await sendWelcomeEmail(data.customer.email);
}

async function handleSubscriptionCancelled(data: SubscriptionPayload) {
  // 記錄取消原因（可選）

  // 發送挽留郵件
  await sendRetentionEmail(data.customer.email);
}

// 退款處理函數
async function handleRefundCreated(data: RefundPayload) {
  console.log(`退款申請建立: ${data.refund_number}`);

  // 記錄退款申請
  await db.refundLog.create({
    data: {
      refundId: data.id,
      refundNumber: data.refund_number,
      amount: data.amount,
      status: 'pending',
    },
  });
}

async function handleRefundSucceeded(data: RefundPayload) {
  console.log(`退款成功: ${data.refund_number}, 金額: ${data.amount}`);

  // 更新訂單狀態
  if (data.order_id) {
    await db.order.update({
      where: { id: data.order_id },
      data: { refundedAmount: data.refunded_amount },
    });
  }

  // 如果是訂閱退款，可能需要停用權限
  if (data.subscription_id && data.customer) {
    const userId = data.customer.external_id;
    await revokeUserAccess(userId, data.subscription_id);
  }

  // 發送退款成功通知
  if (data.customer) {
    await sendRefundConfirmationEmail(data.customer.email, data);
  }
}

async function handleRefundFailed(data: RefundPayload) {
  console.error(`退款失敗: ${data.refund_number}, 原因: ${data.failure_message}`);

  // 通知管理員處理失敗的退款
  await notifyAdminRefundFailed(data);
}
```

## 下一步

- [設定 Webhook 端點](/guides/webhooks/endpoints) - 在後台建立端點
- [Webhook 傳遞機制](/guides/webhooks/delivery) - 了解簽章驗證和重試策略
