
# 联系人 API

联系人是指您发送消息的个人——包括他们的姓名、电话号码、电子邮件、渠道、标签、自定义字段，以及他们所属的列表和营销活动。联系人 API 允许您创建、查找、更新、标记、批量导入和删除联系人，所有这些操作都无需使用仪表板。

此页面上的所有路径均相对于基础 URL：

```
https://api.youraiconnector.com/v1
```

因此 `/contacts` 意味着 `https://api.youraiconnector.com/v1/contacts`。

> **API 新手？** 请先阅读 [API 访问](../integrations/api-access.md) —— 它涵盖了如何生成 API 密钥、三种身份验证方式、速率限制以及错误格式。本页面上的所有内容都假设您已经拥有一个可用的 API 密钥。

---

## 关于联系人 ID

每个联系人都有一个唯一的 ID。您在**创建**联系人时（在 `data.contactId` 中）获得的 ID 与您在其他任何地方使用的 ID 相同——用于获取、更新、标记、发送消息或删除该联系人。保存一次即可重复使用。

您不必为了获取 ID 而创建联系人。您也可以通过电话号码或电子邮件查找 ID（请参阅 [获取联系人](#get-a-contact-by-phone-or-email)），或者翻页查看所有联系人（请参阅 [列出联系人](#list-contacts)）。这些操作返回的 ID 都是相同的。

---

## 创建联系人

`POST /contacts`

向您的账户添加一个新联系人。**必须提供带有国家代码的电话号码**——仅有电子邮件是不够的。其他所有字段均为可选。

您可以选择通过 `listId`（单个列表）或 `listIds`（数组）将新联系人直接放入一个或多个列表中。如果两者同时发送，则 `listIds` 优先。

您发送的任何字段，只要不是下表 **创建联系人** 字段（`phoneNumber`、`firstName`、`lastName`、`email`、`channel`、`is_bot_active`、`is_private`、`lead_profile`、`listId`、`listIds`、`custom_fields`）中的标准字段，都会自动存储为 **自定义字段** —— 因此，来自 Make 或 Zapier 等工具的扁平化有效载荷无需嵌套即可正常工作。您也可以显式传递一个 `custom_fields` 对象。

| 字段 | 必填 | 描述 |
|---|---|---|
| `phoneNumber` | 是 | 联系人的电话号码，包含国家代码（例如 `+15551234567`）。 |
| `firstName` | 否 | 名字。 |
| `lastName` | 否 | 姓氏。 |
| `email` | 否 | 电子邮件地址。 |
| `channel` | 否 | 消息渠道。可选 `whatsapp`、`sms`、`whatsapp_web` 之一。默认为 `whatsapp`。 |
| `is_bot_active` | 否 | AI 助手是否回复此联系人。默认为 `true`。 |
| `is_private` | 否 | 将联系人标记为私有。当为 `true` 时，AI 助手将为他们关闭。默认为 `false`。 |
| `lead_profile` | 否 | 关于潜在客户的自由文本备注。 |
| `listId` | 否 | 要将联系人添加到的单个列表 ID。 |
| `listIds` | 否 | 要将联系人添加到的列表 ID 数组（优先级高于 `listId`）。 |
| `custom_fields` | 否 | 您自己的键/值字段对象。您也可以将这些作为顶级键传递。 |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumber": "+15551234567",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "is_bot_active": true,
    "listIds": ["list123", "list456"]
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/contacts", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phoneNumber: "+15551234567",
    firstName: "Jane",
    lastName: "Smith",
    email: "jane@example.com",
    is_bot_active: true,
    listIds: ["list123", "list456"],
  }),
});
const data = await res.json();
console.log(data.data.contactId);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "phoneNumber": "+15551234567",
        "firstName": "Jane",
        "lastName": "Smith",
        "email": "jane@example.com",
        "is_bot_active": True,
        "listIds": ["list123", "list456"],
    },
)
print(res.json()["data"]["contactId"])
```

**响应**

```json
{
  "success": true,
  "data": {
    "message": "Successfully created new contact",
    "contactId": "contact_abc123",
    "listsAdded": ["list123", "list456"]
  }
}
```

新联系人的 ID 位于 `data.contactId`。它被添加到的列表会在 `data.listsAdded` 中返回。

> **不会创建重复项。** 如果具有相同电话号码的联系人已存在，创建调用将**不会**创建或返回该联系人。响应将返回 HTTP 状态 `200`，并在正文中包含 `error_code` 为 `409` 的内容，因此请根据 `error_code` 进行分支判断，而不是根据 HTTP 状态：
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> 若要在 `error_code` 为 `409` 后处理现有联系人，请使用 [通过电话或电子邮件获取联系人](#get-a-contact-by-phone-or-email) 进行查找 —— `GET /contacts?phoneNumber=...` —— 并复用其返回的 ID。

> **等效的 WhatsApp 拼写方式被视为同一个号码。** 某些国家/地区对于同一个移动线路有两种有效的拼写方式，而 WhatsApp 可能会报告其中任何一种：墨西哥（`+52…` 和旧版 `+521…`）、巴西（带或不带第九位数字）以及阿根廷（在 `+54` 之后带或不带 `9`）。创建时的重复检查和 `GET /contacts?phoneNumber=` 会在两种拼写方式之间进行匹配，因此无论您发送哪种格式，都会返回现有的联系人。存储在联系人上的 `phone_number` 永远不会被重写。

---

## 通过电话或电子邮件获取联系人

`GET /contacts?phoneNumber=...` 或 `GET /contacts?email=...`

查找单个联系人并返回完整的、富化的联系人对象——包括解析为 `{ id, name }` 对的列表、标签和营销活动，以及最后交换的消息。

传入 `phoneNumber`（国际格式）**或** `email`。如果您两者都不传，此端点将切换为[列出联系人](#list-contacts)模式。

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?phoneNumber=%2B15551234567&apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const phone = encodeURIComponent("+15551234567");
const res = await fetch(`https://api.youraiconnector.com/v1/contacts?phoneNumber=${phone}`, {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.contact);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"phoneNumber": "+15551234567"},
)
print(res.json()["contact"])
```

**响应**

```json
{
  "success": true,
  "contactId": "contact_abc123",
  "contact": {
    "id": "contact_abc123",
    "firstName": "Jane",
    "lastName": "Smith",
    "email": "jane@example.com",
    "phoneNumber": "+15551234567",
    "channel": "whatsapp",
    "isBotActive": true,
    "isPrivate": false,
    "doNotDisturb": false,
    "lead_profile": null,
    "avatarUrl": "https://example.com/photo.jpg",
    "customFields": {},
    "lists": [{ "id": "list123", "name": "VIP customers" }],
    "tags": [{ "id": "tagHotLead", "name": "Hot lead" }],
    "campaigns": [{ "id": "campaign789", "name": "Spring promo" }],
    "currentCampaign": { "id": "campaign789", "name": "Spring promo" },
    "lastMessage": {
      "direction": "inbound",
      "body": "Sounds good, thanks!",
      "status": "received",
      "timestamp": "2026-06-09T10:21:00.000Z"
    }
  }
}
```

联系人 ID 会在顶层 (`contactId`) 和对象内部 (`contact.id`) 同时返回。如果没有匹配项，您将收到一个带有 `{ "success": false, "message": "Contact not found" }` 的 `404`。

> **`avatarUrl`** 是联系人的个人资料照片，取自 WhatsApp 或 Meta（当他们给您发消息时）。它是只读的：您无法设置它，并且对于没有照片或通过不共享照片的渠道联系您的联系人，该字段为 `null`。请将此链接视为临时的，不要存储它，因为其中一些照片链接会自动过期并刷新。（在下方的列表端点中，相同的值被称为 `avatar_url`。）

> **URL 中的电话号码。** 查询字符串中的 `+` 符号必须进行 URL 编码为 `%2B`，否则它会被读取为空格。上面的示例已为您处理了这一点。

---

## 通过 ID 获取联系人

`GET /contacts/{contactId}`

当您已经拥有联系人的 ID 时，可以直接获取它。响应格式与上面的查找相同。

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.contact);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["contact"])
```

如果联系人 ID 在您的账户中不存在，则会返回 `404`。

---

## 获取联系人统计信息

`GET /contacts/{contactId}/stats`

返回单个联系人的汇总消息统计信息：总数、AI 与人工回复对比、已消耗额度以及首次/最后一次消息的时间戳。

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/stats?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/stats", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.totalMessages, data.creditsUsed);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/stats",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
print(data["totalMessages"], data["creditsUsed"])
```

**响应**

```json
{
  "success": true,
  "totalMessages": 48,
  "sent": 21,
  "received": 27,
  "aiReplies": 18,
  "humanReplies": 3,
  "creditsUsed": 34,
  "botMessageCount": 18,
  "firstMessageAt": "2026-05-01T09:00:00.000Z",
  "lastMessageAt": "2026-06-09T10:21:00.000Z"
}
```

`botMessageCount` 是应用内联系人“重置”按钮所清零的同一个 AI 消息计数器。`creditsUsed` 是该联系人的累计额度总数，而非仅本次响应的数值。如果您的账户中不存在该联系人 ID，则会返回 `404`。

---

## 列出联系人

`GET /contacts`

调用 `GET /contacts` 时**不带** `phoneNumber` 和 `email`，即可翻阅您的所有联系人，按最新顺序排列。每一页都会返回精简的联系人摘要（列表、标签和营销活动将以 ID 数组而非完整对象的形式返回）以及一个 `next_cursor`。

| 查询参数 | 描述 |
|---|---|
| `limit` | 每页条数。默认为 50，最大为 100。 |
| `cursor` | 上一页的 `next_cursor` 值。第一页请省略此项。 |
| `listId` | 可选。仅返回属于此列表的联系人。 |

要遍历每一页：首先在不带游标的情况下进行第一次调用，然后持续将返回的 `next_cursor` 作为 `cursor` 传回。**当 `next_cursor` 为 `null` 时停止**——这意味着没有更多结果了。

**cURL**

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?limit=50&apiKey=YOUR_API_KEY"

# next page:
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?limit=50&cursor=contact_abc123&apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
async function listAllContacts() {
  const all = [];
  let cursor = null;
  do {
    const url = new URL("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, { headers: { "X-API-Key": "YOUR_API_KEY" } });
    const data = await res.json();
    all.push(...data.contacts);
    cursor = data.next_cursor;
  } while (cursor);
  return all;
}
```

**Python**

```python
import requests

def list_all_contacts():
    all_contacts = []
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        res = requests.get(
            "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts",
            headers={"X-API-Key": "YOUR_API_KEY"},
            params=params,
        )
        data = res.json()
        all_contacts.extend(data["contacts"])
        cursor = data["next_cursor"]
        if not cursor:
            break
    return all_contacts
```

**响应**

```json
{
  "success": true,
  "contacts": [
    {
      "id": "contact_abc123",
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "jane@example.com",
      "phone_number": "+15551234567",
      "channel": "whatsapp",
      "is_bot_active": true,
      "is_private": false,
      "do_not_disturb": false,
      "avatar_url": "https://example.com/photo.jpg",
      "custom_fields": {},
      "created_at": "2026-06-01T09:00:00.000Z",
      "list_ids": ["list123"],
      "tag_ids": ["tagHotLead"],
      "campaign_ids": ["campaign789"],
      "current_campaign_id": "campaign789"
    }
  ],
  "next_cursor": "contact_abc123"
}
```

::: note
**注意：** 按账户中不存在的 `listId` 进行筛选会返回 `404`。无效的 `cursor` 会返回 `400`。
:::


---

## 统计联系人

`GET /contacts/count`

返回符合过滤条件的联系人数量，以及按渠道划分的统计结果，无需进行分页。这是处理任何“有多少”问题的正确调用方式——无论是仪表板磁贴、自动化流程，还是向 Champ 提问。所有过滤器均为可选，组合使用多个过滤器会缩小统计范围（联系人必须匹配您发送的每一个条件）。

| 查询参数 | 描述 |
|---|---|
| `agentId` | 仅限分配给此 AI 代理的联系人。传入 `none` 可获取未分配代理的联系人（这些联系人由渠道的默认代理回复）。 |
| `channel` | 仅限此渠道上的联系人，例如 `whatsapp`、`messenger`、`instagram`、`sms`、`email`、`chat_widget`。 |
| `tag` | 仅限带有此标签的联系人，按标签**名称**匹配（不区分大小写）。如果标签名称不存在，则返回 `404`。 |
| `listId` | 仅限此列表中的联系人。 |
| `botActive` | `true` 或 `false` —— 仅限 AI 助手处于开启或关闭状态的联系人。 |
| `status` | 仅限具有此状态的联系人，例如 `Lead`。 |
| `rules` | 一个 URL 编码的 JSON 规则对象，使用与智能列表相同的格式（请参阅下文的 [The `smart_rules` shape](#the-smart_rules-shape)）。不能与其他过滤器组合使用。 |

如果不发送任何过滤器，您将获得账户中联系人的总数。

**cURL**

```bash
# everything
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count?apiKey=YOUR_API_KEY"

# only the contacts one agent handles on Messenger
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count?agentId=agent_xyz789&channel=messenger&apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count");
url.searchParams.set("agentId", "agent_xyz789");
url.searchParams.set("channel", "messenger");

const res = await fetch(url, { headers: { "X-API-Key": "YOUR_API_KEY" } });
const data = await res.json();
console.log(data.total);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/count",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"agentId": "agent_xyz789", "channel": "messenger"},
)
data = res.json()
print(data["total"])
```

**响应**

```json
{
  "success": true,
  "total": 3423,
  "by_channel": { "messenger": 2744, "instagram": 667, "none": 12 },
  "filters": { "agentId": "agent_xyz789" }
}
```

`by_channel` 会按渠道拆分相同的总数；未在任何渠道上的联系人将计入 `none`。 `filters` 会回显所应用的过滤器，以便您检查调用是否达到了预期效果。

::: note
**注意：** 将 `rules` 与任何其他过滤器一起发送，或发送非有效 JSON 的 `rules` 值，将返回 `400`。如果账户中不存在某个标签名称或列表 ID，则返回 `404`。
:::


---

## 更新联系人

`PUT /contacts/{contactId}`

更新现有联系人。仅更改您包含的字段——不想改动的字段请留空。您必须至少发送一个字段，否则会收到 `400`（“没有要更新的字段”）。

| 字段 | 描述 |
|---|---|
| `firstName` | 名字。 |
| `lastName` | 姓氏。 |
| `email` | 电子邮件地址。 |
| `is_bot_active` | AI 助手是否回复此联系人。 |
| `is_private` | 标记为私有。将其设置为 `true` 也会关闭 AI 助手。 |
| `do_not_disturb` | 暂停对此联系人的自动外联。同时也会停止 AI 回复。 |
| `follow_ups_disabled` | 停止此联系人的所有自动跟进（快速、周期和冷线索），同时 AI 继续回复他们发送的消息。在某人购买后非常有用。保持关闭状态，直到您将其改回 `false`。 |
| `lead_profile` | 自由文本线索备注。 |
| `custom_fields` | 自定义字段对象。**按键合并** — 仅写入您发送的键，其余现有自定义字段将保留。您也可以在顶层传递自定义字段键。 |

**cURL**

```bash
curl -X PUT "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Jane", "do_not_disturb": true }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  method: "PUT",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ firstName: "Jane", do_not_disturb: true }),
});
const data = await res.json();
console.log(data.message);
```

**Python**

```python
import requests

res = requests.put(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"firstName": "Jane", "do_not_disturb": True},
)
print(res.json()["message"])
```

**响应**

```json
{
  "success": true,
  "message": "Contact updated successfully"
}
```

> **自定义字段是合并而非替换。** 发送 `{ "custom_fields": { "tier": "gold" } }` 仅会设置 `tier`——联系人上的任何其他自定义字段将保持原样。若要从所有联系人中彻底删除某个自定义字段，请使用 [删除自定义字段](#delete-a-custom-field)。

---

## 添加或移除标签

`POST /contacts/{contactId}/tags`

在一次调用中为单个联系人添加和/或移除标签。请在 `addTagIds` 和 `removeTagIds` 中传递标签 **ID**。两者中至少有一个必须非空。

这些标签必须已存在于您的账户中 — 请先通过 [标签端点](reference.md) 创建它们。如果联系人或任何引用的标签不存在，您将收到 `404`。

| 字段 | 描述 |
|---|---|
| `addTagIds` | 要添加到联系人的标签 ID 数组。 |
| `removeTagIds` | 要从联系人中移除的标签 ID 数组。 |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/tags?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "addTagIds": ["tagHotLead"], "removeTagIds": ["tagColdLead"] }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/tags", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    addTagIds: ["tagHotLead"],
    removeTagIds: ["tagColdLead"],
  }),
});
const data = await res.json();
console.log(data.added, data.removed);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/tags",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"addTagIds": ["tagHotLead"], "removeTagIds": ["tagColdLead"]},
)
data = res.json()
print(data["added"], data["removed"])
```

**响应**

```json
{
  "success": true,
  "contact_id": "contact_abc123",
  "added": 1,
  "removed": 1
}
```

---

## 管理您的标签库

这些端点用于管理标签本身（即在您的账户中重命名或删除标签），而不是为单个联系人添加或移除标签（请参阅上方的 [添加或移除标签](#add-or-remove-tags)）。您账户中的每个标签都有一个 ID (`tagId`)：即仪表板标签管理器中显示的 ID，也是您使用 `POST /tags` 并发送 JSON 正文 `{ "name": "..." }`（不包含 `phoneNumber`、`email` 或 `contactId`）创建标签时返回的 `data.tag_id`。

### 更新标签

`PUT /tags/{tagId}`

仅发送您需要更改的字段。

| 字段 | 描述 |
|---|---|
| `name` | 标签名称。 |

```bash
curl -X PUT "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/tags/tagHotLead?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Hot lead (Q3)" }'
```

**响应**

```json
{ "success": true, "tag_id": "tagHotLead" }
```

如果您的账户中不存在 `tagId`，则会返回 `404`。

### 删除标签

`DELETE /tags/{tagId}`

按 ID 删除一个标签。**此操作无法撤销**——带有该标签的联系人将直接失去此标签。删除一个已不存在（或从未存在过）的标签会返回 `200` 和 `deleted: 0`，而不是 `404`，因为没有内容可供枚举。

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/tags/tagColdLead?apiKey=YOUR_API_KEY"
```

**响应**

```json
{ "success": true, "deleted": 1 }
```

### 同时删除多个标签

`DELETE /tags`

| 字段 | 描述 |
|---|---|
| `tagIds` | 要删除的标签 ID 数组（最多 1000 个）。 |

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/tags?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tagIds": ["tagColdLead", "tagUnsubscribed"] }'
```

**响应**

```json
{ "success": true, "deleted": 2 }
```

不存在或属于其他账户的 ID 将被静默跳过，且不会计入 `deleted`。

---

## 批量设置标志

`POST /contacts/bulk-flag`

一次性为多个联系人设置一个布尔标志。每个请求最多支持 500 个联系人 ID。账户中不存在的 ID 将被跳过，并计入 `skipped`。

| 字段 | 描述 |
|---|---|
| `contactIds` | 要更新的联系人 ID 数组（最多 500 个）。 |
| `field` | 要设置的标志。可选值：`bot_active`（AI 助手开启/关闭）、`dnd`（暂停自动外呼）、`spam`、`private`。 |
| `value` | 要设置的布尔值。 |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-flag?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactIds": ["contactId1", "contactId2"],
    "field": "bot_active",
    "value": false
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-flag", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contactIds: ["contactId1", "contactId2"],
    field: "bot_active",
    value: false,
  }),
});
const data = await res.json();
console.log(data.updated, data.skipped);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-flag",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "contactIds": ["contactId1", "contactId2"],
        "field": "bot_active",
        "value": False,
    },
)
data = res.json()
print(data["updated"], data["skipped"])
```

**响应**

```json
{
  "success": true,
  "updated": 2,
  "skipped": 0
}
```

---

## 批量导入联系人

`POST /contacts/import`

通过一个 JSON 数组在单次调用中最多创建 500 个联系人。每条记录都需要一个国际格式的 `phone_number`；其他所有字段均为可选。电话号码无效或渠道不受支持的记录将被**跳过**（不创建），且每条被跳过的记录都会报告其索引和原因——这样您只需修复失败的记录并重试即可。

默认情况下，账户中已存在的电话号码会作为 `duplicate` 被跳过。发送 `updateExisting: true` 可**更新**这些联系人：记录中存在的字段将覆盖联系人的原有信息（`first_name`、`last_name`、`email`、`lead_profile` 和 `custom_fields` 会按键合并），`tags` 会被添加，且联系人会被加入到 `listId` 中。现有联系人的渠道、电话号码和机器人标志永远不会被更改。

您可以选择将每个导入（或更新）的联系人通过 `listId` 添加到列表中，为未指定渠道的记录设置 `defaultChannel`，并使用 `tags` 为记录添加标签（标签名称——缺失的标签会被创建，现有的标签匹配时不区分大小写）。

**顶级字段**

| 字段 | 必填 | 说明 |
|---|---|---|
| `contacts` | 是 | 联系人记录数组（最多 500 个）。 |
| `listId` | 否 | 将每个导入（及更新）的联系人添加到的列表。必须是您账户中已有的列表。 |
| `defaultChannel` | 否 | 应用于省略了 `channel` 的记录的渠道。可选 `whatsapp`、`sms`、`whatsapp_web` 之一。默认为 `whatsapp`。 |
| `updateExisting` | 否 | `true` 用于更新电话号码已存在的联系人，而不是将其作为 `duplicate` 跳过。默认为 `false`。 |

**单条记录字段**

| 字段 | 必填 | 说明 |
|---|---|---|
| `phone_number` | 是 | 国际格式的电话号码（如果缺少，会自动添加前导 `+`）。 |
| `first_name` | 否 | 名字。 |
| `last_name` | 否 | 姓氏。 |
| `email` | 否 | 电子邮件地址。 |
| `channel` | 否 | 可选 `whatsapp`、`sms`、`whatsapp_web` 之一。回退至 `defaultChannel`。 |
| `is_bot_active` | 否 | AI 助手是否回复。默认为 `true`。 |
| `is_private` | 否 | 标记为私有。默认为 `false`。 |
| `lead_profile` | 否 | 自由文本形式的潜在客户备注。 |
| `custom_fields` | 否 | 自定义字段键值对对象。 |
| `tags` | 否 | 标签名称数组（单个 `"a; b"` 字符串也适用）。不存在的标签会被创建；现有的标签匹配时不区分大小写。每条记录最多 25 个。 |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "phone_number": "+12025551234", "first_name": "Ann", "last_name": "Lee", "tags": ["vip", "newsletter"] },
      { "phone_number": "+12025551235", "first_name": "Bob" }
    ],
    "listId": "list123",
    "defaultChannel": "whatsapp_web",
    "updateExisting": true
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contacts: [
      { phone_number: "+12025551234", first_name: "Ann", last_name: "Lee", tags: ["vip", "newsletter"] },
      { phone_number: "+12025551235", first_name: "Bob" },
    ],
    listId: "list123",
    defaultChannel: "whatsapp_web",
    updateExisting: true,
  }),
});
const data = await res.json();
console.log(`Imported ${data.imported}, updated ${data.updated}, skipped ${data.skipped.length}`);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "contacts": [
            {"phone_number": "+12025551234", "first_name": "Ann", "last_name": "Lee", "tags": ["vip", "newsletter"]},
            {"phone_number": "+12025551235", "first_name": "Bob"},
        ],
        "listId": "list123",
        "defaultChannel": "whatsapp_web",
        "updateExisting": True,
    },
)
data = res.json()
print(f"Imported {data['imported']}, updated {data['updated']}, skipped {len(data['skipped'])}")
```

**响应**

```json
{
  "success": true,
  "imported": 2,
  "contact_ids": ["contact_abc123", "contact_def456"],
  "updated": 0,
  "updated_contact_ids": [],
  "skipped": []
}
```

如果某些记录无法创建，它们会出现在 `skipped` 中并附带原因（此处未设置 `updateExisting`，因此现有的号码被跳过）：

```json
{
  "success": true,
  "imported": 1,
  "contact_ids": ["contact_abc123"],
  "updated": 0,
  "updated_contact_ids": [],
  "skipped": [
    { "index": 1, "phone_number": "+12025551235", "reason": "duplicate" }
  ]
}
```

使用 `updateExisting: true` 时，相同的请求会将现有的联系人报告在 `updated` / `updated_contact_ids` 下。

可能的跳过原因：`invalid_record`、`missing_phone_number`、`invalid_phone_number`、`invalid_channel`、`duplicate_in_request`、`duplicate`、`contact_limit_reached`、`create_failed`。

> **套餐限制。** 如果您套餐的联系人上限不允许新增这么多联系人，整个请求会预先被拒绝并返回 `403`。如果是在处理过程中达到上限，剩余的记录将作为跳过项返回，原因为 `contact_limit_reached`。

---

## 从 CSV 文件导入联系人

对于超过 [批量导入](#bulk-import-contacts) 支持规模（最多约 50,000 行）的导入任务，请针对已存储在您账户中的 CSV 文件排队执行异步导入作业，然后轮询该作业直至完成。

### 开始导入

`POST /contacts/import-csv`

| 字段 | 必填 | 描述 |
|---|---|---|
| `csvStoragePath` | 是 | CSV 文件的存储路径，位于 `users/{your account id}/imports/` 下，以 `.csv` 结尾。 |
| `listName` | 是 | 创建（或复用）一个具有此名称的列表，并将每个导入的联系人添加到其中。 |
| `existingListRefs` | 否 | 同时将每个导入的联系人添加到其中的现有列表 ID 数组。 |
| `defaultChannel` | 否 | 应用于未指定渠道的行的渠道。 |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "csvStoragePath": "users/abc123/imports/leads.csv",
    "listName": "Webinar signups"
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    csvStoragePath: "users/abc123/imports/leads.csv",
    listName: "Webinar signups",
  }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "csvStoragePath": "users/abc123/imports/leads.csv",
        "listName": "Webinar signups",
    },
)
job_id = res.json()["job_id"]
```

**响应** (`202` — 导入已排队，尚未完成)

```json
{
  "success": true,
  "job_id": "csvimp_abc123",
  "status": "queued"
}
```

> **将文件存入存储。** 此端点用于启动和跟踪导入作业；它本身不接受上传。在调用此端点之前，CSV 文件必须已位于 `csvStoragePath` — 控制台自带的 CSV 导入器会将此作为第一步操作。

### 轮询导入任务

`GET /contacts/import-csv/{jobId}`

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv/csvimp_abc123?apiKey=YOUR_API_KEY"
```

**响应**

```json
{
  "success": true,
  "job_id": "csvimp_abc123",
  "status": "completed",
  "imported": 812,
  "updated": 0,
  "skipped": 14,
  "errors": [],
  "error_message": null
}
```

`status` 会经历 `queued` → `processing` → `completed`，或者以 `error_message` 中的原因进入 `failed` 状态。如果账户中不存在 `jobId`，则返回 `404`。

---

## 导出联系人

启动联系人的异步 CSV 导出任务，并返回一个可供轮询完成状态的作业。

### 开始导出

`POST /contacts/export`

| 字段 | 必填 | 说明 |
|---|---|---|
| `listId` | 否 | 仅导出属于此列表的联系人。 |
| `contactIds` | 否 | 仅导出这些特定的联系人 ID。 |

如果两者都不填，则导出您账户中的所有联系人。

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "listId": "list123" }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ listId: "list123" }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"listId": "list123"},
)
job_id = res.json()["job_id"]
```

**响应** (`202` — 导出任务已加入队列)

```json
{
  "success": true,
  "job_id": "export_abc123",
  "status": "queued"
}
```

### 轮询导出任务

`GET /contacts/export/{jobId}`

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/export/export_abc123?apiKey=YOUR_API_KEY"
```

**响应**

```json
{
  "success": true,
  "job_id": "export_abc123",
  "status": "completed",
  "export_id": "exp_xyz789",
  "contact_count": 812,
  "error_message": null
}
```

> 一旦 `status` 变为 `"completed"`，您将收到 `export_id` 和 `contact_count`。生成的 CSV 文件可在您仪表板的“导出”页面下载。

---

## 向联系人发送消息

`POST /contacts/{contactId}/send-message`

向现有联系人发送消息，发送渠道为其当前所在的渠道。消息会被加入队列并在后台发送——响应仅确认消息已被接收，并不代表消息已送达。

| 字段 | 必填 | 说明 |
|---|---|---|
| `body` | 是 | 要发送的消息文本。 |
| `mediaUrl` | 否 | 要附加的媒体文件的 URL。 |
| `mediaContentType` | 否 | 附加媒体的 MIME 类型（例如 `image/jpeg`）。 |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/send-message?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Hi! Your appointment is confirmed." }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/send-message", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ body: "Hi! Your appointment is confirmed." }),
});
const data = await res.json();
console.log(data.messageId);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/send-message",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"body": "Hi! Your appointment is confirmed."},
)
print(res.json()["messageId"])
```

**响应**

```json
{
  "success": true,
  "messageId": "aB3dE5fG7hI9jK1lM2nO",
  "contactId": "contact_abc123",
  "channel": "whatsapp",
  "message": "Message created successfully. Delivery is being processed."
}
```

> **无法立即发送？** 如果联系人启用了“请勿打扰”或“私有模式”，或者不在支持接收外发消息的渠道上，请求会被拒绝并返回 `422` 以及解释性 `error`。

如需通过电话号码、Instagram ID 或其他渠道标识（而非联系人 ID）进行发送，以及了解更多关于消息传递的信息，请参阅 [消息 API](messages.md)。

---

## 将 AI 代理分配给联系人

`POST /contacts/{contactId}/assign-agent`

将现有对话从下一条消息开始转移给不同的 AI 代理。这与聊天菜单中的 **分配 AI 代理** 功能相同，也是自动化流程中 **分配 AI 代理或营销活动** 操作所使用的步骤。

| 字段 | 必填 | 描述 |
|---|---|---|
| `agentId` | 是 | 应接管对话的 AI 代理 ID，或使用 `null` 清除分配，使对话返回到您的团队收件箱。 |
| `triggerAIResponse` | 否 | `true` 会让新分配的代理立即回复联系人最新未答复的消息。默认为 `false`。 |

> **使用 `triggerAIResponse: true` 时请小心** —— 它会立即向联系人发送消息，因此仅在您希望立即联系他们时使用。在 Messenger 和 Instagram 上，如果联系人上次给您发消息是在 24 小时前，则该消息会发送失败。

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/assign-agent?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "agent_xyz789" }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/assign-agent", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ agentId: "agent_xyz789" }),
});
const data = await res.json();
console.log(data.data.agentId);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/assign-agent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"agentId": "agent_xyz789"},
)
print(res.json()["data"]["agentId"])
```

**响应**

```json
{
  "success": true,
  "data": {
    "contactId": "contact_abc123",
    "agentId": "agent_xyz789",
    "aiResponseTriggered": false
  }
}
```

> 该代理必须与联系人属于同一个账户；否则请求将被拒绝，并返回 `404` 或 `403`。您可以在“AI 代理”页面找到代理 ID（每个代理的 URL 末尾即为其 ID）。

---

## 为多个联系人分配 AI 代理

`POST /contacts/bulk-assign-agent`

通过一次调用将多个会话移动到不同的 AI 代理，或者使用 `null` 清除所有会话的分配。这纯粹是路由更改：**不会发送任何消息，代理也不会回复任何人**。每个联系人只需在下次发送消息时获得新代理即可。（这就是为什么这里没有 `triggerAIResponse` 的原因。）

| 字段 | 必填 | 描述 |
|---|---|---|
| `agentId` | 是 | 应接管的 AI 代理，或使用 `null` 清除分配。 |
| `contactIds` | 三选一 | 最多 500 个要移动的联系人 ID。 |
| `filter` | 三选一 | 在服务器上选择联系人而不是列出它们，按最新时间排序。使用与计数端点过滤器相同的键：`agentId`（或 `none`）、`channel`、`tag`、`listId`、`botActive`、`status`。 |
| `rules` | 三选一 | 智能列表规则对象 —— 请参阅 [The `smart_rules` shape](#the-smart_rules-shape)。 |
| `limit` | 否 | 当您使用 `filter` 或 `rules` 进行选择时，本次调用要移动的联系人数量。范围为 1 到 500，默认为 500。 |

发送 `contactIds`、`filter` 或 `rules` 中的恰好一个。

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "agent_xyz789",
    "filter": { "agentId": "agent_abc123", "channel": "messenger" }
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agentId: "agent_xyz789",
    filter: { agentId: "agent_abc123", channel: "messenger" },
  }),
});
const data = await res.json();
console.log(data.updated, data.remaining);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "agentId": "agent_xyz789",
        "filter": {"agentId": "agent_abc123", "channel": "messenger"},
    },
)
data = res.json()
print(data["updated"], data["remaining"])
```

**响应**

```json
{
  "success": true,
  "agentId": "agent_xyz789",
  "matched": 3415,
  "updated": 500,
  "skipped": 0,
  "remaining": 2915,
  "filters": { "agentId": "agent_abc123" }
}
```

`matched` 是选择找到的联系人总数，`updated` 是本次调用移动的联系人数量，`skipped` 是您发送的 ID 中在账户中未找到的数量，`remaining` 是调用完成后仍匹配条件的联系人数量。

**移动所有人。** 由于一次调用最多移动 500 个联系人，因此处理大型群体需要多次调用。使用一个在联系人移动后即停止匹配的过滤器（例如在分配给 `agent_xyz789` 时使用 `filter: { "agentId": "agent_abc123" }`），并重复执行完全相同的调用，直到 `remaining` 返回 `0`。当您改为传递 `contactIds` 时，`remaining` 始终为 `0`。

---

## 将联系人分配给部门

`POST /contacts/{contactId}/department`

“将此潜在客户分配给销售部” — 将联系人归档到指定的部门下，并默认将其分配给该部门当前联系人最少的人员。这与[分配 AI 代理](#assign-an-ai-agent-to-a-contact)是分开的：部门回答的是“哪个团队拥有此联系人”，而代理回答的是“哪个 AI 处理此联系人”，设置其中一个永远不会清除另一个。

| 字段 | 必填 | 说明 |
|---|---|---|
| `department_id` | 是 | 要将联系人归档到的部门。传入 `null` 可清除该设置。 |
| `hand_to_member` | 否 | 同时将联系人分配给该部门中负载最轻的人员。默认为 `true`。不会重新分配已有负责人拥有的联系人。 |

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/department?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "department_id": "dept_sales" }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/department", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ department_id: "dept_sales" }),
});
const data = await res.json();
console.log(data.assigned_to);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/department",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"department_id": "dept_sales"},
)
print(res.json()["assigned_to"])
```

**响应**

```json
{
  "success": true,
  "department_id": "dept_sales",
  "assigned_to": "member_uid_123"
}
```

当联系人已有负责人，或者您传入了 `hand_to_member: false` 时，`assigned_to` 为 `null`。

---

## 跨渠道关联联系人

“在 WhatsApp 上继续”（或短信）会查找或创建此人在另一个基于电话的渠道上的联系人，并将两者关联起来，以便应用程序的其他部分将其识别为同一个人。

### 链接到另一个渠道

`POST /contacts/{contactId}/link-channel`

| 字段 | 必填 | 描述 |
|---|---|---|
| `channel` | 是 | 要链接到的渠道。可以是 `whatsapp`、`whatsapp_web` 或 `sms` 之一。 |
| `phoneNumber` | 否 | 在新渠道上使用的电话号码。默认为源联系人自己的号码。 |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/link-channel?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "sms" }'
```

**响应**

```json
{
  "success": true,
  "data": {
    "contact_id": "contact_def456",
    "person_id": "person_xyz789",
    "created": true
  }
}
```

`created` 会告知您是为目标渠道铸造了新联系人，还是找到了现有的联系人并进行了关联。第二次调用此方法是安全的——它会返回相同的 `contact_id` 以及 `created: false`，而不会创建重复项。

`422` 表示账户目前无法执行此链接操作：联系人已在该渠道系列中、没有可用的电话号码，或者目标渠道没有已连接的发送方。`409` 表示这两个联系人已经链接到了两个不同的人——请先解除其中一个的链接。

### 列出联系人的已链接对话

`GET /contacts/{contactId}/linked`

返回与此联系人为同一人的其他对话。未链接的联系人会返回一个空数组，而不是 `404`——“此人没有其他渠道”是一种正常状态。

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/linked?apiKey=YOUR_API_KEY"
```

**响应**

```json
{
  "success": true,
  "data": [
    {
      "contact_id": "contact_def456",
      "channel": "sms",
      "custom_channel": null,
      "first_name": "Jane",
      "last_name": "Smith",
      "phone_number": "+15551234567",
      "last_message": "Sounds good, thanks!",
      "last_message_timestamp": "2026-06-09T10:21:00.000Z",
      "linked_from": {
        "contact_id": "contact_abc123",
        "channel": "whatsapp",
        "linked_at": "2026-06-01T09:00:00.000Z",
        "reason": "continue_on_channel"
      }
    }
  ]
}
```

### 解除联系人链接

`DELETE /contacts/{contactId}/link`

单方面将此联系人从其所属的人员组中移除——仍链接到该人员的其他联系人将保留其链接，因此解除三个联系人中的一个并不会解散该组。

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/link?apiKey=YOUR_API_KEY"
```

**响应**

```json
{ "success": true }
```

---

## 获取联系人的头像

`POST /contacts/{contactId}/profile-pic`

按需获取（并缓存）联系人的 WhatsApp 或 Meta 头像——与 [获取联系人](#get-a-contact-by-phone-or-email) 中返回的 `avatarUrl` 相同，但会进行刷新。

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123/profile-pic?apiKey=YOUR_API_KEY"
```

**响应**

```json
{
  "success": true,
  "avatar_url": "https://example.com/photo.jpg",
  "cached": false
}
```

`cached: true` 表示该 URL 来自最近的获取记录，而不是直接从提供商处查询——图片会缓存 7 天，如果提供商报告联系人没有可获取的头像，则会将其缓存为“不可用”状态，有效期为 24 小时。当没有图片可获取时，`avatar_url` 会被省略，并由 `message` 说明原因。

---

## 使用 AI 自动标记联系人

对一个或多个联系人的完整对话历史记录运行您账户的标签规则，并像实时聊天期间运行的实时标记一样应用（或移除）标签——规则相同，每个标签的积分成本也相同。

### 开始运行

`POST /contacts/auto-tag`

| 字段 | 必填 | 说明 |
|---|---|---|
| `scope` | 是 | 使用 `"contacts"` 标记特定联系人，或使用 `"agent"` 标记当前由一个 AI 代理处理的所有对话。 |
| `contact_ids` | 当 `scope` 为 `"contacts"` 时必填 | 联系人 ID 数组，1 到 500 个。 |
| `agent_id` | 当 `scope` 为 `"agent"` 时必填 | 要标记其对话的 AI 代理。当 `scope` 为 `"contacts"` 时，此项可选，仅用于缩小运行该代理的哪些标签规则的范围。 |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/auto-tag?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "contacts", "contact_ids": ["contact_abc123", "contact_def456"] }'
```

**单个**联系人会内联运行并立即返回结果：

```json
{ "success": true, "result": { "tags_applied": 2, "tags_removed": 0 } }
```

**两个或更多**联系人（或 `scope: "agent"`）将作为后台作业运行，并立即返回 `202`：

```json
{ "success": true, "run_id": "m1x2y3-a1b2c3d4", "total": 214 }
```

### 轮询运行状态

`GET /contacts/auto-tag/run`

返回账户当前（或最近一次）的运行状态，以便您可以轮询进度，而无需自行跟踪 `run_id`。

```bash
curl "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/auto-tag/run?apiKey=YOUR_API_KEY"
```

**响应**

```json
{
  "success": true,
  "run": {
    "run_id": "m1x2y3-a1b2c3d4",
    "status": "running",
    "total": 214,
    "processed": 58,
    "tagged_contacts": 12,
    "tags_applied": 15,
    "tags_removed": 2,
    "credits_charged": 15
  }
}
```

当账户从未启动过运行时，`run` 为 `null`。`status` 会从 `"running"` 变为 `"completed"` 或 `"failed"`。

每个账户一次只能进行一个批量运行——在另一个运行的同时启动第二个运行会返回 `409` 和 `error_code: "auto_tag_run_in_progress"`。在单个联系人运行中积分用尽会返回 `402` 和 `error_code: "insufficient_credits"`；而批量运行则会提前停止，并在 `run` 中报告其完成进度。

---

## 删除联系人

`DELETE /contacts/{contactId}`

通过 ID 永久删除一个联系人及其消息历史记录。**此操作无法撤销。** 若要通过单个调用删除多个联系人，请使用下方的 [删除联系人](#delete-contacts)。

**cURL**

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  method: "DELETE",
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.delete(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["success"])
```

**响应**

```json
{
  "success": true
}
```

如果联系人 ID 在您的账户中不存在，或属于其他账户，则会返回 `404`。

---

## 删除联系人

`DELETE /contacts`

通过 ID 在单次调用中永久删除一个或多个联系人（最多 500 个 ID）。账户中不存在的 ID 会被跳过并计入 `skipped`。**此操作不可撤销。**

| 字段 | 描述 |
|---|---|
| `contactIds` | 要删除的联系人 ID 数组（最多 500 个）。 |

**cURL**

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactIds": ["contactId1", "contactId2"] }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts", {
  method: "DELETE",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ contactIds: ["contactId1", "contactId2"] }),
});
const data = await res.json();
console.log(`Deleted ${data.deleted}, skipped ${data.skipped}`);
```

**Python**

```python
import requests

res = requests.delete(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"contactIds": ["contactId1", "contactId2"]},
)
data = res.json()
print(f"Deleted {data['deleted']}, skipped {data['skipped']}")
```

**响应**

```json
{
  "success": true,
  "deleted": 2,
  "skipped": 0
}
```

---

## 删除自定义字段

`DELETE /contacts/custom-fields/{fieldKey}`

从您账户中的**每个**联系人中移除一个自定义字段键。在重命名或停用自定义字段后，请使用此功能进行清理。该键只能包含字母、数字、下划线和连字符。返回已更新的联系人数量。**此操作无法撤销。**

**cURL**

```bash
curl -X DELETE "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/custom-fields/webinar_date_nh?apiKey=YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/custom-fields/webinar_date_nh", {
  method: "DELETE",
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(`Removed from ${data.updated} contacts`);
```

**Python**

```python
import requests

res = requests.delete(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/custom-fields/webinar_date_nh",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(f"Removed from {res.json()['updated']} contacts")
```

**响应**

```json
{
  "success": true,
  "updated": 42
}
```

::: note
**注意：** 包含不支持字符的字段键会返回 `400`。
:::


---

## 列表

列表用于对联系人进行分组。列表可以是**静态的**（由您决定谁在列表中）或**智能的**（成员资格根据规则计算并自动保持最新 — 请参阅 [组织列表与联系人](../get-started/list-and-contact-management.md#smart-lists-auto-updating)）。

| 字段 | 描述 |
|---|---|
| `name` | 创建时必填。最多 100 个字符。 |
| `status` | `live`（默认）或 `draft`。小写。 |
| `contact_ids` | 要放入列表的联系人 ID 数组。**仅限静态列表。** |
| `type` | `static`（默认）或 `smart`。 |
| `smart_rules` | 规则集 — 当 `type` 为 `smart` 时必填。详见下文。 |

### 创建列表

`POST /lists`

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Hot leads (active)",
        "type": "smart",
        "smart_rules": {
          "match": "all",
          "conditions": [
            { "field": "tags", "op": "has_any", "value": ["tagHotLead"] },
            { "field": "last_activity_at", "op": "within_last", "value": { "amount": 90, "unit": "days" } }
          ]
        }
      }'
```

**响应**

```json
{
  "success": true,
  "list_id": "list_abc123",
  "evaluation": { "added": 3, "removed": 0, "total": 3 }
}
```

智能列表会在同一请求中进行**内联**评估，因此 `evaluation` 会准确告诉您最终谁在列表中。对于静态列表，`evaluation` 为 `null`。

### 更新列表

`PUT /lists/{listId}`

仅发送您要更改的字段。更改 `smart_rules` 会立即重新评估列表并返回相同的 `evaluation` 对象。

```bash
curl -X PUT "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists/list_abc123?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "smart_rules": { "match": "any", "conditions": [ { "field": "tags", "op": "has_any", "value": ["tagHotLead", "tagWebinar"] } ] } }'
```

您可以在两种列表类型之间进行切换：

- **静态 → 智能**：发送 `{ "type": "smart", "smart_rules": { … } }`。规则会立即生效。
- **智能 → 静态**：发送 `{ "type": "static" }`。规则将被删除，当前列表中的成员将保持不变。

### `smart_rules` 的结构

```json
{
  "match": "all",
  "conditions": [
    { "field": "tags", "op": "has_any", "value": ["tagHotLead"] },
    { "field": "channel", "op": "is_any", "value": ["whatsapp", "sms"] },
    { "field": "last_incoming_message_at", "op": "not_within_last", "value": { "amount": 7, "unit": "days" } },
    { "field": "created_at", "op": "after", "value": "2026-01-01" },
    { "field": "is_bot_active", "op": "is", "value": true },
    { "field": "email", "op": "is_set" },
    { "field": "custom_field", "key": "Plan", "op": "eq", "value": "pro" }
  ]
}
```

- `match` — `all`（所有条件必须为真）或 `any`（至少满足一个）。
- `conditions` — 1 到 20 个条件，每个条件最多 100 个值，字符串最多 200 个字符。

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | 标签 ID 数组 |
| `lists` | `in_any`, `not_in_any` | 列表 ID 数组（**仅限静态列表** — 智能列表不能由另一个智能列表构建） |
| `channel` | `is_any`, `is_none` | 渠道数组 |
| `status` | `is_any`, `is_none` | 联系人状态数组 |
| `created_at`, `last_activity_at`, `last_incoming_message_at`, `last_outgoing_message_at`, `first_ai_interaction_at`, `last_ai_interaction_at` | `within_last`, `not_within_last` | `{ "amount": 1–3650, "unit": "hours" \| "days" }` |
| 相同日期字段 | `before`, `after` | ISO 日期（`"2026-01-01"`，按整天比较）或完整 ISO 日期时间（`"2026-01-01T14:30:00Z"`，按精确时刻比较） |
| 相同日期字段 | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` 匹配 AI 至少发送过一次消息（任何时候）的联系人 |
| `is_bot_active`, `do_not_disturb`, `is_private`, `has_ever_responded` | `is` | `true` / `false` |
| `email`, `phone_number`, `first_name`, `last_name` | `is_set`, `not_set`, `contains`, `not_contains` | 用于 `contains` 表单的字符串 |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | 用于 `is_any` / `is_none` 表单的 ID 数组 |
| `custom_field`（外加一个 `key`） | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | 用于值表单的字符串 |

`not_within_last` 也会匹配从未设置过日期的联系人（“超过 N 天前，**或从未设置**”），并且文本比较会忽略大小写。

**AI 互动。** `has_interacted_with_ai` 是生命周期标志：`true` 表示您的 AI 至少向其发送过一条消息的每位联系人，`false` 表示其他人（包括仅由您的团队回复过的联系人）。它会在 AI 向联系人发送第一条消息时打上标记，且永远不会清除，因此关闭联系人的 AI 回复或将其移动到另一个营销活动不会重置该标记。对于*周期*——即“我的 AI 本月处理的联系人”，这是常见的计费问题——请改用 `last_ai_interaction_at` 范围：

```json
{ "field": "last_ai_interaction_at", "op": "within_last", "value": { "amount": 30, "unit": "days" } }
```

不要将两者与 `is_bot_active`（AI 被*允许*回复，并不代表它已经回复）或 `has_ever_responded`（*联系人*回复了任何人）混淆。这两个相同的标记会作为 `first_ai_interaction_at` / `last_ai_interaction_at` 返回到每个联系人上，并且整套规则也适用于 `GET /contacts?rules=`，因此您无需创建列表即可计算匹配项。

### 预览规则集

`POST /lists/preview`

统计并采样规则集将匹配的联系人，而无需创建或更改任何内容。在保存规则之前，请使用此功能进行健全性检查。

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists/preview?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "smart_rules": { "match": "all", "conditions": [ { "field": "tags", "op": "has_any", "value": ["tagHotLead"] } ] } }'
```

**响应**

```json
{
  "success": true,
  "count": 3,
  "sample": [
    {
      "id": "contact_abc123",
      "first_name": "Sofia",
      "last_name": "Martinez",
      "phone_number": "+31600000000",
      "email": "sofia@example.com",
      "channel": "whatsapp"
    }
  ]
}
```

`sample` 最多可容纳 10 个联系人，按最近活跃时间排序。

### 立即重新运行智能列表

`POST /lists/{listId}/evaluate`

强制立即进行重新评估（与仪表板中的 **立即刷新** 功能相同）。智能列表在联系人发生更改时会自动更新，并且每 15 分钟会对基于时间的规则进行一次更新，因此仅在您需要“立即”获取结果时才需要此操作。

**响应**

```json
{
  "success": true,
  "list_id": "list_abc123",
  "evaluation": { "added": 2, "removed": 1, "total": 4 }
}
```

`evaluation.skipped: true` 表示该列表的另一次评估已经在运行，因此本次调用未执行任何操作。

### 智能列表拒绝手动添加的成员

当目标列表为智能列表时，成员资格端点会返回 **`409`** 以及 `"This is a smart list — its members are computed from its rules. Edit the rules instead."`。这涵盖了 `POST /contacts/lists`、`DELETE /contacts/lists`、`POST /contacts/lists/batch`、`contact_ids` 在 `POST /lists` 和 `PUT /lists/{listId}` 上的操作，以及选择智能列表作为 CSV 导入目标的情况。请改为更改规则。

在 **静态** 列表上调用 `POST /lists/{listId}/evaluate` 也是 `409` —— 因为它没有可运行的规则。

---

## 联系人 API 错误

联系人端点返回标准的错误信封：

```json
{
  "success": false,
  "error": "Contact not found"
}
```

某些端点还包含 `error_code`，它通常与 HTTP 状态匹配 —— 唯一的例外是下文提到的重复联系人情况，此时 HTTP 状态为 `200`，且仅 `error_code` 携带 `409`。联系人端点特有的代码如下：

| 代码 | 在联系人端点发生的情况 |
|---|---|
| `400` | 错误请求 — 缺少/无效字段、空正文、错误的游标，或批量处理超过 500 个 ID。 |
| `402` | 积分不足，无法对单个联系人执行 AI 标记运行 (`error_code: "insufficient_credits"`)。 |
| `404` | 在您的账户中未找到该联系人、列表或标签。 |
| `409` | 具有该电话号码的联系人已存在（创建时）。在正文中以 `error_code` 返回，HTTP 状态码为 `200`，因此请在此处根据 `error_code` 进行分支处理。当批量自动标记运行已在进行中 (`error_code: "auto_tag_run_in_progress"`)，或者将联系人链接到另一个渠道会导致将已链接到两个不同人员的两个联系人合并时，也会返回此错误。 |
| `422` | 联系人目前无法接收消息（请勿打扰、私密或不支持的渠道）。在渠道链接端点上，也涵盖了无电话号码、不支持的渠道配对或目标渠道没有已连接的发送者的情况。 |

联系人端点上的 `403` 也可能意味着联系人限制或列表权限问题，而非计划访问权限问题。每个端点都可能返回的共享代码 —— `401`、`403`（您的计划不包含 API 访问权限）、`429`（速率限制）和 `500` —— 及其重试指南列在 [错误与分页](errors-and-pagination.md) 中。

---

## 后续步骤

- [消息 API](messages.md) — 通过渠道身份发送消息并管理对话。
- [API 参考](reference.md) — 完整的端点列表，包括标签和列表。
- [API 访问](../integrations/api-access.md) — 身份验证、速率限制和错误处理。
