
# API kontaktów

Kontakt to pojedyncza osoba, do której wysyłasz wiadomości — obejmuje jej imię, numer telefonu, adres e-mail, kanał komunikacji, tagi, pola niestandardowe oraz listy i kampanie, do których należy. API kontaktów umożliwia tworzenie kontaktów, wyszukiwanie ich, aktualizowanie, tagowanie, importowanie masowe oraz usuwanie, a wszystko to bez konieczności korzystania z panelu nawigacyjnego.

Wszystkie ścieżki na tej stronie są względne względem bazowego adresu URL:

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

Zatem `/contacts` oznacza `https://api.youraiconnector.com/v1/contacts`.

> **Dopiero zaczynasz pracę z API?** Najpierw przeczytaj [Dostęp do API](../integrations/api-access.md) — zawiera informacje o tym, jak wygenerować klucz API, trzy sposoby uwierzytelniania, limity zapytań oraz format błędów. Wszystkie informacje na tej stronie zakładają, że masz już działający klucz API.

---

## Informacje o identyfikatorach kontaktów

Każdy kontakt ma unikalny identyfikator (ID). Identyfikator, który otrzymujesz podczas **tworzenia** kontaktu (w `data.contactId`), jest tym samym identyfikatorem, którego używasz wszędzie indziej — do pobierania, aktualizowania, tagowania, wysyłania wiadomości lub usuwania tego kontaktu. Zapisz go raz i używaj ponownie.

Nie musisz tworzyć kontaktu, aby uzyskać jego identyfikator. Możesz go również wyszukać według numeru telefonu lub adresu e-mail (zobacz [Pobierz kontakt](#get-a-contact-by-phone-or-email)) lub przeglądać wszystkie swoje kontakty (zobacz [Lista kontaktów](#list-contacts)). Każda z tych metod zwraca ten sam identyfikator.

---

## Tworzenie kontaktu

`POST /contacts`

Dodaje nowy kontakt do Twojego konta. **Wymagany jest numer telefonu z kodem kraju** — sam adres e-mail nie wystarczy. Wszystkie pozostałe pola są opcjonalne.

Możesz opcjonalnie dodać nowy kontakt bezpośrednio do jednej lub wielu list za pomocą `listId` (pojedyncza lista) lub `listIds` (tablica). Jeśli wysłane zostaną oba, `listIds` ma pierwszeństwo.

Każde pole, które wyślesz, a które nie jest jednym ze standardowych pól tworzenia wymienionych w poniższej tabeli pól **Tworzenie kontaktu** (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`), jest automatycznie zapisywane jako **pole niestandardowe** — dzięki czemu płaska struktura danych z narzędzi takich jak Make czy Zapier działa bez konieczności zagnieżdżania. Możesz również przekazać jawny obiekt `custom_fields`.

| Pole | Wymagane | Opis |
|---|---|---|
| `phoneNumber` | Tak | Numer telefonu kontaktu z kodem kraju (np. `+15551234567`). |
| `firstName` | Nie | Imię. |
| `lastName` | Nie | Nazwisko. |
| `email` | Nie | Adres e-mail. |
| `channel` | Nie | Kanał komunikacji. Jedna z wartości: `whatsapp`, `sms`, `whatsapp_web`. Domyślnie `whatsapp`. |
| `is_bot_active` | Nie | Czy asystent AI odpowiada na wiadomości tego kontaktu. Domyślnie `true`. |
| `is_private` | Nie | Oznacz kontakt jako prywatny. Gdy `true`, asystent AI jest dla niego wyłączony. Domyślnie `false`. |
| `lead_profile` | Nie | Notatki tekstowe dotyczące leada. |
| `listId` | Nie | Identyfikator pojedynczej listy, do której ma zostać dodany kontakt. |
| `listIds` | Nie | Tablica identyfikatorów list, do których ma zostać dodany kontakt (ma pierwszeństwo przed `listId`). |
| `custom_fields` | Nie | Obiekt zawierający własne pola klucz/wartość. Możesz je również przekazać jako klucze najwyższego poziomu. |

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

**Odpowiedź**

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

Identyfikator nowego kontaktu znajduje się w `data.contactId`. Listy, do których został dodany, są zwracane w `data.listsAdded`.

> **Duplikaty nie są tworzone.** Jeśli kontakt o tym samym numerze telefonu już istnieje, wywołanie tworzenia **nie** utworzy go ani nie zwróci. Odpowiedź wraca z kodem statusu HTTP `200` oraz `error_code` o wartości `409` w treści, dlatego należy rozgałęziać logikę na podstawie `error_code`, a nie statusu HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Aby pracować z istniejącym kontaktem po otrzymaniu `error_code` o wartości `409`, wyszukaj go za pomocą [Pobierz kontakt według numeru telefonu lub adresu e-mail](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — i użyj ponownie zwróconego identyfikatora.

> **Równoważne zapisy numerów WhatsApp są traktowane jako ten sam numer.** Niektóre kraje mają dwa poprawne sposoby zapisu tego samego numeru telefonu komórkowego, a WhatsApp może zgłaszać dowolny z nich: Meksyk (`+52…` i starszy `+521…`), Brazylia (z dziewiątą cyfrą lub bez) oraz Argentyna (z `9` po `+54` lub bez). Sprawdzanie duplikatów podczas tworzenia i `GET /contacts?phoneNumber=` działa dla obu zapisów, więc otrzymasz istniejący kontakt niezależnie od tego, w jakiej formie go wyślesz. `phone_number` zapisany w kontakcie nigdy nie jest nadpisywany.

---

## Pobierz kontakt według numeru telefonu lub adresu e-mail

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

Wyszukuje pojedynczy kontakt i zwraca pełny, wzbogacony obiekt kontaktu — w tym jego listy, tagi i kampanie rozwiązane do par `{ id, name }`, a także ostatnią wymienioną wiadomość.

Przekaż **albo** `phoneNumber` (w formacie międzynarodowym), **albo** `email`. Jeśli nie przekażesz żadnego z nich, ten sam punkt końcowy przełączy się w tryb [Listowania kontaktów](#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"])
```

**Odpowiedź**

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

Identyfikator kontaktu jest zwracany zarówno na najwyższym poziomie (`contactId`), jak i wewnątrz obiektu (`contact.id`). Jeśli nic nie pasuje, otrzymasz `404` z `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** to zdjęcie profilowe kontaktu, pobrane z WhatsApp lub Meta, gdy użytkownik wyśle do Ciebie wiadomość. Jest ono tylko do odczytu: nie możesz go ustawić i jest ono `null` dla kontaktów, które nie mają zdjęcia lub kontaktują się przez kanał, który go nie udostępnia. Traktuj ten link jako tymczasowy, zamiast go przechowywać, ponieważ niektóre z tych linków do zdjęć wygasają i są odświeżane automatycznie. (W poniższym punkcie końcowym listy ta sama wartość nazywa się `avatar_url`.)

> **Numery telefonów w adresach URL.** Znak `+` w ciągu zapytania musi być zakodowany jako `%2B`, w przeciwnym razie zostanie odczytany jako spacja. Powyższe przykłady robią to za Ciebie.

---

## Pobierz kontakt według identyfikatora

`GET /contacts/{contactId}`

Jeśli masz już identyfikator kontaktu, pobierz go bezpośrednio. Struktura odpowiedzi jest identyczna jak w przypadku wyszukiwania powyżej.

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

Identyfikator kontaktu, który nie istnieje na Twoim koncie, zwraca `404`.

---

## Pobierz statystyki kontaktu

`GET /contacts/{contactId}/stats`

Zwraca zagregowane statystyki wiadomości dla jednego kontaktu: sumy, odpowiedzi AI kontra ludzkie, zużyte kredyty oraz znaczniki czasu pierwszej i ostatniej wiadomości.

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

**Odpowiedź**

```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` to ten sam licznik wiadomości AI, który zeruje przycisk „reset” w aplikacji dla danego kontaktu. `creditsUsed` to bieżąca suma kredytów dla tego kontaktu, a nie tylko liczby z tej odpowiedzi. Identyfikator kontaktu, który nie istnieje na Twoim koncie, zwraca `404`.

---

## Listowanie kontaktów

`GET /contacts`

Wywołaj `GET /contacts` **bez** `phoneNumber` ani `email`, aby przeglądać wszystkie kontakty, zaczynając od najnowszych. Każda strona zwraca skrócone podsumowania kontaktów (listy, tagi i kampanie są zwracane jako tablice identyfikatorów, a nie pełne obiekty) oraz `next_cursor`.

| Parametr zapytania | Opis |
|---|---|
| `limit` | Rozmiar strony. Domyślnie 50, maksymalnie 100. |
| `cursor` | Wartość `next_cursor` z poprzedniej strony. Pomiń ją na pierwszej stronie. |
| `listId` | Opcjonalne. Zwróć tylko kontakty należące do tej listy. |

Aby przejść przez wszystkie strony: wykonaj pierwsze wywołanie bez kursora, a następnie przekazuj zwrócony `next_cursor` jako `cursor`. **Zatrzymaj się, gdy `next_cursor` będzie `null`** — oznacza to, że nie ma więcej wyników.

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

**Odpowiedź**

```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
**Uwaga:** Filtrowanie według `listId`, który nie istnieje na Twoim koncie, zwraca `404`. Nieprawidłowy `cursor` zwraca `400`.
:::


---

## Liczba kontaktów

`GET /contacts/count`

Zwraca liczbę kontaktów pasujących do filtra oraz podział na kanały, bez konieczności stronicowania. Jest to właściwe wywołanie dla każdego pytania typu „ile” — kafelka na pulpicie nawigacyjnym, automatyzacji lub zapytania do Champa. Wszystkie filtry są opcjonalne, a łączenie kilku z nich zawęża wynik (kontakt musi spełniać każdy z nich).

| Parametr zapytania | Opis |
|---|---|
| `agentId` | Tylko kontakty przypisane do tego agenta AI. Przekaż `none` dla kontaktów bez przypisanego agenta (są one obsługiwane przez domyślnego agenta kanału). |
| `channel` | Tylko kontakty na tym kanale, np. `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Tylko kontakty z tą etykietą, według **nazwy** etykiety (wielkość liter nie ma znaczenia). Nazwa etykiety, której nie posiadasz, zwróci `404`. |
| `listId` | Tylko kontakty na tej liście. |
| `botActive` | `true` lub `false` — tylko kontakty, których asystent AI jest włączony lub wyłączony. |
| `status` | Tylko kontakty o tym statusie, np. `Lead`. |
| `rules` | Obiekt reguł JSON zakodowany w formacie URL, używający tego samego kształtu co lista inteligentna (zobacz [Kształt `smart_rules`](#the-smart_rules-shape) poniżej). Nie można łączyć z innymi filtrami. |

Jeśli nie wyślesz żadnego filtra, otrzymasz całkowitą liczbę kontaktów na swoim koncie.

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

**Odpowiedź**

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

`by_channel` dzieli tę samą sumę według kanałów; kontakty, które nie znajdują się na żadnym kanale, są liczone w `none`. `filters` zwraca zastosowane filtry, dzięki czemu możesz sprawdzić, czy wywołanie zadziałało zgodnie z oczekiwaniami.

::: note
**Uwaga:** Wysłanie `rules` wraz z jakimkolwiek innym filtrem lub wartością `rules`, która nie jest poprawnym kodem JSON, zwróci `400`. Nazwa etykiety lub identyfikator listy, które nie istnieją na Twoim koncie, zwrócą `404`.
:::


---

## Aktualizacja kontaktu

`PUT /contacts/{contactId}`

Aktualizuje istniejący kontakt. Zmieniane są tylko uwzględnione pola — pomiń wszystko, czego nie chcesz zmieniać. Musisz wysłać co najmniej jedno pole, w przeciwnym razie otrzymasz `400` („Brak pól do aktualizacji”).

| Pole | Opis |
|---|---|
| `firstName` | Imię. |
| `lastName` | Nazwisko. |
| `email` | Adres e-mail. |
| `is_bot_active` | Czy asystent AI odpowiada na wiadomości od tego kontaktu. |
| `is_private` | Oznacz jako prywatny. Ustawienie tej wartości na `true` powoduje również wyłączenie asystenta AI. |
| `do_not_disturb` | Wstrzymaj zautomatyzowaną komunikację z tym kontaktem. Powoduje również zatrzymanie odpowiedzi AI. |
| `follow_ups_disabled` | Zatrzymaj wszystkie zautomatyzowane działania następcze dla tego kontaktu (szybkie, cykliczne i zimne leady), podczas gdy AI nadal odpowiada na wysyłane przez niego wiadomości. Przydatne po dokonaniu zakupu. Pozostaje wyłączone, dopóki nie ustawisz tego z powrotem na `false`. |
| `lead_profile` | Notatki dotyczące leada w formie dowolnego tekstu. |
| `custom_fields` | Obiekt pól niestandardowych. **Scalane według klucza** — zapisywane są tylko przesłane klucze, reszta istniejących pól niestandardowych zostaje zachowana. Możesz również przekazać klucze pól niestandardowych na najwyższym poziomie. |

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

**Odpowiedź**

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

> **Pola niestandardowe są scalane, a nie zastępowane.** Wysłanie `{ "custom_fields": { "tier": "gold" } }` ustawia tylko `tier` — wszelkie inne pola niestandardowe w kontakcie pozostają bez zmian. Aby całkowicie usunąć pole niestandardowe ze wszystkich kontaktów, użyj [Usuń pole niestandardowe](#delete-a-custom-field).

---

## Dodawanie lub usuwanie tagów

`POST /contacts/{contactId}/tags`

Dodaje i/lub usuwa tagi dla pojedynczego kontaktu w jednym wywołaniu. Przekaż **identyfikatory** tagów w `addTagIds` i `removeTagIds`. Co najmniej jedno z tych pól musi być niepuste.

Tagi muszą już istnieć na Twoim koncie — utwórz je najpierw za pomocą [punktu końcowego tagów](reference.md). Jeśli kontakt lub jakikolwiek powiązany tag nie istnieje, otrzymasz `404`.

| Pole | Opis |
|---|---|
| `addTagIds` | Tablica identyfikatorów tagów do dodania do kontaktu. |
| `removeTagIds` | Tablica identyfikatorów tagów do usunięcia z kontaktu. |

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

**Odpowiedź**

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

---

## Zarządzaj biblioteką tagów

Te punkty końcowe służą do zarządzania samym tagiem — jego zmianą nazwy lub usunięciem z Twojego konta — w przeciwieństwie do przypisywania lub usuwania tagu u konkretnego kontaktu (zobacz [Dodawanie lub usuwanie tagów](#add-or-remove-tags) powyżej). Każdy tag na Twoim koncie ma identyfikator (`tagId`): ten widoczny w menedżerze tagów w panelu nawigacyjnym oraz ten zwracany jako `data.tag_id` podczas tworzenia tagu za pomocą `POST /tags` i treści JSON `{ "name": "..." }` (bez `phoneNumber`, `email` lub `contactId`).

### Aktualizuj tag

`PUT /tags/{tagId}`

Wyślij tylko te pola, które zmieniasz.

| Pole | Opis |
|---|---|
| `name` | Nazwa tagu. |

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

**Odpowiedź**

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

`tagId`, który nie istnieje na Twoim koncie, zwraca `404`.

### Usuń tag

`DELETE /tags/{tagId}`

Usuwa jeden tag według identyfikatora. **Tej operacji nie można cofnąć** — kontakty posiadające ten tag po prostu go tracą. Usunięcie tagu, którego już nie ma (lub nigdy nie istniał), zwraca `200` z `deleted: 0` zamiast `404`, ponieważ nie ma czego wyliczać.

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

**Odpowiedź**

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

### Usuń kilka tagów jednocześnie

`DELETE /tags`

| Pole | Opis |
|---|---|
| `tagIds` | Tablica identyfikatorów tagów do usunięcia (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"] }'
```

**Odpowiedź**

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

Identyfikatory, które nie istnieją lub należą do innego konta, są pomijane bez powiadomienia i nie są wliczane do `deleted`.

---

## Masowe ustawianie flagi

`POST /contacts/bulk-flag`

Ustawia jedną flagę logiczną dla wielu kontaktów jednocześnie. Maksymalnie 500 identyfikatorów kontaktów na żądanie. Identyfikatory, które nie istnieją na Twoim koncie, są pomijane i zliczane w `skipped`.

| Pole | Opis |
|---|---|
| `contactIds` | Tablica identyfikatorów kontaktów do zaktualizowania (maks. 500). |
| `field` | Flaga do ustawienia. Jedna z `bot_active` (asystent AI włączony/wyłączony), `dnd` (wstrzymaj zautomatyzowany kontakt), `spam`, `private`. |
| `value` | Wartość logiczna, na którą ma zostać ustawiona flaga. |

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

**Odpowiedź**

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

---

## Masowy import kontaktów

`POST /contacts/import`

Tworzy do 500 kontaktów w jednym wywołaniu z tablicy JSON. Każdy rekord wymaga `phone_number` w formacie międzynarodowym; wszystko inne jest opcjonalne. Rekordy z nieprawidłowymi numerami telefonów lub nieobsługiwanymi kanałami są **pomijane** (nie są tworzone), a każdy pominięty rekord jest raportowany wraz z indeksem i powodem — dzięki temu możesz poprawić tylko te, które nie zostały przetworzone, i ponowić próbę.

Numery telefonów, które już istnieją na Twoim koncie, są domyślnie pomijane jako `duplicate`. Wyślij `updateExisting: true`, aby zamiast tego **zaktualizować** te kontakty: pola obecne w rekordzie nadpisują dane kontaktu (`first_name`, `last_name`, `email`, `lead_profile` i `custom_fields` są scalane klucz po kluczu), `tags` są dodawane, a kontakt jest dodawany do `listId`. Kanał, numer telefonu oraz flagi bota nigdy nie są zmieniane w istniejącym kontakcie.

Możesz opcjonalnie dodać każdy zaimportowany (lub zaktualizowany) kontakt do listy za pomocą `listId`, ustawić `defaultChannel` dla rekordów, które go nie określają, oraz otagować rekordy za pomocą `tags` (nazwy tagów — brakujące tagi są tworzone, istniejące są dopasowywane bez uwzględniania wielkości liter).

**Pola najwyższego poziomu**

| Pole | Wymagane | Opis |
|---|---|---|
| `contacts` | Tak | Tablica rekordów kontaktów (maks. 500). |
| `listId` | Nie | Lista, do której ma zostać dodany każdy zaimportowany (i zaktualizowany) kontakt. Musi być listą na Twoim koncie. |
| `defaultChannel` | Nie | Kanał zastosowany do rekordów, które pomijają `channel`. Jeden z `whatsapp`, `sms`, `whatsapp_web`. Domyślnie `whatsapp`. |
| `updateExisting` | Nie | `true`, aby zaktualizować kontakty, których numer telefonu już istnieje, zamiast pomijać je jako `duplicate`. Domyślnie `false`. |

**Pola dla poszczególnych rekordów**

| Pole | Wymagane | Opis |
|---|---|---|
| `phone_number` | Tak | Numer telefonu w formacie międzynarodowym (wiodący `+` jest dodawany, jeśli go brakuje). |
| `first_name` | Nie | Imię. |
| `last_name` | Nie | Nazwisko. |
| `email` | Nie | Adres e-mail. |
| `channel` | Nie | Jeden z `whatsapp`, `sms`, `whatsapp_web`. W razie braku używa `defaultChannel`. |
| `is_bot_active` | Nie | Czy asystent AI odpowiada. Domyślnie `true`. |
| `is_private` | Nie | Oznacz jako prywatny. Domyślnie `false`. |
| `lead_profile` | Nie | Notatki o leadzie w formie dowolnego tekstu. |
| `custom_fields` | Nie | Obiekt z kluczami i wartościami pól niestandardowych. |
| `tags` | Nie | Tablica nazw tagów (działa również pojedynczy ciąg `"a; b"`). Tagi, które nie istnieją, są tworzone; istniejące są dopasowywane bez uwzględniania wielkości liter. Maks. 25 na rekord. |

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

**Odpowiedź**

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

Jeśli niektóre rekordy nie mogą zostać utworzone, pojawią się w `skipped` wraz z powodem (tutaj bez `updateExisting`, więc istniejący numer jest pomijany):

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

Z `updateExisting: true` to samo żądanie raportuje istniejący kontakt w sekcji `updated` / `updated_contact_ids`.

Możliwe przyczyny pominięcia: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Limity planu.** Jeśli limit kontaktów w Twoim planie nie pozwala na dodanie tak dużej liczby nowych kontaktów, całe żądanie zostanie odrzucone na wstępie z kodem `403`. Jeśli limit zostanie osiągnięty w trakcie przetwarzania, pozostałe rekordy zostaną zwrócone jako pominięte z przyczyną `contact_limit_reached`.

---

## Importowanie kontaktów z pliku CSV

W przypadku importów większych niż obsługuje [import masowy](#bulk-import-contacts) (do około 50 000 wierszy), należy dodać asynchroniczne zadanie importu dla pliku CSV znajdującego się już w pamięci masowej konta, a następnie odpytywać o jego status do momentu zakończenia.

### Rozpoczęcie importu

`POST /contacts/import-csv`

| Pole | Wymagane | Opis |
|---|---|---|
| `csvStoragePath` | Tak | Ścieżka w pamięci masowej do pliku CSV, w ramach `users/{your account id}/imports/`, kończąca się na `.csv`. |
| `listName` | Tak | Tworzy (lub ponownie wykorzystuje) listę o tej nazwie i dodaje do niej każdy zaimportowany kontakt. |
| `existingListRefs` | Nie | Tablica identyfikatorów istniejących list, do których również należy dodać każdy zaimportowany kontakt. |
| `defaultChannel` | Nie | Kanał przypisany do wierszy, które go nie określają. |

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

**Odpowiedź** (`202` — import został dodany do kolejki, jeszcze nie zakończony)

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

> **Umieszczanie pliku w pamięci masowej.** Ten punkt końcowy uruchamia i śledzi zadanie importu; sam w sobie nie przyjmuje przesyłanego pliku. Plik CSV musi już znajdować się w `csvStoragePath` przed wywołaniem tego punktu — własny importer CSV w panelu nawigacyjnym wykonuje to jako pierwszy krok.

### Odpytywanie zadania importu

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

**Odpowiedź**

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

`status` przechodzi przez etapy `queued` → `processing` → `completed` lub `failed` z podaniem przyczyny w `error_message`. `jobId`, który nie istnieje na Twoim koncie, zwraca `404`.

---

## Eksportowanie kontaktów

Uruchamia asynchroniczny eksport kontaktów do pliku CSV i zwraca zadanie, którego status należy sprawdzać w celu potwierdzenia zakończenia.

### Rozpocznij eksport

`POST /contacts/export`

| Pole | Wymagane | Opis |
|---|---|---|
| `listId` | Nie | Eksportuj tylko kontakty należące do tej listy. |
| `contactIds` | Nie | Eksportuj tylko te konkretne identyfikatory kontaktów. |

Pozostawienie obu pól pustych spowoduje wyeksportowanie wszystkich kontaktów na Twoim koncie.

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

**Odpowiedź** (`202` — eksport został dodany do kolejki)

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

### Sprawdź status zadania eksportu

`GET /contacts/export/{jobId}`

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

**Odpowiedź**

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

> Gdy `status` będzie `"completed"`, otrzymasz `export_id` oraz `contact_count`. Pobraniu wygenerowanego pliku CSV odbywa się ze strony Eksporty w Twoim panelu nawigacyjnym.

---

## Wyślij wiadomość do kontaktu

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

Wysyła wiadomość do istniejącego kontaktu za pośrednictwem kanału, z którego już korzysta. Wiadomość jest kolejkowana i dostarczana w tle — odpowiedź potwierdza jedynie przyjęcie żądania, a nie fakt dostarczenia wiadomości.

| Pole | Wymagane | Opis |
|---|---|---|
| `body` | Tak | Treść wysyłanej wiadomości. |
| `mediaUrl` | Nie | URL pliku multimedialnego do załączenia. |
| `mediaContentType` | Nie | Typ MIME załączonych mediów (np. `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"])
```

**Odpowiedź**

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

> **Nie można teraz wysłać?** Jeśli kontakt ma włączony tryb „nie przeszkadzać” lub tryb prywatny, albo nie korzysta z kanału, który może odbierać wiadomości wychodzące, żądanie zostanie odrzucone z kodem `422` oraz wyjaśnieniem w polu `error`.

Aby wysłać wiadomość za pomocą numeru telefonu, identyfikatora Instagram lub innego identyfikatora kanału zamiast identyfikatora kontaktu — oraz aby dowiedzieć się więcej o przesyłaniu wiadomości — zobacz [Messages API](messages.md).

---

## Przypisz agenta AI do kontaktu

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

Przenosi istniejącą konwersację do innego agenta AI, począwszy od następnej wiadomości. Jest to to samo, co **Przypisz agenta AI** w menu czatu, oraz ten sam krok, którego używa akcja **Przypisz agenta AI lub kampanię** w Automatyzacjach.

| Pole | Wymagane | Opis |
|---|---|---|
| `agentId` | Tak | Identyfikator agenta AI, który ma przejąć konwersację, lub `null`, aby usunąć przypisanie, dzięki czemu konwersacja wróci do skrzynki odbiorczej zespołu. |
| `triggerAIResponse` | Nie | `true` sprawia, że nowo przypisany agent natychmiast odpowiada na ostatnie nieodebrane wiadomości kontaktu. Wartość domyślna to `false`. |

> **Zachowaj ostrożność przy `triggerAIResponse: true`** — wysyła on wiadomość do kontaktu natychmiast, więc używaj go tylko wtedy, gdy chcesz, aby wiadomość została dostarczona teraz. W przypadku Messengera i Instagrama wiadomość nie zostanie wysłana, jeśli kontakt ostatnio napisał do Ciebie ponad 24 godziny temu.

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

**Odpowiedź**

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

> Agent musi należeć do tego samego konta co kontakt; w przeciwnym razie żądanie zostanie odrzucone z błędem `404` lub `403`. Identyfikatory agentów można znaleźć na stronie Agentów AI (adres URL każdego agenta kończy się jego identyfikatorem).

---

## Przypisz agenta AI do wielu kontaktów

`POST /contacts/bulk-assign-agent`

Przenosi wiele konwersacji do innego agenta AI w jednym wywołaniu — lub usuwa przypisanie dla nich wszystkich za pomocą `null`. Jest to czysta zmiana routingu: **żadna wiadomość nie jest wysyłana, a agent nikomu nie odpowiada**. Każdy kontakt po prostu otrzymuje nowego agenta, gdy napisze następnym razem. (Dlatego nie ma tutaj `triggerAIResponse`).

| Pole | Wymagane | Opis |
|---|---|---|
| `agentId` | Tak | Agent AI, który powinien przejąć obsługę, lub `null`, aby usunąć przypisanie. |
| `contactIds` | Jedno z trzech | Do 500 identyfikatorów kontaktów do przeniesienia. |
| `filter` | Jedno z trzech | Wybierz kontakty na serwerze zamiast ich wypisywania, od najnowszych. Przyjmuje te same klucze co filtry punktu końcowego liczby: `agentId` (lub `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | Jedno z trzech | Obiekt reguł listy inteligentnej — zobacz [Kształt `smart_rules`](#the-smart_rules-shape). |
| `limit` | Nie | Ile kontaktów przenieść w tym wywołaniu przy wyborze za pomocą `filter` lub `rules`. Od 1 do 500, domyślnie 500. |

Wyślij dokładnie jeden z parametrów `contactIds`, `filter` lub `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"])
```

**Odpowiedź**

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

`matched` to liczba kontaktów znalezionych w sumie, `updated` to liczba kontaktów przeniesionych tym wywołaniem, `skipped` to liczba przesłanych identyfikatorów, których nie znaleziono na koncie, a `remaining` to liczba kontaktów, które nadal pasują do kryteriów po zakończeniu wywołania.

**Przenoszenie wszystkich.** Ponieważ jedno wywołanie przenosi maksymalnie 500 kontaktów, duża grupa wymaga kilku wywołań. Użyj filtra, który przestaje pasować do kontaktu po jego przeniesieniu — na przykład `filter: { "agentId": "agent_abc123" }` podczas przypisywania do `agent_xyz789` — i powtarzaj to samo wywołanie, aż `remaining` zwróci `0`. Gdy zamiast tego przekażesz `contactIds`, `remaining` zawsze wynosi `0`.

---

## Przypisz kontakt do działu

`POST /contacts/{contactId}/department`

„Przypisz ten lead do działu sprzedaży” — przypisuje kontakt do nazwanego działu i domyślnie przekazuje go osobie w tym dziale, która ma obecnie najmniej kontaktów. Jest to oddzielne działanie od [przypisywania agenta AI](#assign-an-ai-agent-to-a-contact): dział odpowiada na pytanie „który zespół jest właścicielem tego kontaktu”, agent odpowiada na pytanie „który agent AI odpowiada za ten kontakt”, a ustawienie jednego nigdy nie usuwa drugiego.

| Pole | Wymagane | Opis |
|---|---|---|
| `department_id` | Tak | Dział, do którego ma zostać przypisany kontakt. Przekaż `null`, aby wyczyścić to pole. |
| `hand_to_member` | Nie | Dodatkowo przekaż kontakt osobie z najmniejszą liczbą zadań w tym dziale. Wartość domyślna to `true`. Nigdy nie zmienia przypisania kontaktu, który ma już właściciela. |

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

**Odpowiedź**

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

`assigned_to` jest `null`, gdy kontakt miał już właściciela lub przekazano `hand_to_member: false`.

---

## Połącz kontakt między kanałami

„Kontynuuj na WhatsApp” (lub SMS) znajduje lub tworzy kontakt tej osoby w innym kanale opartym na numerze telefonu i łączy je ze sobą, dzięki czemu reszta aplikacji rozpoznaje je jako tę samą osobę.

### Łączenie z innym kanałem

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

| Pole | Wymagane | Opis |
|---|---|---|
| `channel` | Tak | Kanał, z którym należy nawiązać połączenie. Jeden z `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | Nie | Numer telefonu używany w nowym kanale. Domyślnie jest to własny numer kontaktu źródłowego. |

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

**Odpowiedź**

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

`created` informuje, czy dla kanału docelowego utworzono nowy kontakt, czy znaleziono i połączono istniejący. Ponowne wywołanie tej metody jest bezpieczne — zwraca ten sam `contact_id` z `created: false` zamiast tworzyć duplikat.

`422` oznacza, że konto nie może obecnie wykonać tego połączenia: kontakt znajduje się już w tej rodzinie kanałów, nie ma numeru telefonu do użycia lub nie ma połączonego nadawcy dla kanału docelowego. `409` oznacza, że oba kontakty są już powiązane z dwiema różnymi osobami — najpierw należy rozłączyć jeden z nich.

### Wyświetlanie listy połączonych konwersacji kontaktu

`GET /contacts/{contactId}/linked`

Zwraca pozostałe konwersacje, które należą do tej samej osoby co dany kontakt. Rozłączony kontakt zwraca pustą tablicę, a nie `404` — „ta osoba nie ma innych kanałów” jest normalnym stanem.

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

**Odpowiedź**

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

### Rozłączanie kontaktu

`DELETE /contacts/{contactId}/link`

Usuwa ten kontakt z jego osoby jednostronnie — wszelkie inne kontakty nadal powiązane z tą osobą zachowują swoje połączenie, więc rozłączenie jednego z trzech nie powoduje rozwiązania grupy.

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

**Odpowiedź**

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

---

## Pobieranie zdjęcia profilowego kontaktu

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

Pobiera (i buforuje) zdjęcie profilowe kontaktu z WhatsApp lub Meta na żądanie — to samo zdjęcie, które jest zwracane jako `avatarUrl` w [Pobierz kontakt](#get-a-contact-by-phone-or-email), po odświeżeniu.

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

**Odpowiedź**

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

`cached: true` oznacza, że adres URL pochodzi z niedawnego pobrania, a nie ze świeżego wyszukiwania u dostawcy — zdjęcia są buforowane przez 7 dni, a kontakt, dla którego dostawca zgłasza brak dostępnego zdjęcia, jest buforowany jako niedostępny przez 24 godziny. Gdy nie ma zdjęcia do pobrania, `avatar_url` jest pomijane, a `message` wyjaśnia przyczynę.

---

## Automatyczne tagowanie kontaktów za pomocą AI

Uruchamia reguły tagowania Twojego konta dla pełnej historii konwersacji jednego lub większej liczby kontaktów i stosuje (lub usuwa) tagi dokładnie tak samo, jak tagowanie w czasie rzeczywistym podczas czatu na żywo — te same reguły, ten sam koszt kredytów za tag.

### Rozpocznij uruchomienie

`POST /contacts/auto-tag`

| Pole | Wymagane | Opis |
|---|---|---|
| `scope` | Tak | `"contacts"`, aby otagować określone kontakty, lub `"agent"`, aby otagować każdą konwersację obsługiwaną obecnie przez jednego agenta AI. |
| `contact_ids` | Wymagane, gdy `scope` to `"contacts"` | Tablica identyfikatorów kontaktów, od 1 do 500. |
| `agent_id` | Wymagane, gdy `scope` to `"agent"` | Agent AI, którego konwersacje mają zostać otagowane. Gdy `scope` to `"contacts"`, jest to opcjonalne i jedynie zawęża zakres reguł tagowania agenta, które zostaną uruchomione. |

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

Dla **pojedynczego** kontaktu uruchomienie następuje w trybie inline i zwraca wynik natychmiast:

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

**Dwa lub więcej** kontaktów (lub `scope: "agent"`) uruchamianych jest jako zadanie w tle i zwraca `202` natychmiast:

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

### Odpytywanie o stan uruchomienia

`GET /contacts/auto-tag/run`

Zwraca bieżące (lub ostatnie) uruchomienie konta, dzięki czemu możesz odpytywać o postęp bez samodzielnego śledzenia `run_id`.

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

**Odpowiedź**

```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` to `null`, gdy konto nigdy nie rozpoczęło żadnego uruchomienia. `status` zmienia się z `"running"` na `"completed"` lub `"failed"`.

Na koncie może trwać tylko jedno uruchomienie masowe jednocześnie — rozpoczęcie drugiego, gdy inne jest w toku, zwraca `409` z `error_code: "auto_tag_run_in_progress"`. Brak kredytów przy uruchomieniu dla pojedynczego kontaktu zwraca `402` z `error_code: "insufficient_credits"`; uruchomienie masowe natomiast zatrzymuje się wcześniej i raportuje postęp w `run`.

---

## Usuwanie kontaktu

`DELETE /contacts/{contactId}`

Trwale usuwa jeden kontakt według identyfikatora, wraz z jego historią wiadomości. **Tej operacji nie można cofnąć.** Aby usunąć kilka kontaktów w jednym wywołaniu, użyj [Usuń kontakty](#delete-contacts) poniżej.

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

**Odpowiedź**

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

Identyfikator kontaktu, który nie istnieje na Twoim koncie lub należy do innego konta, zwraca `404`.

---

## Usuń kontakty

`DELETE /contacts`

Trwale usuwa jeden lub więcej kontaktów według identyfikatora w jednym wywołaniu (do 500 identyfikatorów). Identyfikatory, które nie istnieją na Twoim koncie, zostaną pominięte i uwzględnione w `skipped`. **Tej operacji nie można cofnąć.**

| Pole | Opis |
|---|---|
| `contactIds` | Tablica identyfikatorów kontaktów do usunięcia (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']}")
```

**Odpowiedź**

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

---

## Usuwanie pola niestandardowego

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

Usuwa jeden klucz pola niestandardowego z **każdego** kontaktu na Twoim koncie. Użyj tej funkcji, aby posprzątać po zmianie nazwy lub wycofaniu pola niestandardowego. Klucz może zawierać tylko litery, cyfry, podkreślniki i myślniki. Zwraca liczbę zaktualizowanych kontaktów. **Tej operacji nie można cofnąć.**

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

**Odpowiedź**

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

::: note
**Uwaga:** Klucz pola zawierający niedozwolone znaki zwróci `400`.
:::


---

## Listy

Listy grupują kontakty. Lista może być **statyczna** (sam decydujesz, kto się na niej znajduje) lub **inteligentna** (członkostwo jest obliczane na podstawie reguł i automatycznie aktualizowane — zobacz [Organizowanie list i kontaktów](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Pole | Opis |
|---|---|
| `name` | Wymagane przy tworzeniu. Maksymalnie 100 znaków. |
| `status` | `live` (domyślnie) lub `draft`. Małe litery. |
| `contact_ids` | Tablica identyfikatorów kontaktów do umieszczenia na liście. **Tylko listy statyczne.** |
| `type` | `static` (domyślnie) lub `smart`. |
| `smart_rules` | Zestaw reguł — wymagany, gdy `type` to `smart`. Zobacz poniżej. |

### Tworzenie listy

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

**Odpowiedź**

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

Lista inteligentna jest oceniana **wewnątrz** tego samego żądania, więc `evaluation` informuje dokładnie, kto się na niej znalazł. W przypadku listy statycznej `evaluation` to `null`.

### Aktualizacja listy

`PUT /lists/{listId}`

Prześlij tylko te pola, które zmieniasz. Zmiana `smart_rules` powoduje natychmiastową ponowną ocenę listy i zwraca ten sam obiekt `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"] } ] } }'
```

Możesz przełączać listę między dwoma rodzajami:

- **Statyczna → inteligentna**: wyślij `{ "type": "smart", "smart_rules": { … } }`. Reguły zaczną działać natychmiast.
- **Inteligentna → statyczna**: wyślij `{ "type": "static" }`. Reguły zostaną usunięte, a osoby znajdujące się na liście pozostaną na niej.

### Struktura `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` (każdy warunek musi być spełniony) lub `any` (przynajmniej jeden).
- `conditions` — od 1 do 20 warunków, każdy z maksymalnie 100 wartościami, ciągi znaków do 200 znaków.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | tablica identyfikatorów tagów |
| `lists` | `in_any`, `not_in_any` | tablica identyfikatorów list (**tylko listy statyczne** — inteligentna lista nie może być utworzona na podstawie innej inteligentnej listy) |
| `channel` | `is_any`, `is_none` | tablica kanałów |
| `status` | `is_any`, `is_none` | tablica statusów kontaktu |
| `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" }` |
| te same pola daty | `before`, `after` | data ISO (`"2026-01-01"`, porównywana jako pełne dni) lub pełna data i godzina ISO (`"2026-01-01T14:30:00Z"`, porównywana z dokładnym momentem) |
| te same pola daty | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` dopasowuje kontakty, do których AI wysłało wiadomość przynajmniej raz (kiedykolwiek) |
| `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` | ciąg znaków dla formularzy `contains` |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | tablica identyfikatorów dla formularzy `is_any` / `is_none` |
| `custom_field` (plus `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | ciąg znaków dla formularzy wartości |

`not_within_last` dopasowuje również kontakty, dla których data nigdy nie została ustawiona („więcej niż N temu, **lub nigdy**”), a porównania tekstowe ignorują wielkość liter.

**Zaangażowanie AI.** `has_interacted_with_ai` to flaga dożywotnia: `true` dla każdego kontaktu, do którego Twoje AI wysłało przynajmniej jedną wiadomość, `false` dla wszystkich pozostałych (w tym kontaktów, na które odpowiadał tylko Twój zespół). Jest ona nadawana przy pierwszej wiadomości AI do kontaktu i nigdy nie jest usuwana, więc wyłączenie odpowiedzi AI dla kontaktu lub przeniesienie go do innej kampanii nie resetuje jej. W przypadku *okresu* — „kontakty, którymi moje AI zajmowało się w tym miesiącu”, co jest typowym pytaniem rozliczeniowym — należy użyć zakresu `last_ai_interaction_at`:

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

Nie należy mylić żadnego z nich z `is_bot_active` (AI *może* odpowiadać, co nie oznacza, że to zrobiło) ani `has_ever_responded` (kontakt odpisał, komukolwiek). Te same dwa znaczniki są zwracane przy każdym kontakcie jako `first_ai_interaction_at` / `last_ai_interaction_at`, a cały zestaw reguł działa również na `GET /contacts?rules=`, więc możesz zliczać dopasowania bez tworzenia listy.

### Podgląd zestawu reguł

`POST /lists/preview`

Zlicza i pobiera próbki kontaktów, które pasowałyby do zestawu reguł, bez tworzenia ani zmieniania czegokolwiek. Użyj tego, aby sprawdzić poprawność reguł przed ich zapisaniem.

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

**Odpowiedź**

```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` przechowuje do 10 kontaktów, zaczynając od tych najbardziej aktywnych.

### Ponowne uruchomienie listy inteligentnej

`POST /lists/{listId}/evaluate`

Wymusza natychmiastową ponowną ocenę (to samo, co robi przycisk **Odśwież teraz** w panelu). Listy inteligentne są aktualizowane automatycznie po zmianie kontaktu oraz co 15 minut w przypadku reguł opartych na czasie, więc jest to potrzebne tylko wtedy, gdy chcesz uzyskać wynik *natychmiast*.

**Odpowiedź**

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

`evaluation.skipped: true` oznacza, że inna ocena tej samej listy była już w toku i to wywołanie nie przyniosło żadnego efektu.

### Listy inteligentne nie akceptują ręcznie dodawanych członków

Punkty końcowe członkostwa zwracają **`409`** z `"This is a smart list — its members are computed from its rules. Edit the rules instead."`, gdy lista docelowa jest inteligentna. Dotyczy to `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` w `POST /lists` oraz `PUT /lists/{listId}`, a także wyboru listy inteligentnej jako celu importu CSV. Zamiast tego zmień reguły.

Wywołanie `POST /lists/{listId}/evaluate` na liście **statycznej** również kończy się `409` — nie ma ona żadnych reguł do uruchomienia.

---

## Błędy API kontaktów

Punkty końcowe (endpoints) kontaktów zwracają standardową kopertę błędu:

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

Niektóre punkty końcowe zawierają również `error_code`, który zazwyczaj odpowiada statusowi HTTP — jedynym wyjątkiem jest przypadek duplikatu kontaktu poniżej, gdzie status HTTP wynosi `200`, a tylko `error_code` zawiera `409`. Kody specyficzne dla punktów końcowych kontaktów:

| Kod | Kiedy występuje w punkcie końcowym kontaktu |
|---|---|
| `400` | Nieprawidłowe żądanie — brakujące/nieprawidłowe pole, pusty komunikat, nieprawidłowy kursor lub ponad 500 identyfikatorów w partii. |
| `402` | Brak wystarczającej liczby kredytów na wykonanie tagowania AI dla jednego kontaktu (`error_code: "insufficient_credits"`). |
| `404` | Kontaktu, listy lub tagu nie znaleziono na Twoim koncie. |
| `409` | Kontakt o takim numerze telefonu już istnieje (podczas tworzenia). Zwracane jako `error_code` w treści z kodem stanu HTTP `200`, więc należy tutaj rozgałęzić na `error_code`. Zwracane również, gdy masowe automatyczne tagowanie jest już w toku (`error_code: "auto_tag_run_in_progress"`) lub gdy połączenie kontaktu z innym kanałem spowodowałoby połączenie dwóch kontaktów już powiązanych z dwiema różnymi osobami. |
| `422` | Kontakt nie może obecnie odebrać wiadomości (tryb nie przeszkadzać, profil prywatny lub nieobsługiwany kanał). W punkcie końcowym łączenia kanałów obejmuje również brak numeru telefonu, nieobsługiwaną parę kanałów lub brak podłączonego nadawcy dla kanału docelowego. |

`403` w punkcie końcowym kontaktu może również oznaczać problem z limitem kontaktów lub uprawnieniami do listy, a nie z dostępem w ramach planu. Wspólne kody, które może zwrócić każdy punkt końcowy — `401`, `403` (Twój plan nie obejmuje dostępu do API), `429` (limit szybkości) oraz `500` — zostały wymienione wraz ze wskazówkami dotyczącymi ponawiania prób w sekcji [Błędy i stronicowanie](errors-and-pagination.md).

---

## Następne kroki

- [API wiadomości](messages.md) — wysyłaj wiadomości według identyfikatora kanału i zarządzaj konwersacjami.
- [Dokumentacja API](reference.md) — pełna lista punktów końcowych, w tym tagi i listy.
- [Dostęp do API](../integrations/api-access.md) — uwierzytelnianie, limity szybkości i obsługa błędów.
