Webhook 事件類型
所有 Webhook 事件的完整列表和 Payload 格式
事件觸發時序
初次訂閱(Checkout)
下圖說明首次訂閱結帳流程中各事件的觸發順序:
checkout.created vs subscription.created 的差異
checkout.created:顧客進入結帳頁面時觸發,此時尚未填寫任何資訊subscription.created:顧客提交基本資訊後觸發,系統建立 pending 狀態的訂閱記錄
訂單生命週期(付款與退款)
下圖說明訂單從建立到完成,以及退款的完整流程:
退款流程
當需要退款時,流程如下:
訂閱續費(Renewal)
下圖說明訂閱週期扣款流程中各事件的觸發順序:
逾期與重試機制
當續費扣款失敗時,訂閱會進入 past_due 狀態。系統會在寬限期內(預設 3 天)自動重試扣款:
- 重試成功:觸發
invoice.paid+subscription.renewed,訂閱恢復為active - 所有重試失敗:觸發
subscription.expired,訂閱過期
訂閱方案切換(Switching)
下圖說明訂閱方案切換流程中各事件的觸發順序:
方案切換邏輯
- 立即執行:升級(切換到更貴的方案)、平級轉換、月付→年付
- 排程執行:降級(切換到更便宜的方案)、年付→月付
排程執行可確保用戶能享用完已付費的服務直到週期結束。
事件命名規則
所有事件遵循 {resource}.{action} 格式:
- resource:資源類型(checkout、order、subscription、customer)
- action:動作(created、activated、cancelled 等)
事件信封格式
所有事件都使用統一的信封格式:
{
"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 建立時觸發。
{
"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
結帳完成時觸發(付款成功或失敗後)。
{
"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"
}
}使用優惠碼完成結帳的範例:
{
"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.paid
訂單付款成功時觸發。
{
"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"
}
}使用優惠碼付款的範例:
{
"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"
}
}訂單金額與折扣
subtotal:原價金額discount:折扣資訊(包含折扣金額、優惠碼、優惠券詳情)amount:實際付款金額(= subtotal - discount.discount_amount)
billing_reason 可能的值:
| 值 | 說明 |
|---|---|
purchase | 一次性購買 |
subscription_create | 訂閱首次付款 |
subscription_cycle | 訂閱續訂付款 |
subscription_update | 訂閱升降級差額 |
order.payment_failed
訂單付款失敗時觸發。
{
"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.created
訂閱建立時觸發(付款前)。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-01-15T10:05:00.000Z"
}
}使用優惠碼建立訂閱的範例:
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-01-15T10:05:00.000Z"
}
}subscription.activated
訂閱啟用時觸發(首次付款成功)。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-01-15T10:05:30.000Z"
}
}使用優惠碼啟用訂閱的範例:
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-01-15T10:05:30.000Z"
}
}subscription.renewed
訂閱續訂時觸發(週期性扣款成功)。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-15T00:00:30.000Z"
}
}續訂時套用優惠折扣的範例:
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-15T00:00:30.000Z"
}
}折扣期間結束
當週期性折扣用盡後,discount 會變為 null,amount 恢復為 original_amount,表示訂閱已恢復原價計費。
subscription.cancelled
訂閱取消時觸發。
取消後訂閱仍會持續到 current_period_end。期滿後會觸發 subscription.expired。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-10T15:30:00.000Z"
}
}取消時有套用折扣的範例:
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-10T15:30:00.000Z"
}
}subscription.expired
訂閱過期時觸發(取消後期滿或續訂失敗)。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-03-15T00:00:00.000Z"
}
}subscription.trial_ending
試用期即將結束時觸發(提前 3 天通知)。
{
"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",
"created_at": "2024-01-01T10:00:00.000Z",
"updated_at": "2024-01-12T00:00:00.000Z"
}
}試用期結束後將套用折扣的範例:
{
"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",
"created_at": "2024-01-01T10:00:00.000Z",
"updated_at": "2024-01-12T00:00:00.000Z"
}
}subscription.upgraded
訂閱升級時觸發。包含以下情境:
- 方案升級:切換到更高價格的方案(例如:Basic → Pro)
- 平級轉換:切換到同價位的不同方案(例如:方案 A → 方案 B,價格相同)
- 計費週期升級:切換到更長的計費週期(例如:月付 → 年付)
計費週期變更
當用戶將計費週期從月付切換到年付時,系統會立即執行變更並觸發 subscription.upgraded。
這是因為年付通常代表更長期的承諾,被視為「升級」行為。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-01T14:00:00.000Z"
}
}subscription.downgraded
訂閱降級時觸發。包含以下情境:
- 方案降級:切換到更低價格的方案(例如:Pro → Basic)
- 計費週期降級:切換到更短的計費週期(例如:年付 → 月付)
降級的執行時機
方案降級和計費週期降級通常會在當前計費週期結束時才生效。系統會先觸發 subscription.schedule_created 事件,
等到生效時再觸發 subscription.downgraded。這是為了讓用戶能繼續享用已付費的服務直到期滿。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-01T14:00:00.000Z"
}
}subscription.schedule_created
當訂閱變更被排程時觸發(例如:降級或計費週期縮短)。排程的變更會在當前計費週期結束時自動執行。
排程變更適用情境
- 方案降級(切換到更便宜的方案)
- 計費週期縮短(年付 → 月付)
這些變更會先建立排程,等到當前週期結束時才真正執行,以確保用戶能享用完已付費的服務。
{
"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
當排程的訂閱變更被執行時觸發。此事件會在 subscription.downgraded 之後觸發。
{
"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
當排程的訂閱變更被取消時觸發。用戶可以在變更生效前取消排程。
{
"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
訂閱付款失敗時觸發,訂閱進入逾期狀態。
逾期狀態會持續最多 3 天(寬限期),期間系統會自動重試扣款。
若所有重試都失敗,訂閱將會過期(觸發 subscription.expired)。
{
"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",
"created_at": "2024-01-15T10:05:00.000Z",
"updated_at": "2024-02-15T00:05:00.000Z"
}
}Invoice 事件
帳單(週期扣款)相關事件。每次訂閱續費都會建立一張帳單(Invoice)。
invoice.created
帳單建立時觸發(扣款前)。
{
"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"
}
}建立帳單時套用折扣的範例:
{
"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
帳單付款成功時觸發。
{
"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"
}
}續訂時套用週期性折扣的帳單範例:
{
"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"
}
}帳單金額與折扣
subtotal:原價金額discount:折扣資訊(包含折扣金額、優惠碼、優惠券詳情)amount:實際付款金額(= subtotal - discount.discount_amount)
invoice.payment_failed
帳單付款失敗時觸發。
此事件通常伴隨 subscription.past_due 一起觸發。
使用此事件可以發送付款失敗通知給顧客。
{
"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 事件
退款相關事件。當用戶申請退款時會觸發這些事件。退款流程請參考上方的退款流程圖。
退款限制
- 退款必須在付款後 180 天內申請
- 退款金額不能超過原始付款金額
- 支援全額退款和部分退款
refund.created
退款申請建立時觸發。此時退款尚未處理,狀態為 pending。
{
"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
退款處理成功時觸發。款項將退回到原付款方式。
{
"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
退款處理失敗時觸發。可能需要重試或人工介入。
{
"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
新客戶建立時觸發。
{
"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
客戶資料更新時觸發。
{
"id": "evt_cus_updated_001",
"type": "customer.updated",
"timestamp": "2024-01-20T09:00:00.000Z",
"data": {
"id": "cus_xyz789",
"external_id": "my_user_456",
"email": "new-email@example.com",
"name": "王小明",
"status": "active",
"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 | 當前週期結束 |
created_at | string | 建立時間 |
updated_at | string | 更新時間 |
status 可能的值:
| 狀態 | 說明 |
|---|---|
pending | 待付款 |
trialing | 試用中 |
active | 使用中 |
past_due | 付款逾期 |
cancelled | 已取消(期滿前) |
expired | 已過期 |
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
| 欄位 | 類型 | 說明 |
|---|---|---|
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 |
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 | 結帳狀態 |
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 | 客戶狀態 |
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 對照表。
處理範例
依事件類型處理
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 '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 端點 - 在後台建立端點
- Webhook 傳遞機制 - 了解簽章驗證和重試策略