# Webhook 事件類型 (/guides/webhooks/events)





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

事件觸發時序 [#事件觸發時序]

初次訂閱（Checkout） [#初次訂閱checkout]

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

<Mermaid
  chart="
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: 結帳流程完成
"
/>

<Callout type="info">
  **checkout.created vs subscription.created 的差異**

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

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

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

<Mermaid
  chart="
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: 結帳流程完成
"
/>

<Callout type="info">
  **試用期 subscription.activated 的差異**

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

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

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

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

<Mermaid
  chart="
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
  chart="
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） [#訂閱續費renewal]

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

<Mermaid
  chart="
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
"
/>

<Callout type="warn">
  **逾期與重試機制**

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

  **寬限期啟用（預設）：** 訂閱進入 `past_due` 狀態，系統在 3 天寬限期內自動重試扣款（最多 3 次）：

  * 重試成功：觸發 `invoice.paid` + `subscription.renewed`，訂閱恢復為 `active`
  * 所有重試失敗：觸發 `subscription.revoked`，訂閱取消

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

試用期轉正式（Trial Graduation） [#試用期轉正式trial-graduation]

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

<Mermaid
  chart="
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
"
/>

<Callout type="info">
  **Trial Graduation 使用相同的續約機制**

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

訂閱方案切換（Switching） [#訂閱方案切換switching]

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

<Mermaid
  chart="
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
"
/>

<Callout type="info">
  **方案切換邏輯**

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

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

事件命名規則 [#事件命名規則]

所有事件遵循 `{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-事件]

結帳流程相關事件。

checkout.created [#checkoutcreated]

結帳 Session 建立時觸發。

```json
{
  "id": "evt_chk_created_001",
  "type": "checkout.created",
  "timestamp": "2024-01-15T10:00:00.000Z",
  "data": {
    "id": "chk_abc123def456",
    "status": "pending",
    "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 [#checkoutcompleted]

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

```json
{
  "id": "evt_chk_completed_001",
  "type": "checkout.completed",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "chk_abc123def456",
    "status": "completed",
    "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": "completed",
    "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-事件]

訂單付款相關事件。

order.paid [#orderpaid]

訂單付款成功時觸發。

```json
{
  "id": "evt_order_paid_001",
  "type": "order.paid",
  "timestamp": "2024-01-15T10:05:00.000Z",
  "data": {
    "id": "ord_abc123",
    "order_id": "ord_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",
    "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"
  }
}
```

<Callout type="info">
  **訂單金額與折扣**

  * `subtotal`：原價金額
  * `discount`：折扣資訊（包含折扣金額、優惠碼、優惠券詳情）
  * `amount`：實際付款金額（= subtotal - discount.discount\_amount）
</Callout>

**billing\_reason 可能的值：**

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

order.payment_failed [#orderpayment_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",
    "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-事件]

訂閱生命週期相關事件。

subscription.created [#subscriptioncreated]

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

```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"
  }
}
```

<Callout type="info">
  **試用期訂閱的 current\_period\_end**

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

subscription.activated [#subscriptionactivated]

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

```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"
  }
}
```

<Callout type="info">
  **辨別試用期訂閱**

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

subscription.renewed [#subscriptionrenewed]

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

```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"
  }
}
```

<Callout type="info">
  **折扣期間結束**

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

subscription.cancelled [#subscriptioncancelled]

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

<Callout type="info">
  取消後訂閱仍會持續到 `current_period_end`。期滿後會觸發 `subscription.expired`。
</Callout>

```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 [#subscriptionrevoked]

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

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

```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 [#subscriptionexpired]

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

```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 [#subscriptiontrial_ending]

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

<Callout type="warn">
  **即將推出**

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

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

```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 [#subscriptionupgraded]

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

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

<Callout type="info">
  **計費週期變更**

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

```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",
    "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"
  }
}
```

subscription.downgraded [#subscriptiondowngraded]

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

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

<Callout type="info">
  **降級的執行時機**

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

```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",
    "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"
  }
}
```

subscription.schedule_created [#subscriptionschedule_created]

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

<Callout type="info">
  **排程變更適用情境**

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

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

```json
{
  "id": "evt_sub_schedule_created_001",
  "type": "subscription.schedule_created",
  "timestamp": "2024-02-01T14:00:00.000Z",
  "data": {
    "schedule_id": "sch_abc123",
    "subscription_id": "sub_def456",
    "target_product_id": "prod_basic",
    "switch_type": "DOWNGRADE",
    "effective_at": "2024-03-01T00:00:00.000Z",
    "action": "created",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "created_at": "2024-02-01T14:00:00.000Z"
  }
}
```

**switch\_type 可能的值：**

| 值               | 說明                   |
| --------------- | -------------------- |
| `UPGRADE`       | 升級到更高價格方案            |
| `DOWNGRADE`     | 降級到更低價格方案            |
| `PERIOD_CHANGE` | 計費週期變更（如月付→年付或年付→月付） |
| `CROSSGRADE`    | 平級轉換（價格相同的不同方案）      |

subscription.schedule_executed [#subscriptionschedule_executed]

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

```json
{
  "id": "evt_sub_schedule_executed_001",
  "type": "subscription.schedule_executed",
  "timestamp": "2024-03-01T00:00:00.000Z",
  "data": {
    "schedule_id": "sch_abc123",
    "subscription_id": "sub_def456",
    "target_product_id": "prod_basic",
    "switch_type": "DOWNGRADE",
    "effective_at": "2024-03-01T00:00:00.000Z",
    "action": "executed",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "created_at": "2024-03-01T00:00:00.000Z"
  }
}
```

subscription.schedule_cancelled [#subscriptionschedule_cancelled]

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

```json
{
  "id": "evt_sub_schedule_cancelled_001",
  "type": "subscription.schedule_cancelled",
  "timestamp": "2024-02-15T10:00:00.000Z",
  "data": {
    "schedule_id": "sch_abc123",
    "subscription_id": "sub_def456",
    "target_product_id": "prod_basic",
    "switch_type": "DOWNGRADE",
    "effective_at": "2024-03-01T00:00:00.000Z",
    "action": "cancelled",
    "customer": {
      "id": "cus_xyz789",
      "external_id": "my_user_456",
      "email": "user@example.com",
      "name": "王小明"
    },
    "created_at": "2024-02-15T10:00:00.000Z"
  }
}
```

subscription.past_due [#subscriptionpast_due]

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

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

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

```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 [#subscriptionpayment_method_required]

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

觸發此事件後，訂閱會轉為 `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"
  }
}
```

<Callout type="info">
  **建議處理方式**

  收到此事件時，建議：

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

***

Invoice 事件 [#invoice-事件]

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

invoice.created [#invoicecreated]

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

```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",
    "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",
    "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 [#invoicepaid]

帳單付款成功時觸發。

```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",
    "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",
    "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"
  }
}
```

<Callout type="info">
  **帳單金額與折扣**

  * `subtotal`：原價金額
  * `discount`：折扣資訊（包含折扣金額、優惠碼、優惠券詳情）
  * `amount`：實際付款金額（= subtotal - discount.discount\_amount）
</Callout>

invoice.payment_failed [#invoicepayment_failed]

帳單付款失敗時觸發。

<Callout type="info">
  此事件通常伴隨 `subscription.past_due` 一起觸發。
  使用此事件可以發送付款失敗通知給顧客。
</Callout>

```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",
    "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 事件 [#refund-事件]

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

<Callout type="info">
  **退款限制**

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

refund.created [#refundcreated]

退款申請建立時觸發。此時退款尚未處理，狀態為 `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 [#refundsucceeded]

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

```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 [#refundfailed]

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

```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 可能的值：**

<Callout type="info">
  Webhook payload 中的 `reason` 欄位使用 snake\_case 格式，對應到系統中的 RefundReason enum。
</Callout>

| 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-事件]

客戶資料相關事件。

customer.created [#customercreated]

新客戶建立時觸發。

```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 [#customerupdated]

客戶資料更新時觸發。當 `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 欄位說明 [#payload-欄位說明]

discount 物件結構 [#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 折"）    |

<Callout type="info">
  **折扣資訊說明**

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

Subscription Payload [#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`   | 待付款      |
| `trialing`  | 試用中      |
| `active`    | 使用中      |
| `past_due`  | 付款逾期     |
| `cancelled` | 已取消（期滿前） |
| `expired`   | 已過期      |

Order Payload [#order-payload]

| 欄位                | 類型             | 說明                                             |
| ----------------- | -------------- | ---------------------------------------------- |
| `id`              | string         | 訂單 ID                                          |
| `order_id`        | string         | 訂單 ID（同 id）                                    |
| `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 [#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 [#invoice-payload]

| 欄位                | 類型             | 說明                                             |
| ----------------- | -------------- | ---------------------------------------------- |
| `id`              | string         | 帳單 ID                                          |
| `invoice_number`  | string         | 帳單編號                                           |
| `subscription_id` | string         | 訂閱 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 [#checkout-payload]

| 欄位               | 類型             | 說明                                             |
| ---------------- | -------------- | ---------------------------------------------- |
| `id`             | string         | 結帳 Session ID                                  |
| `status`         | string         | 結帳狀態                                           |
| `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-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 物件（嵌套欄位） [#customer-物件嵌套欄位]

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

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

<Callout type="info">
  **使用 external\_id 關聯您的系統**

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

***

處理範例 [#處理範例]

依事件類型處理 [#依事件類型處理]

```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) - 了解簽章驗證和重試策略
