
# API Kontak

Kontak adalah satu orang yang Anda kirimi pesan — nama, nomor telepon, email, saluran, tag, kolom kustom, serta daftar dan kampanye tempat mereka berada. API Kontak memungkinkan Anda membuat kontak, mencarinya, memperbaruinya, memberi tag, mengimpornya secara massal, dan menghapusnya, semuanya tanpa menggunakan dasbor.

Semua jalur di halaman ini bersifat relatif terhadap URL dasar:

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

Jadi `/contacts` berarti `https://api.youraiconnector.com/v1/contacts`.

> **Baru menggunakan API?** Baca [Akses API](../integrations/api-access.md) terlebih dahulu — bagian ini mencakup cara membuat kunci API Anda, tiga cara untuk melakukan autentikasi, batas kecepatan, dan format kesalahan. Semua yang ada di halaman ini mengasumsikan Anda sudah memiliki kunci API yang berfungsi.

---

## Tentang ID kontak

Setiap kontak memiliki ID unik. ID yang Anda dapatkan saat **membuat** kontak (di `data.contactId`) adalah ID yang sama yang Anda gunakan di tempat lain — untuk mengambil, memperbarui, memberi tag, mengirim pesan, atau menghapus kontak tersebut. Simpan sekali dan gunakan kembali.

Anda tidak perlu membuat kontak untuk mendapatkan ID-nya. Anda juga dapat mencarinya berdasarkan nomor telepon atau email (lihat [Mendapatkan kontak](#get-a-contact-by-phone-or-email)), atau menelusuri semua kontak Anda (lihat [Daftar kontak](#list-contacts)). Masing-masing metode tersebut mengembalikan ID yang sama.

---

## Membuat kontak

`POST /contacts`

Menambahkan kontak baru ke akun Anda. **Nomor telepon dengan kode negara wajib diisi** — email saja tidak cukup. Semua kolom lainnya bersifat opsional.

Anda dapat secara opsional memasukkan kontak baru langsung ke dalam satu atau beberapa daftar dengan `listId` (satu daftar) atau `listIds` (sebuah array). Jika keduanya dikirim, `listIds` yang akan digunakan.

Setiap kolom yang Anda kirimkan yang bukan merupakan salah satu kolom pembuatan standar yang tercantum dalam tabel kolom **Buat kontak** di bawah (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`) akan disimpan secara otomatis sebagai **kolom kustom** — sehingga payload datar dari alat seperti Make atau Zapier dapat berfungsi tanpa perlu melakukan nesting. Anda juga dapat meneruskan objek `custom_fields` secara eksplisit.

| Kolom | Wajib | Deskripsi |
|---|---|---|
| `phoneNumber` | Ya | Nomor telepon kontak, dengan kode negara (contoh: `+15551234567`). |
| `firstName` | Tidak | Nama depan. |
| `lastName` | Tidak | Nama belakang. |
| `email` | Tidak | Alamat email. |
| `channel` | Tidak | Saluran pesan. Salah satu dari `whatsapp`, `sms`, `whatsapp_web`. Default-nya adalah `whatsapp`. |
| `is_bot_active` | Tidak | Apakah asisten AI membalas kontak ini. Default-nya adalah `true`. |
| `is_private` | Tidak | Tandai kontak sebagai pribadi. Jika `true`, asisten AI dimatikan untuk mereka. Default-nya adalah `false`. |
| `lead_profile` | Tidak | Catatan teks bebas tentang prospek. |
| `listId` | Tidak | ID daftar tunggal untuk menambahkan kontak. |
| `listIds` | Tidak | Array ID daftar untuk menambahkan kontak (lebih diutamakan daripada `listId`). |
| `custom_fields` | Tidak | Objek kolom kunci/nilai Anda sendiri. Anda juga dapat mengirimkannya sebagai kunci tingkat atas. |

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

**Respons**

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

ID kontak baru ada di `data.contactId`. Daftar tempat kontak tersebut ditambahkan akan dikembalikan dalam `data.listsAdded`.

> **Duplikat tidak dibuat.** Jika kontak dengan nomor telepon yang sama sudah ada, panggilan buat **tidak** akan membuat atau mengembalikannya. Respons yang muncul adalah status HTTP `200` dan `error_code` sebesar `409` di dalam body, jadi lakukan percabangan pada `error_code` alih-alih pada status HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Untuk bekerja dengan kontak yang sudah ada setelah `error_code` sebesar `409`, cari kontak tersebut dengan [Dapatkan kontak berdasarkan telepon atau email](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — dan gunakan kembali ID yang dikembalikannya.

> **Ejaan WhatsApp yang setara dihitung sebagai nomor yang sama.** Beberapa negara memiliki dua ejaan yang valid untuk saluran seluler yang sama dan WhatsApp mungkin melaporkan salah satunya: Meksiko (`+52…` dan `+521…` lama), Brasil (dengan atau tanpa digit kesembilan), dan Argentina (dengan atau tanpa `9` setelah `+54`). Pemeriksaan duplikat saat pembuatan dan pencocokan `GET /contacts?phoneNumber=` berlaku untuk kedua ejaan tersebut, sehingga Anda akan mendapatkan kembali kontak yang sudah ada, apa pun bentuk yang Anda kirimkan. `phone_number` yang tersimpan pada kontak tidak akan pernah ditulis ulang.

---

## Mendapatkan kontak berdasarkan telepon atau email

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

Mencari satu kontak dan mengembalikan objek kontak lengkap yang diperkaya — termasuk daftar, tag, dan kampanye yang diselesaikan menjadi pasangan `{ id, name }`, ditambah pesan terakhir yang dipertukarkan.

Berikan **salah satu** `phoneNumber` (dalam format internasional) **atau** `email`. Jika Anda tidak memberikan keduanya, endpoint yang sama ini akan beralih ke mode [Daftar kontak](#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"])
```

**Respons**

```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 kontak dikembalikan baik di tingkat atas (`contactId`) maupun di dalam objek (`contact.id`). Jika tidak ada yang cocok, Anda akan mendapatkan `404` dengan `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** adalah foto profil kontak, yang diambil dari WhatsApp atau Meta saat mereka mengirim pesan kepada Anda. Ini bersifat baca-saja: Anda tidak dapat mengaturnya, dan ini adalah `null` untuk kontak yang tidak memiliki foto atau yang menghubungi Anda melalui saluran yang tidak membagikannya. Perlakukan tautan tersebut sebagai sementara alih-alih menyimpannya, karena beberapa tautan foto ini kedaluwarsa dan diperbarui secara otomatis. (Dalam endpoint daftar di bawah, nilai yang sama disebut `avatar_url`.)

> **Nomor telepon di URL.** Tanda `+` dalam string kueri harus di-encode URL sebagai `%2B`, jika tidak, tanda tersebut akan dibaca sebagai spasi. Contoh di atas sudah melakukannya untuk Anda.

---

## Mendapatkan kontak berdasarkan ID

`GET /contacts/{contactId}`

Jika Anda sudah memiliki ID kontak, ambil datanya secara langsung. Bentuk responsnya identik dengan pencarian di atas.

**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 kontak yang tidak ada di akun Anda akan mengembalikan `404`.

---

## Mendapatkan statistik kontak

`GET /contacts/{contactId}/stats`

Mengembalikan statistik pesan gabungan untuk satu kontak: total, balasan AI vs manusia, kredit yang digunakan, serta stempel waktu pesan pertama/terakhir.

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

**Respons**

```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` adalah penghitung pesan AI yang sama dengan yang diatur ulang oleh tombol "reset" dalam aplikasi pada sebuah kontak. `creditsUsed` adalah total kredit berjalan untuk kontak ini, bukan hanya angka untuk respons ini saja. ID kontak yang tidak ada di akun Anda akan mengembalikan `404`.

---

## Mencantumkan kontak

`GET /contacts`

Panggil `GET /contacts` **tanpa** `phoneNumber` maupun `email` untuk menelusuri semua kontak Anda, dimulai dari yang terbaru. Setiap halaman mengembalikan ringkasan kontak yang ringkas (daftar, tag, dan kampanye dikembalikan sebagai array ID, bukan objek lengkap) dan sebuah `next_cursor`.

| Parameter kueri | Deskripsi |
|---|---|
| `limit` | Ukuran halaman. Default-nya 50, maksimum 100. |
| `cursor` | Nilai `next_cursor` dari halaman sebelumnya. Hilangkan pada halaman pertama. |
| `listId` | Opsional. Hanya mengembalikan kontak yang termasuk dalam daftar ini. |

Untuk menelusuri setiap halaman: lakukan panggilan pertama tanpa kursor, lalu terus berikan `next_cursor` yang dikembalikan sebagai `cursor`. **Berhenti saat `next_cursor` bernilai `null`** — itu berarti tidak ada lagi hasil yang tersisa.

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

**Respons**

```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
**Catatan:** Memfilter berdasarkan `listId` yang tidak ada di akun Anda akan mengembalikan `404`. `cursor` yang tidak valid akan mengembalikan `400`.
:::


---

## Menghitung kontak

`GET /contacts/count`

Mengembalikan berapa banyak kontak yang cocok dengan filter, ditambah pembagian per saluran, tanpa perlu melakukan paging. Ini adalah panggilan yang tepat untuk pertanyaan "berapa banyak" — ubin dasbor, otomatisasi, atau bertanya kepada Champ. Semua filter bersifat opsional, dan menggabungkan beberapa filter akan mempersempit jumlahnya (kontak harus cocok dengan setiap filter yang Anda kirim).

| Parameter kueri | Deskripsi |
|---|---|
| `agentId` | Hanya kontak yang ditetapkan ke agen AI ini. Berikan `none` untuk kontak tanpa agen yang ditetapkan (kontak tersebut dijawab oleh agen default saluran). |
| `channel` | Hanya kontak di saluran ini, contohnya `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Hanya kontak yang memiliki tag ini, berdasarkan **nama** tag (huruf besar/kecil tidak berpengaruh). Nama tag yang tidak Anda miliki akan mengembalikan `404`. |
| `listId` | Hanya kontak di daftar ini. |
| `botActive` | `true` atau `false` — hanya kontak yang asisten AI-nya aktif atau nonaktif. |
| `status` | Hanya kontak dengan status ini, contohnya `Lead`. |
| `rules` | Objek aturan JSON yang di-encode URL, menggunakan bentuk yang sama dengan daftar cerdas (lihat [Bentuk `smart_rules`](#the-smart_rules-shape) di bawah). Tidak dapat digabungkan dengan filter lainnya. |

Jangan kirim filter apa pun dan Anda akan mendapatkan jumlah total kontak di akun Anda.

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

**Respons**

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

`by_channel` membagi total yang sama per saluran; kontak yang tidak ada di saluran mana pun dihitung di bawah `none`. `filters` menggemakan kembali filter yang diterapkan, sehingga Anda dapat memeriksa apakah panggilan tersebut melakukan apa yang Anda maksudkan.

::: note
**Catatan:** Mengirim `rules` bersama dengan filter lain, atau nilai `rules` yang bukan JSON valid, akan mengembalikan `400`. Nama tag atau ID daftar yang tidak ada di akun Anda akan mengembalikan `404`.
:::


---

## Memperbarui kontak

`PUT /contacts/{contactId}`

Memperbarui kontak yang sudah ada. Hanya kolom yang Anda sertakan yang akan diubah — abaikan kolom yang tidak ingin Anda ubah. Anda harus mengirim setidaknya satu kolom, atau Anda akan mendapatkan `400` ("Tidak ada kolom untuk diperbarui").

| Bidang | Deskripsi |
|---|---|
| `firstName` | Nama depan. |
| `lastName` | Nama belakang. |
| `email` | Alamat email. |
| `is_bot_active` | Apakah asisten AI membalas kontak ini. |
| `is_private` | Tandai sebagai pribadi. Mengatur ini ke `true` juga akan mematikan asisten AI. |
| `do_not_disturb` | Jeda penjangkauan otomatis ke kontak ini. Juga menghentikan AI untuk membalas. |
| `follow_ups_disabled` | Hentikan semua tindak lanjut otomatis untuk kontak ini (cepat, siklus, dan prospek dingin) sementara AI tetap membalas pesan yang mereka kirim. Berguna setelah seseorang melakukan pembelian. Tetap nonaktif hingga Anda mengaturnya kembali ke `false`. |
| `lead_profile` | Catatan prospek teks bebas. |
| `custom_fields` | Objek bidang kustom. **Digabungkan per kunci** — hanya kunci yang Anda kirim yang akan ditulis, sisa bidang kustom yang ada akan tetap dipertahankan. Anda juga dapat meneruskan kunci bidang kustom di tingkat atas. |

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

**Respons**

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

> **Kolom kustom digabungkan, bukan diganti.** Mengirim `{ "custom_fields": { "tier": "gold" } }` hanya akan mengatur `tier` — kolom kustom lainnya pada kontak akan tetap seperti semula. Untuk menghapus kolom kustom sepenuhnya di semua kontak, gunakan [Hapus kolom kustom](#delete-a-custom-field).

---

## Menambah atau menghapus tag

`POST /contacts/{contactId}/tags`

Menambah dan/atau menghapus tag pada satu kontak dalam satu panggilan. Teruskan **ID** tag di `addTagIds` dan `removeTagIds`. Setidaknya salah satu dari keduanya harus tidak kosong.

Tag tersebut harus sudah ada di akun Anda — buat tag terlebih dahulu melalui [endpoint tag](reference.md). Jika kontak atau tag yang dirujuk tidak ada, Anda akan mendapatkan `404`.

| Bidang | Deskripsi |
|---|---|
| `addTagIds` | Larik ID tag untuk ditambahkan ke kontak. |
| `removeTagIds` | Larik ID tag untuk dihapus dari kontak. |

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

**Respons**

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

---

## Mengelola pustaka tag Anda

Endpoint ini mengelola tag itu sendiri — mengganti nama atau menghapusnya dari akun Anda — berbeda dengan menerapkan atau menghapus tag pada satu kontak (lihat [Menambah atau menghapus tag](#add-or-remove-tags) di atas). Setiap tag di akun Anda memiliki ID (`tagId`): ID yang ditampilkan di pengelola tag dasbor Anda, dan ID yang dikembalikan sebagai `data.tag_id` saat Anda membuat tag dengan `POST /tags` dan badan JSON `{ "name": "..." }` (tanpa `phoneNumber`, `email`, atau `contactId`).

### Memperbarui tag

`PUT /tags/{tagId}`

Kirim hanya kolom yang ingin Anda ubah.

| Kolom | Deskripsi |
|---|---|
| `name` | Nama tag. |

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

**Respons**

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

`tagId` yang tidak ada di akun Anda akan mengembalikan `404`.

### Menghapus tag

`DELETE /tags/{tagId}`

Menghapus satu tag berdasarkan ID. **Tindakan ini tidak dapat dibatalkan** — kontak yang memiliki tag tersebut akan kehilangan tag tersebut. Menghapus tag yang sudah tidak ada (atau tidak pernah ada) akan mengembalikan `200` dengan `deleted: 0` alih-alih `404`, karena tidak ada yang perlu dihitung.

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

**Respons**

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

### Menghapus beberapa tag sekaligus

`DELETE /tags`

| Field | Description |
|---|---|
| `tagIds` | Array ID tag untuk dihapus (maks 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"] }'
```

**Respons**

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

ID yang tidak ada, atau milik akun lain, akan dilewati secara diam-diam dan tidak dihitung dalam `deleted`.

---

## Mengatur flag secara massal

`POST /contacts/bulk-flag`

Mengatur satu flag boolean pada banyak kontak sekaligus. Hingga 500 ID kontak per permintaan. ID yang tidak ada di akun Anda akan dilewati dan dihitung dalam `skipped`.

| Bidang | Deskripsi |
|---|---|
| `contactIds` | Larik ID kontak untuk diperbarui (maks 500). |
| `field` | Flag mana yang akan diatur. Salah satu dari `bot_active` (asisten AI aktif/nonaktif), `dnd` (jeda jangkauan otomatis), `spam`, `private`. |
| `value` | Nilai boolean untuk mengatur flag tersebut. |

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

**Respons**

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

---

## Impor kontak massal

`POST /contacts/import`

Membuat hingga 500 kontak dalam satu panggilan dari array JSON. Setiap catatan memerlukan `phone_number` dalam format internasional; sisanya bersifat opsional. Catatan dengan nomor telepon yang tidak valid atau saluran yang tidak didukung akan **dilewati** (tidak dibuat), dan setiap catatan yang dilewati akan dilaporkan beserta indeks dan alasannya — sehingga Anda dapat memperbaiki kegagalan tersebut dan mencoba lagi.

Nomor telepon yang sudah ada di akun Anda akan dilewati sebagai `duplicate` secara default. Kirim `updateExisting: true` untuk **memperbarui** kontak tersebut sebagai gantinya: bidang yang ada dalam catatan akan menimpa data kontak (`first_name`, `last_name`, `email`, `lead_profile`, dan `custom_fields` digabungkan per kunci), `tags` akan ditambahkan, dan kontak akan ditambahkan ke `listId`. Saluran, nomor telepon, dan tanda bot tidak akan pernah diubah pada kontak yang sudah ada.

Anda dapat menambahkan setiap kontak yang diimpor (atau diperbarui) ke daftar dengan `listId` secara opsional, menetapkan `defaultChannel` untuk catatan yang tidak menentukannya, dan menandai catatan dengan `tags` (nama tag — tag yang tidak ada akan dibuat, tag yang sudah ada dicocokkan tanpa mempedulikan huruf besar/kecil).

**Bidang tingkat atas**

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `contacts` | Ya | Array catatan kontak (maks 500). |
| `listId` | Tidak | Daftar untuk menambahkan setiap kontak yang diimpor (dan diperbarui). Harus berupa daftar di akun Anda. |
| `defaultChannel` | Tidak | Saluran yang diterapkan ke catatan yang tidak menyertakan `channel`. Salah satu dari `whatsapp`, `sms`, `whatsapp_web`. Default ke `whatsapp`. |
| `updateExisting` | Tidak | `true` untuk memperbarui kontak yang nomor teleponnya sudah ada alih-alih melewatinya sebagai `duplicate`. Default ke `false`. |

**Bidang per catatan**

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `phone_number` | Ya | Nomor telepon dalam format internasional (`+` di depan akan ditambahkan jika tidak ada). |
| `first_name` | Tidak | Nama depan. |
| `last_name` | Tidak | Nama belakang. |
| `email` | Tidak | Alamat email. |
| `channel` | Tidak | Salah satu dari `whatsapp`, `sms`, `whatsapp_web`. Kembali ke `defaultChannel`. |
| `is_bot_active` | Tidak | Apakah asisten AI membalas. Default ke `true`. |
| `is_private` | Tidak | Tandai pribadi. Default ke `false`. |
| `lead_profile` | Tidak | Catatan prospek teks bebas. |
| `custom_fields` | Tidak | Objek kunci dan nilai bidang kustom. |
| `tags` | Tidak | Array nama tag (satu string `"a; b"` juga berfungsi). Tag yang tidak ada akan dibuat; tag yang sudah ada dicocokkan dengan mengabaikan huruf besar/kecil. Maks 25 per catatan. |

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

**Respons**

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

Jika beberapa catatan tidak dapat dibuat, catatan tersebut akan muncul di `skipped` beserta alasannya (di sini tanpa `updateExisting`, sehingga nomor yang sudah ada dilewati):

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

Dengan `updateExisting: true`, permintaan yang sama akan melaporkan kontak yang sudah ada di bawah `updated` / `updated_contact_ids` sebagai gantinya.

Kemungkinan alasan dilewati: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Batas paket.** Jika batas kontak paket Anda tidak memungkinkan penambahan kontak sebanyak ini, seluruh permintaan akan ditolak di awal dengan `403`. Jika batas tercapai di tengah proses, catatan yang tersisa akan dikembalikan sebagai dilewati dengan alasan `contact_limit_reached`.

---

## Impor kontak dari file CSV

Untuk impor yang lebih besar dari yang didukung oleh [impor massal](#bulk-import-contacts) (hingga sekitar 50.000 baris), antrekan pekerjaan impor asinkron terhadap file CSV yang sudah ada di penyimpanan akun Anda, lalu lakukan polling hingga selesai.

### Memulai impor

`POST /contacts/import-csv`

| Field | Wajib | Deskripsi |
|---|---|---|
| `csvStoragePath` | Ya | Jalur penyimpanan file CSV, di bawah `users/{your account id}/imports/`, diakhiri dengan `.csv`. |
| `listName` | Ya | Membuat (atau menggunakan kembali) daftar dengan nama ini dan menambahkan setiap kontak yang diimpor ke dalamnya. |
| `existingListRefs` | Tidak | Array ID daftar yang ada untuk juga menambahkan setiap kontak yang diimpor ke dalamnya. |
| `defaultChannel` | Tidak | Saluran yang diterapkan ke baris yang tidak menentukannya. |

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

**Respons** (`202` — impor sedang diantrekan, belum selesai)

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

> **Memasukkan file ke penyimpanan.** Endpoint ini memulai dan melacak pekerjaan impor; endpoint ini tidak menerima unggahan secara langsung. File CSV harus sudah berada di `csvStoragePath` sebelum Anda memanggilnya — pengimpor CSV dasbor itu sendiri melakukan ini sebagai langkah pertamanya.

### Polling pekerjaan impor

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

**Respons**

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

`status` bergerak melalui `queued` → `processing` → `completed`, atau `failed` dengan alasan di `error_message`. `jobId` yang tidak ada di akun Anda akan mengembalikan `404`.

---

## Ekspor kontak

Memulai ekspor CSV asinkron dari kontak Anda dan mengembalikan pekerjaan yang dapat Anda polling untuk penyelesaiannya.

### Memulai ekspor

`POST /contacts/export`

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `listId` | Tidak | Hanya ekspor kontak yang termasuk dalam daftar ini. |
| `contactIds` | Tidak | Hanya ekspor ID kontak spesifik ini. |

Jika keduanya dikosongkan, semua kontak di akun Anda akan diekspor.

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

**Respons** (`202` — ekspor sedang dalam antrean)

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

### Memeriksa status pekerjaan ekspor

`GET /contacts/export/{jobId}`

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

**Respons**

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

> Setelah `status` berstatus `"completed"`, Anda akan mendapatkan `export_id` dan `contact_count`. Pengunduhan file CSV yang dihasilkan dilakukan melalui halaman Ekspor di dasbor Anda.

---

## Kirim pesan ke kontak

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

Mengirim pesan ke kontak yang sudah ada di saluran mana pun yang sedang mereka gunakan. Pesan akan diantrekan dan dikirim di latar belakang — respons mengonfirmasi bahwa pesan telah diterima, bukan bahwa pesan tersebut sudah terkirim.

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `body` | Ya | Teks pesan yang akan dikirim. |
| `mediaUrl` | Tidak | URL file media untuk dilampirkan. |
| `mediaContentType` | Tidak | Tipe MIME dari media yang dilampirkan (contoh: `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"])
```

**Respons**

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

> **Tidak dapat mengirim sekarang?** Jika kontak mengaktifkan mode jangan ganggu atau mode pribadi, atau tidak berada di saluran yang dapat menerima pesan keluar, permintaan akan ditolak dengan `422` dan `error` yang menjelaskan alasannya.

Untuk mengirim melalui nomor telepon, ID Instagram, atau identitas saluran lainnya alih-alih ID kontak — dan untuk informasi lebih lanjut tentang pengiriman pesan secara umum — lihat [Messages API](messages.md).

---

## Menetapkan agen AI ke kontak

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

Memindahkan percakapan yang sudah ada ke agen AI yang berbeda, mulai dari pesan berikutnya. Tindakan ini sama dengan **Tetapkan Agen AI** di menu obrolan, dan langkah yang sama yang digunakan oleh tindakan **Tetapkan agen atau kampanye AI** dalam Otomatisasi.

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `agentId` | Ya | ID agen AI yang harus mengambil alih, atau `null` untuk menghapus penetapan agar percakapan kembali ke kotak masuk tim Anda. |
| `triggerAIResponse` | Tidak | `true` membuat agen yang baru ditetapkan langsung membalas pesan kontak yang belum terjawab. Default-nya adalah `false`. |

> **Berhati-hatilah dengan `triggerAIResponse: true`** — ini mengirim pesan ke kontak saat itu juga, jadi gunakan hanya jika Anda ingin mereka dikirimi pesan sekarang. Di Messenger dan Instagram, pesan tersebut gagal jika kontak terakhir menulis kepada Anda lebih dari 24 jam yang lalu.

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

**Respons**

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

> Agen harus berasal dari akun yang sama dengan kontak; jika tidak, permintaan akan ditolak dengan `404` atau `403`. Temukan ID agen di halaman Agen AI (URL setiap agen diakhiri dengan ID-nya).

---

## Menetapkan agen AI ke banyak kontak

`POST /contacts/bulk-assign-agent`

Memindahkan banyak percakapan ke agen AI yang berbeda dalam satu panggilan — atau menghapus penetapan untuk semuanya dengan `null`. Ini murni perubahan perutean: **tidak ada pesan yang dikirim dan agen tidak membalas siapa pun**. Setiap kontak cukup mendapatkan agen baru saat mereka menulis pesan berikutnya. (Itulah sebabnya tidak ada `triggerAIResponse` di sini.)

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `agentId` | Ya | Agen AI yang harus mengambil alih, atau `null` untuk menghapus penetapan. |
| `contactIds` | Salah satu dari tiga | Hingga 500 ID kontak untuk dipindahkan. |
| `filter` | Salah satu dari tiga | Pilih kontak di server alih-alih mencantumkannya, yang terbaru terlebih dahulu. Menggunakan kunci yang sama dengan filter endpoint hitung: `agentId` (atau `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | Salah satu dari tiga | Objek aturan daftar cerdas — lihat [Bentuk `smart_rules`](#the-smart_rules-shape). |
| `limit` | Tidak | Berapa banyak kontak yang akan dipindahkan dalam panggilan ini saat Anda memilih dengan `filter` atau `rules`. 1 hingga 500, defaultnya 500. |

Kirim tepat satu dari `contactIds`, `filter`, atau `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"])
```

**Respons**

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

`matched` adalah berapa banyak kontak yang ditemukan oleh pilihan tersebut secara total, `updated` berapa banyak yang dipindahkan oleh panggilan ini, `skipped` berapa banyak ID yang Anda kirim tidak ditemukan di akun Anda, dan `remaining` berapa banyak yang masih cocok sekarang setelah panggilan ini selesai.

**Memindahkan semua orang.** Karena satu panggilan memindahkan paling banyak 500 kontak, grup besar memerlukan beberapa panggilan. Gunakan filter yang berhenti mencocokkan kontak setelah dipindahkan — misalnya `filter: { "agentId": "agent_abc123" }` saat menetapkan ke `agent_xyz789` — dan ulangi panggilan yang sama persis hingga `remaining` kembali sebagai `0`. Saat Anda memberikan `contactIds` sebagai gantinya, `remaining` selalu `0`.

---

## Menetapkan kontak ke departemen

`POST /contacts/{contactId}/department`

"Tetapkan prospek ini ke Penjualan" — mengarsipkan kontak di bawah departemen tertentu dan, secara default, menyerahkannya kepada siapa pun di departemen tersebut yang saat ini memiliki kontak paling sedikit. Ini terpisah dari [menetapkan agen AI](#assign-an-ai-agent-to-a-contact): departemen menjawab "tim mana yang memiliki ini," agen menjawab "AI mana yang menjawab ini," dan menetapkan salah satunya tidak akan pernah menghapus yang lain.

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `department_id` | Ya | Departemen tempat kontak akan diarsipkan. Berikan `null` untuk menghapusnya. |
| `hand_to_member` | Tidak | Juga serahkan kontak kepada orang dengan beban kerja paling sedikit di departemen tersebut. Default-nya adalah `true`. Tidak akan pernah menetapkan ulang kontak yang sudah dimiliki seseorang. |

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

**Respons**

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

`assigned_to` bernilai `null` jika kontak tersebut sudah dimiliki oleh seseorang, atau Anda memberikan `hand_to_member: false`.

---

## Menautkan kontak lintas saluran

"Lanjutkan di WhatsApp" (atau SMS) akan mencari atau membuat kontak orang ini di saluran berbasis telepon lain dan menautkan keduanya, sehingga aplikasi mengenali mereka sebagai orang yang sama.

### Tautkan ke saluran lain

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

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `channel` | Ya | Saluran untuk ditautkan. Salah satu dari `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | Tidak | Nomor telepon yang digunakan pada saluran baru. Default-nya adalah nomor kontak sumber itu sendiri. |

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

**Respons**

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

`created` memberi tahu Anda apakah kontak baru dibuat untuk saluran target atau kontak yang sudah ada ditemukan dan ditautkan. Memanggil ini untuk kedua kalinya aman — fungsi ini mengembalikan `contact_id` yang sama dengan `created: false` alih-alih membuat duplikat.

`422` berarti akun tidak dapat melakukan penautan ini saat ini: kontak sudah berada di keluarga saluran tersebut, tidak memiliki nomor telepon untuk digunakan, atau tidak ada pengirim yang terhubung untuk saluran target. `409` berarti kedua kontak sudah ditautkan ke dua orang yang berbeda — batalkan tautan salah satunya terlebih dahulu.

### Mencantumkan percakapan yang ditautkan dari kontak

`GET /contacts/{contactId}/linked`

Mengembalikan percakapan lain yang merupakan orang yang sama dengan kontak ini. Kontak yang tidak ditautkan akan mengembalikan array kosong, bukan `404` — "orang ini tidak memiliki saluran lain" adalah status yang normal.

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

**Respons**

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

### Batalkan tautan kontak

`DELETE /contacts/{contactId}/link`

Menghapus kontak ini dari orangnya, secara sepihak — kontak lain yang masih ditautkan ke orang tersebut tetap mempertahankan tautannya, jadi membatalkan tautan satu dari tiga kontak tidak akan membubarkan grup tersebut.

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

**Respons**

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

---

## Mengambil foto profil kontak

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

Mengambil (dan menyimpan dalam cache) foto profil WhatsApp atau Meta kontak sesuai permintaan — foto yang sama dikembalikan sebagai `avatarUrl` pada [Dapatkan kontak](#get-a-contact-by-phone-or-email), yang diperbarui.

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

**Respons**

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

`cached: true` berarti URL berasal dari pengambilan terbaru, bukan pencarian penyedia yang baru — foto disimpan dalam cache selama 7 hari, dan kontak yang menurut penyedia tidak memiliki foto yang dapat dijangkau akan disimpan dalam cache sebagai tidak tersedia selama 24 jam. Jika tidak ada foto untuk diambil, `avatar_url` dihilangkan dan `message` menjelaskan alasannya.

---

## Menandai kontak secara otomatis dengan AI

Menjalankan aturan tag akun Anda pada riwayat percakapan lengkap satu atau beberapa kontak dan menerapkan (atau menghapus) tag persis seperti penandaan waktu nyata yang berjalan selama obrolan langsung — aturan yang sama, biaya kredit per tag yang sama.

### Memulai proses

`POST /contacts/auto-tag`

| Bidang | Wajib | Deskripsi |
|---|---|---|
| `scope` | Ya | `"contacts"` untuk menandai kontak tertentu, atau `"agent"` untuk menandai setiap percakapan yang saat ini ditangani oleh satu agen AI. |
| `contact_ids` | Wajib jika `scope` adalah `"contacts"` | Larik ID kontak, 1 hingga 500. |
| `agent_id` | Wajib jika `scope` adalah `"agent"` | Agen AI yang percakapannya akan ditandai. Jika `scope` adalah `"contacts"`, bidang ini bersifat opsional dan hanya mempersempit aturan tag agen mana yang dijalankan. |

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

**Satu** kontak dijalankan secara inline dan langsung mengembalikan hasilnya:

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

**Dua atau lebih** kontak (atau `scope: "agent"`) dijalankan sebagai tugas latar belakang dan langsung mengembalikan `202`:

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

### Melakukan polling proses

`GET /contacts/auto-tag/run`

Mengembalikan proses akun saat ini (atau yang terbaru), sehingga Anda dapat melakukan polling kemajuan tanpa harus melacak `run_id` sendiri.

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

**Respons**

```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` adalah `null` jika akun belum pernah memulai satu proses pun. `status` berpindah dari `"running"` ke `"completed"` atau `"failed"`.

Hanya satu proses massal yang dapat berjalan per akun dalam satu waktu — memulai proses kedua saat proses lain sedang berjalan akan mengembalikan `409` dengan `error_code: "auto_tag_run_in_progress"`. Kehabisan kredit pada proses satu kontak akan mengembalikan `402` dengan `error_code: "insufficient_credits"`; proses massal justru akan berhenti lebih awal dan melaporkan sejauh mana proses tersebut berjalan di `run`.

---

## Menghapus kontak

`DELETE /contacts/{contactId}`

Menghapus satu kontak secara permanen berdasarkan ID, beserta riwayat pesannya. **Tindakan ini tidak dapat dibatalkan.** Untuk menghapus beberapa kontak dalam satu panggilan, gunakan [Hapus kontak](#delete-contacts) di bawah ini.

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

**Respons**

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

ID kontak yang tidak ada di akun Anda, atau milik akun yang berbeda, akan mengembalikan `404`.

---

## Menghapus kontak

`DELETE /contacts`

Menghapus satu atau beberapa kontak secara permanen berdasarkan ID dalam satu panggilan (hingga 500 ID). ID yang tidak ada di akun Anda akan dilewati dan dihitung dalam `skipped`. **Tindakan ini tidak dapat dibatalkan.**

| Bidang | Deskripsi |
|---|---|
| `contactIds` | Larik ID kontak yang akan dihapus (maks 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']}")
```

**Respons**

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

---

## Menghapus bidang kustom

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

Menghapus satu kunci bidang kustom dari **setiap** kontak di akun Anda. Gunakan ini untuk membersihkan data setelah mengganti nama atau menonaktifkan bidang kustom. Kunci hanya boleh berisi huruf, angka, garis bawah, dan tanda hubung. Mengembalikan jumlah kontak yang diperbarui. **Tindakan ini tidak dapat dibatalkan.**

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

**Respons**

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

::: note
**Catatan:** Kunci kolom dengan karakter yang tidak didukung akan mengembalikan `400`.
:::


---

## Daftar

Daftar mengelompokkan kontak. Daftar bisa berupa **statis** (Anda yang menentukan siapa yang ada di dalamnya) atau **cerdas** (keanggotaan dihitung berdasarkan aturan dan diperbarui secara otomatis — lihat [Mengatur Daftar & Kontak](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Bidang | Deskripsi |
|---|---|
| `name` | Wajib diisi saat pembuatan. Maksimal 100 karakter. |
| `status` | `live` (default) atau `draft`. Huruf kecil. |
| `contact_ids` | Larik ID kontak untuk dimasukkan ke dalam daftar. **Hanya untuk daftar statis.** |
| `type` | `static` (default) atau `smart`. |
| `smart_rules` | Kumpulan aturan — wajib diisi saat `type` adalah `smart`. Lihat di bawah. |

### Membuat daftar

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

**Respons**

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

Daftar cerdas dievaluasi **secara inline**, dalam permintaan yang sama, sehingga `evaluation` memberi tahu Anda dengan tepat siapa saja yang masuk ke dalamnya. Pada daftar statis, `evaluation` adalah `null`.

### Memperbarui daftar

`PUT /lists/{listId}`

Kirim hanya bidang yang ingin Anda ubah. Mengubah `smart_rules` akan mengevaluasi ulang daftar secara langsung dan mengembalikan objek `evaluation` yang sama.

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

Anda dapat mengubah jenis daftar di antara keduanya:

- **Statis → cerdas**: kirim `{ "type": "smart", "smart_rules": { … } }`. Aturan akan langsung diterapkan.
- **Cerdas → statis**: kirim `{ "type": "static" }`. Aturan akan dihapus dan siapa pun yang ada di dalam daftar akan tetap berada di sana.

### Bentuk `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` (setiap kondisi harus benar) atau `any` (setidaknya satu).
- `conditions` — 1 hingga 20 kondisi, masing-masing maksimal 100 nilai, string hingga 200 karakter.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | larik ID tag |
| `lists` | `in_any`, `not_in_any` | larik ID daftar (**hanya daftar statis** — daftar cerdas tidak dapat dibuat dari daftar cerdas lainnya) |
| `channel` | `is_any`, `is_none` | larik saluran |
| `status` | `is_any`, `is_none` | larik status kontak |
| `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" }` |
| kolom tanggal yang sama | `before`, `after` | tanggal ISO (`"2026-01-01"`, dibandingkan sebagai hari penuh) atau tanggal-waktu ISO lengkap (`"2026-01-01T14:30:00Z"`, dibandingkan hingga momen yang tepat) |
| kolom tanggal yang sama | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` mencocokkan kontak yang pernah dikirimi pesan oleh AI setidaknya sekali (kapan pun) |
| `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` | string untuk formulir `contains` |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | larik ID untuk formulir `is_any` / `is_none` |
| `custom_field` (ditambah `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | string untuk formulir nilai |

`not_within_last` juga mencocokkan kontak yang tanggalnya tidak pernah ditetapkan ("lebih dari N yang lalu, **atau tidak pernah**"), dan perbandingan teks mengabaikan huruf besar/kecil.

**Keterlibatan AI.** `has_interacted_with_ai` adalah penanda seumur hidup: `true` untuk setiap kontak yang telah dikirimi setidaknya satu pesan oleh AI Anda, `false` untuk yang lainnya (termasuk kontak yang hanya pernah dijawab oleh tim Anda). Penanda ini dibubuhkan pada pesan pertama AI ke kontak dan tidak pernah dihapus, jadi mematikan balasan AI untuk kontak tersebut atau memindahkan mereka ke kampanye lain tidak akan meresetnya. Untuk *periode* — "kontak yang ditangani AI saya bulan ini", pertanyaan penagihan yang umum — gunakan rentang `last_ai_interaction_at` sebagai gantinya:

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

Jangan tertukar dengan `is_bot_active` (AI *diizinkan* untuk membalas, bukan berarti sudah membalas) atau `has_ever_responded` (kontak *membalas*, kepada siapa pun). Kedua penanda yang sama dikembalikan pada setiap kontak sebagai `first_ai_interaction_at` / `last_ai_interaction_at`, dan seluruh rangkaian aturan juga berfungsi pada `GET /contacts?rules=`, sehingga Anda dapat menghitung kecocokan tanpa membuat daftar.

### Pratinjau kumpulan aturan

`POST /lists/preview`

Menghitung dan mengambil sampel kontak yang akan dicocokkan oleh kumpulan aturan, tanpa membuat atau mengubah apa pun. Gunakan ini untuk memeriksa kewajaran aturan sebelum Anda menyimpannya.

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

**Respons**

```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` menampung hingga 10 kontak, dengan yang paling baru aktif di urutan pertama.

### Jalankan ulang daftar cerdas sekarang

`POST /lists/{listId}/evaluate`

Memaksa evaluasi ulang segera (sama dengan fungsi **Segarkan sekarang** di dasbor). Daftar cerdas sudah diperbarui saat kontak berubah, dan setiap 15 menit untuk aturan berbasis waktu, jadi ini hanya diperlukan saat Anda menginginkan hasilnya *saat ini juga*.

**Respons**

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

`evaluation.skipped: true` berarti evaluasi lain dari daftar yang sama sudah berjalan dan panggilan ini tidak melakukan apa pun.

### Daftar cerdas menolak anggota yang dipilih secara manual

Endpoint keanggotaan mengembalikan **`409`** dengan `"This is a smart list — its members are computed from its rules. Edit the rules instead."` saat daftar target adalah daftar cerdas. Ini mencakup `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` pada `POST /lists` dan `PUT /lists/{listId}`, serta memilih daftar cerdas sebagai target impor CSV. Ubah aturannya sebagai gantinya.

Memanggil `POST /lists/{listId}/evaluate` pada daftar **statis** juga merupakan `409` — daftar tersebut tidak memiliki aturan untuk dijalankan.

---

## Kesalahan API Kontak

Endpoint kontak mengembalikan amplop error standar:

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

Beberapa endpoint juga menyertakan `error_code`, yang biasanya cocok dengan status HTTP — satu-satunya pengecualian adalah kasus kontak duplikat di bawah, di mana status HTTP adalah `200` dan hanya `error_code` yang membawa `409`. Kode yang spesifik untuk endpoint kontak:

| Kode | Kapan ini terjadi pada endpoint kontak |
|---|---|
| `400` | Permintaan buruk — kolom yang hilang/tidak valid, isi kosong, kursor buruk, atau lebih dari 500 ID dalam satu batch. |
| `402` | Kredit tidak cukup untuk menyelesaikan proses penandaan AI pada satu kontak (`error_code: "insufficient_credits"`). |
| `404` | Kontak, daftar, atau tag tidak ditemukan di akun Anda. |
| `409` | Kontak dengan nomor telepon tersebut sudah ada (saat pembuatan). Dikembalikan sebagai `error_code` di dalam isi dengan status HTTP `200`, jadi buat percabangan pada `error_code` di sini. Juga dikembalikan saat proses penandaan otomatis massal sedang berlangsung (`error_code: "auto_tag_run_in_progress"`), atau saat menautkan kontak ke saluran lain yang akan menggabungkan dua kontak yang sudah ditautkan ke dua orang berbeda. |
| `422` | Kontak tidak dapat menerima pesan saat ini (jangan ganggu, pribadi, atau saluran yang tidak didukung). Pada endpoint penautan saluran, ini juga mencakup tidak ada nomor telepon, pemasangan saluran yang tidak didukung, atau tidak ada pengirim yang terhubung untuk saluran target. |

`403` pada endpoint kontak juga dapat berarti masalah batas kontak atau izin daftar, bukan akses paket. Kode bersama yang dapat dikembalikan oleh setiap endpoint — `401`, `403` (paket Anda tidak menyertakan akses API), `429` (batas kecepatan), dan `500` — tercantum beserta panduan percobaan ulang di [Kesalahan & Penomoran Halaman](errors-and-pagination.md).

---

## Langkah berikutnya

- [API Pesan](messages.md) — mengirim pesan berdasarkan identitas saluran dan mengelola percakapan.
- [Referensi API](reference.md) — daftar endpoint lengkap, termasuk tag dan daftar.
- [Akses API](../integrations/api-access.md) — autentikasi, batas kecepatan, dan penanganan error.
