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,訂閱過期
事件命名規則
所有事件遵循 {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",
"amount": 299,
"currency": "TWD",
"product_id": "prod_pro_monthly",
"customer_id": 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",
"amount": 299,
"currency": "TWD",
"product_id": "prod_pro_monthly",
"customer_id": "cus_xyz789",
"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",
"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",
"product_id": "prod_pro_monthly",
"checkout_id": "chk_abc123def456",
"subscription_id": "sub_def456"
}
}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",
"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",
"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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "pending",
"amount": 299,
"billing_period": "monthly",
"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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "active",
"amount": 299,
"billing_period": "monthly",
"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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "active",
"amount": 299,
"billing_period": "monthly",
"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"
}
}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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "cancelled",
"amount": 299,
"billing_period": "monthly",
"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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "expired",
"amount": 299,
"billing_period": "monthly",
"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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "trialing",
"amount": 299,
"billing_period": "monthly",
"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
訂閱升級時觸發。
{
"id": "evt_sub_upgraded_001",
"type": "subscription.upgraded",
"timestamp": "2024-02-01T14:00:00.000Z",
"data": {
"id": "sub_def456",
"customer_id": "cus_xyz789",
"plan_id": "plan_enterprise",
"price_id": "price_enterprise_monthly",
"status": "active",
"amount": 999,
"billing_period": "monthly",
"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
訂閱降級時觸發。
{
"id": "evt_sub_downgraded_001",
"type": "subscription.downgraded",
"timestamp": "2024-02-01T14:00:00.000Z",
"data": {
"id": "sub_def456",
"customer_id": "cus_xyz789",
"plan_id": "plan_basic",
"price_id": "price_basic_monthly",
"status": "active",
"amount": 99,
"billing_period": "monthly",
"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.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",
"plan_id": "plan_pro",
"price_id": "price_pro_monthly",
"status": "past_due",
"amount": 299,
"billing_period": "monthly",
"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",
"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"
}
}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",
"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"
}
}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",
"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",
"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",
"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",
"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",
"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",
"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 欄位說明
Subscription Payload
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | 訂閱 ID |
customer_id | string | 客戶 ID |
plan_id | string | 方案 ID |
price_id | string | 價格 ID |
status | string | 訂閱狀態 |
amount | number | 訂閱金額(新台幣) |
billing_period | string | 計費週期(weekly, monthly, yearly) |
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 |
amount | number | 訂單金額 |
currency | string | 幣別(TWD) |
status | string | 訂單狀態 |
billing_reason | string | 帳單原因 |
payment_method | string | 付款方式 |
paid_at | string | null | 付款時間 |
customer_id | string | 客戶 ID |
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_id | string | null | 客戶 ID |
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 | 退款失敗 |
處理範例
依事件類型處理
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) {
// 啟用用戶權限
await enableUserAccess(data.customer_id, data.plan_id);
// 發送歡迎郵件
await sendWelcomeEmail(data.customer_id);
}
async function handleSubscriptionCancelled(data: SubscriptionPayload) {
// 記錄取消原因(可選)
// 發送挽留郵件
await sendRetentionEmail(data.customer_id);
}
// 退款處理函數
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) {
await revokeUserAccess(data.customer_id, data.subscription_id);
}
// 發送退款成功通知
await sendRefundConfirmationEmail(data.customer_id, data);
}
async function handleRefundFailed(data: RefundPayload) {
console.error(`退款失敗: ${data.refund_number}, 原因: ${data.failure_message}`);
// 通知管理員處理失敗的退款
await notifyAdminRefundFailed(data);
}下一步
- 設定 Webhook 端點 - 在後台建立端點
- Webhook 傳遞機制 - 了解簽章驗證和重試策略