---
title: Customers API
description: 客戶資料查詢與管理 API
---

# Customers API

客戶（Customer）是 Recur 中代表付費用戶的核心實體。每個客戶都與一個 email 綁定，並可選擇性地關聯您系統中的外部 ID。

  所有 Customers API 端點都需要 **Secret Key** 認證，僅限後端使用。這是為了保護客戶資料安全，避免透過公開的 Publishable Key 被惡意存取或修改。

## 客戶物件

```json
{
  "object": "customer",
  "id": "cus_xxxxx",
  "email": "user@example.com",
  "name": "王小明",
  "external_id": "user_123",
  "email_verified": false,
  "status": "ACTIVE",
  "created_at": "2025-01-01T00:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z",
  "livemode": false
}
```

### 欄位說明

| 欄位 | 類型 | 說明 |
|------|------|------|
| `object` | string | 固定為 `"customer"` |
| `id` | string | Recur 內部客戶 ID |
| `email` | string | 客戶 Email（建立後不可變更） |
| `name` | string \| null | 客戶名稱 |
| `external_id` | string \| null | 您系統中的客戶 ID（可透過 API 更新） |
| `email_verified` | boolean | Email 是否已驗證 |
| `status` | string | 客戶狀態：`ACTIVE`、`BANNED` |
| `created_at` | string | 建立時間（ISO 8601） |
| `updated_at` | string | 最後更新時間（ISO 8601） |
| `livemode` | boolean | 是否為正式環境 |

---

## 列出客戶

取得客戶列表，支援分頁和篩選。

### 端點

```
GET /customers
```

### 請求參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `limit` | number | 每頁數量（預設 10，最大 100） |
| `starting_after` | string | 分頁游標（客戶 ID） |
| `email` | string | 依 Email 篩選（精確比對） |
| `status` | string | 依狀態篩選（`ACTIVE`、`BANNED`） |

### 回應範例

```json
{
  "object": "list",
  "data": [
    {
      "object": "customer",
      "id": "cus_xxxxx",
      "email": "user@example.com",
      "name": "王小明",
      "external_id": "user_123",
      "email_verified": false,
      "status": "ACTIVE",
      "created_at": "2025-01-01T00:00:00.000Z",
      "updated_at": "2025-01-15T10:00:00.000Z",
      "subscriptions_count": 2,
      "orders_count": 5
    }
  ],
  "has_more": true,
  "next_cursor": "cus_yyyyy",
  "livemode": false
}
```

### 程式碼範例

```typescript
// 列出所有客戶
const response = await fetch(
  'https://api.recur.tw/v1/customers?limit=20',
  {
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
    },
  }
);

const { data, has_more, next_cursor } = await response.json();

console.log(`共 ${data.length} 位客戶`);

// 分頁取得更多
if (has_more) {
  const nextPage = await fetch(
    `https://api.recur.tw/v1/customers?starting_after=${next_cursor}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
      },
    }
  );
}
```

```bash
# 列出客戶
curl -X GET "https://api.recur.tw/v1/customers?limit=20" \
  -H "Authorization: Bearer sk_test_xxx"

# 依 Email 篩選
curl -X GET "https://api.recur.tw/v1/customers?email=user@example.com" \
  -H "Authorization: Bearer sk_test_xxx"
```

---

## 取得客戶

透過 ID 取得單一客戶的詳細資訊。

### 端點

```
GET /customers/{id}
```

### 路徑參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `id` | string | 客戶 ID 或外部 ID |

### 查詢參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `by_external_id` | boolean | 設為 `true` 時，將 `id` 視為外部 ID 查詢 |

### 回應範例

```json
{
  "object": "customer",
  "id": "cus_xxxxx",
  "email": "user@example.com",
  "name": "王小明",
  "external_id": "user_123",
  "email_verified": false,
  "status": "ACTIVE",
  "created_at": "2025-01-01T00:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z",
  "subscriptions": [
    {
      "id": "sub_xxxxx",
      "status": "ACTIVE",
      "current_period_start": "2025-01-01T00:00:00.000Z",
      "current_period_end": "2025-02-01T00:00:00.000Z",
      "product": {
        "id": "prod_xxxxx",
        "name": "Pro Plan",
        "slug": "pro-monthly"
      }
    }
  ],
  "livemode": false
}
```

### 程式碼範例

```typescript
// 透過內部 ID 查詢
const response = await fetch(
  'https://api.recur.tw/v1/customers/cus_xxxxx',
  {
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
    },
  }
);

const customer = await response.json();
console.log(`客戶: ${customer.name} (${customer.email})`);

// 透過外部 ID 查詢
const response2 = await fetch(
  'https://api.recur.tw/v1/customers/user_123?by_external_id=true',
  {
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
    },
  }
);
```

```bash
# 透過內部 ID 查詢
curl -X GET "https://api.recur.tw/v1/customers/cus_xxxxx" \
  -H "Authorization: Bearer sk_test_xxx"

# 透過外部 ID 查詢
curl -X GET "https://api.recur.tw/v1/customers/user_123?by_external_id=true" \
  -H "Authorization: Bearer sk_test_xxx"
```

---

## 更新客戶

更新客戶資訊。

  **安全性說明**：此端點僅允許 Secret Key 認證。這是因為 Checkout Session 可透過 Publishable Key 建立，如果允許 Publishable Key 更新客戶資料，惡意使用者可能會竄改其他客戶的資訊。

### 端點

```
PATCH /customers/{id}
```

### 路徑參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `id` | string | 客戶 ID 或外部 ID |

### 查詢參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `by_external_id` | boolean | 設為 `true` 時，將 `id` 視為外部 ID 查詢 |

### 請求參數

| 參數 | 類型 | 必填 | 說明 |
|------|------|------|------|
| `name` | string | 否 | 客戶名稱（1-255 字元） |
| `external_id` | string \| null | 否 | 外部 ID（設為新值或 `null` 解除綁定） |
| `metadata` | object | 否 | 自訂鍵值資料 |

  `email` 在建立後不可變更，如需變更 Email 請建立新客戶。`external_id` 可透過此 API 更新或設為 `null` 解除綁定。

### 回應範例

```json
{
  "object": "customer",
  "id": "cus_xxxxx",
  "email": "user@example.com",
  "name": "王大明",
  "external_id": "user_123",
  "email_verified": false,
  "status": "ACTIVE",
  "created_at": "2025-01-01T00:00:00.000Z",
  "updated_at": "2025-01-15T10:30:00.000Z",
  "livemode": false
}
```

### 程式碼範例

```typescript
// 更新客戶名稱
const response = await fetch(
  'https://api.recur.tw/v1/customers/cus_xxxxx',
  {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: '王大明',
    }),
  }
);

const customer = await response.json();
console.log(`已更新客戶名稱: ${customer.name}`);

// 透過外部 ID 更新
const response2 = await fetch(
  'https://api.recur.tw/v1/customers/user_123?by_external_id=true',
  {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: '新名字',
    }),
  }
);
```

```python
import requests
import os

# 更新客戶名稱
response = requests.patch(
    'https://api.recur.tw/v1/customers/cus_xxxxx',
    headers={
        'Authorization': f'Bearer {os.environ["RECUR_SECRET_KEY"]}',
        'Content-Type': 'application/json',
    },
    json={
        'name': '王大明',
    }
)

customer = response.json()
print(f'已更新客戶名稱: {customer["name"]}')
```

```bash
# 更新客戶名稱
curl -X PATCH "https://api.recur.tw/v1/customers/cus_xxxxx" \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "王大明"}'

# 透過外部 ID 更新
curl -X PATCH "https://api.recur.tw/v1/customers/user_123?by_external_id=true" \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "新名字"}'
```

---

## 透過外部 ID 操作

除了使用查詢參數 `?by_external_id=true`，Recur 也提供更簡潔的 URL 格式讓您透過外部 ID 操作客戶資料。

  **何時使用這個端點？** 如果您的系統已經有自己的使用者 ID（external_id），使用 `/customers/external/{external_id}` 端點可以讓您的程式碼更簡潔，不需要先查詢 Recur 內部的客戶 ID。

### 透過外部 ID 取得客戶

#### 端點

```
GET /customers/external/{external_id}
```

#### 路徑參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `external_id` | string | 您系統中的客戶 ID |

#### 回應範例

```json
{
  "object": "customer",
  "id": "cus_xxxxx",
  "email": "user@example.com",
  "name": "王小明",
  "external_id": "user_123",
  "email_verified": false,
  "status": "ACTIVE",
  "created_at": "2025-01-01T00:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z",
  "subscriptions": [
    {
      "id": "sub_xxxxx",
      "status": "ACTIVE",
      "current_period_start": "2025-01-01T00:00:00.000Z",
      "current_period_end": "2025-02-01T00:00:00.000Z",
      "product": {
        "id": "prod_xxxxx",
        "name": "Pro Plan",
        "slug": "pro-monthly"
      }
    }
  ],
  "livemode": false
}
```

#### 程式碼範例

```typescript
// 透過外部 ID 取得客戶
const externalId = 'user_123';
const response = await fetch(
  `https://api.recur.tw/v1/customers/external/${externalId}`,
  {
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
    },
  }
);

const customer = await response.json();
console.log(`客戶: ${customer.name} (${customer.email})`);
console.log(`訂閱數: ${customer.subscriptions.length}`);
```

```bash
curl -X GET "https://api.recur.tw/v1/customers/external/user_123" \
  -H "Authorization: Bearer sk_test_xxx"
```

---

### 透過外部 ID 更新客戶

#### 端點

```
PATCH /customers/external/{external_id}
```

#### 路徑參數

| 參數 | 類型 | 說明 |
|------|------|------|
| `external_id` | string | 您系統中的客戶 ID |

#### 請求參數

| 參數 | 類型 | 必填 | 說明 |
|------|------|------|------|
| `name` | string | 否 | 客戶名稱（1-255 字元） |
| `external_id` | string \| null | 否 | 外部 ID（設為新值或 `null` 解除綁定） |
| `metadata` | object | 否 | 自訂鍵值資料 |

#### 回應範例

```json
{
  "object": "customer",
  "id": "cus_xxxxx",
  "email": "user@example.com",
  "name": "王大明",
  "external_id": "user_123",
  "email_verified": false,
  "status": "ACTIVE",
  "created_at": "2025-01-01T00:00:00.000Z",
  "updated_at": "2025-01-15T10:30:00.000Z",
  "livemode": false
}
```

#### 程式碼範例

```typescript
// 透過外部 ID 更新客戶名稱
const externalId = 'user_123';
const response = await fetch(
  `https://api.recur.tw/v1/customers/external/${externalId}`,
  {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: '王大明',
    }),
  }
);

const customer = await response.json();
console.log(`已更新客戶名稱: ${customer.name}`);

// 更新外部 ID（例如資料庫遷移後）
const response2 = await fetch(
  `https://api.recur.tw/v1/customers/external/${externalId}`,
  {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      external_id: 'new_user_456',
    }),
  }
);
```

```python
import requests
import os

# 透過外部 ID 更新客戶名稱
external_id = 'user_123'
response = requests.patch(
    f'https://api.recur.tw/v1/customers/external/{external_id}',
    headers={
        'Authorization': f'Bearer {os.environ["RECUR_SECRET_KEY"]}',
        'Content-Type': 'application/json',
    },
    json={
        'name': '王大明',
    }
)

customer = response.json()
print(f'已更新客戶名稱: {customer["name"]}')
```

```bash
# 更新客戶名稱
curl -X PATCH "https://api.recur.tw/v1/customers/external/user_123" \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"name": "王大明"}'

# 更新外部 ID
curl -X PATCH "https://api.recur.tw/v1/customers/external/user_123" \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "new_user_456"}'

# 解除外部 ID 綁定
curl -X PATCH "https://api.recur.tw/v1/customers/external/user_123" \
  -H "Authorization: Bearer sk_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{"external_id": null}'
```

---

## 使用案例

### 1. 同步客戶資料

當您的系統中客戶資料更新時，同步到 Recur：

```typescript
// app/api/webhooks/user-updated/route.ts
export async function POST(req: NextRequest) {
  const { userId, name, email } = await req.json();

  // 透過外部 ID 更新 Recur 客戶（使用更簡潔的 URL 格式）
  const response = await fetch(
    `${process.env.RECUR_API_URL}/v1/customers/external/${userId}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name }),
    }
  );

  if (response.ok) {
    console.log('Customer synced to Recur');
  }

  return NextResponse.json({ success: true });
}
```

### 2. 客戶管理後台

在您的管理後台顯示客戶列表：

```typescript
// app/admin/customers/page.tsx
async function getCustomers(cursor?: string) {
  const url = new URL('https://api.recur.tw/v1/customers');
  url.searchParams.set('limit', '50');
  if (cursor) {
    url.searchParams.set('starting_after', cursor);
  }

  const response = await fetch(url.toString(), {
    headers: {
      'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
    },
    next: { revalidate: 60 }, // Cache for 1 minute
  });

  return response.json();
}

export default async function CustomersPage() {
  const { data: customers, has_more, next_cursor } = await getCustomers();

  return (
    <div>
      <h1>客戶列表</h1>
      <table>
        <thead>
          <tr>
            <th>Email</th>
            <th>名稱</th>
            <th>訂閱數</th>
            <th>建立時間</th>
          </tr>
        </thead>
        <tbody>
          {customers.map((customer) => (
            <tr key={customer.id}>
              <td>{customer.email}</td>
              <td>{customer.name || '-'}</td>
              <td>{customer.subscriptions_count}</td>
              <td>{new Date(customer.created_at).toLocaleDateString()}</td>
            </tr>
          ))}
        </tbody>
      </table>
      {has_more && (
        <button onClick={() => loadMore(next_cursor)}>
          載入更多
        </button>
      )}
    </div>
  );
}
```

### 3. 資料庫遷移後批量更新外部 ID

當您的系統進行資料庫遷移導致使用者 ID 變更時，可批量更新對應的 `external_id`：

```typescript
// scripts/migrate-external-ids.ts
const idMapping = [
  { oldId: 'old_user_1', newId: 'new_user_1' },
  { oldId: 'old_user_2', newId: 'new_user_2' },
  // ...
];

for (const { oldId, newId } of idMapping) {
  const response = await fetch(
    `https://api.recur.tw/v1/customers/external/${oldId}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ external_id: newId }),
    }
  );

  if (response.ok) {
    console.log(`Migrated ${oldId} → ${newId}`);
  } else {
    const error = await response.json();
    console.error(`Failed ${oldId}: ${error.error.message}`);
  }
}
```

### 4. 修正錯誤的客戶名稱

當客戶反映名稱錯誤時，客服可以協助修正：

```typescript
async function fixCustomerName(email: string, correctName: string) {
  // 先用 email 找到客戶
  const listResponse = await fetch(
    `https://api.recur.tw/v1/customers?email=${encodeURIComponent(email)}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
      },
    }
  );

  const { data } = await listResponse.json();

  if (data.length === 0) {
    throw new Error('Customer not found');
  }

  const customerId = data[0].id;

  // 更新名稱
  const updateResponse = await fetch(
    `https://api.recur.tw/v1/customers/${customerId}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.RECUR_SECRET_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: correctName }),
    }
  );

  return updateResponse.json();
}
```

---

## 錯誤處理

| HTTP 狀態 | 錯誤碼 | 說明 |
|-----------|--------|------|
| 400 | `invalid_request` | 請求參數無效 |
| 401 | `unauthorized` | 需要 Secret Key 認證 |
| 404 | `resource_not_found` | 客戶不存在 |
| 409 | `conflict` | 外部 ID 已被同環境中的其他客戶使用 |
| 422 | `validation_error` | 外部 ID 格式不合法（僅允許英數字、底線、連字號） |
| 500 | `internal_error` | 伺服器錯誤 |

---

## 關於 Checkout Session 中的客戶資料

  **為什麼 Checkout Session 不會更新現有客戶資料？**

  Checkout Session 可透過 Publishable Key 建立，而 Publishable Key 是公開的（會暴露在前端）。

  如果允許透過 Checkout Session 更新客戶資料，惡意使用者只需要：
  1. 取得您的 Publishable Key（本來就公開）
  2. 知道某客戶的 Email
  3. 建立 Checkout Session 並傳入惡意的 `customer_name`

  這樣就能竄改其他客戶的資料，造成安全風險。

  因此，客戶資料的更新僅能透過 **Secret Key** 認證的 Customer Update API 進行。

---

## 下一步

- [Subscriptions API](/api-reference/subscriptions) - 查詢和管理訂閱
- [API 認證](/getting-started/authentication) - 了解 API Key 使用方式
- [Webhook 整合](/guides/webhooks) - 接收客戶相關事件通知
