Recur
開發者指南Webhooks

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
  }
}
欄位類型說明
idstring唯一事件 ID
typestring事件類型
timestampstringISO 8601 時間戳
dataobject事件 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 會變為 nullamount 恢復為 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_CUSTOMERcustomer_request客戶主動要求退款
DUPLICATE_CHARGEduplicate重複付款
FRAUDULENTfraudulent詐騙或未授權交易
SUBSCRIPTION_CANCELEDsubscription_cancelled訂閱取消退款
PRODUCT_NOT_DELIVEREDproduct_not_delivered商品/服務未提供
PRODUCT_UNSATISFACTORYproduct_unsatisfactory商品/服務不滿意
OTHERother其他原因(需填 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_amountnumber折扣金額(新台幣)
promotion_code_idstring | null優惠碼 ID
promotion_codestring | null優惠碼代碼(例如 "NEWYEAR2024")
coupon_idstring | null優惠券 ID
coupon_namestring | null優惠券名稱(例如 "新年優惠 8 折")

折扣資訊說明

  • 當沒有套用折扣時,discountnull
  • 當有折扣時,discount_amount 為實際折扣金額
  • promotion_code 是用戶輸入的優惠碼
  • coupon 是優惠碼所對應的優惠券

Subscription Payload

欄位類型說明
idstring訂閱 ID
customerobject客戶物件(見下方說明)
product_idstring商品 ID
price_idstring價格 ID
statusstring訂閱狀態
original_amountnumber原價金額(新台幣)
discountobject | null折扣資訊(見上方 discount 物件結構)
amountnumber實際金額(= original_amount - discount.discount_amount)
intervalstring計費週期單位(day, week, month, year)
interval_countnumber計費週期數量(例如:2 表示每 2 個週期)
next_billing_datestring | null下次扣款日期
trial_ends_atstring | null試用結束日期
current_period_startstring當前週期開始
current_period_endstring當前週期結束
created_atstring建立時間
updated_atstring更新時間

status 可能的值:

狀態說明
pending待付款
trialing試用中
active使用中
past_due付款逾期
cancelled已取消(期滿前)
expired已過期

Order Payload

欄位類型說明
idstring訂單 ID
order_idstring訂單 ID(同 id)
subtotalnumber原價金額(新台幣)
discountobject | null折扣資訊(見上方 discount 物件結構)
amountnumber實際付款金額(= subtotal - discount.discount_amount)
currencystring幣別(TWD)
statusstring訂單狀態
billing_reasonstring帳單原因
payment_methodstring付款方式
paid_atstring | null付款時間
created_atstring建立時間
customerobject客戶物件(見下方說明)
product_idstring | null商品 ID
checkout_idstring | null結帳 ID
subscription_idstring | null訂閱 ID

Refund Payload

欄位類型說明
idstring退款 ID
refund_numberstring退款編號
charge_idstring原付款 Charge ID
charge_numberstring原付款編號
order_idstring | null訂單 ID(首次付款)
invoice_idstring | null帳單 ID(續費付款)
subscription_idstring | null關聯的訂閱 ID
customerobject | null客戶物件(見下方說明)
amountnumber退款金額
currencystring幣別(TWD)
statusstring退款狀態
reasonstring退款原因代碼
reason_detailstring | null退款原因詳情
original_amountnumber原付款金額
refunded_amountnumber已退款總額
created_atstring建立時間
processed_atstring | null處理完成時間
failed_atstring | null失敗時間
failure_codestring | null失敗代碼
failure_messagestring | null失敗訊息

status 可能的值:

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

Invoice Payload

欄位類型說明
idstring帳單 ID
invoice_numberstring帳單編號
subscription_idstring訂閱 ID
customerobject客戶物件(見下方說明)
subtotalnumber原價金額(新台幣)
discountobject | null折扣資訊(見上方 discount 物件結構)
amountnumber實際付款金額(= subtotal - discount.discount_amount)
currencystring幣別(TWD)
statusstring帳單狀態
billing_reasonstring帳單原因
period_startstring週期開始
period_endstring週期結束
paid_atstring | null付款時間
created_atstring建立時間

Checkout Payload

欄位類型說明
idstring結帳 Session ID
statusstring結帳狀態
subtotalnumber原價金額(新台幣)
discountobject | null折扣資訊(見上方 discount 物件結構)
amountnumber實際付款金額(= subtotal - discount.discount_amount)
currencystring幣別(TWD)
product_idstring商品 ID
customerobject | null客戶物件(完成後才有,見下方說明)
customer_emailstring客戶 Email
created_atstring建立時間
completed_atstring | null完成時間

Customer Payload

用於 customer.createdcustomer.updated 事件。

欄位類型說明
idstring客戶 ID
external_idstring | null外部 ID(開發者系統中的 ID)
emailstring電子郵件
namestring姓名
statusstring客戶狀態
created_atstring建立時間
updated_atstring更新時間

Customer 物件(嵌套欄位)

在其他 Payload(如 Subscription、Order、Invoice 等)中的 customer 欄位使用此結構:

欄位類型說明
idstring客戶 ID
external_idstring | null外部 ID(開發者系統中的 ID,可用於與您的系統關聯)
emailstring電子郵件
namestring | 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);
}

下一步

On this page