---
title: Subscriptions API
description: 訂閱狀態查詢與管理 API
---

# Subscriptions API

訂閱相關的 API 端點，用於查詢和管理訂閱狀態。

## 查詢訂閱狀態

查詢訂閱狀態，支援列出所有訂閱或按客戶篩選。此端點需要 Secret Key 認證，適合後端使用。

### 端點

```
GET /subscriptions
```

### 認證

  此端點需要 **Secret Key** 認證，僅限後端使用。

支援以下認證方式：

```bash
# 方式 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 |

### 回應格式

#### 按客戶篩選（有訂閱）

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

#### 列出所有訂閱（無客戶篩選）

當不提供任何客戶識別參數時，每筆訂閱會包含對應的客戶資訊：

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

**含優惠的訂閱範例：**

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

#### 無有效訂閱的客戶

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

#### 找不到客戶

當提供客戶篩選參數但找不到對應客戶時：

```json
{
  "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` | 已暫停 |

### 程式碼範例

```typescript
// 使用 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}`,
    },
  }
);
```

```python
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',
    }
)
```

```bash
# 列出所有有效訂閱（不需指定客戶）
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）

在您的後端驗證用戶是否有權限存取付費功能：

```typescript
// 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. 查詢用戶所有訂閱

不指定方案，取得用戶的所有訂閱歷史：

```typescript
// 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. 顯示訂閱資訊

在用戶設定頁面顯示當前訂閱狀態：

```typescript
// 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：

```typescript
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，提醒客戶綁卡。

### 回應格式

#### 成功建立

```json
{
  "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 訂閱

```json
{
  "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 錯誤：

```json
{
  "error": {
    "code": "conflict",
    "message": "Customer already has an active subscription to this product",
    "details": [
      {
        "existing_subscription_id": "sub_xxxxx",
        "status": "ACTIVE"
      }
    ]
  }
}
```

**處理重複訂閱錯誤**

當收到 409 錯誤時，您可以：
1. 使用 `details[0].existing_subscription_id` 查詢現有訂閱詳情
2. 引導客戶到 Customer Portal 管理現有訂閱
3. 如需切換方案，使用[訂閱方案切換](#訂閱方案切換) API

### 程式碼範例

```typescript
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);
}
```

```python
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"]}')
```

```bash
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 |

#### 回應範例

```json
{
  "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 | 無法切換的原因（如有） |

#### 程式碼範例

```typescript
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}`);
  }
}
```

```bash
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` |

#### 回應範例（立即執行）

升級或月轉年會立即執行：

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

#### 回應範例（排程執行）

降級或年轉月會排程至當期結束：

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

#### 程式碼範例

```typescript
// 執行升級
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}`);
}
```

```bash
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
```

#### 回應範例（有排程）

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

#### 回應範例（無排程）

```json
{
  "object": "schedule",
  "has_pending_schedule": false,
  "schedule": null
}
```

---

### 取消排程切換

取消待執行的排程切換，訂閱將維持當前方案。

#### 端點

```
DELETE /subscriptions/{subscription_id}/schedule
```

#### 回應範例

```json
{
  "object": "schedule_cancellation",
  "cancelled": true,
  "subscription_id": "sub_xxxxx"
}
```

#### 程式碼範例

```typescript
// 取消排程的降級
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('排程切換已取消，訂閱將維持當前方案');
}
```

```bash
curl -X DELETE "https://api.recur.tw/v1/subscriptions/sub_xxxxx/schedule" \
  -H "Authorization: Bearer sk_test_xxx"
```

---

### 使用案例

#### 1. 客服協助升級

當客服需要幫客戶升級方案時：

```typescript
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. 顯示待執行的降級

在用戶設定頁面顯示排程中的降級：

```typescript
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 整合](/guides/webhooks) - 接收訂閱狀態變更通知
- [API 認證](/getting-started/authentication) - 了解 API Key 使用方式
- [Customer Portal](/guides/portal) - 讓客戶自助管理訂閱
