---
title: 錯誤碼參考
description: Recur API 錯誤碼完整說明與處理建議
---

# 錯誤碼參考

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)

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

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

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

---

### unauthorized (401)

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

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

**處理建議**：
- 確認 API Key 是否正確
- 確認使用正確的環境（Sandbox/Production）
- 確認 API Key 未被撤銷

---

### payment_required (402)

付款失敗。

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

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

---

### forbidden (403)

無權存取此資源。

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

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

---

### not_found (404)

找不到請求的資源。

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

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

---

### 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)

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

此檢查會在以下端點觸發：
- `POST /v1/checkouts`（Embedded Checkout）— 當帶入 `customerEmail` 或 `externalCustomerId` 時
- `POST /v1/checkout/sessions`（Hosted Checkout）— 當帶入 `customerEmail` 時
- `POST /v1/subscriptions`（直接建立訂閱）

**為什麼在 Checkout 建立時就檢查？**

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

**details 欄位說明**：

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

**處理建議**：

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

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

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

---

### 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)

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

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

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

---

### internal_server_error (500)

伺服器發生錯誤。

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

**處理建議**：
- 稍後重試請求
- 如果問題持續，請聯繫 Recur 支援團隊

---

## PAYUNi SDK 錯誤碼（Client-side）

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

### SDK Token 錯誤（OBJ*）

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

### iframe 交易錯誤（IFTRADE*）

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

### SDK 解密錯誤（DEF*）

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

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

---

## SDK 錯誤處理

### 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

```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. 針對特定錯誤碼處理

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

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

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

### 2. 使用 details 獲取更多資訊

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

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

### 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 管理
