
# API Contatti

Un contatto è una singola persona a cui invii messaggi: il suo nome, numero di telefono, email, canale, tag, campi personalizzati e le liste e campagne a cui appartiene. L'API Contatti ti consente di creare contatti, cercarli, aggiornarli, etichettarli, importarli in blocco e rimuoverli, il tutto senza utilizzare la dashboard.

Tutti i percorsi in questa pagina sono relativi all'URL di base:

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

Quindi `/contacts` significa `https://api.youraiconnector.com/v1/contacts`.

> **Nuovo dell'API?** Leggi prima [Accesso API](../integrations/api-access.md): spiega come generare la tua chiave API, i tre modi per autenticarsi, i limiti di frequenza e il formato degli errori. Tutto ciò che è presente in questa pagina presuppone che tu abbia già una chiave API funzionante.

---

## Informazioni sugli ID contatto

Ogni contatto ha un ID univoco. L'ID che ricevi quando **crei** un contatto (in `data.contactId`) è lo stesso ID che utilizzi ovunque: per recuperare, aggiornare, etichettare, inviare un messaggio o eliminare quel contatto. Salvalo una volta e riutilizzalo.

Non è necessario creare un contatto per ottenerne l'ID. Puoi anche cercarlo tramite numero di telefono o email (vedi [Ottieni un contatto](#get-a-contact-by-phone-or-email)), oppure scorrere tutti i tuoi contatti (vedi [Elenca contatti](#list-contacts)). Ognuno di questi restituisce lo stesso ID.

---

## Crea un contatto

`POST /contacts`

Aggiunge un nuovo contatto al tuo account. È **richiesto un numero di telefono con prefisso internazionale**: un'email da sola non è sufficiente. Tutto il resto è facoltativo.

Puoi facoltativamente inserire il nuovo contatto direttamente in una o più liste con `listId` (una singola lista) o `listIds` (un array). Se vengono inviati entrambi, `listIds` ha la precedenza.

Qualsiasi campo inviato che non sia uno dei campi di creazione standard elencati nella tabella dei campi **Crea un contatto** qui sotto (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`) viene archiviato automaticamente come **campo personalizzato**; pertanto, un payload flat proveniente da uno strumento come Make o Zapier funziona senza nidificazione. È anche possibile passare un oggetto `custom_fields` esplicito.

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `phoneNumber` | Sì | Il numero di telefono del contatto, con prefisso internazionale (es. `+15551234567`). |
| `firstName` | No | Nome. |
| `lastName` | No | Cognome. |
| `email` | No | Indirizzo email. |
| `channel` | No | Canale di messaggistica. Uno tra `whatsapp`, `sms`, `whatsapp_web`. L'impostazione predefinita è `whatsapp`. |
| `is_bot_active` | No | Indica se l'assistente AI risponde a questo contatto. L'impostazione predefinita è `true`. |
| `is_private` | No | Contrassegna il contatto come privato. Quando è `true`, l'assistente AI viene disattivato per lui. L'impostazione predefinita è `false`. |
| `lead_profile` | No | Note a testo libero sul lead. |
| `listId` | No | Un singolo ID lista a cui aggiungere il contatto. |
| `listIds` | No | Un array di ID lista a cui aggiungere il contatto (ha la precedenza su `listId`). |
| `custom_fields` | No | Un oggetto con i tuoi campi chiave/valore. Puoi anche passarli come chiavi di primo livello. |

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

**Risposta**

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

L'ID del nuovo contatto si trova in `data.contactId`. Gli elenchi a cui è stato aggiunto vengono riportati in `data.listsAdded`.

> **I duplicati non vengono creati.** Se esiste già un contatto con lo stesso numero di telefono, la chiamata di creazione **non** lo crea né lo restituisce. La risposta viene restituita con stato HTTP `200` e un `error_code` di `409` nel corpo, quindi esegui il branching su `error_code` anziché sullo stato HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Per lavorare con un contatto esistente dopo un `error_code` di `409`, cercalo con [Ottieni un contatto tramite telefono o email](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — e riutilizza l'ID restituito.

> **Le grafie equivalenti di WhatsApp contano come lo stesso numero.** Alcuni paesi hanno due grafie valide per la stessa linea mobile e WhatsApp può segnalarne una qualsiasi: Messico (`+52…` e il precedente `+521…`), Brasile (con o senza la nona cifra) e Argentina (con o senza il `9` dopo il `+54`). Il controllo dei duplicati alla creazione e la corrispondenza `GET /contacts?phoneNumber=` funzionano con entrambe le grafie, quindi otterrai il contatto esistente indipendentemente dalla forma inviata. Il `phone_number` memorizzato sul contatto non viene mai sovrascritto.

---

## Ottieni un contatto tramite telefono o email

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

Cerca un singolo contatto e restituisce l'oggetto contatto completo e arricchito, inclusi i suoi elenchi, tag e campagne risolti in coppie `{ id, name }`, oltre all'ultimo messaggio scambiato.

Passa **o** `phoneNumber` (in formato internazionale) **o** `email`. Se non ne passi nessuno, questo stesso endpoint passa alla modalità [Elenca contatti](#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"])
```

**Risposta**

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

L'ID del contatto viene restituito sia al livello principale (`contactId`) che all'interno dell'oggetto (`contact.id`). Se non ci sono corrispondenze, riceverai un `404` con `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** è la foto del profilo del contatto, presa da WhatsApp o Meta quando ti inviano un messaggio. È di sola lettura: non puoi impostarla ed è `null` per i contatti che non hanno una foto o che ti raggiungono su un canale che non ne condivide una. Considera il link come temporaneo invece di memorizzarlo, poiché alcuni di questi link alle foto scadono e vengono aggiornati automaticamente. (Nell'endpoint dell'elenco qui sotto, lo stesso valore è chiamato `avatar_url`.)

> **Numeri di telefono negli URL.** Un segno `+` in una stringa di query deve essere codificato come URL `%2B`, altrimenti viene letto come uno spazio. Gli esempi sopra lo fanno per te.

---

## Ottieni un contatto tramite ID

`GET /contacts/{contactId}`

Quando disponi già dell'ID di un contatto, recuperalo direttamente. La struttura della risposta è identica a quella della ricerca precedente.

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

Un ID contatto che non esiste nel tuo account restituisce un `404`.

---

## Ottieni le statistiche del contatto

`GET /contacts/{contactId}/stats`

Restituisce le statistiche aggregate dei messaggi per un contatto: totali, risposte AI vs umane, crediti spesi e timestamp del primo/ultimo messaggio.

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

**Risposta**

```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` è lo stesso contatore di messaggi AI che il pulsante "reset" in-app su un contatto azzera. `creditsUsed` è il totale dei crediti correnti per questo contatto, non solo i numeri di questa risposta. Un ID contatto che non esiste nel tuo account restituisce un `404`.

---

## Elenca contatti

`GET /contacts`

Chiama `GET /contacts` **senza** `phoneNumber` né `email` per scorrere tutti i tuoi contatti, partendo dai più recenti. Ogni pagina restituisce riepiloghi compatti dei contatti (elenchi, tag e campagne vengono restituiti come array di ID anziché come oggetti completi) e un `next_cursor`.

| Parametro di query | Descrizione |
|---|---|
| `limit` | Dimensione della pagina. Il valore predefinito è 50, il massimo è 100. |
| `cursor` | Il valore `next_cursor` della pagina precedente. Ometterlo nella prima pagina. |
| `listId` | Opzionale. Restituisce solo i contatti che appartengono a questo elenco. |

Per scorrere ogni pagina: effettua la prima chiamata senza un cursore, quindi continua a passare il `next_cursor` restituito come `cursor`. **Fermati quando `next_cursor` è `null`**: significa che non ci sono più risultati.

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

**Risposta**

```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
**Nota:** Il filtraggio tramite un `listId` che non esiste nel tuo account restituisce un `404`. Un `cursor` non valido restituisce un `400`.
:::


---

## Conta i contatti

`GET /contacts/count`

Restituisce il numero di contatti che corrispondono a un filtro, con una suddivisione per canale, senza doverli impaginare. Questa è la chiamata corretta per qualsiasi domanda del tipo "quanti sono": un riquadro della dashboard, un'automazione o una richiesta a Champ. Tutti i filtri sono facoltativi e combinarne diversi restringe il conteggio (un contatto deve corrispondere a ognuno di quelli inviati).

| Parametro di query | Descrizione |
|---|---|
| `agentId` | Solo i contatti assegnati a questo agente AI. Passa `none` per i contatti senza un agente assegnato (a questi risponde l'agente predefinito del canale). |
| `channel` | Solo i contatti su questo canale, ad es. `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Solo i contatti che portano questo tag, tramite il **nome** del tag (le maiuscole/minuscole non contano). Un nome di tag che non possiedi restituisce un `404`. |
| `listId` | Solo i contatti in questo elenco. |
| `botActive` | `true` o `false` — solo i contatti il cui assistente AI è attivo o disattivo. |
| `status` | Solo i contatti con questo stato, ad es. `Lead`. |
| `rules` | Un oggetto regole JSON codificato in URL, che utilizza la stessa forma di un elenco intelligente (vedi [La forma `smart_rules`](#the-smart_rules-shape) più avanti). Non può essere combinato con gli altri filtri. |

Non inviare alcun filtro per ottenere il numero totale di contatti sul tuo account.

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

**Risposta**

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

`by_channel` suddivide lo stesso totale per canale; i contatti che non si trovano su alcun canale vengono conteggiati sotto `none`. `filters` riporta i filtri che sono stati applicati, così puoi verificare che la chiamata abbia fatto ciò che intendevi.

::: note
**Nota:** L'invio di `rules` insieme a qualsiasi altro filtro, o un valore `rules` che non sia un JSON valido, restituisce un `400`. Un nome di tag o un ID elenco che non esiste sul tuo account restituisce un `404`.
:::


---

## Aggiorna un contatto

`PUT /contacts/{contactId}`

Aggiorna un contatto esistente. Vengono modificati solo i campi inclusi: tralascia tutto ciò che non vuoi toccare. Devi inviare almeno un campo, altrimenti riceverai un `400` ("Nessun campo da aggiornare").

| Campo | Descrizione |
|---|---|
| `firstName` | Nome. |
| `lastName` | Cognome. |
| `email` | Indirizzo email. |
| `is_bot_active` | Indica se l'assistente AI risponde a questo contatto. |
| `is_private` | Contrassegna come privato. Impostare questo valore su `true` disattiva anche l'assistente AI. |
| `do_not_disturb` | Metti in pausa l'attività di outreach automatizzata verso questo contatto. Interrompe anche le risposte dell'AI. |
| `follow_ups_disabled` | Interrompi tutti i follow-up automatizzati per questo contatto (rapidi, ciclici e cold-lead) mentre l'AI continua a rispondere ai messaggi inviati. Utile una volta effettuato un acquisto. Rimane disattivato finché non lo reimposti su `false`. |
| `lead_profile` | Note sul lead in formato testo libero. |
| `custom_fields` | Un oggetto di campi personalizzati. **Uniti per chiave**: vengono scritti solo i campi inviati, mentre i restanti campi personalizzati esistenti vengono mantenuti. È anche possibile passare le chiavi dei campi personalizzati al livello principale. |

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

**Risposta**

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

> **I campi personalizzati vengono uniti, non sostituiti.** L'invio di `{ "custom_fields": { "tier": "gold" } }` imposta solo `tier`: tutti gli altri campi personalizzati sul contatto rimangono esattamente come erano. Per rimuovere completamente un campo personalizzato da tutti i contatti, utilizza [Elimina un campo personalizzato](#delete-a-custom-field).

---

## Aggiungi o rimuovi tag

`POST /contacts/{contactId}/tags`

Aggiunge e/o rimuove tag su un singolo contatto in un'unica chiamata. Passa gli **ID** dei tag in `addTagIds` e `removeTagIds`. Almeno uno dei due deve essere non vuoto.

I tag devono già esistere nel tuo account: creali prima tramite l'[endpoint dei tag](reference.md). Se il contatto o uno qualsiasi dei tag di riferimento non esiste, riceverai un `404`.

| Campo | Descrizione |
|---|---|
| `addTagIds` | Array di ID tag da aggiungere al contatto. |
| `removeTagIds` | Array di ID tag da rimuovere dal contatto. |

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

**Risposta**

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

---

## Gestisci la tua libreria di tag

Questi endpoint gestiscono il tag stesso — rinominandolo o eliminandolo dal tuo account — al contrario dell'applicazione o rimozione di un tag su un singolo contatto (vedi [Aggiungi o rimuovi tag](#add-or-remove-tags) sopra). Ogni tag sul tuo account ha un ID (`tagId`): quello mostrato nel gestore tag della tua dashboard e quello restituito come `data.tag_id` quando crei un tag con `POST /tags` e un corpo JSON di `{ "name": "..." }` (senza `phoneNumber`, `email` o `contactId`).

### Aggiorna un tag

`PUT /tags/{tagId}`

Invia solo i campi che stai modificando.

| Campo | Descrizione |
|---|---|
| `name` | Il nome del 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)" }'
```

**Risposta**

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

Un `tagId` che non esiste nel tuo account restituisce un `404`.

### Elimina un tag

`DELETE /tags/{tagId}`

Elimina un tag tramite ID. **Questa operazione non può essere annullata** — i contatti che possiedono il tag lo perderanno semplicemente. L'eliminazione di un tag già rimosso (o mai esistito) restituisce `200` con `deleted: 0` invece di un `404`, poiché non c'è nulla da enumerare.

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

**Risposta**

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

### Elimina diversi tag contemporaneamente

`DELETE /tags`

| Campo | Descrizione |
|---|---|
| `tagIds` | Array di ID tag da eliminare (massimo 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"] }'
```

**Risposta**

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

Gli ID che non esistono o che appartengono a un altro account vengono ignorati silenziosamente e non conteggiati in `deleted`.

---

## Imposta un flag in blocco

`POST /contacts/bulk-flag`

Imposta un flag booleano su molti contatti contemporaneamente. Fino a 500 ID contatto per richiesta. Gli ID che non esistono nel tuo account vengono ignorati e conteggiati in `skipped`.

| Campo | Descrizione |
|---|---|
| `contactIds` | Array di ID contatto da aggiornare (max 500). |
| `field` | Quale flag impostare. Uno tra `bot_active` (assistente AI attivo/disattivo), `dnd` (sospendi outreach automatizzato), `spam`, `private`. |
| `value` | Il valore booleano a cui impostare il flag. |

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

**Risposta**

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

---

## Importazione contatti in blocco

`POST /contacts/import`

Crea fino a 500 contatti in una sola chiamata da un array JSON. Ogni record necessita di un `phone_number` in formato internazionale; tutto il resto è facoltativo. I record con numeri di telefono non validi o canali non supportati vengono **saltati** (non creati) e ogni record saltato viene segnalato con il relativo indice e motivo: in questo modo puoi correggere solo gli errori e riprovare.

I numeri di telefono già esistenti nel tuo account vengono saltati come `duplicate` per impostazione predefinita. Invia `updateExisting: true` per **aggiornare** invece quei contatti: i campi presenti nel record sovrascrivono quelli del contatto (`first_name`, `last_name`, `email`, `lead_profile` e `custom_fields` uniti chiave per chiave), i `tags` vengono aggiunti e il contatto viene inserito in `listId`. Il canale, il numero di telefono e i flag del bot non vengono mai modificati su un contatto esistente.

Puoi facoltativamente aggiungere ogni contatto importato (o aggiornato) a un elenco con `listId`, impostare un `defaultChannel` per i record che non ne specificano uno e taggare i record con `tags` (nomi dei tag: i tag mancanti vengono creati, quelli esistenti vengono abbinati senza distinzione tra maiuscole e minuscole).

**Campi di primo livello**

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `contacts` | Sì | Array di record di contatto (max 500). |
| `listId` | No | Elenco a cui aggiungere ogni contatto importato (e aggiornato). Deve essere un elenco presente nel tuo account. |
| `defaultChannel` | No | Canale applicato ai record che omettono `channel`. Uno tra `whatsapp`, `sms`, `whatsapp_web`. L'impostazione predefinita è `whatsapp`. |
| `updateExisting` | No | `true` per aggiornare i contatti il cui numero di telefono esiste già invece di saltarli come `duplicate`. L'impostazione predefinita è `false`. |

**Campi per record**

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `phone_number` | Sì | Numero di telefono in formato internazionale (se manca, viene aggiunto un `+` iniziale). |
| `first_name` | No | Nome. |
| `last_name` | No | Cognome. |
| `email` | No | Indirizzo email. |
| `channel` | No | Uno tra `whatsapp`, `sms`, `whatsapp_web`. Ricade su `defaultChannel`. |
| `is_bot_active` | No | Indica se l'assistente AI risponde. L'impostazione predefinita è `true`. |
| `is_private` | No | Contrassegna come privato. L'impostazione predefinita è `false`. |
| `lead_profile` | No | Note sul lead in testo libero. |
| `custom_fields` | No | Oggetto contenente chiavi e valori dei campi personalizzati. |
| `tags` | No | Array di nomi di tag (funziona anche una singola stringa `"a; b"`). I tag che non esistono vengono creati; quelli esistenti vengono abbinati ignorando le maiuscole/minuscole. Max 25 per record. |

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

**Risposta**

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

Se alcuni record non possono essere creati, appaiono in `skipped` con il motivo (qui senza `updateExisting`, quindi il numero esistente viene saltato):

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

Con `updateExisting: true` la stessa richiesta segnala il contatto esistente sotto `updated` / `updated_contact_ids`.

Possibili motivi di esclusione: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Limiti del piano.** Se il limite di contatti del tuo piano non consente un numero così elevato di nuovi contatti, l'intera richiesta viene rifiutata in anticipo con un `403`. Se il limite viene raggiunto durante l'elaborazione, i record rimanenti vengono restituiti come saltati con il motivo `contact_limit_reached`.

---

## Importare contatti da un file CSV

Per importazioni più grandi di quanto supportato dall'[importazione massiva](#bulk-import-contacts) (fino a circa 50.000 righe), accoda un processo di importazione asincrono per un file CSV già presente nello spazio di archiviazione del tuo account, quindi esegui il polling finché non viene completato.

### Avviare l'importazione

`POST /contacts/import-csv`

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `csvStoragePath` | Sì | Percorso di archiviazione del file CSV, sotto `users/{your account id}/imports/`, che termina con `.csv`. |
| `listName` | Sì | Crea (o riutilizza) un elenco con questo nome e vi aggiunge ogni contatto importato. |
| `existingListRefs` | No | Array di ID di elenchi esistenti a cui aggiungere anche ogni contatto importato. |
| `defaultChannel` | No | Canale applicato alle righe che non ne specificano uno. |

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

**Risposta** (`202` — l'importazione è in coda, non ancora terminata)

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

> **Caricamento del file nell'archiviazione.** Questo endpoint avvia e traccia il processo di importazione; non accetta direttamente un caricamento. Il file CSV deve già trovarsi in `csvStoragePath` prima di chiamarlo: l'importatore CSV della dashboard esegue questa operazione come primo passaggio.

### Interroga il processo di importazione

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

**Risposta**

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

`status` passa attraverso `queued` → `processing` → `completed`, oppure `failed` con il motivo in `error_message`. Un `jobId` che non esiste nel tuo account restituisce un `404`.

---

## Esportare contatti

Avvia un'esportazione CSV asincrona dei tuoi contatti e restituisce un processo di cui eseguire il polling per il completamento.

### Avvia l'esportazione

`POST /contacts/export`

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `listId` | No | Esporta solo i contatti che appartengono a questo elenco. |
| `contactIds` | No | Esporta solo questi ID contatto specifici. |

Se non viene specificato nessuno dei due, verranno esportati tutti i contatti presenti nel tuo account.

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

**Risposta** (`202` — l'esportazione è in coda)

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

### Verifica lo stato del processo di esportazione

`GET /contacts/export/{jobId}`

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

**Risposta**

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

> Una volta che `status` è `"completed"` riceverai `export_id` e `contact_count`. Il download del file CSV generato avviene dalla pagina Esportazioni della tua dashboard.

---

## Invia un messaggio a un contatto

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

Invia un messaggio a un contatto esistente sul canale che sta già utilizzando. Il messaggio viene messo in coda e consegnato in background: la risposta conferma che è stato accettato, non che sia già stato consegnato.

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `body` | Sì | Il testo del messaggio da inviare. |
| `mediaUrl` | No | URL di un file multimediale da allegare. |
| `mediaContentType` | No | Tipo MIME del file multimediale allegato (ad es. `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"])
```

**Risposta**

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

> **Non è possibile inviare il messaggio in questo momento?** Se il contatto ha attivato la modalità non disturbare o privata, o non si trova su un canale in grado di ricevere messaggi in uscita, la richiesta viene rifiutata con un `422` e un `error` esplicativo.

Per l'invio tramite numero di telefono, ID Instagram o altra identità di canale invece di un ID contatto — e per ulteriori informazioni sulla messaggistica in generale — consulta le [API Messaggi](messages.md).

---

## Assegna un agente IA a un contatto

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

Sposta una conversazione esistente su un agente IA diverso, a partire dal messaggio successivo. È la stessa operazione di **Assegna agente IA** nel menu di una chat, e lo stesso passaggio utilizzato dall'azione **Assegna agente IA o campagna** nelle Automazioni.

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `agentId` | Sì | L'ID dell'agente IA che deve subentrare, oppure `null` per rimuovere l'assegnazione in modo che la conversazione torni alla posta in arrivo del tuo team. |
| `triggerAIResponse` | No | `true` fa sì che l'agente appena assegnato risponda immediatamente agli ultimi messaggi senza risposta del contatto. Il valore predefinito è `false`. |

> **Attenzione con `triggerAIResponse: true`** — invia un messaggio al contatto immediatamente, quindi usalo solo quando vuoi che ricevano il messaggio subito. Su Messenger e Instagram, il messaggio non viene recapitato se il contatto ti ha scritto l'ultima volta più di 24 ore fa.

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

**Risposta**

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

> L'agente deve appartenere allo stesso account del contatto; in caso contrario, la richiesta viene rifiutata con un `404` o `403`. Trova gli ID degli agenti nella pagina Agenti IA (l'URL di ogni agente termina con il suo ID).

---

## Assegna un agente AI a molti contatti

`POST /contacts/bulk-assign-agent`

Sposta molte conversazioni su un agente AI diverso con una sola chiamata, oppure cancella l'assegnazione per tutti loro con `null`. Si tratta puramente di una modifica di instradamento: **non viene inviato alcun messaggio e l'agente non risponde a nessuno**. Ogni contatto riceve semplicemente il nuovo agente la prossima volta che scrive. (Ecco perché qui non c'è `triggerAIResponse`.)

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `agentId` | Sì | L'agente AI che dovrebbe subentrare, o `null` per cancellare l'assegnazione. |
| `contactIds` | Uno dei tre | Fino a 500 ID contatto da spostare. |
| `filter` | Uno dei tre | Seleziona i contatti sul server invece di elencarli, dal più recente al meno recente. Accetta le stesse chiavi dei filtri dell'endpoint di conteggio: `agentId` (o `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | Uno dei tre | Un oggetto regole per elenchi intelligenti — vedi [La forma `smart_rules`](#the-smart_rules-shape). |
| `limit` | No | Quanti contatti spostare in questa chiamata quando selezioni con `filter` o `rules`. Da 1 a 500, il valore predefinito è 500. |

Invia esattamente uno tra `contactIds`, `filter` o `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"])
```

**Risposta**

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

`matched` è il numero totale di contatti trovati dalla selezione, `updated` quanti ne sono stati spostati da questa chiamata, `skipped` quanti degli ID inviati non sono stati trovati sul tuo account e `remaining` quanti corrispondono ancora ora che la chiamata è terminata.

**Spostare tutti.** Poiché una chiamata sposta al massimo 500 contatti, un gruppo numeroso richiede alcune chiamate. Usa un filtro che smette di corrispondere a un contatto una volta che è stato spostato — ad esempio `filter: { "agentId": "agent_abc123" }` durante l'assegnazione a `agent_xyz789` — e ripeti esattamente la stessa chiamata finché `remaining` non torna come `0`. Quando passi `contactIds` invece, `remaining` è sempre `0`.

---

## Assegna un contatto a un dipartimento

`POST /contacts/{contactId}/department`

"Assegna questo lead alle Vendite" — archivia un contatto sotto un dipartimento specifico e, per impostazione predefinita, lo assegna alla persona di quel dipartimento che attualmente ha meno contatti. Questa operazione è distinta dall'[assegnazione di un agente AI](#assign-an-ai-agent-to-a-contact): un dipartimento risponde alla domanda "quale team è responsabile di questo", un agente risponde alla domanda "quale AI gestisce questo", e l'impostazione di uno non cancella mai l'altro.

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `department_id` | Sì | Il dipartimento sotto cui archiviare il contatto. Passa `null` per cancellarlo. |
| `hand_to_member` | No | Assegna il contatto anche alla persona meno occupata di quel dipartimento. Il valore predefinito è `true`. Non riassegna mai un contatto già posseduto da qualcuno. |

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

**Risposta**

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

`assigned_to` è `null` quando il contatto era già posseduto da qualcuno, o hai passato `hand_to_member: false`.

---

## Collega un contatto tra diversi canali

"Continua su WhatsApp" (o SMS) trova o crea il contatto di questa persona su un altro canale basato su telefono e collega i due profili, in modo che il resto dell'app li riconosca come la stessa persona.

### Collega a un altro canale

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

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `channel` | Sì | Il canale a cui collegarsi. Uno tra `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | No | Numero di telefono da utilizzare sul nuovo canale. Per impostazione predefinita utilizza il numero del contatto di origine. |

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

**Risposta**

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

`created` indica se è stato creato un nuovo contatto per il canale di destinazione o se ne è stato trovato uno esistente e collegato. Chiamare questo metodo una seconda volta è sicuro: restituisce lo stesso `contact_id` con `created: false` invece di creare un duplicato.

Un `422` significa che l'account non può eseguire questo collegamento al momento: il contatto è già presente in quella famiglia di canali, non ha un numero di telefono da utilizzare o non c'è alcun mittente connesso per il canale di destinazione. Un `409` significa che i due contatti sono già collegati a due persone diverse: scollegane prima uno.

### Elenca le conversazioni collegate di un contatto

`GET /contacts/{contactId}/linked`

Restituisce le altre conversazioni che corrispondono alla stessa persona di questo contatto. Un contatto non collegato restituisce un array vuoto, non un `404`: "questa persona non ha altri canali" è uno stato normale.

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

**Risposta**

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

### Scollega un contatto

`DELETE /contacts/{contactId}/link`

Rimuove questo contatto dalla sua persona, in modo unilaterale: qualsiasi altro contatto ancora collegato a quella persona mantiene il proprio collegamento, quindi scollegare uno dei tre non scioglie il gruppo.

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

**Risposta**

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

---

## Recupera l'immagine del profilo di un contatto

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

Recupera (e memorizza nella cache) la foto del profilo WhatsApp o Meta del contatto su richiesta: la stessa foto restituita come `avatarUrl` in [Ottieni un contatto](#get-a-contact-by-phone-or-email), aggiornata.

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

**Risposta**

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

`cached: true` significa che l'URL proviene da un recupero recente anziché da una nuova ricerca presso il provider: le immagini vengono memorizzate nella cache per 7 giorni e un contatto per il quale il provider segnala l'assenza di una foto raggiungibile viene memorizzato come non disponibile per 24 ore. Quando non c'è alcuna immagine da recuperare, `avatar_url` viene omesso e `message` ne spiega il motivo.

---

## Assegna tag automaticamente ai contatti con l'IA

Esegue le regole di tagging del tuo account sull'intera cronologia delle conversazioni di uno o più contatti e applica (o rimuove) i tag esattamente come il tagging in tempo reale che avviene durante una chat dal vivo: stesse regole, stesso costo in crediti per tag.

### Avvia un'esecuzione

`POST /contacts/auto-tag`

| Campo | Obbligatorio | Descrizione |
|---|---|---|
| `scope` | Sì | `"contacts"` per assegnare tag a contatti specifici, o `"agent"` per assegnare tag a ogni conversazione attualmente gestita da un agente IA. |
| `contact_ids` | Obbligatorio quando `scope` è `"contacts"` | Array di ID contatto, da 1 a 500. |
| `agent_id` | Obbligatorio quando `scope` è `"agent"` | L'agente IA le cui conversazioni devono essere taggate. Quando `scope` è `"contacts"`, questo è facoltativo e serve solo a restringere le regole di tagging dell'agente da eseguire. |

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

Un **singolo** contatto viene eseguito in linea e restituisce immediatamente il risultato:

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

**Due o più** contatti (o `scope: "agent"`) vengono eseguiti come processo in background e restituiscono `202` immediatamente:

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

### Monitora un'esecuzione

`GET /contacts/auto-tag/run`

Restituisce l'esecuzione corrente (o più recente) dell'account, in modo da poter monitorare l'avanzamento senza dover tracciare personalmente `run_id`.

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

**Risposta**

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

`run` è `null` quando l'account non ne ha mai avviata una. `status` passa da `"running"` a `"completed"` o `"failed"`.

Può essere in corso solo un'esecuzione massiva per account alla volta: avviare una seconda esecuzione mentre un'altra è in corso restituisce `409` con `error_code: "auto_tag_run_in_progress"`. L'esaurimento dei crediti durante un'esecuzione su un singolo contatto restituisce `402` con `error_code: "insufficient_credits"`; un'esecuzione massiva invece si interrompe anticipatamente e riporta a che punto è arrivata in `run`.

---

## Elimina un contatto

`DELETE /contacts/{contactId}`

Elimina definitivamente un contatto tramite ID, insieme alla sua cronologia dei messaggi. **Questa operazione non può essere annullata.** Per eliminare diversi contatti in un'unica chiamata, utilizza [Elimina contatti](#delete-contacts) qui sotto.

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

**Risposta**

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

Un ID contatto che non esiste nel tuo account, o che appartiene a un account diverso, restituisce un `404`.

---

## Eliminare i contatti

`DELETE /contacts`

Elimina in modo permanente uno o più contatti tramite ID in un'unica chiamata (fino a 500 ID). Gli ID che non esistono nel tuo account vengono ignorati e conteggiati in `skipped`. **Questa operazione non può essere annullata.**

| Campo | Descrizione |
|---|---|
| `contactIds` | Array di ID contatto da eliminare (max 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']}")
```

**Risposta**

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

---

## Elimina un campo personalizzato

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

Rimuove una chiave di campo personalizzato da **ogni** contatto nel tuo account. Utilizzalo per fare pulizia dopo aver rinominato o ritirato un campo personalizzato. La chiave può contenere solo lettere, numeri, trattini bassi e trattini. Restituisce il numero di contatti aggiornati. **Questa operazione non può essere annullata.**

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

**Risposta**

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

::: note
**Nota:** Una chiave di campo con caratteri non supportati restituisce un `400`.
:::


---

## Liste

Le liste raggruppano i contatti. Una lista può essere **statica** (decidi tu chi ne fa parte) o **smart** (l'appartenenza viene calcolata in base a regole e mantenuta aggiornata automaticamente — vedi [Organizzazione di liste e contatti](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Campo | Descrizione |
|---|---|
| `name` | Obbligatorio durante la creazione. Fino a 100 caratteri. |
| `status` | `live` (predefinito) o `draft`. Minuscolo. |
| `contact_ids` | Array di ID contatto da inserire nella lista. **Solo per liste statiche.** |
| `type` | `static` (predefinito) o `smart`. |
| `smart_rules` | Il set di regole — obbligatorio quando `type` è `smart`. Vedi sotto. |

### Creare una lista

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

**Risposta**

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

Una lista smart viene valutata **inline**, nella stessa richiesta, quindi `evaluation` ti indica esattamente chi ne fa parte. In una lista statica `evaluation` è `null`.

### Aggiornare una lista

`PUT /lists/{listId}`

Invia solo i campi che stai modificando. La modifica di `smart_rules` rivaluta immediatamente la lista e restituisce lo stesso oggetto `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"] } ] } }'
```

Puoi convertire una lista da un tipo all'altro:

- **Statica → smart**: invia `{ "type": "smart", "smart_rules": { … } }`. Le regole diventano immediatamente operative.
- **Smart → statica**: invia `{ "type": "static" }`. Le regole vengono rimosse e chiunque sia presente nella lista vi rimane.

### La struttura di `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` (ogni condizione deve essere vera) o `any` (almeno una).
- `conditions` — da 1 a 20 condizioni, ciascuna con al massimo 100 valori, stringhe fino a 200 caratteri.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | array di ID tag |
| `lists` | `in_any`, `not_in_any` | array di ID elenco (**solo elenchi statici** — uno smart list non può essere creato a partire da un altro smart list) |
| `channel` | `is_any`, `is_none` | array di canali |
| `status` | `is_any`, `is_none` | array di stati del contatto |
| `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" }` |
| stessi campi data | `before`, `after` | data ISO (`"2026-01-01"`, confrontata come giorni interi) o data-ora ISO completa (`"2026-01-01T14:30:00Z"`, confrontata con il momento esatto) |
| stessi campi data | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` corrisponde ai contatti a cui l'IA ha inviato almeno un messaggio (in assoluto) |
| `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` | stringa per i moduli `contains` |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | array di ID per i moduli `is_any` / `is_none` |
| `custom_field` (più un `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | stringa per i moduli di valore |

`not_within_last` corrisponde anche ai contatti per i quali la data non è mai stata impostata ("più di N tempo fa, **o mai**"), e i confronti testuali ignorano maiuscole e minuscole.

**Coinvolgimento dell'IA.** `has_interacted_with_ai` è il flag di durata: `true` per ogni contatto a cui la tua IA ha inviato almeno un messaggio, `false` per tutti gli altri (inclusi i contatti a cui ha risposto solo il tuo team). Viene impresso al primo messaggio dell'IA a un contatto e non viene mai cancellato, quindi disattivare le risposte dell'IA per il contatto o spostarlo in un'altra campagna non lo reimposta. Per un *periodo* — "i contatti gestiti dalla mia IA questo mese", la solita domanda di fatturazione — utilizza invece l'intervallo `last_ai_interaction_at`:

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

Non confondere nessuno dei due con `is_bot_active` (l'IA è *autorizzata* a rispondere, non che lo abbia fatto) o `has_ever_responded` (il *contatto* ha risposto, a chiunque). Entrambi i timestamp vengono restituiti su ogni contatto come `first_ai_interaction_at` / `last_ai_interaction_at`, e l'intero set di regole funziona anche su `GET /contacts?rules=`, così puoi contare le corrispondenze senza creare un elenco.

### Anteprima di un set di regole

`POST /lists/preview`

Conta e campiona i contatti che un set di regole corrisponderebbe, senza creare o modificare nulla. Usalo per verificare la correttezza delle regole prima di salvarle.

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

**Risposta**

```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` contiene fino a 10 contatti, ordinati dal più recentemente attivo.

### Esegui di nuovo un elenco smart ora

`POST /lists/{listId}/evaluate`

Forza una rivalutazione immediata (la stessa operazione eseguita da **Aggiorna ora** nella dashboard). Gli elenchi smart si aggiornano già quando un contatto cambia e ogni 15 minuti per le regole basate sul tempo, quindi questa operazione è necessaria solo quando si desidera il risultato *immediatamente*.

**Risposta**

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

`evaluation.skipped: true` significa che un'altra valutazione dello stesso elenco era già in esecuzione e questa chiamata non ha prodotto alcun effetto.

### Gli elenchi smart rifiutano i membri selezionati manualmente

Gli endpoint di appartenenza restituiscono **`409`** con `"This is a smart list — its members are computed from its rules. Edit the rules instead."` quando l'elenco di destinazione è smart. Ciò copre `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` su `POST /lists` e `PUT /lists/{listId}`, nonché la scelta di un elenco smart come destinazione per un'importazione CSV. Modifica invece le regole.

Chiamare `POST /lists/{listId}/evaluate` su un elenco **statico** è anche un `409`: non ha regole da eseguire.

---

## Errori dell'API Contatti

Gli endpoint dei contatti restituiscono il pacchetto di errore standard:

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

Alcuni endpoint includono anche `error_code`, che solitamente corrisponde allo stato HTTP; l'unica eccezione è il caso del contatto duplicato qui sotto, in cui lo stato HTTP è `200` e solo `error_code` riporta il `409`. I codici specifici per gli endpoint dei contatti:

| Codice | Quando si verifica su un endpoint di contatto |
|---|---|
| `400` | Richiesta non valida: campo mancante/non valido, corpo vuoto, cursore errato o più di 500 ID in un batch. |
| `402` | Crediti insufficienti per completare un'operazione di tagging AI su un contatto (`error_code: "insufficient_credits"`). |
| `404` | Il contatto, l'elenco o il tag non è stato trovato nel tuo account. |
| `409` | Un contatto con quel numero di telefono esiste già (durante la creazione). Restituito come `error_code` nel corpo con uno stato HTTP `200`, quindi esegui il branching su `error_code` qui. Restituito anche quando un'operazione di auto-tagging in blocco è già in corso (`error_code: "auto_tag_run_in_progress"`), o quando il collegamento di un contatto a un altro canale unirebbe due contatti già collegati a due persone diverse. |
| `422` | Il contatto non può ricevere un messaggio in questo momento (non disturbare, privato o canale non supportato). Sull'endpoint di collegamento del canale, copre anche l'assenza di un numero di telefono, un abbinamento di canali non supportato o l'assenza di un mittente collegato per il canale di destinazione. |

Un `403` su un endpoint di contatto può anche indicare un problema relativo al limite di contatti o alle autorizzazioni della lista, piuttosto che all'accesso al piano. I codici condivisi che ogni endpoint può restituire — `401`, `403` (il tuo piano non include l'accesso API), `429` (limite di frequenza) e `500` — sono elencati con indicazioni sui tentativi in [Errori e Paginazione](errors-and-pagination.md).

---

## Passaggi successivi

- [API Messaggi](messages.md) — invia messaggi tramite identità del canale e gestisci le conversazioni.
- [Riferimento API](reference.md) — elenco completo degli endpoint, inclusi tag ed elenchi.
- [Accesso API](../integrations/api-access.md) — autenticazione, limiti di frequenza e gestione degli errori.
