# 錯誤碼參考 (/api/error-codes)





錯誤碼參考 [#錯誤碼參考]

Recur API 使用標準的 HTTP 狀態碼和統一的錯誤格式。本頁說明所有可能的錯誤碼及處理建議。

錯誤回應格式 [#錯誤回應格式]

所有 API 錯誤都使用以下格式：

```json
{
  "error": {
    "code": "error_code",
    "message": "人類可讀的錯誤訊息",
    "doc_url": "https://docs.recur.tw/api/error-codes#error_code",
    "details": [
      {
        "key": "額外資訊"
      }
    ]
  }
}
```

| 欄位        | 類型     | 說明                            |
| --------- | ------ | ----------------------------- |
| `code`    | string | 錯誤碼（如 `conflict`、`not_found`） |
| `message` | string | 人類可讀的錯誤訊息                     |
| `doc_url` | string | 相關文件連結（選填）                    |
| `details` | array  | 錯誤相關的額外資訊（選填）                 |

錯誤碼列表 [#錯誤碼列表]

bad_request (400) [#bad_request-400]

請求參數無效或缺少必要參數。

```json
{
  "error": {
    "code": "bad_request",
    "message": "Missing required fields: sessionId, productId, email"
  }
}
```

**處理建議**：檢查請求參數是否正確，參考 API 文件確認必要欄位。

***

unauthorized (401) [#unauthorized-401]

API Key 無效、缺失或已過期。

```json
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid API key"
  }
}
```

**處理建議**：

* 確認 API Key 是否正確
* 確認使用正確的環境（Sandbox/Production）
* 確認 API Key 未被撤銷

***

payment_required (402) [#payment_required-402]

付款失敗。

```json
{
  "error": {
    "code": "payment_required",
    "message": "Payment failed: card declined"
  }
}
```

**處理建議**：引導用戶更換付款方式或聯繫銀行。

***

forbidden (403) [#forbidden-403]

無權存取此資源。

```json
{
  "error": {
    "code": "forbidden",
    "message": "Access denied to this resource"
  }
}
```

**處理建議**：確認 API Key 有足夠權限。Secret Key 才能存取某些端點。

***

not_found (404) [#not_found-404]

找不到請求的資源。

```json
{
  "error": {
    "code": "not_found",
    "message": "Product not found or inactive"
  }
}
```

**處理建議**：確認資源 ID 是否正確，或資源是否已被刪除。

***

conflict (409) [#conflict-409]

資源衝突。最常見的情況是客戶已有該商品的有效訂閱。

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

重複訂閱 (Duplicate Subscription) [#重複訂閱-duplicate-subscription]

當客戶嘗試建立已有有效訂閱（ACTIVE、TRIAL 或 PAST\_DUE）的商品訂閱時會觸發此錯誤。

此檢查會在以下端點觸發：

* `POST /v1/checkouts`（Embedded Checkout）— 當帶入 `customerEmail` 或 `externalCustomerId` 時
* `POST /v1/checkout/sessions`（Hosted Checkout）— 當帶入 `customerEmail` 時
* `POST /v1/subscriptions`（直接建立訂閱）

<Callout type="info">
  **為什麼在 Checkout 建立時就檢查？**

  在結帳流程的最早階段阻擋重複訂閱，可以避免客戶被不必要的扣款，也省去商家手動退款的麻煩。如果沒有帶入 `customerEmail`（例如在結帳頁才收集 email），此檢查會在付款完成後進行。
</Callout>

**details 欄位說明**：

| 欄位                         | 類型     | 說明                              |
| -------------------------- | ------ | ------------------------------- |
| `existing_subscription_id` | string | 現有訂閱的 ID                        |
| `status`                   | string | 現有訂閱的狀態（ACTIVE、TRIAL、PAST\_DUE） |

**處理建議**：

<Tabs items="['引導到 Portal', '查詢訂閱', '切換方案']">
  <Tab value="引導到 Portal">
    ```typescript
    // 引導用戶到 Customer Portal 管理現有訂閱
    if (error.code === 'conflict') {
      const portalUrl = await recur.portal.sessions.create({
        customer: customerId,
        returnUrl: window.location.href,
      });
      window.location.href = portalUrl.url;
    }
    ```
  </Tab>

  <Tab value="查詢訂閱">
    ```typescript
    // 使用 details 中的 ID 查詢現有訂閱詳情
    if (error.code === 'conflict') {
      const existingSubId = error.details?.[0]?.existing_subscription_id;
      const response = await fetch(
        `https://api.recur.tw/v1/subscriptions?customer_id=${customerId}`,
        { headers: { 'Authorization': `Bearer ${secretKey}` } }
      );
      const { subscriptions } = await response.json();
      const subscription = subscriptions.find(s => s.id === existingSubId);
      console.log('現有訂閱:', subscription);
    }
    ```
  </Tab>

  <Tab value="切換方案">
    ```typescript
    // 如需切換方案，使用訂閱方案切換 API
    if (error.code === 'conflict') {
      const existingSubId = error.details?.[0]?.existing_subscription_id;
      const response = await fetch(
        `https://api.recur.tw/v1/subscriptions/${existingSubId}/switch`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${secretKey}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            target_product_id: newProductId,
          }),
        }
      );
    }
    ```
  </Tab>
</Tabs>

***

validation_error (422) [#validation_error-422]

請求參數驗證失敗。

```json
{
  "error": {
    "code": "validation_error",
    "message": "Invalid request parameters",
    "details": [
      {
        "path": "email",
        "message": "Invalid email format",
        "code": "invalid_type"
      }
    ]
  }
}
```

**處理建議**：根據 `details` 中的資訊修正參數格式。

***

rate_limit_exceeded (429) [#rate_limit_exceeded-429]

請求過於頻繁，已超過速率限制。

```json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests"
  }
}
```

**處理建議**：

* 實作指數退避重試（exponential backoff）
* 減少請求頻率
* 考慮使用 Webhook 取代輪詢

***

internal_server_error (500) [#internal_server_error-500]

伺服器發生錯誤。

```json
{
  "error": {
    "code": "internal_server_error",
    "message": "An unexpected error occurred"
  }
}
```

**處理建議**：

* 稍後重試請求
* 如果問題持續，請聯繫 Recur 支援團隊

***

PAYUNi SDK 錯誤碼（Client-side） [#payuni-sdk-錯誤碼client-side]

結帳表單中的信用卡 iframe 由 PAYUNi 的 UNi Embed SDK 提供。以下錯誤直接在客戶端發生，**不經過 Recur API**。

SDK Token 錯誤（OBJ*） [#sdk-token-錯誤obj]

| 錯誤碼        | 說明         | 可重試                  |
| ---------- | ---------- | -------------------- |
| `OBJ01001` | 查無 Token   | ❌                    |
| `OBJ01002` | Token 格式錯誤 | ❌                    |
| `OBJ01003` | Token 已過期  | ✅（refresh token 後重試） |
| `OBJ01004` | Token 已使用  | ✅（refresh token 後重試） |
| `OBJ01005` | 商店未啟用      | ❌                    |

iframe 交易錯誤（IFTRADE*） [#iframe-交易錯誤iftrade]

| 錯誤碼            | 說明       | 可重試 |
| -------------- | -------- | --- |
| `IFTRADE04001` | 未有 Token | ❌   |
| `IFTRADE04002` | 未有交易設定資料 | ❌   |

SDK 解密錯誤（DEF*） [#sdk-解密錯誤def]

| 錯誤碼        | 說明     | 可重試 |
| ---------- | ------ | --- |
| `DEF01001` | 資料解密失敗 | ❌   |
| `DEF01002` | 資料解密失敗 | ❌   |

<Callout type="info">
  使用最新版本的 `recur-tw` SDK 可自動處理 Token 刷新，避免大部分 OBJ01003/OBJ01004 錯誤。詳見[錯誤處理指南](/guides/checkout/error-handling#payuni-sdk-錯誤碼client-side)。
</Callout>

***

SDK 錯誤處理 [#sdk-錯誤處理]

TypeScript / JavaScript [#typescript--javascript]

SDK 提供型別定義方便處理錯誤：

```typescript
import type { ApiErrorCode, CheckoutError } from 'recur-tw';

function handleError(error: CheckoutError) {
  switch (error.code as ApiErrorCode) {
    case 'conflict':
      // 重複訂閱
      const details = error.details?.[0];
      console.log('現有訂閱:', details?.existing_subscription_id);
      break;

    case 'payment_required':
      // 付款失敗
      console.log('請更換付款方式');
      break;

    case 'not_found':
      // 找不到資源
      console.log('商品不存在');
      break;

    default:
      console.log('發生錯誤:', error.message);
  }
}
```

Server SDK [#server-sdk]

```typescript
import { Recur, RecurAPIError } from 'recur-tw/server';

const recur = new Recur(process.env.RECUR_SECRET_KEY!);

try {
  const session = await recur.portal.sessions.create({
    email: 'user@example.com',
  });
} catch (error) {
  if (error instanceof RecurAPIError) {
    console.log('錯誤碼:', error.code);
    console.log('狀態碼:', error.statusCode);
    console.log('詳情:', error.details);
  }
}
```

最佳實踐 [#最佳實踐]

1\. 針對特定錯誤碼處理 [#1-針對特定錯誤碼處理]

不要只檢查 HTTP 狀態碼，應該檢查 `error.code`：

```typescript
// ✅ 好的做法
if (error.code === 'conflict') {
  // 處理重複訂閱
}

// ❌ 不好的做法
if (response.status === 409) {
  // 409 可能有多種含義
}
```

2\. 使用 details 獲取更多資訊 [#2-使用-details-獲取更多資訊]

`details` 陣列包含錯誤的額外資訊：

```typescript
if (error.code === 'conflict' && error.details?.[0]) {
  const { existing_subscription_id, status } = error.details[0];
  // 使用這些資訊提供更好的用戶體驗
}
```

3\. 記錄錯誤用於除錯 [#3-記錄錯誤用於除錯]

```typescript
onError: (error) => {
  // 記錄完整錯誤資訊
  console.error('[Recur Error]', {
    code: error.code,
    message: error.message,
    details: error.details,
    docUrl: error.docUrl,
  });

  // 發送到錯誤追蹤服務
  Sentry.captureException(error);
}
```

下一步 [#下一步]

* [錯誤處理指南](/guides/checkout/error-handling) - SDK 錯誤處理最佳實踐
* [Webhook 整合](/guides/webhooks) - 接收即時事件通知
* [API 認證](/getting-started/authentication) - API Key 管理
