
# API Liên hệ

Liên hệ là một cá nhân mà bạn nhắn tin — bao gồm tên, số điện thoại, email, kênh, thẻ, trường tùy chỉnh, cũng như các danh sách và chiến dịch mà họ thuộc về. API Liên hệ cho phép bạn tạo, tra cứu, cập nhật, gắn thẻ, nhập hàng loạt và xóa liên hệ mà không cần sử dụng bảng điều khiển.

Tất cả các đường dẫn trên trang này đều tương đối so với URL cơ sở:

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

Vì vậy `/contacts` có nghĩa là `https://api.youraiconnector.com/v1/contacts`.

> **Bạn mới sử dụng API?** Hãy đọc [Truy cập API](../integrations/api-access.md) trước — phần này bao gồm cách tạo khóa API, ba cách xác thực, giới hạn tốc độ và định dạng lỗi. Mọi nội dung trên trang này giả định rằng bạn đã có khóa API đang hoạt động.

---

## Về ID liên hệ

Mỗi liên hệ có một ID duy nhất. ID bạn nhận được khi **tạo** một liên hệ (trong `data.contactId`) cũng chính là ID bạn sử dụng ở mọi nơi khác — để tìm nạp, cập nhật, gắn thẻ, gửi tin nhắn hoặc xóa liên hệ đó. Hãy lưu lại một lần và sử dụng lại.

Bạn không cần phải tạo liên hệ để lấy ID của nó. Bạn cũng có thể tra cứu theo số điện thoại hoặc email (xem [Lấy thông tin liên hệ](#get-a-contact-by-phone-or-email)), hoặc duyệt qua tất cả các liên hệ của bạn (xem [Liệt kê liên hệ](#list-contacts)). Mỗi phương thức đó đều trả về cùng một ID.

---

## Tạo liên hệ

`POST /contacts`

Thêm một liên hệ mới vào tài khoản của bạn. **Bắt buộc phải có số điện thoại kèm mã quốc gia** — chỉ email là không đủ. Mọi thông tin khác đều là tùy chọn.

Bạn có thể tùy chọn đưa liên hệ mới trực tiếp vào một hoặc nhiều danh sách với `listId` (một danh sách đơn lẻ) hoặc `listIds` (một mảng). Nếu cả hai đều được gửi, `listIds` sẽ được ưu tiên.

Bất kỳ trường nào bạn gửi không nằm trong các trường tạo tiêu chuẩn được liệt kê trong bảng trường **Tạo liên hệ** bên dưới (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`) sẽ tự động được lưu trữ dưới dạng **trường tùy chỉnh** — vì vậy một payload phẳng từ các công cụ như Make hoặc Zapier sẽ hoạt động mà không cần lồng ghép. Bạn cũng có thể truyền một đối tượng `custom_fields` tường minh.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `phoneNumber` | Có | Số điện thoại của liên hệ, kèm mã quốc gia (ví dụ: `+15551234567`). |
| `firstName` | Không | Tên. |
| `lastName` | Không | Họ. |
| `email` | Không | Địa chỉ email. |
| `channel` | Không | Kênh nhắn tin. Một trong các giá trị `whatsapp`, `sms`, `whatsapp_web`. Mặc định là `whatsapp`. |
| `is_bot_active` | Không | Liệu trợ lý AI có trả lời liên hệ này hay không. Mặc định là `true`. |
| `is_private` | Không | Đánh dấu liên hệ là riêng tư. Khi là `true`, trợ lý AI sẽ bị tắt đối với họ. Mặc định là `false`. |
| `lead_profile` | Không | Ghi chú văn bản tự do về khách hàng tiềm năng. |
| `listId` | Không | Một ID danh sách đơn lẻ để thêm liên hệ vào. |
| `listIds` | Không | Một mảng các ID danh sách để thêm liên hệ vào (ưu tiên hơn `listId`). |
| `custom_fields` | Không | Một đối tượng chứa các trường khóa/giá trị của riêng bạn. Bạn cũng có thể truyền các trường này dưới dạng khóa cấp cao nhất. |

**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"])
```

**Phản hồi**

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

ID của liên hệ mới nằm tại `data.contactId`. Các danh sách mà liên hệ đó được thêm vào sẽ được phản hồi lại trong `data.listsAdded`.

> **Các bản sao sẽ không được tạo.** Nếu một liên hệ có cùng số điện thoại đã tồn tại, lệnh gọi tạo sẽ **không** tạo hoặc trả về liên hệ đó. Phản hồi trả về với trạng thái HTTP `200` và `error_code` là `409` trong phần thân, vì vậy hãy phân nhánh dựa trên `error_code` thay vì trạng thái HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Để làm việc với một liên hệ hiện có sau khi nhận được `error_code` là `409`, hãy tìm kiếm liên hệ đó bằng [Lấy liên hệ theo số điện thoại hoặc email](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — và sử dụng lại ID mà nó trả về.

> **Các cách viết số WhatsApp tương đương được tính là cùng một số.** Một số quốc gia có hai cách viết hợp lệ cho cùng một đường dây di động và WhatsApp có thể báo cáo một trong hai cách: Mexico (`+52…` và định dạng cũ `+521…`), Brazil (có hoặc không có chữ số thứ chín) và Argentina (có hoặc không có `9` sau `+54`). Kiểm tra trùng lặp khi tạo và khớp `GET /contacts?phoneNumber=` trên cả hai cách viết, vì vậy bạn sẽ nhận lại liên hệ hiện có bất kể bạn gửi dưới dạng nào. `phone_number` được lưu trữ trên liên hệ sẽ không bao giờ bị ghi đè.

---

## Lấy thông tin liên hệ theo số điện thoại hoặc email

`GET /contacts?phoneNumber=...` hoặc `GET /contacts?email=...`

Tìm kiếm một liên hệ duy nhất và trả về đối tượng liên hệ đầy đủ, đã được làm giàu — bao gồm các danh sách, thẻ và chiến dịch của nó được phân giải thành các cặp `{ id, name }`, cộng với tin nhắn cuối cùng đã trao đổi.

Truyền **hoặc** `phoneNumber` (theo định dạng quốc tế) **hoặc** `email`. Nếu bạn không truyền cả hai, điểm cuối này sẽ chuyển sang chế độ [Liệt kê các liên hệ](#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"])
```

**Phản hồi**

```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 liên hệ được trả về ở cả cấp cao nhất (`contactId`) và bên trong đối tượng (`contact.id`). Nếu không có kết quả khớp, bạn sẽ nhận được `404` với `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** là ảnh hồ sơ của liên hệ, được lấy từ WhatsApp hoặc Meta khi họ nhắn tin cho bạn. Đây là trường chỉ đọc: bạn không thể thiết lập nó và nó sẽ là `null` đối với các liên hệ không có ảnh hoặc liên hệ với bạn qua kênh không chia sẻ ảnh. Hãy coi liên kết này là tạm thời thay vì lưu trữ nó, vì một số liên kết ảnh này sẽ hết hạn và được làm mới tự động. (Trong điểm cuối danh sách bên dưới, giá trị tương tự được gọi là `avatar_url`.)

> **Số điện thoại trong URL.** Dấu `+` trong chuỗi truy vấn phải được mã hóa URL thành `%2B`, nếu không nó sẽ được đọc là một khoảng trắng. Các ví dụ trên đã thực hiện việc này cho bạn.

---

## Lấy thông tin liên hệ theo ID

`GET /contacts/{contactId}`

Khi bạn đã có ID của một liên hệ, hãy truy xuất trực tiếp thông tin đó. Cấu trúc phản hồi giống hệt với thao tác tra cứu ở trên.

**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"])
```

Một ID liên hệ không tồn tại trong tài khoản của bạn sẽ trả về `404`.

---

## Lấy thống kê liên hệ

`GET /contacts/{contactId}/stats`

Trả về thống kê tin nhắn tổng hợp cho một liên hệ: tổng số, số lượng phản hồi của AI so với con người, số tín dụng đã sử dụng và dấu thời gian của tin nhắn đầu tiên/cuối cùng.

**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"])
```

**Phản hồi**

```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` là bộ đếm tin nhắn AI tương tự như nút "đặt lại" trong ứng dụng mà bạn dùng để đưa số liệu về không cho một liên hệ. `creditsUsed` là tổng số tín dụng đang chạy cho liên hệ này, không chỉ là các con số của phản hồi này. ID liên hệ không tồn tại trong tài khoản của bạn sẽ trả về `404`.

---

## Liệt kê các liên hệ

`GET /contacts`

Gọi `GET /contacts` mà **không** kèm theo `phoneNumber` hoặc `email` để phân trang qua tất cả các liên hệ của bạn, hiển thị theo thứ tự mới nhất trước. Mỗi trang trả về các bản tóm tắt liên hệ rút gọn (danh sách, thẻ và chiến dịch được trả về dưới dạng mảng ID thay vì đối tượng đầy đủ) và một `next_cursor`.

| Tham số truy vấn | Mô tả |
|---|---|
| `limit` | Kích thước trang. Mặc định là 50, tối đa là 100. |
| `cursor` | Giá trị `next_cursor` từ trang trước. Bỏ qua tham số này ở trang đầu tiên. |
| `listId` | Tùy chọn. Chỉ trả về các liên hệ thuộc danh sách này. |

Để duyệt qua mọi trang: thực hiện cuộc gọi đầu tiên mà không cần con trỏ (cursor), sau đó tiếp tục truyền `next_cursor` nhận được vào `cursor`. **Dừng lại khi `next_cursor` là `null`** — điều đó có nghĩa là không còn kết quả nào nữa.

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

**Phản hồi**

```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
**Lưu ý:** Việc lọc theo `listId` không tồn tại trong tài khoản của bạn sẽ trả về `404`. Một `cursor` không hợp lệ sẽ trả về `400`.
:::


---

## Đếm liên hệ

`GET /contacts/count`

Trả về số lượng liên hệ khớp với bộ lọc, kèm theo phân tách theo từng kênh, mà không cần phân trang. Đây là lệnh gọi phù hợp cho bất kỳ câu hỏi "bao nhiêu" nào — một ô trên bảng điều khiển, một quy trình tự động hóa hoặc khi hỏi Champ. Tất cả các bộ lọc đều là tùy chọn và việc kết hợp nhiều bộ lọc sẽ thu hẹp kết quả đếm (một liên hệ phải khớp với mọi bộ lọc bạn gửi).

| Tham số truy vấn | Mô tả |
|---|---|
| `agentId` | Chỉ các liên hệ được chỉ định cho AI agent này. Truyền `none` cho các liên hệ không có agent được chỉ định (những liên hệ này được trả lời bởi agent mặc định của kênh). |
| `channel` | Chỉ các liên hệ trên kênh này, ví dụ: `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Chỉ các liên hệ mang thẻ này, theo **tên** thẻ (không phân biệt chữ hoa/thường). Tên thẻ không tồn tại sẽ trả về `404`. |
| `listId` | Chỉ các liên hệ trong danh sách này. |
| `botActive` | `true` hoặc `false` — chỉ các liên hệ có trợ lý AI đang bật hoặc tắt. |
| `status` | Chỉ các liên hệ có trạng thái này, ví dụ: `Lead`. |
| `rules` | Một đối tượng quy tắc JSON đã mã hóa URL, sử dụng cùng định dạng với danh sách thông minh (xem [Định dạng `smart_rules`](#the-smart_rules-shape) bên dưới). Không thể kết hợp với các bộ lọc khác. |

Không gửi bộ lọc nào và bạn sẽ nhận được tổng số liên hệ trong tài khoản của mình.

**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"])
```

**Phản hồi**

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

`by_channel` phân tách tổng số tương tự theo từng kênh; các liên hệ không nằm trên bất kỳ kênh nào sẽ được tính trong `none`. `filters` phản hồi lại các bộ lọc đã được áp dụng để bạn có thể kiểm tra xem lệnh gọi đã thực hiện đúng ý mình chưa.

::: note
**Lưu ý:** Gửi `rules` cùng với bất kỳ bộ lọc nào khác, hoặc giá trị `rules` không phải là JSON hợp lệ, sẽ trả về `400`. Tên thẻ hoặc ID danh sách không tồn tại trong tài khoản của bạn sẽ trả về `404`.
:::


---

## Cập nhật liên hệ

`PUT /contacts/{contactId}`

Cập nhật một liên hệ hiện có. Chỉ những trường bạn bao gồm mới bị thay đổi — hãy bỏ qua bất kỳ nội dung nào bạn không muốn chỉnh sửa. Bạn phải gửi ít nhất một trường, nếu không bạn sẽ nhận được `400` ("Không có trường nào để cập nhật").

| Trường | Mô tả |
|---|---|
| `firstName` | Tên. |
| `lastName` | Họ. |
| `email` | Địa chỉ email. |
| `is_bot_active` | Liệu trợ lý AI có trả lời liên hệ này hay không. |
| `is_private` | Đánh dấu là riêng tư. Đặt giá trị này thành `true` cũng sẽ tắt trợ lý AI. |
| `do_not_disturb` | Tạm dừng tiếp cận tự động đối với liên hệ này. Đồng thời ngăn AI trả lời. |
| `follow_ups_disabled` | Dừng tất cả các hoạt động theo dõi tự động cho liên hệ này (nhanh, chu kỳ và khách hàng tiềm năng lạnh) trong khi AI vẫn tiếp tục trả lời các tin nhắn họ gửi. Hữu ích sau khi ai đó đã mua hàng. Trạng thái này sẽ duy trì cho đến khi bạn đặt lại thành `false`. |
| `lead_profile` | Ghi chú về khách hàng tiềm năng dưới dạng văn bản tự do. |
| `custom_fields` | Một đối tượng chứa các trường tùy chỉnh. **Được hợp nhất theo khóa** — chỉ các khóa bạn gửi mới được ghi lại, các trường tùy chỉnh hiện có còn lại sẽ được giữ nguyên. Bạn cũng có thể truyền các khóa trường tùy chỉnh ở cấp cao nhất. |

**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"])
```

**Phản hồi**

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

> **Các trường tùy chỉnh được hợp nhất, không phải thay thế.** Việc gửi `{ "custom_fields": { "tier": "gold" } }` chỉ thiết lập `tier` — bất kỳ trường tùy chỉnh nào khác trên liên hệ vẫn giữ nguyên như cũ. Để xóa hoàn toàn một trường tùy chỉnh trên tất cả các liên hệ, hãy sử dụng [Xóa trường tùy chỉnh](#delete-a-custom-field).

---

## Thêm hoặc xóa thẻ

`POST /contacts/{contactId}/tags`

Thêm và/hoặc xóa thẻ trên một liên hệ duy nhất trong một lệnh gọi. Truyền **ID** thẻ vào `addTagIds` và `removeTagIds`. Ít nhất một trong hai phải không được để trống.

Các thẻ phải tồn tại trong tài khoản của bạn — hãy tạo chúng trước thông qua [endpoint thẻ](reference.md). Nếu liên hệ hoặc bất kỳ thẻ nào được tham chiếu không tồn tại, bạn sẽ nhận được `404`.

| Trường | Mô tả |
|---|---|
| `addTagIds` | Mảng các ID thẻ cần thêm vào liên hệ. |
| `removeTagIds` | Mảng các ID thẻ cần xóa khỏi liên hệ. |

**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"])
```

**Phản hồi**

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

---

## Quản lý thư viện thẻ của bạn

Các điểm cuối này quản lý chính thẻ đó — đổi tên hoặc xóa thẻ trên tài khoản của bạn — trái ngược với việc áp dụng hoặc xóa thẻ trên một liên hệ (xem [Thêm hoặc xóa thẻ](#add-or-remove-tags) ở trên). Mọi thẻ trên tài khoản của bạn đều có một ID (`tagId`): ID được hiển thị trong trình quản lý thẻ trên bảng điều khiển của bạn và ID được trả về dưới dạng `data.tag_id` khi bạn tạo thẻ bằng `POST /tags` và phần thân JSON là `{ "name": "..." }` (không có `phoneNumber`, `email` hoặc `contactId`).

### Cập nhật thẻ

`PUT /tags/{tagId}`

Chỉ gửi các trường bạn đang thay đổi.

| Trường | Mô tả |
|---|---|
| `name` | Tên của thẻ. |

```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)" }'
```

**Phản hồi**

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

Một `tagId` không tồn tại trong tài khoản của bạn sẽ trả về `404`.

### Xóa thẻ

`DELETE /tags/{tagId}`

Xóa một thẻ theo ID. **Hành động này không thể hoàn tác** — các liên hệ đang mang thẻ đó sẽ bị mất thẻ. Việc xóa một thẻ đã không còn tồn tại (hoặc chưa từng tồn tại) sẽ trả về `200` với `deleted: 0` thay vì `404`, vì không có gì để liệt kê.

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

**Phản hồi**

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

### Xóa nhiều thẻ cùng lúc

`DELETE /tags`

| Trường | Mô tả |
|---|---|
| `tagIds` | Mảng các ID thẻ cần xóa (tối đa 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"] }'
```

**Phản hồi**

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

Các ID không tồn tại hoặc thuộc về tài khoản khác sẽ bị bỏ qua một cách âm thầm và không được tính vào `deleted`.

---

## Thiết lập cờ hàng loạt

`POST /contacts/bulk-flag`

Thiết lập một cờ boolean trên nhiều liên hệ cùng lúc. Tối đa 500 ID liên hệ mỗi yêu cầu. Các ID không tồn tại trong tài khoản của bạn sẽ bị bỏ qua và được tính trong `skipped`.

| Trường | Mô tả |
|---|---|
| `contactIds` | Mảng các ID liên hệ cần cập nhật (tối đa 500). |
| `field` | Cờ cần thiết lập. Một trong các giá trị `bot_active` (bật/tắt trợ lý AI), `dnd` (tạm dừng tiếp cận tự động), `spam`, `private`. |
| `value` | Giá trị boolean để thiết lập cho cờ. |

**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"])
```

**Phản hồi**

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

---

## Nhập liên hệ hàng loạt

`POST /contacts/import`

Tạo tối đa 500 liên hệ trong một lệnh gọi từ một mảng JSON. Mỗi bản ghi cần có một `phone_number` ở định dạng quốc tế; mọi thông tin khác đều là tùy chọn. Các bản ghi có số điện thoại không hợp lệ hoặc kênh không được hỗ trợ sẽ bị **bỏ qua** (không được tạo), và mọi bản ghi bị bỏ qua sẽ được báo cáo kèm theo chỉ mục và lý do — để bạn có thể chỉ sửa các lỗi và thử lại.

Các số điện thoại đã tồn tại trong tài khoản của bạn sẽ bị bỏ qua dưới dạng `duplicate` theo mặc định. Gửi `updateExisting: true` để **cập nhật** các liên hệ đó thay thế: các trường có trong bản ghi sẽ ghi đè lên các trường của liên hệ (`first_name`, `last_name`, `email`, `lead_profile` và `custom_fields` được hợp nhất theo từng khóa), `tags` sẽ được thêm vào, và liên hệ sẽ được thêm vào `listId`. Kênh, số điện thoại và cờ bot không bao giờ bị thay đổi trên một liên hệ hiện có.

Bạn có thể tùy chọn thêm mọi liên hệ đã nhập (hoặc cập nhật) vào một danh sách bằng `listId`, đặt `defaultChannel` cho các bản ghi không chỉ định, và gắn thẻ các bản ghi bằng `tags` (tên thẻ — các thẻ chưa tồn tại sẽ được tạo mới, các thẻ hiện có sẽ được khớp không phân biệt chữ hoa chữ thường).

**Các trường cấp cao nhất**

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `contacts` | Có | Mảng các bản ghi liên hệ (tối đa 500). |
| `listId` | Không | Danh sách để thêm mọi liên hệ đã nhập (và cập nhật) vào. Phải là một danh sách có trong tài khoản của bạn. |
| `defaultChannel` | Không | Kênh được áp dụng cho các bản ghi bỏ qua `channel`. Một trong các giá trị `whatsapp`, `sms`, `whatsapp_web`. Mặc định là `whatsapp`. |
| `updateExisting` | Không | `true` để cập nhật các liên hệ có số điện thoại đã tồn tại thay vì bỏ qua chúng dưới dạng `duplicate`. Mặc định là `false`. |

**Các trường cho mỗi bản ghi**

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `phone_number` | Có | Số điện thoại ở định dạng quốc tế (dấu `+` ở đầu sẽ được thêm vào nếu thiếu). |
| `first_name` | Không | Tên. |
| `last_name` | Không | Họ. |
| `email` | Không | Địa chỉ email. |
| `channel` | Không | Một trong các giá trị `whatsapp`, `sms`, `whatsapp_web`. Sẽ quay về `defaultChannel` nếu không được chỉ định. |
| `is_bot_active` | Không | Liệu trợ lý AI có trả lời hay không. Mặc định là `true`. |
| `is_private` | Không | Đánh dấu là riêng tư. Mặc định là `false`. |
| `lead_profile` | Không | Ghi chú về khách hàng tiềm năng dưới dạng văn bản tự do. |
| `custom_fields` | Không | Đối tượng chứa các khóa và giá trị của trường tùy chỉnh. |
| `tags` | Không | Mảng các tên thẻ (một chuỗi `"a; b"` đơn lẻ cũng hoạt động). Các thẻ chưa tồn tại sẽ được tạo mới; các thẻ hiện có sẽ được khớp không phân biệt chữ hoa chữ thường. Tối đa 25 thẻ mỗi bản ghi. |

**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'])}")
```

**Phản hồi**

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

Nếu một số bản ghi không thể được tạo, chúng sẽ xuất hiện trong `skipped` kèm theo lý do (ở đây không có `updateExisting`, vì vậy số điện thoại hiện có sẽ bị bỏ qua):

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

Với `updateExisting: true`, cùng một yêu cầu đó sẽ báo cáo liên hệ hiện có dưới dạng `updated` / `updated_contact_ids` thay vì bỏ qua.

Các lý do có thể bị bỏ qua: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Giới hạn gói.** Nếu giới hạn liên hệ của gói dịch vụ không cho phép thêm số lượng liên hệ mới này, toàn bộ yêu cầu sẽ bị từ chối ngay từ đầu với mã `403`. Nếu đạt đến giới hạn trong quá trình xử lý, các bản ghi còn lại sẽ được trả về dưới dạng bị bỏ qua với lý do `contact_limit_reached`.

---

## Nhập danh bạ từ tệp CSV

Đối với các tệp nhập lớn hơn mức [nhập hàng loạt](#bulk-import-contacts) hỗ trợ (lên đến khoảng 50.000 hàng), hãy xếp hàng một tác vụ nhập không đồng bộ đối với tệp CSV đã có sẵn trong bộ lưu trữ của tài khoản, sau đó thăm dò tác vụ đó cho đến khi hoàn tất.

### Bắt đầu nhập

`POST /contacts/import-csv`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `csvStoragePath` | Có | Đường dẫn lưu trữ của tệp CSV, nằm dưới `users/{your account id}/imports/`, kết thúc bằng `.csv`. |
| `listName` | Có | Tạo (hoặc sử dụng lại) một danh sách với tên này và thêm mọi liên hệ đã nhập vào đó. |
| `existingListRefs` | Không | Mảng các ID danh sách hiện có để thêm mọi liên hệ đã nhập vào đó. |
| `defaultChannel` | Không | Kênh được áp dụng cho các hàng không chỉ định kênh. |

```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"]
```

**Phản hồi** (`202` — tác vụ nhập đang được xếp hàng, chưa hoàn tất)

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

> **Đưa tệp vào bộ lưu trữ.** Điểm cuối này bắt đầu và theo dõi tác vụ nhập; nó không tự chấp nhận tệp tải lên. Tệp CSV cần phải có sẵn tại `csvStoragePath` trước khi bạn gọi nó — trình nhập CSV của chính bảng điều khiển thực hiện việc này như bước đầu tiên.

### Thăm dò tác vụ nhập

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

**Phản hồi**

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

`status` chuyển qua các trạng thái `queued` → `processing` → `completed`, hoặc `failed` với lý do trong `error_message`. Một `jobId` không tồn tại trong tài khoản của bạn sẽ trả về `404`.

---

## Xuất danh bạ

Khởi chạy một tác vụ xuất CSV không đồng bộ cho danh bạ của bạn và trả về một tác vụ để bạn thăm dò trạng thái hoàn thành.

### Bắt đầu xuất dữ liệu

`POST /contacts/export`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `listId` | Không | Chỉ xuất các liên hệ thuộc danh sách này. |
| `contactIds` | Không | Chỉ xuất các ID liên hệ cụ thể này. |

Nếu để trống cả hai, hệ thống sẽ xuất tất cả liên hệ trong tài khoản của bạn.

```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"]
```

**Phản hồi** (`202` — quá trình xuất đang chờ xử lý)

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

### Kiểm tra trạng thái công việc xuất dữ liệu

`GET /contacts/export/{jobId}`

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

**Phản hồi**

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

> Khi `status` ở trạng thái `"completed"`, bạn sẽ nhận được `export_id` và `contact_count`. Việc tải xuống tệp CSV đã tạo được thực hiện từ trang Xuất dữ liệu trên bảng điều khiển của bạn.

---

## Gửi tin nhắn cho một liên hệ

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

Gửi tin nhắn đến một liên hệ hiện có trên bất kỳ kênh nào mà họ đang sử dụng. Tin nhắn sẽ được xếp hàng và gửi trong nền — phản hồi xác nhận rằng tin nhắn đã được chấp nhận, chứ không phải là đã được gửi đi.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `body` | Có | Nội dung văn bản của tin nhắn cần gửi. |
| `mediaUrl` | Không | URL của tệp phương tiện cần đính kèm. |
| `mediaContentType` | Không | Loại MIME của phương tiện đính kèm (ví dụ: `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"])
```

**Phản hồi**

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

> **Không thể gửi ngay bây giờ?** Nếu liên hệ đã bật chế độ không làm phiền hoặc chế độ riêng tư, hoặc không ở trên kênh có thể nhận tin nhắn gửi đi, yêu cầu sẽ bị từ chối kèm theo mã `422` và `error` giải thích.

Để gửi bằng số điện thoại, ID Instagram hoặc định danh kênh khác thay vì ID liên hệ — và để biết thêm thông tin về việc nhắn tin nói chung — hãy xem [Messages API](messages.md).

---

## Gán một tác nhân AI cho một liên hệ

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

Chuyển một cuộc hội thoại hiện có sang một tác nhân AI khác, bắt đầu từ tin nhắn tiếp theo trở đi. Hành động này tương tự như **Gán tác nhân AI** trong menu của cuộc trò chuyện, và cũng là bước tương tự mà hành động **Gán tác nhân hoặc chiến dịch AI** trong Tự động hóa sử dụng.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `agentId` | Có | ID của tác nhân AI sẽ tiếp quản, hoặc `null` để xóa việc gán để cuộc hội thoại quay trở lại hộp thư nhóm của bạn. |
| `triggerAIResponse` | Không | `true` khiến tác nhân mới được gán trả lời ngay các tin nhắn chưa được phản hồi gần nhất của liên hệ. Mặc định là `false`. |

> **Cẩn thận với `triggerAIResponse: true`** — nó gửi tin nhắn cho liên hệ ngay lập tức, vì vậy chỉ sử dụng khi bạn muốn họ nhận tin nhắn ngay bây giờ. Trên Messenger và Instagram, tin nhắn đó sẽ thất bại nếu liên hệ đã nhắn tin cho bạn cách đây hơn 24 giờ.

**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"])
```

**Phản hồi**

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

> Tác nhân phải thuộc cùng một tài khoản với liên hệ; nếu không, yêu cầu sẽ bị từ chối với `404` hoặc `403`. Tìm ID tác nhân trên trang Tác nhân AI (URL của mỗi tác nhân kết thúc bằng ID của nó).

---

## Chỉ định AI agent cho nhiều liên hệ

`POST /contacts/bulk-assign-agent`

Di chuyển nhiều cuộc hội thoại sang một AI agent khác trong một lệnh gọi — hoặc xóa chỉ định cho tất cả chúng bằng `null`. Đây hoàn toàn là thay đổi về định tuyến: **không có tin nhắn nào được gửi đi và agent không trả lời bất kỳ ai**. Mỗi liên hệ sẽ đơn giản là nhận được agent mới vào lần tiếp theo họ nhắn tin. (Đó là lý do tại sao không có `triggerAIResponse` ở đây.)

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `agentId` | Có | AI agent sẽ tiếp quản, hoặc `null` để xóa chỉ định. |
| `contactIds` | Một trong ba | Tối đa 500 ID liên hệ để di chuyển. |
| `filter` | Một trong ba | Chọn các liên hệ trên máy chủ thay vì liệt kê chúng, ưu tiên liên hệ mới nhất. Sử dụng các khóa tương tự như bộ lọc của endpoint đếm: `agentId` (hoặc `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | Một trong ba | Một đối tượng quy tắc danh sách thông minh — xem [Định dạng `smart_rules`](#the-smart_rules-shape). |
| `limit` | Không | Số lượng liên hệ cần di chuyển trong lệnh gọi này khi bạn chọn bằng `filter` hoặc `rules`. Từ 1 đến 500, mặc định là 500. |

Gửi chính xác một trong các tham số `contactIds`, `filter` hoặc `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"])
```

**Phản hồi**

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

`matched` là tổng số liên hệ mà lựa chọn tìm thấy, `updated` là số lượng đã được di chuyển bởi lệnh gọi này, `skipped` là số lượng ID bạn gửi không tìm thấy trong tài khoản của bạn, và `remaining` là số lượng vẫn còn khớp sau khi lệnh gọi này hoàn tất.

**Di chuyển tất cả mọi người.** Vì một lệnh gọi chỉ di chuyển tối đa 500 liên hệ, một nhóm lớn sẽ cần vài lệnh gọi. Hãy sử dụng một bộ lọc ngừng khớp với liên hệ sau khi nó đã được di chuyển — ví dụ `filter: { "agentId": "agent_abc123" }` trong khi chỉ định cho `agent_xyz789` — và lặp lại chính xác lệnh gọi đó cho đến khi `remaining` trả về là `0`. Khi bạn truyền `contactIds` thay thế, `remaining` luôn là `0`.

---

## Chỉ định liên hệ cho một phòng ban

`POST /contacts/{contactId}/department`

"Chỉ định khách hàng tiềm năng này cho bộ phận Kinh doanh" — lưu trữ liên hệ dưới một phòng ban được đặt tên và theo mặc định, sẽ chuyển liên hệ đó cho người nào trong phòng ban hiện đang có ít liên hệ nhất. Việc này tách biệt với [chỉ định một tác nhân AI](#assign-an-ai-agent-to-a-contact): phòng ban trả lời cho câu hỏi "nhóm nào sở hữu liên hệ này," tác nhân trả lời cho câu hỏi "AI nào xử lý việc này," và việc thiết lập một cái sẽ không bao giờ xóa cái kia.

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `department_id` | Có | Phòng ban để lưu trữ liên hệ. Truyền `null` để xóa thông tin này. |
| `hand_to_member` | Không | Đồng thời chuyển liên hệ cho người có ít việc nhất trong phòng ban đó. Mặc định là `true`. Không bao giờ gán lại liên hệ mà ai đó đã sở hữu. |

**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"])
```

**Phản hồi**

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

`assigned_to` là `null` khi liên hệ đã được sở hữu bởi một người nào đó, hoặc bạn đã truyền `hand_to_member: false`.

---

## Liên kết liên hệ trên các kênh

"Tiếp tục trên WhatsApp" (hoặc SMS) sẽ tìm hoặc tạo liên hệ của người này trên một kênh dựa trên số điện thoại khác và liên kết hai kênh lại với nhau, để phần còn lại của ứng dụng nhận diện họ là cùng một người.

### Liên kết tới một kênh khác

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

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `channel` | Có | Kênh cần liên kết tới. Một trong các giá trị `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | Không | Số điện thoại sẽ sử dụng trên kênh mới. Mặc định là số điện thoại của chính liên hệ nguồn. |

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

**Phản hồi**

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

`created` cho bạn biết liệu một liên hệ mới đã được tạo cho kênh đích hay một liên hệ hiện có đã được tìm thấy và liên kết. Việc gọi hàm này lần thứ hai là an toàn — nó sẽ trả về cùng một `contact_id` với `created: false` thay vì tạo ra bản sao trùng lặp.

Một `422` có nghĩa là tài khoản không thể thực hiện liên kết này ngay bây giờ: liên hệ đã nằm trong nhóm kênh đó, không có số điện thoại để sử dụng hoặc không có người gửi được kết nối cho kênh đích. Một `409` có nghĩa là hai liên hệ đã được liên kết với hai người khác nhau — hãy hủy liên kết một trong hai trước.

### Liệt kê các cuộc hội thoại được liên kết của một liên hệ

`GET /contacts/{contactId}/linked`

Trả về các cuộc hội thoại khác cùng là một người với liên hệ này. Một liên hệ chưa được liên kết sẽ trả về một mảng trống, không phải là `404` — "người này không có kênh nào khác" là một trạng thái bình thường.

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

**Phản hồi**

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

### Hủy liên kết một liên hệ

`DELETE /contacts/{contactId}/link`

Loại bỏ liên hệ này khỏi người của nó, theo một chiều — bất kỳ liên hệ nào khác vẫn được liên kết với người đó sẽ giữ nguyên liên kết của chúng, vì vậy việc hủy liên kết một trong ba liên hệ sẽ không làm tan rã nhóm.

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

**Phản hồi**

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

---

## Lấy ảnh đại diện của liên hệ

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

Lấy (và lưu vào bộ nhớ đệm) ảnh hồ sơ WhatsApp hoặc Meta của liên hệ theo yêu cầu — cùng một ảnh được trả về dưới dạng `avatarUrl` trong [Lấy thông tin liên hệ](#get-a-contact-by-phone-or-email), đã được làm mới.

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

**Phản hồi**

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

`cached: true` có nghĩa là URL đến từ một lần lấy dữ liệu gần đây thay vì tra cứu mới từ nhà cung cấp — ảnh được lưu vào bộ nhớ đệm trong 7 ngày, và một liên hệ mà nhà cung cấp báo cáo là không có ảnh khả dụng sẽ được lưu vào bộ nhớ đệm là không khả dụng trong 24 giờ. Khi không có ảnh để lấy, `avatar_url` sẽ bị lược bỏ và `message` sẽ giải thích lý do.

---

## Tự động gắn thẻ liên hệ bằng AI

Chạy các quy tắc gắn thẻ của tài khoản bạn trên toàn bộ lịch sử hội thoại của một hoặc nhiều liên hệ và áp dụng (hoặc xóa) thẻ chính xác như cách gắn thẻ thời gian thực diễn ra trong cuộc trò chuyện trực tiếp — cùng quy tắc, cùng chi phí tín dụng cho mỗi thẻ.

### Bắt đầu một lượt chạy

`POST /contacts/auto-tag`

| Trường | Bắt buộc | Mô tả |
|---|---|---|
| `scope` | Có | `"contacts"` để gắn thẻ các liên hệ cụ thể, hoặc `"agent"` để gắn thẻ mọi cuộc hội thoại hiện đang được xử lý bởi một tác nhân AI. |
| `contact_ids` | Bắt buộc khi `scope` là `"contacts"` | Mảng các ID liên hệ, từ 1 đến 500. |
| `agent_id` | Bắt buộc khi `scope` là `"agent"` | Tác nhân AI có các cuộc hội thoại cần gắn thẻ. Khi `scope` là `"contacts"`, trường này là tùy chọn và chỉ thu hẹp phạm vi các quy tắc gắn thẻ của tác nhân được chạy. |

```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"] }'
```

Một liên hệ **duy nhất** sẽ chạy nội tuyến và trả về kết quả ngay lập tức:

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

**Hai liên hệ trở lên** (hoặc `scope: "agent"`) sẽ chạy dưới dạng tác vụ nền và trả về `202` ngay lập tức:

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

### Kiểm tra trạng thái lượt chạy

`GET /contacts/auto-tag/run`

Trả về lượt chạy hiện tại (hoặc gần đây nhất) của tài khoản, để bạn có thể kiểm tra tiến độ mà không cần tự theo dõi `run_id`.

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

**Phản hồi**

```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` là `null` khi tài khoản chưa từng bắt đầu lượt chạy nào. `status` chuyển từ `"running"` sang `"completed"` hoặc `"failed"`.

Mỗi tài khoản chỉ có thể thực hiện một lượt chạy hàng loạt tại một thời điểm — bắt đầu lượt chạy thứ hai khi lượt khác đang chạy sẽ trả về `409` cùng với `error_code: "auto_tag_run_in_progress"`. Hết tín dụng khi chạy cho một liên hệ duy nhất sẽ trả về `402` cùng với `error_code: "insufficient_credits"`; lượt chạy hàng loạt thay vào đó sẽ tự dừng sớm và báo cáo tiến độ đã đạt được trong `run`.

---

## Xóa liên hệ

`DELETE /contacts/{contactId}`

Xóa vĩnh viễn một liên hệ theo ID, cùng với lịch sử tin nhắn của liên hệ đó. **Hành động này không thể hoàn tác.** Để xóa nhiều liên hệ trong một lệnh gọi, hãy sử dụng [Xóa liên hệ](#delete-contacts) bên dưới.

**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"])
```

**Phản hồi**

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

Một ID liên hệ không tồn tại trong tài khoản của bạn, hoặc thuộc về một tài khoản khác, sẽ trả về `404`.

---

## Xóa liên hệ

`DELETE /contacts`

Xóa vĩnh viễn một hoặc nhiều liên hệ theo ID trong một lần gọi (tối đa 500 ID). Các ID không tồn tại trong tài khoản của bạn sẽ bị bỏ qua và được tính vào `skipped`. **Hành động này không thể hoàn tác.**

| Trường | Mô tả |
|---|---|
| `contactIds` | Mảng các ID liên hệ cần xóa (tối đa 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']}")
```

**Phản hồi**

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

---

## Xóa một trường tùy chỉnh

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

Xóa một khóa trường tùy chỉnh khỏi **mọi** liên hệ trong tài khoản của bạn. Sử dụng tính năng này để dọn dẹp sau khi đổi tên hoặc ngừng sử dụng một trường tùy chỉnh. Khóa chỉ có thể chứa các chữ cái, số, dấu gạch dưới và dấu gạch nối. Trả về số lượng liên hệ đã được cập nhật. **Thao tác này không thể hoàn tác.**

**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")
```

**Phản hồi**

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

::: note
**Lưu ý:** Một khóa trường có các ký tự không được hỗ trợ sẽ trả về `400`.
:::


---

## Danh sách

Danh sách dùng để nhóm các liên hệ. Một danh sách có thể là **tĩnh** (bạn tự quyết định ai nằm trong đó) hoặc **thông minh** (thành viên được tính toán dựa trên các quy tắc và được cập nhật tự động — xem [Tổ chức Danh sách & Liên hệ](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Trường | Mô tả |
|---|---|
| `name` | Bắt buộc khi tạo. Tối đa 100 ký tự. |
| `status` | `live` (mặc định) hoặc `draft`. Chữ thường. |
| `contact_ids` | Mảng các ID liên hệ để đưa vào danh sách. **Chỉ dành cho danh sách tĩnh.** |
| `type` | `static` (mặc định) hoặc `smart`. |
| `smart_rules` | Bộ quy tắc — bắt buộc khi `type` là `smart`. Xem bên dưới. |

### Tạo danh sách

`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" } }
          ]
        }
      }'
```

**Phản hồi**

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

Một danh sách thông minh được đánh giá **trực tiếp** (inline), trong cùng một yêu cầu, vì vậy `evaluation` cho bạn biết chính xác những ai đã được thêm vào danh sách đó. Đối với danh sách tĩnh, `evaluation` là `null`.

### Cập nhật danh sách

`PUT /lists/{listId}`

Chỉ gửi các trường bạn muốn thay đổi. Việc thay đổi `smart_rules` sẽ đánh giá lại danh sách ngay lập tức và trả về cùng một đối tượng `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"] } ] } }'
```

Bạn có thể chuyển đổi danh sách giữa hai loại:

- **Tĩnh → thông minh**: gửi `{ "type": "smart", "smart_rules": { … } }`. Các quy tắc sẽ được áp dụng ngay lập tức.
- **Thông minh → tĩnh**: gửi `{ "type": "static" }`. Các quy tắc sẽ bị loại bỏ và những người hiện có trong danh sách sẽ được giữ lại.

### Cấu trúc `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` (tất cả các điều kiện phải đúng) hoặc `any` (ít nhất một điều kiện đúng).
- `conditions` — 1 đến 20 điều kiện, mỗi điều kiện tối đa 100 giá trị, chuỗi tối đa 200 ký tự.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | mảng các ID thẻ |
| `lists` | `in_any`, `not_in_any` | mảng các ID danh sách (**chỉ dành cho danh sách tĩnh** — danh sách thông minh không thể được tạo từ một danh sách thông minh khác) |
| `channel` | `is_any`, `is_none` | mảng các kênh |
| `status` | `is_any`, `is_none` | mảng các trạng thái liên hệ |
| `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" }` |
| các trường ngày tương tự | `before`, `after` | ngày ISO (`"2026-01-01"`, được so sánh theo ngày trọn vẹn) hoặc ngày-giờ ISO đầy đủ (`"2026-01-01T14:30:00Z"`, được so sánh đến thời điểm chính xác) |
| các trường ngày tương tự | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` khớp với các liên hệ mà AI đã nhắn tin ít nhất một lần (từ trước đến nay) |
| `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` | chuỗi cho các biểu mẫu `contains` |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | mảng các ID cho các biểu mẫu `is_any` / `is_none` |
| `custom_field` (cộng với `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | chuỗi cho các biểu mẫu giá trị |

`not_within_last` cũng khớp với các liên hệ chưa từng được thiết lập ngày ("hơn N trước đây, **hoặc chưa bao giờ**"), và các so sánh văn bản sẽ bỏ qua phân biệt chữ hoa/thường.

**Tương tác AI.** `has_interacted_with_ai` là cờ trọn đời: `true` cho mọi liên hệ mà AI của bạn đã gửi ít nhất một tin nhắn, `false` cho tất cả những người khác (bao gồm cả các liên hệ mà chỉ nhóm của bạn từng phản hồi). Nó được đóng dấu vào tin nhắn đầu tiên của AI gửi cho một liên hệ và không bao giờ bị xóa, vì vậy việc tắt tính năng trả lời của AI cho liên hệ đó hoặc chuyển họ sang chiến dịch khác sẽ không đặt lại nó. Đối với một *khoảng thời gian* — "các liên hệ mà AI của tôi đã xử lý trong tháng này", câu hỏi thanh toán thông thường — hãy sử dụng phạm vi qua `last_ai_interaction_at` thay thế:

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

Đừng nhầm lẫn cả hai với `is_bot_active` (AI *được phép* trả lời, không phải là nó đã trả lời) hoặc `has_ever_responded` (liên hệ *đã viết lại*, cho bất kỳ ai). Hai dấu tương tự được trả về trên mỗi liên hệ dưới dạng `first_ai_interaction_at` / `last_ai_interaction_at`, và toàn bộ tập hợp quy tắc cũng hoạt động trên `GET /contacts?rules=`, vì vậy bạn có thể đếm các kết quả khớp mà không cần tạo danh sách.

### Xem trước một bộ quy tắc

`POST /lists/preview`

Đếm và lấy mẫu các liên hệ mà một bộ quy tắc sẽ khớp, mà không tạo hoặc thay đổi bất kỳ điều gì. Hãy sử dụng tính năng này để kiểm tra tính hợp lệ của các quy tắc trước khi bạn lưu chúng.

```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"] } ] } }'
```

**Phản hồi**

```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` chứa tối đa 10 liên hệ, ưu tiên các liên hệ hoạt động gần đây nhất.

### Chạy lại danh sách thông minh ngay bây giờ

`POST /lists/{listId}/evaluate`

Buộc đánh giá lại ngay lập tức (tương tự như hành động **Làm mới ngay** trong bảng điều khiển). Các danh sách thông minh đã tự động cập nhật khi một liên hệ thay đổi và cứ sau 15 phút đối với các quy tắc dựa trên thời gian, vì vậy hành động này chỉ cần thiết khi bạn muốn có kết quả *ngay lập tức*.

**Phản hồi**

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

`evaluation.skipped: true` có nghĩa là một quá trình đánh giá khác của cùng danh sách đó đang chạy và lệnh gọi này không thực hiện bất kỳ thay đổi nào.

### Danh sách thông minh từ chối các thành viên được chọn thủ công

Các điểm cuối thành viên trả về **`409`** với `"This is a smart list — its members are computed from its rules. Edit the rules instead."` khi danh sách mục tiêu là danh sách thông minh. Điều này bao gồm `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` trên `POST /lists` và `PUT /lists/{listId}`, cũng như việc chọn một danh sách thông minh làm đích nhập CSV. Hãy thay đổi các quy tắc thay vì làm vậy.

Việc gọi `POST /lists/{listId}/evaluate` trên một danh sách **tĩnh** cũng là một `409` — vì danh sách đó không có quy tắc nào để chạy.

---

## Các lỗi API liên hệ

Các điểm cuối (endpoint) liên hệ trả về phong bì lỗi tiêu chuẩn:

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

Một số endpoint cũng bao gồm `error_code`, thường khớp với trạng thái HTTP — ngoại lệ duy nhất là trường hợp liên hệ trùng lặp bên dưới, nơi trạng thái HTTP là `200` và chỉ có `error_code` mang giá trị `409`. Các mã cụ thể cho các endpoint liên hệ:

| Mã | Khi nào nó xảy ra trên một endpoint liên hệ |
|---|---|
| `400` | Yêu cầu không hợp lệ — thiếu/sai trường, nội dung trống, con trỏ (cursor) không hợp lệ, hoặc vượt quá 500 ID trong một lô. |
| `402` | Không đủ tín dụng để hoàn tất quá trình gắn thẻ AI cho một liên hệ (`error_code: "insufficient_credits"`). |
| `404` | Liên hệ, danh sách hoặc thẻ không được tìm thấy trong tài khoản của bạn. |
| `409` | Một liên hệ với số điện thoại đó đã tồn tại (khi tạo mới). Được trả về dưới dạng `error_code` trong phần nội dung với trạng thái HTTP là `200`, vì vậy hãy phân nhánh dựa trên `error_code` tại đây. Cũng được trả về khi quá trình tự động gắn thẻ hàng loạt đang diễn ra (`error_code: "auto_tag_run_in_progress"`), hoặc khi việc liên kết một liên hệ với một kênh khác sẽ hợp nhất hai liên hệ vốn đã được liên kết với hai người khác nhau. |
| `422` | Liên hệ hiện không thể nhận tin nhắn (do chế độ không làm phiền, riêng tư hoặc kênh không được hỗ trợ). Trên endpoint liên kết kênh, lỗi này cũng bao gồm trường hợp không có số điện thoại, ghép nối kênh không được hỗ trợ, hoặc không có người gửi được kết nối cho kênh mục tiêu. |

Mã `403` trên một endpoint liên hệ cũng có thể có nghĩa là vấn đề về giới hạn liên hệ hoặc quyền danh sách thay vì quyền truy cập gói. Các mã dùng chung mà mọi endpoint có thể trả về — `401`, `403` (gói của bạn không bao gồm quyền truy cập API), `429` (giới hạn tốc độ) và `500` — được liệt kê cùng với hướng dẫn thử lại trong phần [Lỗi & Phân trang](errors-and-pagination.md).

---

## Các bước tiếp theo

- [API Tin nhắn](messages.md) — gửi tin nhắn theo danh tính kênh và quản lý các cuộc hội thoại.
- [Tham chiếu API](reference.md) — danh sách đầy đủ các điểm cuối, bao gồm thẻ và danh sách.
- [Truy cập API](../integrations/api-access.md) — xác thực, giới hạn tốc độ và xử lý lỗi.
