
# API de Contatos

Um contato é uma pessoa individual com quem você troca mensagens — seu nome, número de telefone, e-mail, canal, tags, campos personalizados e as listas e campanhas às quais pertencem. A API de Contatos permite que você crie contatos, pesquise-os, atualize-os, adicione tags, importe-os em massa e remova-os, tudo sem usar o painel de controle.

Todos os caminhos nesta página são relativos à URL base:

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

Portanto, `/contacts` significa `https://api.youraiconnector.com/v1/contacts`.

> **Novo na API?** Leia [Acesso à API](../integrations/api-access.md) primeiro — ele aborda como gerar sua chave de API, as três formas de autenticação, limites de taxa e o formato de erro. Tudo nesta página pressupõe que você já tenha uma chave de API funcional.

---

## Sobre IDs de contato

Cada contato possui um ID exclusivo. O ID que você recebe ao **criar** um contato (em `data.contactId`) é o mesmo ID que você usa em todos os outros lugares — para buscar, atualizar, adicionar tags, enviar uma mensagem ou excluir esse contato. Salve-o uma vez e reutilize-o.

Você não precisa criar um contato para obter seu ID. Você também pode pesquisar um por número de telefone ou e-mail (veja [Obter um contato](#get-a-contact-by-phone-or-email)), ou percorrer todos os seus contatos (veja [Listar contatos](#list-contacts)). Cada um deles retorna o mesmo ID.

---

## Criar um contato

`POST /contacts`

Adiciona um novo contato à sua conta. Um **número de telefone com código do país é obrigatório** — apenas um e-mail não é suficiente. Todo o resto é opcional.

Você pode, opcionalmente, inserir o novo contato diretamente em uma ou mais listas com `listId` (uma única lista) ou `listIds` (uma matriz). Se ambos forem enviados, `listIds` prevalece.

Qualquer campo que você enviar que não seja um dos campos de criação padrão listados na tabela de campos **Criar um contato** abaixo (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`) é armazenado automaticamente como um **campo personalizado** — portanto, um payload simples de uma ferramenta como Make ou Zapier funciona sem aninhamento. Você também pode passar um objeto `custom_fields` explícito.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `phoneNumber` | Sim | O número de telefone do contato, com código do país (por exemplo, `+15551234567`). |
| `firstName` | Não | Primeiro nome. |
| `lastName` | Não | Sobrenome. |
| `email` | Não | Endereço de e-mail. |
| `channel` | Não | Canal de mensagens. Um entre `whatsapp`, `sms`, `whatsapp_web`. O padrão é `whatsapp`. |
| `is_bot_active` | Não | Se o assistente de IA responde a este contato. O padrão é `true`. |
| `is_private` | Não | Marcar o contato como privado. Quando `true`, o assistente de IA é desativado para ele. O padrão é `false`. |
| `lead_profile` | Não | Notas de texto livre sobre o lead. |
| `listId` | Não | Um único ID de lista para adicionar o contato. |
| `listIds` | Não | Uma matriz de IDs de lista para adicionar o contato (tem precedência sobre `listId`). |
| `custom_fields` | Não | Um objeto com seus próprios campos de chave/valor. Você também pode passá-los como chaves de nível superior. |

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

**Resposta**

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

O ID do novo contato está em `data.contactId`. As listas às quais ele foi adicionado são retornadas em `data.listsAdded`.

> **Duplicatas não são criadas.** Se um contato com o mesmo número de telefone já existir, a chamada de criação **não** o cria nem o retorna. A resposta retorna com status HTTP `200` e um `error_code` de `409` no corpo, portanto, crie uma ramificação baseada em `error_code` em vez do status HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Para trabalhar com um contato existente após um `error_code` de `409`, procure-o com [Obter um contato por telefone ou e-mail](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — e reutilize o ID retornado.

> **Grafias equivalentes do WhatsApp contam como o mesmo número.** Alguns países possuem duas grafias válidas para a mesma linha móvel e o WhatsApp pode informar qualquer uma delas: México (`+52…` e o legado `+521…`), Brasil (com ou sem o nono dígito) e Argentina (com ou sem o `9` após o `+54`). A verificação de duplicatas na criação e a correspondência `GET /contacts?phoneNumber=` funcionam em ambas as grafias, portanto, você recebe o contato existente de volta, independentemente da forma que enviar. O `phone_number` armazenado no contato nunca é reescrito.

---

## Obter um contato por telefone ou e-mail

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

Pesquisa um único contato e retorna o objeto de contato completo e enriquecido — incluindo suas listas, tags e campanhas resolvidas em pares `{ id, name }`, além da última mensagem trocada.

Passe **ou** `phoneNumber` (em formato internacional) **ou** `email`. Se você não passar nenhum dos dois, este mesmo endpoint alterna para o modo [Listar contatos](#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"])
```

**Resposta**

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

O ID do contato é retornado tanto no nível superior (`contactId`) quanto dentro do objeto (`contact.id`). Se nada for encontrado, você recebe um `404` com `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** é a foto de perfil do contato, obtida do WhatsApp ou da Meta quando eles enviam uma mensagem para você. Ela é somente leitura: você não pode defini-la, e ela é `null` para contatos que não possuem foto ou que entram em contato por um canal que não compartilha uma. Trate o link como temporário em vez de armazená-lo, já que alguns desses links de fotos expiram e são atualizados automaticamente. (No endpoint de lista abaixo, o mesmo valor é chamado de `avatar_url`.)

> **Números de telefone em URLs.** Um sinal de `+` em uma string de consulta deve ser codificado em URL como `%2B`, caso contrário, ele será lido como um espaço. Os exemplos acima fazem isso para você.

---

## Obter um contato por ID

`GET /contacts/{contactId}`

Quando você já tiver o ID de um contato, busque-o diretamente. O formato da resposta é idêntico ao da consulta acima.

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

Um ID de contato que não existe em sua conta retorna um `404`.

---

## Obter estatísticas de contato

`GET /contacts/{contactId}/stats`

Retorna estatísticas agregadas de mensagens para um contato: totais, respostas de IA versus humanas, créditos gastos e carimbos de data/hora da primeira e última mensagem.

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

**Resposta**

```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` é o mesmo contador de mensagens de IA que o botão "resetar" no aplicativo para um contato zera. `creditsUsed` é o total de créditos acumulados para este contato, não apenas os números desta resposta. Um ID de contato que não existe na sua conta retorna um `404`.

---

## Listar contatos

`GET /contacts`

Chame `GET /contacts` **sem** `phoneNumber` nem `email` para percorrer todos os seus contatos, do mais recente para o mais antigo. Cada página retorna resumos compactos de contatos (listas, tags e campanhas retornam como arrays de IDs em vez de objetos completos) e um `next_cursor`.

| Parâmetro de consulta | Descrição |
|---|---|
| `limit` | Tamanho da página. O padrão é 50, máximo de 100. |
| `cursor` | O valor `next_cursor` da página anterior. Omitir na primeira página. |
| `listId` | Opcional. Retorna apenas contatos que pertencem a esta lista. |

Para percorrer todas as páginas: faça a primeira chamada sem um cursor e, em seguida, continue passando o `next_cursor` retornado como `cursor`. **Pare quando `next_cursor` for `null`** — isso significa que não há mais resultados.

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

**Resposta**

```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:** Filtrar por um `listId` que não existe em sua conta retorna um `404`. Um `cursor` inválido retorna um `400`.
:::


---

## Contar contatos

`GET /contacts/count`

Retorna quantos contatos correspondem a um filtro, além de uma divisão por canal, sem precisar paginar por eles. Esta é a chamada correta para qualquer pergunta do tipo "quantos" — um bloco de painel, uma automação ou uma pergunta ao Champ. Todos os filtros são opcionais, e combinar vários deles restringe a contagem (um contato precisa corresponder a todos os que você enviar).

| Parâmetro de consulta | Descrição |
|---|---|
| `agentId` | Apenas contatos atribuídos a este agente de IA. Passe `none` para contatos sem agente atribuído (aqueles que são respondidos pelo agente padrão do canal). |
| `channel` | Apenas contatos neste canal, por exemplo, `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Apenas contatos com esta tag, pelo **nome** da tag (maiúsculas/minúsculas não importam). Um nome de tag que você não possui retorna um `404`. |
| `listId` | Apenas contatos nesta lista. |
| `botActive` | `true` ou `false` — apenas contatos cujo assistente de IA está ligado ou desligado. |
| `status` | Apenas contatos com este status, por exemplo, `Lead`. |
| `rules` | Um objeto JSON de regras codificado em URL, usando o mesmo formato de uma lista inteligente (veja [O formato `smart_rules`](#the-smart_rules-shape) mais abaixo). Não pode ser combinado com os outros filtros. |

Envie nenhum filtro e você obterá o número total de contatos em sua conta.

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

**Resposta**

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

`by_channel` divide o mesmo total por canal; contatos que não estão em nenhum canal são contados em `none`. `filters` ecoa os filtros que foram aplicados, para que você possa verificar se a chamada fez o que você pretendia.

::: note
**Nota:** Enviar `rules` junto com qualquer outro filtro, ou um valor `rules` que não seja um JSON válido, retorna um `400`. Um nome de tag ou ID de lista que não existe em sua conta retorna um `404`.
:::


---

## Atualizar um contato

`PUT /contacts/{contactId}`

Atualiza um contato existente. Apenas os campos que você incluir serão alterados — omita qualquer coisa que não queira modificar. Você deve enviar pelo menos um campo, ou receberá um `400` ("Nenhum campo para atualizar").

| Campo | Descrição |
|---|---|
| `firstName` | Primeiro nome. |
| `lastName` | Sobrenome. |
| `email` | Endereço de e-mail. |
| `is_bot_active` | Se o assistente de IA responde a este contato. |
| `is_private` | Marcar como privado. Definir isto como `true` também desativa o assistente de IA. |
| `do_not_disturb` | Pausar o alcance automatizado para este contato. Também impede que a IA responda. |
| `follow_ups_disabled` | Parar todos os acompanhamentos automatizados para este contato (rápido, ciclo e lead frio) enquanto a IA continua respondendo às mensagens que eles enviam. Útil quando alguém já comprou. Permanece desativado até que você defina novamente como `false`. |
| `lead_profile` | Notas de lead em texto livre. |
| `custom_fields` | Um objeto de campos personalizados. **Mesclado por chave** — apenas as chaves que você enviar serão gravadas, o restante dos campos personalizados existentes será mantido. Você também pode passar chaves de campos personalizados no nível superior. |

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

**Resposta**

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

> **Campos personalizados são mesclados, não substituídos.** Enviar `{ "custom_fields": { "tier": "gold" } }` apenas define `tier` — quaisquer outros campos personalizados no contato permanecem exatamente como estavam. Para remover um campo personalizado inteiramente de todos os contatos, use [Excluir um campo personalizado](#delete-a-custom-field).

---

## Adicionar ou remover tags

`POST /contacts/{contactId}/tags`

Adiciona e/ou remove tags de um único contato em uma única chamada. Passe os **IDs** das tags em `addTagIds` e `removeTagIds`. Pelo menos um dos dois deve estar preenchido.

As tags já devem existir na sua conta — crie-as primeiro através do [endpoint de tags](reference.md). Se o contato ou qualquer tag referenciada não existir, você receberá um `404`.

| Campo | Descrição |
|---|---|
| `addTagIds` | Matriz de IDs de tags para adicionar ao contato. |
| `removeTagIds` | Matriz de IDs de tags para remover do contato. |

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

**Resposta**

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

---

## Gerencie sua biblioteca de tags

Estes endpoints gerenciam a tag em si — renomeando-a ou excluindo-a da sua conta — ao contrário de aplicar ou remover uma tag em um contato (veja [Adicionar ou remover tags](#add-or-remove-tags) acima). Cada tag na sua conta possui um ID (`tagId`): aquele exibido no gerenciador de tags do seu painel, e aquele retornado como `data.tag_id` quando você cria uma tag com `POST /tags` e um corpo JSON de `{ "name": "..." }` (sem `phoneNumber`, `email` ou `contactId`).

### Atualizar uma tag

`PUT /tags/{tagId}`

Envie apenas os campos que você está alterando.

| Campo | Descrição |
|---|---|
| `name` | O nome da 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)" }'
```

**Resposta**

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

Um `tagId` que não existe na sua conta retorna um `404`.

### Excluir uma tag

`DELETE /tags/{tagId}`

Exclui uma tag por ID. **Isso não pode ser desfeito** — contatos que possuem a tag simplesmente a perdem. Excluir uma tag que já foi removida (ou que nunca existiu) retorna `200` com `deleted: 0` em vez de um `404`, já que não há nada para enumerar.

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

**Resposta**

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

### Excluir várias tags de uma vez

`DELETE /tags`

| Campo | Descrição |
|---|---|
| `tagIds` | Matriz de IDs de tags a serem excluídas (máx. 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"] }'
```

**Resposta**

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

IDs que não existem ou pertencem a outra conta são ignorados silenciosamente e não contados em `deleted`.

---

## Definir sinalizador em massa

`POST /contacts/bulk-flag`

Define um sinalizador booleano em vários contatos de uma só vez. Até 500 IDs de contato por solicitação. IDs que não existem na sua conta são ignorados e contados em `skipped`.

| Campo | Descrição |
|---|---|
| `contactIds` | Matriz de IDs de contato para atualizar (máx. 500). |
| `field` | Qual sinalizador definir. Um de `bot_active` (assistente de IA ligado/desligado), `dnd` (pausar alcance automatizado), `spam`, `private`. |
| `value` | O valor booleano para definir o sinalizador. |

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

**Resposta**

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

---

## Importar contatos em massa

`POST /contacts/import`

Cria até 500 contatos em uma única chamada a partir de um array JSON. Cada registro precisa de um `phone_number` em formato internacional; todo o resto é opcional. Registros com números de telefone inválidos ou canais não suportados são **ignorados** (não criados), e cada registro ignorado é relatado com seu índice e motivo — para que você possa corrigir apenas as falhas e tentar novamente.

Números de telefone que já existem em sua conta são ignorados como `duplicate` por padrão. Envie `updateExisting: true` para **atualizar** esses contatos: os campos presentes no registro sobrescrevem os do contato (`first_name`, `last_name`, `email`, `lead_profile` e `custom_fields` mesclados chave por chave), `tags` são adicionados e o contato é adicionado à `listId`. Canal, número de telefone e sinalizadores de bot nunca são alterados em um contato existente.

Você pode, opcionalmente, adicionar cada contato importado (ou atualizado) a uma lista com `listId`, definir um `defaultChannel` para registros que não especificam um, e marcar registros com `tags` (nomes de tags — tags ausentes são criadas, as existentes são correspondidas sem diferenciar maiúsculas de minúsculas).

**Campos de nível superior**

| Campo | Obrigatório | Descrição |
|---|---|---|
| `contacts` | Sim | Array de registros de contato (máx. 500). |
| `listId` | Não | Lista para adicionar cada contato importado (e atualizado). Deve ser uma lista em sua conta. |
| `defaultChannel` | Não | Canal aplicado a registros que omitem `channel`. Um entre `whatsapp`, `sms`, `whatsapp_web`. O padrão é `whatsapp`. |
| `updateExisting` | Não | `true` para atualizar contatos cujo número de telefone já existe, em vez de ignorá-los como `duplicate`. O padrão é `false`. |

**Campos por registro**

| Campo | Obrigatório | Descrição |
|---|---|---|
| `phone_number` | Sim | Número de telefone em formato internacional (um `+` inicial é adicionado se estiver faltando). |
| `first_name` | Não | Primeiro nome. |
| `last_name` | Não | Sobrenome. |
| `email` | Não | Endereço de e-mail. |
| `channel` | Não | Um entre `whatsapp`, `sms`, `whatsapp_web`. Recai sobre `defaultChannel`. |
| `is_bot_active` | Não | Se o assistente de IA responde. O padrão é `true`. |
| `is_private` | Não | Marcar como privado. O padrão é `false`. |
| `lead_profile` | Não | Notas de lead em texto livre. |
| `custom_fields` | Não | Objeto de chaves e valores de campos personalizados. |
| `tags` | Não | Array de nomes de tags (uma única string `"a; b"` também funciona). Tags que não existem são criadas; as existentes são correspondidas ignorando maiúsculas/minúsculas. Máx. 25 por registro. |

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

**Resposta**

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

Se alguns registros não puderem ser criados, eles aparecerão em `skipped` com o motivo (aqui sem `updateExisting`, portanto o número existente é ignorado):

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

Com `updateExisting: true`, a mesma solicitação relata o contato existente em `updated` / `updated_contact_ids`.

Possíveis motivos para ignorar: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Limites do plano.** Se o limite de contatos do seu plano não permitir essa quantidade de novos contatos, toda a solicitação será rejeitada antecipadamente com um `403`. Se o limite for atingido durante o processo, os registros restantes retornarão como ignorados com o motivo `contact_limit_reached`.

---

## Importar contatos de um arquivo CSV

Para importações maiores do que o [bulk import](#bulk-import-contacts) suporta (até aproximadamente 50.000 linhas), enfileire um trabalho de importação assíncrono para um arquivo CSV que já esteja no armazenamento da sua conta e, em seguida, faça a sondagem (polling) até que ele seja concluído.

### Iniciar a importação

`POST /contacts/import-csv`

| Campo | Obrigatório | Descrição |
|---|---|---|
| `csvStoragePath` | Sim | Caminho de armazenamento do arquivo CSV, em `users/{your account id}/imports/`, terminando em `.csv`. |
| `listName` | Sim | Cria (ou reutiliza) uma lista com este nome e adiciona todos os contatos importados a ela. |
| `existingListRefs` | Não | Matriz de IDs de listas existentes para as quais também adicionar todos os contatos importados. |
| `defaultChannel` | Não | Canal aplicado às linhas que não especificam um. |

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

**Resposta** (`202` — a importação está na fila, ainda não foi concluída)

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

> **Colocando o arquivo no armazenamento.** Este endpoint inicia e rastreia o trabalho de importação; ele não aceita um upload diretamente. O arquivo CSV precisa já estar em `csvStoragePath` antes de você chamá-lo — o próprio importador de CSV do painel faz isso como seu primeiro passo.

### Consultar o trabalho de importação

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

**Resposta**

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

`status` passa por `queued` → `processing` → `completed`, ou `failed` com o motivo em `error_message`. Um `jobId` que não existe na sua conta retorna um `404`.

---

## Exportar contatos

Inicia uma exportação CSV assíncrona dos seus contatos e retorna um trabalho que você deve sondar para verificar a conclusão.

### Iniciar a exportação

`POST /contacts/export`

| Campo | Obrigatório | Descrição |
|---|---|---|
| `listId` | Não | Exporta apenas os contatos que pertencem a esta lista. |
| `contactIds` | Não | Exporta apenas estes IDs de contato específicos. |

Deixar ambos em branco exporta todos os contatos da sua conta.

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

**Resposta** (`202` — a exportação está na fila)

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

### Verificar o status do trabalho de exportação

`GET /contacts/export/{jobId}`

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

**Resposta**

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

> Assim que `status` estiver `"completed"`, você receberá `export_id` e `contact_count`. O download do arquivo CSV gerado é feito na página de Exportações do seu painel.

---

## Enviar uma mensagem para um contato

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

Envia uma mensagem para um contato existente no canal em que ele já está. A mensagem é colocada na fila e entregue em segundo plano — a resposta confirma que ela foi aceita, não que já foi entregue.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `body` | Sim | O texto da mensagem a ser enviada. |
| `mediaUrl` | Não | URL de um arquivo de mídia para anexar. |
| `mediaContentType` | Não | Tipo MIME da mídia anexada (por exemplo, `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"])
```

**Resposta**

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

> **Não consegue enviar agora?** Se o contato estiver com o modo não perturbe ou privado ativado, ou não estiver em um canal que possa receber mensagens de saída, a solicitação será rejeitada com um `422` e uma `error` explicativa.

Para enviar por número de telefone, ID do Instagram ou outra identidade de canal em vez de um ID de contato — e para mais informações sobre mensagens em geral — consulte a [API de Mensagens](messages.md).

---

## Atribuir um agente de IA a um contato

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

Move uma conversa existente para um agente de IA diferente, a partir da próxima mensagem. É a mesma coisa que **Atribuir Agente de IA** no menu de um chat, e a mesma etapa que a ação **Atribuir agente de IA ou campanha** usa em Automações.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `agentId` | Sim | O ID do agente de IA que deve assumir, ou `null` para limpar a atribuição para que a conversa volte para a caixa de entrada da sua equipe. |
| `triggerAIResponse` | Não | `true` faz com que o agente recém-atribuído responda às últimas mensagens não respondidas do contato imediatamente. O padrão é `false`. |

> **Cuidado com `triggerAIResponse: true`** — ele envia uma mensagem ao contato imediatamente, portanto, use-o apenas quando quiser que eles sejam contatados agora. No Messenger e no Instagram, essa mensagem falha se o contato escreveu para você pela última vez há mais de 24 horas.

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

**Resposta**

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

> O agente deve pertencer à mesma conta que o contato; caso contrário, a solicitação será rejeitada com um `404` ou `403`. Encontre os IDs dos agentes na página Agentes de IA (a URL de cada agente termina com seu ID).

---

## Atribuir um agente de IA a vários contatos

`POST /contacts/bulk-assign-agent`

Move várias conversas para um agente de IA diferente em uma única chamada — ou limpa a atribuição de todas elas com `null`. É puramente uma mudança de roteamento: **nenhuma mensagem é enviada e o agente não responde a ninguém**. Cada contato simplesmente recebe o novo agente na próxima vez que escrever. (É por isso que não há `triggerAIResponse` aqui.)

| Campo | Obrigatório | Descrição |
|---|---|---|
| `agentId` | Sim | O agente de IA que deve assumir, ou `null` para limpar a atribuição. |
| `contactIds` | Um dos três | Até 500 IDs de contato para mover. |
| `filter` | Um dos três | Escolha os contatos no servidor em vez de listá-los, do mais recente para o mais antigo. Aceita as mesmas chaves dos filtros do endpoint de contagem: `agentId` (ou `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | Um dos três | Um objeto de regras de lista inteligente — veja [O formato `smart_rules`](#the-smart_rules-shape). |
| `limit` | Não | Quantos contatos mover nesta chamada ao selecionar com `filter` ou `rules`. De 1 a 500, o padrão é 500. |

Envie exatamente um entre `contactIds`, `filter` ou `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"])
```

**Resposta**

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

`matched` é quantos contatos a seleção encontrou no total, `updated` quantos foram movidos por esta chamada, `skipped` quantos dos IDs que você enviou não foram encontrados em sua conta, e `remaining` quantos ainda correspondem agora que esta chamada foi concluída.

**Movendo todos.** Como uma chamada move no máximo 500 contatos, um grupo grande requer algumas chamadas. Use um filtro que pare de corresponder a um contato assim que ele for movido — por exemplo, `filter: { "agentId": "agent_abc123" }` ao atribuir a `agent_xyz789` — e repita exatamente a mesma chamada até que `remaining` retorne como `0`. Quando você passa `contactIds` em vez disso, `remaining` é sempre `0`.

---

## Atribuir um contato a um departamento

`POST /contacts/{contactId}/department`

"Atribuir este lead ao departamento de Vendas" — registra um contato em um departamento nomeado e, por padrão, o encaminha para quem, nesse departamento, tiver menos contatos no momento. Isso é diferente de [atribuir um agente de IA](#assign-an-ai-agent-to-a-contact): um departamento responde a "qual equipe é responsável por isso", um agente responde a "qual IA responde a isso", e definir um nunca limpa o outro.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `department_id` | Sim | O departamento no qual registrar o contato. Passe `null` para limpar. |
| `hand_to_member` | Não | Também encaminha o contato para a pessoa com menos carga de trabalho nesse departamento. O padrão é `true`. Nunca reatribui um contato que alguém já possua. |

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

**Resposta**

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

`assigned_to` é `null` quando o contato já pertencia a alguém ou você passou `hand_to_member: false`.

---

## Vincular um contato entre canais

"Continuar no WhatsApp" (ou SMS) localiza ou cria o contato desta pessoa em outro canal baseado em telefone e vincula os dois, para que o restante do aplicativo os reconheça como a mesma pessoa.

### Vincular a outro canal

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

| Campo | Obrigatório | Descrição |
|---|---|---|
| `channel` | Sim | O canal ao qual vincular. Um entre `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | Não | Número de telefone a ser usado no novo canal. O padrão é o próprio número do contato de origem. |

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

**Resposta**

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

`created` informa se um novo contato foi criado para o canal de destino ou se um existente foi encontrado e vinculado. Chamar isso uma segunda vez é seguro — ele retorna o mesmo `contact_id` com `created: false` em vez de criar uma duplicata.

Um `422` significa que a conta não pode realizar este vínculo no momento: o contato já está nessa família de canais, não possui número de telefone para usar ou não há remetente conectado para o canal de destino. Um `409` significa que os dois contatos já estão vinculados a duas pessoas diferentes — desvincule um primeiro.

### Listar conversas vinculadas de um contato

`GET /contacts/{contactId}/linked`

Retorna as outras conversas que são a mesma pessoa que este contato. Um contato não vinculado retorna um array vazio, não um `404` — "esta pessoa não tem outros canais" é um estado normal.

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

**Resposta**

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

### Desvincular um contato

`DELETE /contacts/{contactId}/link`

Remove este contato de sua pessoa, de forma unilateral — quaisquer outros contatos ainda vinculados a essa pessoa mantêm seu vínculo, portanto, desvincular um de três não dissolve o grupo.

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

**Resposta**

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

---

## Buscar a foto de perfil de um contato

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

Busca (e armazena em cache) a foto de perfil do WhatsApp ou Meta do contato sob demanda — a mesma foto retornada como `avatarUrl` em [Obter um contato](#get-a-contact-by-phone-or-email), atualizada.

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

**Resposta**

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

`cached: true` significa que a URL veio de uma busca recente em vez de uma consulta nova ao provedor — as fotos são armazenadas em cache por 7 dias, e um contato que o provedor relata não ter foto acessível é armazenado como indisponível por 24 horas. Quando não há foto para buscar, `avatar_url` é omitido e `message` explica o motivo.

---

## Auto-tag de contatos com IA

Executa as regras de tag da sua conta em todo o histórico de conversas de um ou mais contatos e aplica (ou remove) tags exatamente como a marcação em tempo real que ocorre durante um chat ao vivo — mesmas regras, mesmo custo de crédito por tag.

### Iniciar uma execução

`POST /contacts/auto-tag`

| Campo | Obrigatório | Descrição |
|---|---|---|
| `scope` | Sim | `"contacts"` para marcar contatos específicos, ou `"agent"` para marcar todas as conversas atualmente gerenciadas por um agente de IA. |
| `contact_ids` | Obrigatório quando `scope` é `"contacts"` | Array de IDs de contato, de 1 a 500. |
| `agent_id` | Obrigatório quando `scope` é `"agent"` | O agente de IA cujas conversas devem ser marcadas. Quando `scope` é `"contacts"`, isso é opcional e apenas restringe quais regras de tag do agente serão executadas. |

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

Um **único** contato é executado em linha e retorna o resultado imediatamente:

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

**Dois ou mais** contatos (ou `scope: "agent"`) são executados como um trabalho em segundo plano e retornam `202` imediatamente:

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

### Consultar uma execução

`GET /contacts/auto-tag/run`

Retorna a execução atual (ou mais recente) da conta, para que você possa consultar o progresso sem precisar rastrear o `run_id` por conta própria.

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

**Resposta**

```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 a conta nunca iniciou uma. `status` muda de `"running"` para `"completed"` ou `"failed"`.

Apenas uma execução em lote pode estar em andamento por conta de cada vez — iniciar uma segunda enquanto outra está em execução retorna `409` com `error_code: "auto_tag_run_in_progress"`. Ficar sem créditos em uma execução de contato único retorna `402` com `error_code: "insufficient_credits"`; uma execução em lote, em vez disso, para prematuramente e relata até onde chegou em `run`.

---

## Excluir um contato

`DELETE /contacts/{contactId}`

Exclui permanentemente um contato por ID, juntamente com seu histórico de mensagens. **Isso não pode ser desfeito.** Para excluir vários contatos em uma única chamada, use [Excluir contatos](#delete-contacts) abaixo.

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

**Resposta**

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

Um ID de contato que não existe na sua conta, ou que pertence a uma conta diferente, retorna um `404`.

---

## Excluir contatos

`DELETE /contacts`

Exclui permanentemente um ou mais contatos por ID em uma única chamada (até 500 IDs). IDs que não existem na sua conta são ignorados e contados em `skipped`. **Isso não pode ser desfeito.**

| Campo | Descrição |
|---|---|
| `contactIds` | Matriz de IDs de contato para excluir (máx. 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']}")
```

**Resposta**

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

---

## Excluir um campo personalizado

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

Remove uma chave de campo personalizado de **todos** os contatos em sua conta. Use isso para limpar após renomear ou desativar um campo personalizado. A chave pode conter apenas letras, números, sublinhados e hifens. Retorna quantos contatos foram atualizados. **Isso não pode ser desfeito.**

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

**Resposta**

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

::: note
**Nota:** Uma chave de campo com caracteres não suportados retorna um `400`.
:::


---

## Listas

Listas agrupam contatos. Uma lista pode ser **estática** (você decide quem faz parte dela) ou **inteligente** (a associação é calculada a partir de regras e mantida atualizada automaticamente — veja [Organizando Listas e Contatos](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Campo | Descrição |
|---|---|
| `name` | Obrigatório na criação. Até 100 caracteres. |
| `status` | `live` (padrão) ou `draft`. Minúsculo. |
| `contact_ids` | Matriz de IDs de contato para colocar na lista. **Apenas listas estáticas.** |
| `type` | `static` (padrão) ou `smart`. |
| `smart_rules` | O conjunto de regras — obrigatório quando `type` é `smart`. Veja abaixo. |

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

**Resposta**

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

Uma lista inteligente é avaliada **inline**, na mesma solicitação, portanto `evaluation` informa exatamente quem acabou nela. Em uma lista estática, `evaluation` é `null`.

### Atualizar uma lista

`PUT /lists/{listId}`

Envie apenas os campos que você está alterando. Alterar `smart_rules` reavalia a lista imediatamente e retorna o mesmo objeto `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"] } ] } }'
```

Você pode alternar uma lista entre os dois tipos:

- **Estática → inteligente**: envie `{ "type": "smart", "smart_rules": { … } }`. As regras assumem o controle imediatamente.
- **Inteligente → estática**: envie `{ "type": "static" }`. As regras são descartadas e quem estiver na lista permanece nela.

### A estrutura de `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` (todas as condições devem ser verdadeiras) ou `any` (pelo menos uma).
- `conditions` — 1 a 20 condições, cada uma com no máximo 100 valores, strings de até 200 caracteres.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | matriz de IDs de tag |
| `lists` | `in_any`, `not_in_any` | matriz de IDs de lista (**apenas listas estáticas** — uma lista inteligente não pode ser criada a partir de outra lista inteligente) |
| `channel` | `is_any`, `is_none` | matriz de canais |
| `status` | `is_any`, `is_none` | matriz de status de contato |
| `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" }` |
| mesmos campos de data | `before`, `after` | data ISO (`"2026-01-01"`, comparada como dias inteiros) ou data-hora ISO completa (`"2026-01-01T14:30:00Z"`, comparada ao momento exato) |
| mesmos campos de data | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` corresponde a contatos para os quais a IA enviou pelo menos uma mensagem (alguma vez) |
| `is_bot_active`, `do_not_disturb`, `is_private`, `has_ever_responded` | `is` | `true` / `false` |
| `email`, `phone_number`, `first_name`, `last_name` | `is_set`, `not_set`, `contains`, `not_contains` | string para os formulários `contains` |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | matriz de IDs para os formulários `is_any` / `is_none` |
| `custom_field` (mais um `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | string para os formulários de valor |

`not_within_last` também corresponde a contatos para os quais a data nunca foi definida ("mais de N atrás, **ou nunca**"), e as comparações de texto ignoram maiúsculas/minúsculas.

**Engajamento da IA.** `has_interacted_with_ai` é o sinalizador de tempo de vida: `true` para cada contato para o qual sua IA enviou pelo menos uma mensagem, `false` para todos os outros (incluindo contatos que apenas sua equipe respondeu). Ele é marcado na primeira mensagem da IA para um contato e nunca é limpo, portanto, desativar as respostas da IA do contato ou movê-lo para outra campanha não o redefine. Para um *período* — "os contatos que minha IA atendeu este mês", a pergunta de faturamento usual — use o intervalo em `last_ai_interaction_at`:

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

Não confunda nenhum dos dois com `is_bot_active` (a IA tem *permissão* para responder, não que ela tenha respondido) ou `has_ever_responded` (o *contato* respondeu, para qualquer pessoa). Os mesmos dois selos são retornados em cada contato como `first_ai_interaction_at` / `last_ai_interaction_at`, e todo o conjunto de regras também funciona em `GET /contacts?rules=`, para que você possa contar correspondências sem criar uma lista.

### Visualizar um conjunto de regras

`POST /lists/preview`

Conta e amostra os contatos que um conjunto de regras corresponderia, sem criar ou alterar nada. Use isso para verificar a integridade das regras antes de salvá-las.

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

**Resposta**

```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` mantém até 10 contatos, com os mais recentemente ativos primeiro.

### Executar novamente uma lista inteligente agora

`POST /lists/{listId}/evaluate`

Força uma reavaliação imediata (a mesma coisa que **Atualizar agora** faz no painel). As listas inteligentes já são atualizadas quando um contato muda e a cada 15 minutos para regras baseadas em tempo, portanto, isso só é necessário quando você deseja o resultado *agora mesmo*.

**Resposta**

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

`evaluation.skipped: true` significa que outra avaliação da mesma lista já estava em execução e esta chamada não fez nada.

### Listas inteligentes recusam membros escolhidos manualmente

Os endpoints de associação retornam **`409`** com `"This is a smart list — its members are computed from its rules. Edit the rules instead."` quando a lista de destino é inteligente. Isso cobre `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` em `POST /lists` e `PUT /lists/{listId}`, e a escolha de uma lista inteligente como destino de importação CSV. Altere as regras em vez disso.

Chamar `POST /lists/{listId}/evaluate` em uma lista **estática** também é um `409` — ela não tem regras para executar.

---

## Erros da API de Contatos

Os endpoints de contato retornam o envelope de erro padrão:

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

Alguns endpoints também incluem `error_code`, que geralmente corresponde ao status HTTP — a única exceção é o caso de contato duplicado abaixo, onde o status HTTP é `200` e apenas `error_code` carrega o `409`. Os códigos específicos para endpoints de contato:

| Código | Quando ocorre em um endpoint de contato |
|---|---|
| `400` | Solicitação inválida — um campo ausente/inválido, corpo vazio, cursor incorreto ou mais de 500 IDs em um lote. |
| `402` | Créditos insuficientes para concluir uma execução de marcação por IA em um contato (`error_code: "insufficient_credits"`). |
| `404` | O contato, lista ou tag não foi encontrado na sua conta. |
| `409` | Um contato com esse número de telefone já existe (ao criar). Retornado como `error_code` no corpo com um status HTTP de `200`, portanto, verifique `error_code` aqui. Também retornado quando uma execução de marcação automática em massa já está em andamento (`error_code: "auto_tag_run_in_progress"`), ou quando vincular um contato a outro canal uniria dois contatos já vinculados a duas pessoas diferentes. |
| `422` | O contato não pode receber uma mensagem no momento (não perturbe, privado ou canal não suportado). No endpoint de vinculação de canal, também cobre a ausência de número de telefone, um emparelhamento de canal não suportado ou a ausência de um remetente conectado para o canal de destino. |

Um `403` em um endpoint de contato também pode significar um problema de limite de contatos ou permissão de lista, em vez de acesso ao plano. Os códigos compartilhados que todos os endpoints podem retornar — `401`, `403` (seu plano não inclui acesso à API), `429` (limite de taxa) e `500` — estão listados com orientações de nova tentativa em [Erros e Paginação](errors-and-pagination.md).

---

## Próximos passos

- [API de Mensagens](messages.md) — envie mensagens por identidade de canal e gerencie conversas.
- [Referência da API](reference.md) — lista completa de endpoints, incluindo tags e listas.
- [Acesso à API](../integrations/api-access.md) — autenticação, limites de taxa e tratamento de erros.
