# Customers API (/api/customers)





Customers API [#customers-api]

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

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

客戶物件 [#客戶物件]

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

程式碼範例 [#程式碼範例]

<Tabs items="['Node.js', 'cURL']">
  <Tab value="Node.js">
    ```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}`,
          },
        }
      );
    }
    ```
  </Tab>

  <Tab value="cURL">
    ```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"
    ```
  </Tab>
</Tabs>

***

取得客戶 [#取得客戶]

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

端點 [#端點-1]

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

路徑參數 [#路徑參數]

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

查詢參數 [#查詢參數]

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

回應範例 [#回應範例-1]

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

程式碼範例 [#程式碼範例-1]

<Tabs items="['Node.js', 'cURL']">
  <Tab value="Node.js">
    ```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}`,
        },
      }
    );
    ```
  </Tab>

  <Tab value="cURL">
    ```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"
    ```
  </Tab>
</Tabs>

***

更新客戶 [#更新客戶]

更新客戶資訊。

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

端點 [#端點-2]

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

路徑參數 [#路徑參數-1]

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

查詢參數 [#查詢參數-1]

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

請求參數 [#請求參數-1]

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

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

回應範例 [#回應範例-2]

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

程式碼範例 [#程式碼範例-2]

<Tabs items="['Node.js', 'Python', 'cURL']">
  <Tab value="Node.js">
    ```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: '新名字',
        }),
      }
    );
    ```
  </Tab>

  <Tab value="Python">
    ```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"]}')
    ```
  </Tab>

  <Tab value="cURL">
    ```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": "新名字"}'
    ```
  </Tab>
</Tabs>

***

透過外部 ID 操作 [#透過外部-id-操作]

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

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

透過外部 ID 取得客戶 [#透過外部-id-取得客戶]

端點 [#端點-3]

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

路徑參數 [#路徑參數-2]

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

回應範例 [#回應範例-3]

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

程式碼範例 [#程式碼範例-3]

<Tabs items="['Node.js', 'cURL']">
  <Tab value="Node.js">
    ```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}`);
    ```
  </Tab>

  <Tab value="cURL">
    ```bash
    curl -X GET "https://api.recur.tw/v1/customers/external/user_123" \
      -H "Authorization: Bearer sk_test_xxx"
    ```
  </Tab>
</Tabs>

***

透過外部 ID 更新客戶 [#透過外部-id-更新客戶]

端點 [#端點-4]

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

路徑參數 [#路徑參數-3]

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

請求參數 [#請求參數-2]

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

回應範例 [#回應範例-4]

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

程式碼範例 [#程式碼範例-4]

<Tabs items="['Node.js', 'Python', 'cURL']">
  <Tab value="Node.js">
    ```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',
        }),
      }
    );
    ```
  </Tab>

  <Tab value="Python">
    ```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"]}')
    ```
  </Tab>

  <Tab value="cURL">
    ```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}'
    ```
  </Tab>
</Tabs>

***

使用案例 [#使用案例]

1\. 同步客戶資料 [#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\. 客戶管理後台 [#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 [#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\. 修正錯誤的客戶名稱 [#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-中的客戶資料]

<Callout type="info">
  **為什麼 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 進行。
</Callout>

***

下一步 [#下一步]

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