Subscriptions API
訂閱狀態查詢與管理 API
Subscriptions API
訂閱相關的 API 端點,用於查詢和管理訂閱狀態。
查詢訂閱狀態
查詢訂閱狀態,支援列出所有訂閱或按客戶篩選。此端點需要 Secret Key 認證,適合後端使用。
端點
GET /subscriptions認證
此端點需要 Secret Key 認證,僅限後端使用。
支援以下認證方式:
# 方式 1: Authorization Header(推薦)
Authorization: Bearer sk_test_xxx
# 方式 2: 專用 Header
X-Recur-Secret-Key: sk_test_xxx請求參數
客戶篩選(選填)
| 參數 | 類型 | 說明 |
|---|---|---|
email | string | 客戶 Email |
external_id | string | 您系統中的客戶 ID(外部 ID) |
customer_id | string | Recur 內部客戶 ID |
若不提供任何客戶識別參數,將返回該組織下所有訂閱。每筆訂閱會包含對應的客戶資訊。
商品篩選(選填)
| 參數 | 類型 | 說明 |
|---|---|---|
product_id | string | 商品 ID |
product_slug | string | 商品 Slug |
product_id 和 product_slug 為選填。若不提供,將返回該客戶的所有訂閱。
狀態篩選(選填)
| 參數 | 類型 | 說明 |
|---|---|---|
active | boolean | 設為 true 僅返回有效訂閱(ACTIVE、TRIAL、PAST_DUE) |
status | string | 篩選特定狀態(如 ACTIVE、CANCELED) |
分頁參數(選填)
| 參數 | 類型 | 說明 |
|---|---|---|
limit | number | 每頁返回數量(預設 10,最大 100) |
starting_after | string | 分頁游標,傳入上一頁最後一筆訂閱的 ID |
回應格式
按客戶篩選(有訂閱)
{
"object": "list",
"has_active_subscription": true,
"data": [
{
"object": "subscription",
"id": "sub_xxxxx",
"status": "ACTIVE",
"product_id": "prod_xxxxx",
"product_slug": "pro-monthly",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"current_period_start": "2025-01-01T00:00:00.000Z",
"current_period_end": "2025-02-01T00:00:00.000Z",
"canceled_at": null,
"started_at": "2024-12-01T00:00:00.000Z",
"next_billing_date": "2025-02-01T00:00:00.000Z",
"metadata": null,
"coupon": null,
"coupon_remaining_cycles": null,
"discount_amount": 0,
"promotion_code": null
}
],
"customer": {
"id": "cus_xxxxx",
"email": "user@example.com",
"name": "王小明",
"external_id": "user_123"
},
"has_more": false,
"next_cursor": null,
"livemode": false
}列出所有訂閱(無客戶篩選)
當不提供任何客戶識別參數時,每筆訂閱會包含對應的客戶資訊:
{
"object": "list",
"has_active_subscription": true,
"data": [
{
"object": "subscription",
"id": "sub_xxxxx",
"status": "ACTIVE",
"product_id": "prod_xxxxx",
"product_slug": "pro-monthly",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"current_period_start": "2025-01-01T00:00:00.000Z",
"current_period_end": "2025-02-01T00:00:00.000Z",
"canceled_at": null,
"started_at": "2024-12-01T00:00:00.000Z",
"next_billing_date": "2025-02-01T00:00:00.000Z",
"metadata": null,
"customer": {
"id": "cus_xxxxx",
"email": "user@example.com",
"name": "王小明",
"external_id": "user_123"
}
},
{
"object": "subscription",
"id": "sub_yyyyy",
"status": "ACTIVE",
"product_id": "prod_xxxxx",
"product_slug": "pro-monthly",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"current_period_start": "2025-01-05T00:00:00.000Z",
"current_period_end": "2025-02-05T00:00:00.000Z",
"canceled_at": null,
"started_at": "2024-12-05T00:00:00.000Z",
"next_billing_date": "2025-02-05T00:00:00.000Z",
"metadata": null,
"customer": {
"id": "cus_yyyyy",
"email": "another@example.com",
"name": "李小華",
"external_id": "user_456"
}
}
],
"customer": null,
"has_more": true,
"next_cursor": "sub_yyyyy",
"livemode": false
}含優惠的訂閱範例:
{
"object": "list",
"has_active_subscription": true,
"data": [
{
"object": "subscription",
"id": "sub_xxxxx",
"status": "ACTIVE",
"product_id": "prod_xxxxx",
"product_slug": "pro-monthly",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"current_period_start": "2025-01-01T00:00:00.000Z",
"current_period_end": "2025-02-01T00:00:00.000Z",
"canceled_at": null,
"started_at": "2024-12-01T00:00:00.000Z",
"next_billing_date": "2025-02-01T00:00:00.000Z",
"metadata": null,
"coupon": {
"id": "cpn_xxxxx",
"name": "新年優惠 8 折",
"discount_type": "PERCENTAGE",
"discount_amount": 2000,
"duration": "REPEATING"
},
"coupon_remaining_cycles": 2,
"discount_amount": 60,
"promotion_code": "NEWYEAR2025"
}
],
"customer": {
"id": "cus_xxxxx",
"email": "user@example.com",
"name": "王小明",
"external_id": "user_123"
},
"has_more": false,
"next_cursor": null,
"livemode": false
}無有效訂閱的客戶
{
"object": "list",
"has_active_subscription": false,
"data": [
{
"object": "subscription",
"id": "sub_xxxxx",
"status": "CANCELED",
"product_id": "prod_xxxxx",
"product_slug": "pro-monthly",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"current_period_start": "2024-12-01T00:00:00.000Z",
"current_period_end": "2025-01-01T00:00:00.000Z",
"canceled_at": "2025-01-01T00:00:00.000Z",
"started_at": "2024-12-01T00:00:00.000Z",
"next_billing_date": null,
"metadata": null,
"coupon": null,
"coupon_remaining_cycles": null,
"discount_amount": 0,
"promotion_code": null
}
],
"customer": {
"id": "cus_xxxxx",
"email": "user@example.com",
"name": "王小明",
"external_id": null
},
"has_more": false,
"next_cursor": null,
"livemode": false
}找不到客戶
當提供客戶篩選參數但找不到對應客戶時:
{
"object": "list",
"has_active_subscription": false,
"data": [],
"customer": null,
"has_more": false,
"next_cursor": null,
"livemode": false
}回應欄位說明
| 欄位 | 類型 | 說明 |
|---|---|---|
object | string | 固定為 "list" |
has_active_subscription | boolean | 是否有有效訂閱(ACTIVE、TRIAL、PAST_DUE) |
data | array | 所有符合條件的訂閱(按建立時間降冪排序) |
customer | object | null | 客戶資訊(僅在按客戶篩選時有值) |
has_more | boolean | 是否有更多結果(用於分頁) |
next_cursor | string | null | 下一頁的游標(傳入 starting_after 參數) |
livemode | boolean | 是否為正式環境 |
訂閱物件欄位
| 欄位 | 類型 | 說明 |
|---|---|---|
object | string | 固定為 "subscription" |
id | string | 訂閱 ID |
status | string | 訂閱狀態 |
product_id | string | 商品 ID |
product_slug | string | 商品 Slug |
product_name | string | 商品名稱 |
amount | number | 金額 |
interval | string | 週期單位(month、year) |
interval_count | number | 週期數量 |
current_period_start | string | 當期開始時間 |
current_period_end | string | 當期結束時間 |
canceled_at | string | null | 取消時間 |
started_at | string | 開始時間 |
next_billing_date | string | null | 下次扣款時間 |
metadata | object | null | 開發者自訂 key-value 資料 |
customer | object | 客戶資訊(僅在無客戶篩選時出現) |
coupon | object | null | 套用的優惠資訊(見下方說明) |
coupon_remaining_cycles | number | null | 週期性折扣剩餘期數(僅 REPEATING 類型) |
discount_amount | number | 本期折扣金額 |
promotion_code | string | null | 使用的推廣代碼 |
優惠物件結構(coupon)
| 欄位 | 類型 | 說明 |
|---|---|---|
id | string | 優惠 ID |
name | string | 優惠名稱 |
discount_type | string | 折扣類型:FIXED_AMOUNT、PERCENTAGE、FIRST_PERIOD_PRICE |
discount_amount | number | 折扣數值(固定金額為分、百分比為 basis points) |
duration | string | 折扣期間:ONCE、REPEATING、FOREVER |
訂閱狀態說明
以下狀態視為「有效訂閱」(has_active_subscription: true):
| 狀態 | 說明 |
|---|---|
ACTIVE | 正常訂閱中 |
TRIAL | 試用期中 |
PAST_DUE | 逾期但尚未取消(仍有效) |
以下狀態視為「無效訂閱」:
| 狀態 | 說明 |
|---|---|
CANCELED | 已取消 |
EXPIRED | 已過期 |
PAUSED | 已暫停 |
程式碼範例
// 使用 email 查詢所有訂閱
const response = await fetch(
'https://api.recur.tw/v1/subscriptions?' +
new URLSearchParams({
email: 'user@example.com',
}),
{
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);
const result = await response.json();
if (result.has_active_subscription) {
console.log('用戶有有效訂閱');
console.log('訂閱數:', result.data.length);
console.log('第一筆訂閱狀態:', result.data[0].status);
} else {
console.log('用戶沒有有效訂閱');
}
// 使用 external_id 查詢特定方案
const response2 = await fetch(
'https://api.recur.tw/v1/subscriptions?' +
new URLSearchParams({
external_id: 'user_123',
product_slug: 'pro-monthly',
}),
{
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);import requests
import os
# 使用 email 查詢所有訂閱
response = requests.get(
'https://api.recur.tw/v1/subscriptions',
headers={
'Authorization': f'Bearer {os.environ["RECUR_SECRET_KEY"]}',
},
params={
'email': 'user@example.com',
}
)
result = response.json()
if result['has_active_subscription']:
print('用戶有有效訂閱')
print(f'訂閱數: {len(result["data"])}')
print(f'第一筆訂閱狀態: {result["data"][0]["status"]}')
else:
print('用戶沒有有效訂閱')
# 使用 external_id 查詢
response2 = requests.get(
'https://api.recur.tw/v1/subscriptions',
headers={
'Authorization': f'Bearer {os.environ["RECUR_SECRET_KEY"]}',
},
params={
'external_id': 'user_123',
'active': 'true',
}
)# 列出所有有效訂閱(不需指定客戶)
curl -X GET "https://api.recur.tw/v1/subscriptions?active=true&limit=50" \
-H "Authorization: Bearer sk_test_xxx"
# 分頁:取得下一頁
curl -X GET "https://api.recur.tw/v1/subscriptions?active=true&limit=50&starting_after=sub_xxxxx" \
-H "Authorization: Bearer sk_test_xxx"
# 使用 email 查詢特定客戶的訂閱
curl -X GET "https://api.recur.tw/v1/subscriptions?email=user@example.com" \
-H "Authorization: Bearer sk_test_xxx"
# 使用 external_id 查詢特定方案
curl -X GET "https://api.recur.tw/v1/subscriptions?external_id=user_123&product_slug=pro-monthly" \
-H "X-Recur-Secret-Key: sk_test_xxx"
# 使用 customer_id 查詢僅有效訂閱
curl -X GET "https://api.recur.tw/v1/subscriptions?customer_id=cus_xxxxx&active=true" \
-H "Authorization: Bearer sk_test_xxx"
# 篩選特定狀態
curl -X GET "https://api.recur.tw/v1/subscriptions?email=user@example.com&status=CANCELED" \
-H "Authorization: Bearer sk_test_xxx"使用案例
1. 權限控制(使用 external_id)
在您的後端驗證用戶是否有權限存取付費功能:
// middleware/checkSubscription.ts
import { NextRequest, NextResponse } from 'next/server';
export async function checkSubscription(req: NextRequest, userId: string) {
// 使用您系統的 userId 作為 external_id 查詢
const response = await fetch(
`${process.env.RECUR_API_URL}/v1/subscriptions?` +
new URLSearchParams({
external_id: userId,
active: 'true', // 只查詢有效訂閱
}),
{
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);
const { has_active_subscription } = await response.json();
if (!has_active_subscription) {
return NextResponse.json(
{ error: '此功能需要付費訂閱' },
{ status: 403 }
);
}
return null; // 允許通過
}2. 查詢用戶所有訂閱
不指定方案,取得用戶的所有訂閱歷史:
// app/api/user/subscriptions/route.ts
export async function GET(req: NextRequest) {
const session = await getSession();
const response = await fetch(
`${process.env.RECUR_API_URL}/v1/subscriptions?` +
new URLSearchParams({
email: session.user.email,
// 不指定 product_slug,返回所有訂閱
}),
{
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);
const result = await response.json();
// Find active subscription
const activeSubscription = result.data.find(
(sub: { status: string }) => ['ACTIVE', 'TRIAL', 'PAST_DUE'].includes(sub.status)
);
return NextResponse.json({
hasActiveSubscription: result.has_active_subscription,
currentProduct: activeSubscription?.product_name,
allSubscriptions: result.data.map((sub: { product_name: string; status: string; started_at: string; current_period_end: string }) => ({
product: sub.product_name,
status: sub.status,
startedAt: sub.started_at,
expiresAt: sub.current_period_end,
})),
});
}3. 顯示訂閱資訊
在用戶設定頁面顯示當前訂閱狀態:
// app/api/user/subscription/route.ts
export async function GET(req: NextRequest) {
const session = await getSession();
const response = await fetch(
`${process.env.RECUR_API_URL}/v1/subscriptions?` +
new URLSearchParams({
email: session.user.email,
product_slug: 'pro',
}),
{
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);
const result = await response.json();
// Get the first subscription (sorted by createdAt desc)
const subscription = result.data[0];
return NextResponse.json({
isSubscribed: result.has_active_subscription,
product: subscription?.product_name,
renewsAt: subscription?.current_period_end,
status: subscription?.status,
});
}錯誤處理
| HTTP 狀態 | 錯誤碼 | 說明 |
|---|---|---|
| 401 | unauthorized | 需要 Secret Key 認證或 API Key 無效 |
| 404 | not_found | 找不到指定的商品(僅在提供 product_id/product_slug 時) |
注意事項
安全提醒:此端點使用 Secret Key 認證,請勿在前端調用。所有查詢應透過您的後端進行。
快取建議:為了效能考量,建議在您的後端實作適當的快取機制,避免頻繁查詢相同用戶的訂閱狀態。
避免重複訂閱
Recur 在多個層級防止同一客戶重複訂閱同一商品:
| 檢查時機 | 端點 | 條件 |
|---|---|---|
| Checkout 建立時 | POST /v1/checkouts、POST /v1/checkout/sessions | 帶入 customerEmail 時自動檢查 |
| 訂閱建立時 | POST /v1/subscriptions | 永遠檢查 |
| 付款完成後 | Webhook handler | 最終防線 |
若偵測到重複,API 回傳 409 conflict,details 中包含 existing_subscription_id 和 status。
建議做法:建立前先查詢
在觸發結帳前,先用 GET /v1/subscriptions 確認客戶是否已有訂閱,可以提供更好的 UX:
const res = await fetch(
`https://api.recur.tw/v1/subscriptions?email=${email}&product_id=${productId}&active=true`,
{ headers: { 'Authorization': `Bearer ${secretKey}` } }
);
const { has_active_subscription } = await res.json();
if (has_active_subscription) {
// 顯示「您已訂閱此方案」,提供管理訂閱或切換方案的選項
} else {
// 建立 Checkout Session
}建立訂閱
透過 API 建立新訂閱。支援兩種使用方式:
- 標準流程:建立 PENDING 訂閱,等待付款完成(用於 UNi Embed 流程)
- 匯入流程:直接建立 ACTIVE 或 TRIAL 訂閱(用於從其他平台遷移或為既有客戶建立訂閱)
端點
POST /subscriptions認證
此端點需要 API Key 認證。
請求參數
基本參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
product_id | string | 是 | 商品 ID(或使用 plan_id) |
customer_email | string | 是 | 客戶 Email |
customer_name | string | 否 | 客戶名稱 |
external_id | string | 否 | 您系統中的客戶 ID(用於關聯) |
匯入參數(選填)
| 參數 | 類型 | 預設值 | 說明 |
|---|---|---|---|
status | string | PENDING | 訂閱初始狀態:PENDING、ACTIVE、TRIAL |
billing_anchor_date | ISO datetime | — | 計費起算日,系統從此日期推算當前計費週期(與 next_billing_date 互斥) |
next_billing_date | ISO datetime | — | 下次扣款日,系統反推當前計費週期(與 billing_anchor_date 互斥) |
trial_end | ISO datetime | — | 試用期結束時間(status 為 TRIAL 時必填) |
amount | number | 商品價格 | 覆寫訂閱金額(整數,單位為 TWD) |
metadata | object | — | 開發者自訂 key-value 資料(如 {"source": "stripe", "original_id": "sub_xxx"}) |
skip_webhooks | boolean | false | 跳過 Webhook 觸發(適用於批量匯入) |
匯入既有訂閱
當 status 設為 ACTIVE 或 TRIAL 時,訂閱會立即生效,客戶馬上獲得產品權限。由於沒有綁定信用卡,訂閱到期時系統會觸發 subscription.payment_method_required Webhook,並將訂閱轉為 PAST_DUE,提醒客戶綁卡。
回應格式
成功建立
{
"subscription": {
"id": "sub_xxxxx",
"status": "PENDING",
"product_id": "prod_xxxxx",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"trial_days": 7,
"current_period_start": "2025-01-01T00:00:00.000Z",
"current_period_end": "2025-02-01T00:00:00.000Z",
"next_billing_date": "2025-02-01T00:00:00.000Z",
"metadata": null
},
"customer": {
"id": "cus_xxxxx",
"email": "user@example.com",
"name": "王小明",
"external_id": "user_123"
},
"next_steps": {
"get_sdk_token": "/api/v1/subscriptions/sdk-token",
"complete_subscription": "/api/v1/subscriptions/sub_xxxxx/complete"
},
"livemode": false
}匯入 ACTIVE 訂閱
{
"subscription": {
"id": "sub_xxxxx",
"status": "ACTIVE",
"product_id": "prod_xxxxx",
"product_name": "Pro Plan",
"amount": 299,
"interval": "month",
"interval_count": 1,
"trial_days": null,
"current_period_start": "2025-01-01T00:00:00.000Z",
"current_period_end": "2025-02-01T00:00:00.000Z",
"next_billing_date": "2025-02-01T00:00:00.000Z",
"metadata": {
"source": "stripe",
"original_id": "sub_xxx"
}
},
"customer": {
"id": "cus_xxxxx",
"email": "user@example.com",
"name": "王小明",
"external_id": "user_123"
},
"livemode": false
}當 status 為 ACTIVE 或 TRIAL 時,回應不包含 next_steps,因為不需要走付款流程。
錯誤處理
| HTTP 狀態 | 錯誤碼 | 說明 |
|---|---|---|
| 400 | bad_request | 缺少必要參數或參數無效 |
| 401 | unauthorized | API Key 無效 |
| 404 | not_found | 找不到指定的商品 |
| 409 | conflict | 客戶已有該商品的有效訂閱 |
重複訂閱錯誤(409)
當客戶已有該商品的有效訂閱(ACTIVE、TRIAL 或 PAST_DUE)時,API 會返回 409 錯誤:
{
"error": {
"code": "conflict",
"message": "Customer already has an active subscription to this product",
"details": [
{
"existing_subscription_id": "sub_xxxxx",
"status": "ACTIVE"
}
]
}
}處理重複訂閱錯誤
當收到 409 錯誤時,您可以:
- 使用
details[0].existing_subscription_id查詢現有訂閱詳情 - 引導客戶到 Customer Portal 管理現有訂閱
- 如需切換方案,使用訂閱方案切換 API
程式碼範例
const response = await fetch('https://api.recur.tw/v1/subscriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RECUR_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: 'prod_xxxxx',
customer_email: 'user@example.com',
customer_name: '王小明',
external_id: 'user_123',
}),
});
const data = await response.json();
if (response.status === 409) {
// 客戶已有訂閱
const existingSubId = data.error.details[0].existing_subscription_id;
console.log(`客戶已有訂閱: ${existingSubId}`);
} else if (response.ok) {
console.log('訂閱建立成功:', data.subscription.id);
}import requests
import os
response = requests.post(
'https://api.recur.tw/v1/subscriptions',
headers={
'Authorization': f'Bearer {os.environ["RECUR_API_KEY"]}',
'Content-Type': 'application/json',
},
json={
'product_id': 'prod_xxxxx',
'customer_email': 'user@example.com',
'customer_name': '王小明',
'external_id': 'user_123',
}
)
data = response.json()
if response.status_code == 409:
# 客戶已有訂閱
existing_sub_id = data['error']['details'][0]['existing_subscription_id']
print(f'客戶已有訂閱: {existing_sub_id}')
elif response.ok:
print(f'訂閱建立成功: {data["subscription"]["id"]}')curl -X POST "https://api.recur.tw/v1/subscriptions" \
-H "Authorization: Bearer pk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_xxxxx",
"customer_email": "user@example.com",
"customer_name": "王小明",
"external_id": "user_123"
}'
# 匯入 ACTIVE 訂閱(從其他平台遷移)
curl -X POST "https://api.recur.tw/v1/subscriptions" \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod_xxxxx",
"customer_email": "user@example.com",
"status": "ACTIVE",
"next_billing_date": "2025-04-01T00:00:00Z",
"metadata": { "source": "stripe", "original_id": "sub_xxx" },
"skip_webhooks": true
}'訂閱方案切換
允許您代替客戶執行訂閱方案的升級、降級或週期變更。
所有訂閱切換 API 都需要 Secret Key 認證,僅限後端使用。
切換類型說明
| 類型 | 說明 | 執行方式 |
|---|---|---|
UPGRADE | 升級到更高價方案 | 立即執行,按比例計算差額 |
DOWNGRADE | 降級到較低價方案 | 排程至當期結束時執行 |
PERIOD_CHANGE | 變更計費週期(月→年或年→月) | 月→年立即執行;年→月排程執行 |
CROSSGRADE | 切換到同價方案 | 立即執行,無需補差額 |
預覽切換
在執行切換前,預覽切換結果和費用計算。
端點
GET /subscriptions/{subscription_id}/switch-preview請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
target_product_id | string | 是 | 目標方案 ID |
回應範例
{
"object": "switch_preview",
"subscription_id": "sub_xxxxx",
"switch_type": "UPGRADE",
"execution_mode": "immediate",
"current_plan": {
"product_id": "prod_basic",
"product_name": "Basic Plan",
"amount": 299,
"currency": "TWD",
"interval": "month",
"interval_count": 1,
"monthly_equivalent": 299
},
"new_plan": {
"product_id": "prod_pro",
"product_name": "Pro Plan",
"amount": 599,
"currency": "TWD",
"interval": "month",
"interval_count": 1,
"monthly_equivalent": 599
},
"proration": {
"credit_amount": 150,
"charge_amount": 599,
"net_amount": 449,
"unused_days": 15,
"total_days_in_period": 30,
"credit_description": "15 天未使用的 Basic Plan"
},
"effective_date": "2025-01-15T10:00:00.000Z",
"next_billing_date": "2025-02-15T00:00:00.000Z",
"requires_payment": true,
"can_proceed": true,
"is_in_trial": false
}回應欄位說明
| 欄位 | 類型 | 說明 |
|---|---|---|
switch_type | string | 切換類型(UPGRADE、DOWNGRADE、PERIOD_CHANGE、CROSSGRADE) |
execution_mode | string | 執行方式(immediate 立即執行、scheduled 排程執行) |
proration | object | null | 按比例計算結果(降級時為 null) |
proration.net_amount | number | 客戶需支付的淨額(charge - credit) |
requires_payment | boolean | 是否需要付款 |
can_proceed | boolean | 是否可執行切換 |
blocking_reason | string | null | 無法切換的原因(如有) |
程式碼範例
const response = await fetch(
`https://api.recur.tw/v1/subscriptions/${subscriptionId}/switch-preview?` +
new URLSearchParams({
target_product_id: 'prod_pro',
}),
{
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);
const preview = await response.json();
if (preview.can_proceed) {
console.log(`切換類型: ${preview.switch_type}`);
console.log(`執行方式: ${preview.execution_mode}`);
if (preview.proration) {
console.log(`需付金額: ${preview.proration.net_amount}`);
}
}curl -X GET "https://api.recur.tw/v1/subscriptions/sub_xxxxx/switch-preview?target_product_id=prod_pro" \
-H "Authorization: Bearer sk_test_xxx"執行切換
執行訂閱方案切換。根據切換類型,會立即執行或排程至當期結束。
端點
POST /subscriptions/{subscription_id}/switch請求參數
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
target_product_id | string | 是 | 目標方案 ID |
proration_behavior | string | 否 | 按比例計算行為:create_prorations(預設)、none |
回應範例(立即執行)
升級或月轉年會立即執行:
{
"object": "switch_result",
"execution_mode": "immediate",
"subscription": {
"id": "sub_xxxxx",
"product_id": "prod_pro",
"status": "ACTIVE",
"amount": 599,
"currency": "TWD",
"interval": "month",
"interval_count": 1,
"current_period_start": "2025-01-15T10:00:00.000Z",
"current_period_end": "2025-02-15T00:00:00.000Z",
"next_billing_date": "2025-02-15T00:00:00.000Z",
"previous_product_id": "prod_basic",
"switched_at": "2025-01-15T10:00:00.000Z",
"switch_type": "UPGRADE"
},
"invoice": {
"id": "inv_xxxxx",
"invoice_number": "INV-20250115-ABC123",
"amount": 449,
"currency": "TWD",
"status": "PAID",
"billing_reason": "SUBSCRIPTION_UPDATE",
"billing_entries": [
{
"id": "entry_1",
"type": "PRORATION_CREDIT",
"direction": "CREDIT",
"amount": 150,
"description": "15 天未使用的 Basic Plan"
},
{
"id": "entry_2",
"type": "SUBSCRIPTION",
"direction": "CHARGE",
"amount": 599,
"description": "Pro Plan (月繳)"
}
]
},
"schedule": null,
"switch_type": "UPGRADE",
"proration": {
"credit_amount": 150,
"charge_amount": 599,
"net_amount": 449,
"unused_days": 15,
"total_days_in_period": 30,
"credit_description": "15 天未使用的 Basic Plan"
},
"effective_date": "2025-01-15T10:00:00.000Z"
}回應範例(排程執行)
降級或年轉月會排程至當期結束:
{
"object": "switch_result",
"execution_mode": "scheduled",
"subscription": {
"id": "sub_xxxxx"
},
"invoice": null,
"schedule": {
"id": "sched_xxxxx",
"target_product_id": "prod_basic",
"switch_type": "DOWNGRADE",
"effective_at": "2025-02-15T00:00:00.000Z",
"status": "PENDING",
"created_at": "2025-01-15T10:00:00.000Z"
},
"switch_type": "DOWNGRADE",
"proration": null,
"effective_date": "2025-02-15T00:00:00.000Z"
}程式碼範例
// 執行升級
const response = await fetch(
`https://api.recur.tw/v1/subscriptions/${subscriptionId}/switch`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_product_id: 'prod_pro',
}),
}
);
const result = await response.json();
if (result.execution_mode === 'immediate') {
console.log('切換已立即生效');
console.log(`新方案: ${result.subscription.product_id}`);
if (result.invoice) {
console.log(`帳單金額: ${result.invoice.amount}`);
}
} else {
console.log('切換已排程');
console.log(`生效日期: ${result.schedule.effective_at}`);
}curl -X POST "https://api.recur.tw/v1/subscriptions/sub_xxxxx/switch" \
-H "Authorization: Bearer sk_test_xxx" \
-H "Content-Type: application/json" \
-d '{"target_product_id": "prod_pro"}'查詢排程切換
查詢訂閱是否有待執行的排程切換。
端點
GET /subscriptions/{subscription_id}/schedule回應範例(有排程)
{
"object": "schedule",
"has_pending_schedule": true,
"schedule": {
"id": "sched_xxxxx",
"subscription_id": "sub_xxxxx",
"target_product_id": "prod_basic",
"target_product_name": "Basic Plan",
"switch_type": "DOWNGRADE",
"effective_at": "2025-02-15T00:00:00.000Z",
"status": "PENDING",
"created_at": "2025-01-15T10:00:00.000Z"
}
}回應範例(無排程)
{
"object": "schedule",
"has_pending_schedule": false,
"schedule": null
}取消排程切換
取消待執行的排程切換,訂閱將維持當前方案。
端點
DELETE /subscriptions/{subscription_id}/schedule回應範例
{
"object": "schedule_cancellation",
"cancelled": true,
"subscription_id": "sub_xxxxx"
}程式碼範例
// 取消排程的降級
const response = await fetch(
`https://api.recur.tw/v1/subscriptions/${subscriptionId}/schedule`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
},
}
);
const result = await response.json();
if (result.cancelled) {
console.log('排程切換已取消,訂閱將維持當前方案');
}curl -X DELETE "https://api.recur.tw/v1/subscriptions/sub_xxxxx/schedule" \
-H "Authorization: Bearer sk_test_xxx"使用案例
1. 客服協助升級
當客服需要幫客戶升級方案時:
async function upgradeSubscription(subscriptionId: string, newProductId: string) {
// 1. 先預覽切換
const previewRes = await fetch(
`${RECUR_API_URL}/v1/subscriptions/${subscriptionId}/switch-preview?` +
new URLSearchParams({ target_product_id: newProductId }),
{
headers: { 'Authorization': `Bearer ${RECUR_SECRET_KEY}` },
}
);
const preview = await previewRes.json();
if (!preview.can_proceed) {
throw new Error(`無法切換: ${preview.blocking_reason}`);
}
// 2. 確認後執行切換
const switchRes = await fetch(
`${RECUR_API_URL}/v1/subscriptions/${subscriptionId}/switch`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${RECUR_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ target_product_id: newProductId }),
}
);
return switchRes.json();
}2. 顯示待執行的降級
在用戶設定頁面顯示排程中的降級:
async function getPendingDowngrade(subscriptionId: string) {
const res = await fetch(
`${RECUR_API_URL}/v1/subscriptions/${subscriptionId}/schedule`,
{
headers: { 'Authorization': `Bearer ${RECUR_SECRET_KEY}` },
}
);
const data = await res.json();
if (data.has_pending_schedule) {
return {
hasPendingChange: true,
newPlan: data.schedule.target_product_name,
effectiveDate: data.schedule.effective_at,
canCancel: true,
};
}
return { hasPendingChange: false };
}錯誤處理
| HTTP 狀態 | 錯誤碼 | 說明 |
|---|---|---|
| 401 | unauthorized | 需要 Secret Key 認證 |
| 404 | subscription_not_found | 找不到訂閱 |
| 404 | product_not_found | 找不到目標方案 |
| 400 | same_product | 目標方案與當前相同 |
| 400 | subscription_not_active | 訂閱非有效狀態 |
| 400 | past_due_blocks_switch | 訂閱逾期中,無法切換 |
| 402 | payment_required | 需要付款但付款失敗 |
下一步
- Webhook 整合 - 接收訂閱狀態變更通知
- API 認證 - 了解 API Key 使用方式
- Customer Portal - 讓客戶自助管理訂閱
Last updated on