
# API de Contactos

Um contacto é uma pessoa individual com quem comunica — o seu nome, número de telefone, e-mail, canal, etiquetas, campos personalizados e as listas e campanhas a que pertence. A API de Contactos permite-lhe criar contactos, pesquisá-los, atualizá-los, etiquetá-los, importá-los em massa e removê-los, tudo sem utilizar o painel de controlo.

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

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

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

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

---

## Sobre os IDs de contacto

Cada contacto tem um ID único. O ID que recebe quando **cria** um contacto (em `data.contactId`) é o mesmo ID que utiliza em qualquer outro lugar — para obter, atualizar, etiquetar, enviar uma mensagem ou eliminar esse contacto. Guarde-o uma vez e reutilize-o.

Não precisa de criar um contacto para obter o seu ID. Também pode pesquisar um pelo número de telefone ou e-mail (consulte [Obter um contacto](#get-a-contact-by-phone-or-email)), ou percorrer todos os seus contactos (consulte [Listar contactos](#list-contacts)). Cada um desses métodos devolve o mesmo ID.

---

## Criar um contacto

`POST /contacts`

Adiciona um novo contacto à sua conta. É **obrigatório um número de telefone com código de país** — um e-mail por si só não é suficiente. Tudo o resto é opcional.

Pode, opcionalmente, adicionar o novo contacto diretamente a uma ou mais listas com `listId` (uma única lista) ou `listIds` (uma matriz). Se ambos forem enviados, `listIds` tem prioridade.

Qualquer campo que envie que não seja um dos campos de criação padrão listados na tabela de campos **Criar um contacto** abaixo (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`) é automaticamente guardado como um **campo personalizado** — por isso, um payload simples de uma ferramenta como o Make ou o Zapier funciona sem necessidade de aninhamento. Também pode passar um objeto `custom_fields` explícito.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `phoneNumber` | Sim | O número de telefone do contacto, com código de país (por exemplo, `+15551234567`). |
| `firstName` | Não | Nome próprio. |
| `lastName` | Não | Apelido. |
| `email` | Não | Endereço de e-mail. |
| `channel` | Não | Canal de mensagens. Um dos seguintes: `whatsapp`, `sms`, `whatsapp_web`. O padrão é `whatsapp`. |
| `is_bot_active` | Não | Se o assistente de IA responde a este contacto. O padrão é `true`. |
| `is_private` | Não | Marcar o contacto como privado. Quando `true`, o assistente de IA é desativado para o mesmo. O padrão é `false`. |
| `lead_profile` | Não | Notas de texto livre sobre o potencial cliente. |
| `listId` | Não | Um ID de lista único para adicionar o contacto. |
| `listIds` | Não | Uma matriz de IDs de lista para adicionar o contacto (tem prioridade sobre `listId`). |
| `custom_fields` | Não | Um objeto com os seus próprios campos de chave/valor. 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 contacto encontra-se em `data.contactId`. As listas às quais foi adicionado são devolvidas em `data.listsAdded`.

> **Não são criadas duplicatas.** Se um contacto com o mesmo número de telefone já existir, a chamada de criação **não** o cria nem o devolve. A resposta é devolvida com o estado HTTP `200` e um `error_code` de `409` no corpo, por isso baseie a sua lógica no `error_code` em vez do estado HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Para trabalhar com um contacto existente após um `error_code` de `409`, procure-o com [Obter um contacto por telefone ou e-mail](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — e reutilize o ID que este devolve.

> **Grafias equivalentes no WhatsApp contam como o mesmo número.** Alguns países têm duas grafias válidas para a mesma linha móvel e o WhatsApp pode comunicar 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 duplicados na criação e a correspondência `GET /contacts?phoneNumber=` funcionam em ambas as grafias, pelo que obtém o contacto existente independentemente da forma que enviar. O `phone_number` guardado no contacto nunca é reescrito.

---

## Obter um contacto por telefone ou e-mail

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

Procura um único contacto e devolve o objeto de contacto completo e enriquecido — incluindo as suas listas, etiquetas e campanhas resolvidas em pares `{ id, name }`, além da última mensagem trocada.

Passe **ou** `phoneNumber` (em formato internacional) **ou** `email`. Se não passar nenhum, este mesmo endpoint muda para o modo [Listar contactos](#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 contacto é devolvido tanto ao nível superior (`contactId`) como dentro do objeto (`contact.id`). Se nada corresponder, recebe um `404` com `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** é a fotografia de perfil do contacto, obtida a partir do WhatsApp ou da Meta quando lhe enviam uma mensagem. É apenas de leitura: não a pode definir e fica `null` para contactos que não tenham fotografia ou que o contactem através de um canal que não a partilhe. Trate a ligação como temporária em vez de a guardar, uma vez que algumas destas ligações de fotografias expiram e são atualizadas automaticamente. (No endpoint da lista abaixo, o mesmo valor é designado por `avatar_url`.)

> **Números de telefone em URLs.** Um sinal `+` numa query string deve ser codificado como URL `%2B`, caso contrário, é lido como um espaço. Os exemplos acima fazem isto por si.

---

## Obter um contacto por ID

`GET /contacts/{contactId}`

Quando já tiver o ID de um contacto, obtenha-o diretamente. A estrutura da resposta é idêntica à 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 contacto que não exista na sua conta devolve um `404`.

---

## Obter estatísticas de contacto

`GET /contacts/{contactId}/stats`

Devolve estatísticas agregadas de mensagens para um contacto: totais, respostas de IA vs. 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 "repor" na aplicação, num contacto, coloca a zero. `creditsUsed` é o total de créditos acumulados para este contacto, não apenas os números desta resposta. Um ID de contacto que não exista na sua conta devolve um `404`.

---

## Listar contactos

`GET /contacts`

Chame `GET /contacts` **sem** `phoneNumber` nem `email` para percorrer todos os seus contactos, do mais recente para o mais antigo. Cada página devolve resumos compactos dos contactos (as listas, etiquetas e campanhas são devolvidas como matrizes 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. Devolver apenas os contactos que pertencem a esta lista. |

Para percorrer todas as páginas: faça a primeira chamada sem um cursor e, em seguida, continue a passar o `next_cursor` devolvido como `cursor`. **Pare quando `next_cursor` for `null`** — isso significa que não existem 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 na sua conta devolve um `404`. Um `cursor` inválido devolve um `400`.
:::


---

## Contar contactos

`GET /contacts/count`

Devolve quantos contactos correspondem a um filtro, além de uma divisão por canal, sem necessidade de paginação. Esta é a chamada correta para qualquer pergunta do tipo "quantos" — um elemento de dashboard, uma automatização ou uma pergunta ao Champ. Todos os filtros são opcionais e a combinação de vários reduz a contagem (um contacto tem de corresponder a todos os que enviar).

| Parâmetro de consulta | Descrição |
|---|---|
| `agentId` | Apenas contactos atribuídos a este agente de IA. Utilize `none` para contactos sem agente atribuído (esses são respondidos pelo agente predefinido do canal). |
| `channel` | Apenas contactos neste canal, p. ex., `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Apenas contactos com esta etiqueta, pelo **nome** da etiqueta (maiúsculas/minúsculas não importam). Um nome de etiqueta que não possua devolve um `404`. |
| `listId` | Apenas contactos nesta lista. |
| `botActive` | `true` ou `false` — apenas contactos cujo assistente de IA está ligado ou desligado. |
| `status` | Apenas contactos com este estado, p. ex., `Lead`. |
| `rules` | Um objeto de regras JSON codificado em URL, usando a mesma forma que uma lista inteligente (ver [A forma `smart_rules`](#the-smart_rules-shape) mais abaixo). Não pode ser combinado com os outros filtros. |

Se não enviar qualquer filtro, obterá o número total de contactos na 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; os contactos que não estão em nenhum canal são contados em `none`. `filters` reflete os filtros que foram aplicados, para que possa verificar se a chamada fez o que pretendia.

::: note
**Nota:** Enviar `rules` juntamente com qualquer outro filtro, ou um valor `rules` que não seja JSON válido, devolve um `400`. Um nome de etiqueta ou ID de lista que não exista na sua conta devolve um `404`.
:::


---

## Atualizar um contacto

`PUT /contacts/{contactId}`

Atualiza um contacto existente. Apenas os campos que incluir são alterados — omita tudo o que não pretender modificar. Tem de enviar pelo menos um campo, caso contrário receberá um `400` ("No fields to update").

| Campo | Descrição |
|---|---|
| `firstName` | Nome próprio. |
| `lastName` | Apelido. |
| `email` | Endereço de e-mail. |
| `is_bot_active` | Se o assistente de IA responde a este contacto. |
| `is_private` | Marcar como privado. Definir isto como `true` também desativa o assistente de IA. |
| `do_not_disturb` | Pausar o contacto automatizado para este contacto. Também impede a IA de responder. |
| `follow_ups_disabled` | Parar todos os seguimentos automatizados para este contacto (rápidos, de ciclo e de leads frias) enquanto a IA continua a responder às mensagens que eles enviam. Útil após uma compra. Permanece desativado até que o defina novamente para `false`. |
| `lead_profile` | Notas de lead em texto livre. |
| `custom_fields` | Um objeto de campos personalizados. **Fundido por chave** — apenas as chaves que enviar são escritas, o resto dos campos personalizados existentes são mantidos. Também pode passar chaves de campos personalizados ao 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"
}
```

> **Os campos personalizados são fundidos, não substituídos.** Enviar `{ "custom_fields": { "tier": "gold" } }` apenas define `tier` — quaisquer outros campos personalizados no contacto permanecem exatamente como estavam. Para remover um campo personalizado por completo em todos os contactos, utilize [Eliminar um campo personalizado](#delete-a-custom-field).

---

## Adicionar ou remover etiquetas

`POST /contacts/{contactId}/tags`

Adiciona e/ou remove etiquetas num único contacto numa única chamada. Passe os **IDs** das etiquetas em `addTagIds` e `removeTagIds`. Pelo menos um dos dois tem de estar preenchido.

As etiquetas têm de existir previamente na sua conta — crie-as primeiro através do [endpoint de etiquetas](reference.md). Se o contacto ou qualquer etiqueta referenciada não existir, receberá um `404`.

| Campo | Descrição |
|---|---|
| `addTagIds` | Matriz de IDs de etiquetas a adicionar ao contacto. |
| `removeTagIds` | Matriz de IDs de etiquetas a remover do contacto. |

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

---

## Gerir a sua biblioteca de etiquetas

Estes endpoints gerem a própria etiqueta — renomeando-a ou eliminando-a na sua conta — ao contrário de aplicar ou remover uma etiqueta num contacto (consulte [Adicionar ou remover etiquetas](#add-or-remove-tags) acima). Cada etiqueta na sua conta tem um ID (`tagId`): aquele que é apresentado no gestor de etiquetas do seu painel de controlo e o que é devolvido como `data.tag_id` quando cria uma etiqueta com `POST /tags` e um corpo JSON de `{ "name": "..." }` (sem `phoneNumber`, `email` ou `contactId`).

### Atualizar uma etiqueta

`PUT /tags/{tagId}`

Envie apenas os campos que pretende alterar.

| Campo | Descrição |
|---|---|
| `name` | O nome da etiqueta. |

```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 exista na sua conta devolve um `404`.

### Eliminar uma etiqueta

`DELETE /tags/{tagId}`

Elimina uma etiqueta por ID. **Isto não pode ser anulado** — os contactos que possuem a etiqueta simplesmente perdem-na. Eliminar uma etiqueta que já não existe (ou que nunca existiu) devolve `200` com `deleted: 0` em vez de um `404`, uma vez 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 }
```

### Eliminar várias etiquetas de uma só vez

`DELETE /tags`

| Campo | Descrição |
|---|---|
| `tagIds` | Matriz de IDs de etiquetas a eliminar (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 }
```

Os IDs que não existem, ou que pertencem a outra conta, são ignorados silenciosamente e não contam para `deleted`.

---

## Definir um sinalizador em massa

`POST /contacts/bulk-flag`

Define um sinalizador booleano em vários contactos de uma só vez. Até 500 IDs de contacto por pedido. Os IDs que não existirem na sua conta serão ignorados e contados em `skipped`.

| Campo | Descrição |
|---|---|
| `contactIds` | Matriz de IDs de contacto a atualizar (máx. 500). |
| `field` | Que sinalizador definir. Um de `bot_active` (assistente de IA ligado/desligado), `dnd` (pausar divulgação automatizada), `spam`, `private`. |
| `value` | O valor booleano para o qual 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 contactos em massa

`POST /contacts/import`

Cria até 500 contactos numa única chamada a partir de uma matriz JSON. Cada registo necessita de um `phone_number` em formato internacional; tudo o resto é opcional. Os registos com números de telefone inválidos ou canais não suportados são **ignorados** (não criados), e cada registo ignorado é reportado com o seu índice e motivo — para que possa corrigir apenas as falhas e tentar novamente.

Os números de telefone que já existem na sua conta são ignorados como `duplicate` por predefinição. Envie `updateExisting: true` para **atualizar** esses contactos: os campos presentes no registo substituem os do contacto (`first_name`, `last_name`, `email`, `lead_profile` e `custom_fields` fundidos chave a chave), os `tags` são adicionados e o contacto é adicionado à `listId`. O canal, o número de telefone e as flags do bot nunca são alterados num contacto existente.

Pode opcionalmente adicionar cada contacto importado (ou atualizado) a uma lista com `listId`, definir um `defaultChannel` para registos que não especifiquem um, e etiquetar registos com `tags` (nomes das etiquetas — as etiquetas em falta são criadas, as existentes são correspondidas sem distinção entre maiúsculas e minúsculas).

**Campos de nível superior**

| Campo | Obrigatório | Descrição |
|---|---|---|
| `contacts` | Sim | Matriz de registos de contactos (máx. 500). |
| `listId` | Não | Lista à qual adicionar cada contacto importado (e atualizado). Tem de ser uma lista na sua conta. |
| `defaultChannel` | Não | Canal aplicado a registos que omitam `channel`. Um de `whatsapp`, `sms`, `whatsapp_web`. O predefinido é `whatsapp`. |
| `updateExisting` | Não | `true` para atualizar contactos cujo número de telefone já exista em vez de os ignorar como `duplicate`. O predefinido é `false`. |

**Campos por registo**

| Campo | Obrigatório | Descrição |
|---|---|---|
| `phone_number` | Sim | Número de telefone em formato internacional (é adicionado um `+` inicial se estiver em falta). |
| `first_name` | Não | Nome próprio. |
| `last_name` | Não | Apelido. |
| `email` | Não | Endereço de e-mail. |
| `channel` | Não | Um de `whatsapp`, `sms`, `whatsapp_web`. Recorre a `defaultChannel`. |
| `is_bot_active` | Não | Se o assistente de IA responde. O predefinido é `true`. |
| `is_private` | Não | Marcar como privado. O predefinido é `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 | Matriz de nomes de etiquetas (uma única string `"a; b"` também funciona). As etiquetas que não existem são criadas; as existentes são correspondidas ignorando maiúsculas/minúsculas. Máx. 25 por registo. |

**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 registos não puderem ser criados, aparecem em `skipped` com o motivo (aqui sem `updateExisting`, pelo que 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`, o mesmo pedido reporta o contacto existente em `updated` / `updated_contact_ids`.

Possíveis motivos de exclusão: `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 contactos do seu plano não permitir este número de novos contactos, todo o pedido é rejeitado à partida com um `403`. Se o limite for atingido a meio do processo, os registos restantes são devolvidos como ignorados com o motivo `contact_limit_reached`.

---

## Importar contactos a partir de um ficheiro CSV

Para importações maiores do que o que a [importação em massa](#bulk-import-contacts) suporta (até cerca de 50 000 linhas), coloque em fila um trabalho de importação assíncrono para um ficheiro CSV que já se encontre no armazenamento da sua conta e, em seguida, consulte-o até que seja concluído.

### Iniciar a importação

`POST /contacts/import-csv`

| Campo | Obrigatório | Descrição |
|---|---|---|
| `csvStoragePath` | Sim | Caminho de armazenamento do ficheiro CSV, em `users/{your account id}/imports/`, terminado em `.csv`. |
| `listName` | Sim | Cria (ou reutiliza) uma lista com este nome e adiciona-lhe todos os contactos importados. |
| `existingListRefs` | Não | Matriz de IDs de listas existentes às quais também adicionar todos os contactos 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 terminou)

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

> **Colocar o ficheiro no armazenamento.** Este endpoint inicia e acompanha o trabalho de importação; não aceita o carregamento em si. O ficheiro CSV precisa de já estar em `csvStoragePath` antes de o chamar — o próprio importador de CSV do painel de controlo faz isto como 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` progride através de `queued` → `processing` → `completed`, ou `failed` com o motivo em `error_message`. Um `jobId` que não exista na sua conta devolve um `404`.

---

## Exportar contactos

Inicia uma exportação CSV assíncrona dos seus contactos e devolve um trabalho que pode consultar para verificar a conclusão.

### Iniciar a exportação

`POST /contacts/export`

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

Se deixar ambos em branco, exportará todos os contactos 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 estado da tarefa 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"`, receberá `export_id` e `contact_count`. O descarregamento do ficheiro CSV gerado é feito a partir da página de Exportações do seu painel de controlo.

---

## Enviar uma mensagem a um contacto

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

Envia uma mensagem para um contacto existente no canal em que este se encontra. A mensagem é colocada numa fila e entregue em segundo plano — a resposta confirma que foi aceite, não que já foi entregue.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `body` | Sim | O texto da mensagem a enviar. |
| `mediaUrl` | Não | URL de um ficheiro multimédia a anexar. |
| `mediaContentType` | Não | Tipo MIME do conteúdo multimédia anexado (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 contacto tiver o modo de não incomodar ou o modo privado ativado, ou não estiver num canal que possa receber mensagens de saída, o pedido é rejeitado 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 contacto — e para mais informações sobre mensagens em geral — consulte a [API de Mensagens](messages.md).

---

## Atribuir um agente de IA a um contacto

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

Move uma conversação existente para um agente de IA diferente, a partir da mensagem seguinte. É o mesmo que **Atribuir Agente de IA** no menu de uma conversação, e o mesmo passo que a ação **Atribuir agente de IA ou campanha** utiliza nas Automações.

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

> **Cuidado com `triggerAIResponse: true`** — envia uma mensagem ao contacto imediatamente, por isso utilize-a apenas quando quiser que a mensagem seja enviada agora. No Messenger e no Instagram, essa mensagem falha se o contacto lhe tiver escrito 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 contacto; caso contrário, o pedido é rejeitado com um `404` ou `403`. Encontre os IDs dos agentes na página Agentes de IA (o URL de cada agente termina com o seu ID).

---

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

`POST /contacts/bulk-assign-agent`

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

| Campo | Obrigatório | Descrição |
|---|---|---|
| `agentId` | Sim | O agente de IA que deve assumir o controlo, ou `null` para limpar a atribuição. |
| `contactIds` | Um dos três | Até 500 IDs de contacto para mover. |
| `filter` | Um dos três | Selecione os contactos no servidor em vez de os listar, começando pelos mais recentes. Aceita as mesmas chaves que os 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 — ver [A forma `smart_rules`](#the-smart_rules-shape). |
| `limit` | Não | Quantos contactos mover nesta chamada quando seleciona com `filter` ou `rules`. De 1 a 500, o padrão é 500. |

Envie exatamente um de `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 contactos a seleção encontrou no total, `updated` quantos foram movidos por esta chamada, `skipped` quantos dos IDs que enviou não foram encontrados na sua conta e `remaining` quantos ainda correspondem agora que esta chamada terminou.

**Mover todos.** Como uma chamada move no máximo 500 contactos, um grupo grande requer algumas chamadas. Utilize um filtro que deixe de corresponder a um contacto assim que este for movido — por exemplo `filter: { "agentId": "agent_abc123" }` enquanto atribui a `agent_xyz789` — e repita exatamente a mesma chamada até que `remaining` devolva `0`. Quando passa `contactIds` em vez disso, `remaining` é sempre `0`.

---

## Atribuir um contacto a um departamento

`POST /contacts/{contactId}/department`

"Atribuir este lead a Vendas" — regista um contacto num departamento específico e, por predefinição, entrega-o a quem, nesse departamento, tiver atualmente menos contactos. Isto é diferente de [atribuir um agente de IA](#assign-an-ai-agent-to-a-contact): um departamento responde a "que equipa é responsável por isto", um agente responde a "que IA responde a isto", e definir um nunca elimina o outro.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `department_id` | Sim | O departamento onde registar o contacto. Utilize `null` para limpar. |
| `hand_to_member` | Não | Atribuir também o contacto à pessoa com menos carga de trabalho nesse departamento. O valor predefinido é `true`. Nunca reatribui um contacto que já pertença a alguém. |

**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 contacto já pertencia a alguém, ou se tiver utilizado `hand_to_member: false`.

---

## Ligar um contacto entre canais

"Continuar no WhatsApp" (ou SMS) encontra ou cria o contacto desta pessoa noutro canal baseado em telefone e liga os dois, para que o resto da aplicação os reconheça como a mesma pessoa.

### Ligar a outro canal

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

| Campo | Obrigatório | Descrição |
|---|---|---|
| `channel` | Sim | O canal a ligar. Um de `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | Não | Número de telefone a utilizar no novo canal. Por predefinição, utiliza o número do próprio contacto 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` indica se foi criado um novo contacto para o canal de destino ou se foi encontrado e ligado um contacto existente. Chamar isto uma segunda vez é seguro — devolve o mesmo `contact_id` com `created: false` em vez de criar um duplicado.

Um `422` significa que a conta não pode efetuar esta ligação neste momento: o contacto já se encontra nessa família de canais, não tem número de telefone para utilizar ou não existe nenhum remetente ligado para o canal de destino. Um `409` significa que os dois contactos já estão ligados a duas pessoas diferentes — desligue um primeiro.

### Listar as conversas ligadas de um contacto

`GET /contacts/{contactId}/linked`

Devolve as outras conversas que correspondem à mesma pessoa que este contacto. Um contacto não ligado devolve uma matriz vazia, 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"
      }
    }
  ]
}
```

### Desligar um contacto

`DELETE /contacts/{contactId}/link`

Remove este contacto da sua pessoa, de forma unilateral — quaisquer outros contactos ainda ligados a essa pessoa mantêm a sua ligação, pelo que desligar 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 }
```

---

## Obter a fotografia de perfil de um contacto

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

Obtém (e coloca em cache) a fotografia de perfil do WhatsApp ou Meta do contacto a pedido — a mesma fotografia devolvida como `avatarUrl` em [Obter um contacto](#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 o URL provém de uma obtenção recente em vez de uma consulta direta ao fornecedor — as fotografias são colocadas em cache durante 7 dias, e um contacto que o fornecedor indica não ter fotografia acessível é colocado em cache como indisponível durante 24 horas. Quando não existe fotografia para obter, `avatar_url` é omitido e `message` explica o motivo.

---

## Etiquetar contactos automaticamente com IA

Executa as regras de etiquetas da sua conta sobre o histórico completo de conversas de um ou mais contactos e aplica (ou remove) etiquetas exatamente como a etiquetagem em tempo real que ocorre durante um chat ao vivo — mesmas regras, mesmo custo de crédito por etiqueta.

### Iniciar uma execução

`POST /contacts/auto-tag`

| Campo | Obrigatório | Descrição |
|---|---|---|
| `scope` | Sim | `"contacts"` para etiquetar contactos específicos, ou `"agent"` para etiquetar todas as conversas atualmente geridas por um agente de IA. |
| `contact_ids` | Obrigatório quando `scope` é `"contacts"` | Matriz de IDs de contacto, de 1 a 500. |
| `agent_id` | Obrigatório quando `scope` é `"agent"` | O agente de IA cujas conversas devem ser etiquetadas. Quando `scope` é `"contacts"`, isto é opcional e apenas restringe quais das regras de etiqueta do agente sã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** contacto é executado em linha e devolve o resultado imediatamente:

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

**Dois ou mais** contactos (ou `scope: "agent"`) são executados como uma tarefa em segundo plano e devolvem `202` imediatamente:

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

### Consultar uma execução

`GET /contacts/auto-tag/run`

Devolve a execução atual (ou mais recente) da conta, para que possa consultar o progresso sem ter de controlar o `run_id` por si próprio.

```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. O `status` passa de `"running"` para `"completed"` ou `"failed"`.

Apenas uma execução em massa pode estar em curso por conta de cada vez — iniciar uma segunda enquanto outra está a decorrer devolve `409` com `error_code: "auto_tag_run_in_progress"`. Ficar sem créditos numa execução de contacto único devolve `402` com `error_code: "insufficient_credits"`; uma execução em massa, pelo contrário, para antecipadamente e comunica até onde chegou em `run`.

---

## Eliminar um contacto

`DELETE /contacts/{contactId}`

Elimina permanentemente um contacto por ID, juntamente com o seu histórico de mensagens. **Isto não pode ser anulado.** Para eliminar vários contactos numa única chamada, utilize [Eliminar contactos](#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 contacto que não existe na sua conta, ou que pertence a uma conta diferente, devolve um `404`.

---

## Eliminar contactos

`DELETE /contacts`

Elimina permanentemente um ou mais contactos por ID numa única chamada (até 500 IDs). Os IDs que não existirem na sua conta são ignorados e contabilizados em `skipped`. **Esta ação não pode ser anulada.**

| Campo | Descrição |
|---|---|
| `contactIds` | Matriz de IDs de contacto a eliminar (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
}
```

---

## Eliminar um campo personalizado

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

Remove uma chave de campo personalizado de **todos** os contactos na sua conta. Utilize isto para limpar após mudar o nome ou retirar um campo personalizado. A chave pode conter apenas letras, números, sublinhados e hífenes. Devolve o número de contactos atualizados. **Esta ação não pode ser desfeita.**

**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 devolve um `400`.
:::


---

## Listas

As listas agrupam contactos. Uma lista pode ser **estática** (você decide quem a integra) ou **inteligente** (a adesão é calculada com base em regras e mantida atualizada automaticamente — consulte [Organizar Listas e Contactos](../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` (predefinição) ou `draft`. Em minúsculas. |
| `contact_ids` | Matriz de IDs de contacto a incluir na lista. **Apenas listas estáticas.** |
| `type` | `static` (predefiniçã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 **em linha**, no mesmo pedido, pelo que `evaluation` lhe indica exatamente quem acabou por a integrar. Numa lista estática, `evaluation` é `null`.

### Atualizar uma lista

`PUT /lists/{listId}`

Envie apenas os campos que pretende alterar. Alterar `smart_rules` reavalia a lista imediatamente e devolve 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"] } ] } }'
```

Pode alternar uma lista entre os dois tipos:

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

### A estrutura `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 até 200 caracteres.

| `field` | `op` | `value` |
|---|---|---|
| `tags` | `has_any`, `has_all`, `has_none` | matriz de IDs de etiquetas |
| `lists` | `in_any`, `not_in_any` | matriz de IDs de listas (**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 estados de contacto |
| `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 completos) ou data-hora ISO completa (`"2026-01-01T14:30:00Z"`, comparada com o momento exato) |
| mesmos campos de data | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` corresponde a contactos aos 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 contactos para os quais a data nunca foi definida ("há mais de N, **ou nunca**"), e as comparações de texto ignoram maiúsculas/minúsculas.

**Interação da IA.** `has_interacted_with_ai` é o sinalizador de ciclo de vida: `true` para cada contacto a quem a sua IA enviou pelo menos uma mensagem, `false` para todos os outros (incluindo contactos aos quais apenas a sua equipa respondeu). É marcado na primeira mensagem da IA para um contacto e nunca é apagado, pelo que desativar as respostas da IA do contacto ou movê-lo para outra campanha não o reinicia. Para um *período* — "os contactos que a minha IA geriu este mês", a questão habitual de faturação — utilize o intervalo `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 o tenha feito) ou `has_ever_responded` (o *contacto* respondeu, a qualquer pessoa). As mesmas duas marcas são devolvidas em cada contacto como `first_ai_interaction_at` / `last_ai_interaction_at`, e todo o conjunto de regras também funciona em `GET /contacts?rules=`, pelo que pode contar correspondências sem criar uma lista.

### Pré-visualizar um conjunto de regras

`POST /lists/preview`

Conta e amostra os contactos que um conjunto de regras corresponderia, sem criar ou alterar nada. Utilize-o para verificar a validade das regras antes de as guardar.

```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` contém até 10 contactos, ordenados pelos mais recentemente ativos.

### Voltar a executar uma lista inteligente agora

`POST /lists/{listId}/evaluate`

Força uma reavaliação imediata (o mesmo que **Atualizar agora** faz no painel). As listas inteligentes já são atualizadas quando um contacto é alterado e a cada 15 minutos para regras baseadas no tempo, por isso isto só é necessário quando pretende 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.

### As listas inteligentes recusam membros selecionados manualmente

Os endpoints de associação devolvem **`409`** com `"This is a smart list — its members are computed from its rules. Edit the rules instead."` quando a lista de destino é inteligente. Isto abrange `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` numa lista **estática** também é um `409` — não tem regras para executar.

---

## Erros da API de Contactos

Os endpoints de contactos devolvem o envelope de erro padrão:

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

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

| Código | Quando ocorre num endpoint de contacto |
|---|---|
| `400` | Pedido inválido — um campo em falta/inválido, corpo vazio, cursor incorreto ou mais de 500 IDs num lote. |
| `402` | Créditos insuficientes para concluir uma execução de etiquetagem por IA num contacto (`error_code: "insufficient_credits"`). |
| `404` | O contacto, lista ou etiqueta não foi encontrado na sua conta. |
| `409` | Já existe um contacto com esse número de telefone (ao criar). Devolvido como `error_code` no corpo com um estado HTTP de `200`, por isso ramifique em `error_code` aqui. Também devolvido quando uma execução de etiquetagem automática em massa já está em curso (`error_code: "auto_tag_run_in_progress"`), ou quando a ligação de um contacto a outro canal uniria dois contactos já ligados a duas pessoas diferentes. |
| `422` | O contacto não pode receber uma mensagem neste momento (não incomodar, privado ou canal não suportado). No endpoint de ligação de canal, também abrange a ausência de número de telefone, um emparelhamento de canal não suportado ou a ausência de um remetente ligado para o canal de destino. |

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

---

## Próximos passos

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