
# API de contactos

Un contacto es una persona a la que envías mensajes: su nombre, número de teléfono, correo electrónico, canal, etiquetas, campos personalizados y las listas y campañas a las que pertenece. La API de contactos te permite crear contactos, buscarlos, actualizarlos, etiquetarlos, importarlos de forma masiva y eliminarlos, todo ello sin usar el panel de control.

Todas las rutas de esta página son relativas a la URL base:

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

Por lo tanto, `/contacts` significa `https://api.youraiconnector.com/v1/contacts`.

> **¿Eres nuevo en la API?** Lee primero [Acceso a la API](../integrations/api-access.md): cubre cómo generar tu clave de API, las tres formas de autenticación, los límites de frecuencia y el formato de error. Todo lo que aparece en esta página asume que ya tienes una clave de API funcional.

---

## Acerca de los ID de contacto

Cada contacto tiene un ID único. El ID que recibes al **crear** un contacto (en `data.contactId`) es el mismo ID que utilizas en cualquier otro lugar: para obtener, actualizar, etiquetar, enviar un mensaje o eliminar ese contacto. Guárdalo una vez y reutilízalo.

No tienes que crear un contacto para obtener su ID. También puedes buscarlo por número de teléfono o correo electrónico (consulta [Obtener un contacto](#get-a-contact-by-phone-or-email)), o navegar por todos tus contactos (consulta [Listar contactos](#list-contacts)). Cada una de esas opciones devuelve el mismo ID.

---

## Crear un contacto

`POST /contacts`

Añade un nuevo contacto a tu cuenta. Se **requiere un número de teléfono con código de país**; un correo electrónico por sí solo no es suficiente. Todo lo demás es opcional.

Opcionalmente, puedes añadir el nuevo contacto directamente a una o más listas con `listId` (una sola lista) o `listIds` (una matriz). Si se envían ambos, `listIds` tiene prioridad.

Cualquier campo que envíe que no sea uno de los campos de creación estándar enumerados en la tabla de campos **Crear un contacto** a continuación (`phoneNumber`, `firstName`, `lastName`, `email`, `channel`, `is_bot_active`, `is_private`, `lead_profile`, `listId`, `listIds`, `custom_fields`) se almacena automáticamente como un **campo personalizado**, por lo que una carga útil plana de una herramienta como Make o Zapier funciona sin anidamiento. También puede pasar un objeto `custom_fields` explícito.

| Campo | Requerido | Descripción |
|---|---|---|
| `phoneNumber` | Sí | El número de teléfono del contacto, con código de país (p. ej., `+15551234567`). |
| `firstName` | No | Nombre. |
| `lastName` | No | Apellido. |
| `email` | No | Dirección de correo electrónico. |
| `channel` | No | Canal de mensajería. Uno de `whatsapp`, `sms`, `whatsapp_web`. El valor predeterminado es `whatsapp`. |
| `is_bot_active` | No | Si el asistente de IA responde a este contacto. El valor predeterminado es `true`. |
| `is_private` | No | Marcar el contacto como privado. Cuando es `true`, el asistente de IA se desactiva para ellos. El valor predeterminado es `false`. |
| `lead_profile` | No | Notas de texto libre sobre el cliente potencial. |
| `listId` | No | Un único ID de lista al que añadir el contacto. |
| `listIds` | No | Una matriz de ID de lista a los que añadir el contacto (tiene prioridad sobre `listId`). |
| `custom_fields` | No | Un objeto con tus propios campos de clave/valor. También puedes pasarlos como claves de nivel 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"])
```

**Respuesta**

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

El ID del nuevo contacto se encuentra en `data.contactId`. Las listas a las que fue añadido se devuelven en `data.listsAdded`.

> **No se crean duplicados.** Si ya existe un contacto con el mismo número de teléfono, la llamada de creación **no** lo crea ni lo devuelve. La respuesta regresa con un estado HTTP `200` y un `error_code` de `409` en el cuerpo, así que realice la bifurcación en `error_code` en lugar de en el estado HTTP:
>
> ```json
> { "success": false, "error_code": 409, "error": "A contact with this phone number already exists for the current user." }
> ```
>
> Para trabajar con un contacto existente después de un `error_code` de `409`, búsquelo con [Obtener un contacto por teléfono o correo electrónico](#get-a-contact-by-phone-or-email) — `GET /contacts?phoneNumber=...` — y reutilice el ID que devuelve.

> **Las grafías equivalentes de WhatsApp cuentan como el mismo número.** Algunos países tienen dos grafías válidas para la misma línea móvil y WhatsApp puede informar cualquiera de ellas: México (`+52…` y la `+521…` heredada), Brasil (con o sin el noveno dígito) y Argentina (con o sin el `9` después del `+54`). La comprobación de duplicados al crear y `GET /contacts?phoneNumber=` coincide en ambas grafías, por lo que obtendrá el contacto existente independientemente de la forma que envíe. El `phone_number` almacenado en el contacto nunca se sobrescribe.

---

## Obtener un contacto por teléfono o correo electrónico

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

Busca un único contacto y devuelve el objeto de contacto completo y enriquecido, incluyendo sus listas, etiquetas y campañas resueltas en pares `{ id, name }`, además del último mensaje intercambiado.

Pasa **o bien** `phoneNumber` (en formato internacional) **o bien** `email`. Si no pasas ninguno, este mismo endpoint cambia al 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"])
```

**Respuesta**

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

El ID del contacto se devuelve tanto en el nivel superior (`contactId`) como dentro del objeto (`contact.id`). Si no hay coincidencias, obtienes un `404` con `{ "success": false, "message": "Contact not found" }`.

> **`avatarUrl`** es la foto de perfil del contacto, obtenida de WhatsApp o Meta cuando le envían un mensaje. Es de solo lectura: no puede establecerla y aparece como `null` para los contactos que no tienen foto o que le contactan a través de un canal que no la comparte. Trate el enlace como temporal en lugar de almacenarlo, ya que algunos de estos enlaces de fotos caducan y se actualizan automáticamente. (En el punto final de la lista a continuación, el mismo valor se denomina `avatar_url`.)

> **Números de teléfono en URLs.** Un signo `+` en una cadena de consulta debe estar codificado en la URL como `%2B`; de lo contrario, se interpreta como un espacio. Los ejemplos anteriores hacen esto por ti.

---

## Obtener un contacto por ID

`GET /contacts/{contactId}`

Cuando ya tenga el ID de un contacto, recupérelo directamente. La estructura de la respuesta es idéntica a la de la búsqueda anterior.

**cURL**

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

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123", {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.contact);
```

**Python**

```python
import requests

res = requests.get(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/contact_abc123",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["contact"])
```

Un ID de contacto que no existe en su cuenta devuelve un `404`.

---

## Obtener estadísticas de contacto

`GET /contacts/{contactId}/stats`

Devuelve estadísticas agregadas de mensajes para un contacto: totales, respuestas de IA frente a humanas, créditos gastados y marcas de tiempo del primer y último mensaje.

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

**Respuesta**

```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` es el mismo contador de mensajes de IA que el botón "restablecer" en la aplicación pone a cero para un contacto. `creditsUsed` es el total de créditos acumulados para este contacto, no solo los números de esta respuesta. Un ID de contacto que no existe en su cuenta devuelve un `404`.

---

## Listar contactos

`GET /contacts`

Llame a `GET /contacts` **sin** `phoneNumber` ni `email` para paginar a través de todos sus contactos, empezando por los más recientes. Cada página devuelve resúmenes compactos de los contactos (las listas, etiquetas y campañas se devuelven como matrices de ID en lugar de objetos completos) y un `next_cursor`.

| Parámetro de consulta | Descripción |
|---|---|
| `limit` | Tamaño de página. El valor predeterminado es 50, el máximo es 100. |
| `cursor` | El valor `next_cursor` de la página anterior. Omítalo en la primera página. |
| `listId` | Opcional. Solo devuelve los contactos que pertenecen a esta lista. |

Para recorrer cada página: realice la primera llamada sin un cursor y, a continuación, siga pasando el `next_cursor` devuelto como `cursor`. **Deténgase cuando `next_cursor` sea `null`**; eso significa que no hay más 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
```

**Respuesta**

```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 un `listId` que no existe en su cuenta devuelve un `404`. Un `cursor` no válido devuelve un `400`.
:::


---

## Contar contactos

`GET /contacts/count`

Devuelve cuántos contactos coinciden con un filtro, además de un desglose por canal, sin necesidad de paginarlos. Esta es la llamada correcta para cualquier pregunta de tipo "cuántos": un widget de panel, una automatización o preguntar a Champ. Todos los filtros son opcionales y combinar varios reduce el recuento (un contacto debe coincidir con todos los que envíes).

| Parámetro de consulta | Descripción |
|---|---|
| `agentId` | Solo contactos asignados a este agente de IA. Pasa `none` para contactos sin agente asignado (aquellos que son atendidos por el agente predeterminado del canal). |
| `channel` | Solo contactos en este canal, p. ej., `whatsapp`, `messenger`, `instagram`, `sms`, `email`, `chat_widget`. |
| `tag` | Solo contactos que tengan esta etiqueta, por **nombre** de etiqueta (las mayúsculas/minúsculas no importan). Un nombre de etiqueta que no tengas devuelve un `404`. |
| `listId` | Solo contactos en esta lista. |
| `botActive` | `true` o `false`: solo contactos cuyo asistente de IA esté activado o desactivado. |
| `status` | Solo contactos con este estado, p. ej., `Lead`. |
| `rules` | Un objeto de reglas JSON codificado en URL, usando la misma estructura que una lista inteligente (consulta [La estructura `smart_rules`](#the-smart_rules-shape) más abajo). No se puede combinar con los otros filtros. |

Si no envías ningún filtro, obtendrás el número total de contactos en tu cuenta.

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

**Respuesta**

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

`by_channel` divide el mismo total por canal; los contactos que no están en ningún canal se cuentan bajo `none`. `filters` devuelve los filtros que se aplicaron, para que puedas verificar que la llamada hizo lo que pretendías.

::: note
**Nota:** Enviar `rules` junto con cualquier otro filtro, o un valor `rules` que no sea JSON válido, devuelve un `400`. Un nombre de etiqueta o ID de lista que no exista en tu cuenta devuelve un `404`.
:::


---

## Actualizar un contacto

`PUT /contacts/{contactId}`

Actualiza un contacto existente. Solo se modifican los campos que incluya; omita todo lo que no desee cambiar. Debe enviar al menos un campo, o recibirá un `400` ("No hay campos para actualizar").

| Campo | Descripción |
|---|---|
| `firstName` | Nombre. |
| `lastName` | Apellido. |
| `email` | Dirección de correo electrónico. |
| `is_bot_active` | Si el asistente de IA responde a este contacto. |
| `is_private` | Marcar como privado. Establecer esto en `true` también desactiva el asistente de IA. |
| `do_not_disturb` | Pausar el alcance automatizado a este contacto. También evita que la IA responda. |
| `follow_ups_disabled` | Detener todos los seguimientos automatizados para este contacto (rápidos, de ciclo y de clientes potenciales en frío) mientras la IA sigue respondiendo a los mensajes que envían. Útil una vez que alguien ha comprado. Permanece desactivado hasta que lo vuelva a establecer en `false`. |
| `lead_profile` | Notas de cliente potencial en texto libre. |
| `custom_fields` | Un objeto de campos personalizados. **Combinado por clave**: solo se escriben las claves que envía, el resto de los campos personalizados existentes se mantienen. También puede pasar claves de campos personalizados en el nivel 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"])
```

**Respuesta**

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

> **Los campos personalizados se combinan, no se reemplazan.** Enviar `{ "custom_fields": { "tier": "gold" } }` solo establece `tier`; cualquier otro campo personalizado en el contacto permanece exactamente como estaba. Para eliminar un campo personalizado por completo en todos los contactos, utilice [Eliminar un campo personalizado](#delete-a-custom-field).

---

## Agregar o eliminar etiquetas

`POST /contacts/{contactId}/tags`

Agrega y/o elimina etiquetas en un solo contacto en una llamada. Pase los **ID** de las etiquetas en `addTagIds` y `removeTagIds`. Al menos uno de los dos debe estar completo.

Las etiquetas ya deben existir en su cuenta; créelas primero a través del [punto de conexión de etiquetas](reference.md). Si el contacto o alguna etiqueta referenciada no existe, recibirá un `404`.

| Campo | Descripción |
|---|---|
| `addTagIds` | Matriz de IDs de etiquetas para añadir al contacto. |
| `removeTagIds` | Matriz de IDs de etiquetas para eliminar del 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"])
```

**Respuesta**

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

---

## Administre su biblioteca de etiquetas

Estos endpoints administran la etiqueta en sí (cambiarle el nombre o eliminarla de su cuenta), a diferencia de aplicar o eliminar una etiqueta en un contacto (consulte [Agregar o eliminar etiquetas](#add-or-remove-tags) arriba). Cada etiqueta en su cuenta tiene un ID (`tagId`): el que se muestra en el administrador de etiquetas de su panel de control y el que se devuelve como `data.tag_id` cuando crea una etiqueta con `POST /tags` y un cuerpo JSON de `{ "name": "..." }` (sin `phoneNumber`, `email` o `contactId`).

### Actualizar una etiqueta

`PUT /tags/{tagId}`

Envíe solo los campos que va a cambiar.

| Campo | Descripción |
|---|---|
| `name` | El nombre de la 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)" }'
```

**Respuesta**

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

Un `tagId` que no existe en su cuenta devuelve un `404`.

### Eliminar una etiqueta

`DELETE /tags/{tagId}`

Elimina una etiqueta por ID. **Esto no se puede deshacer**: los contactos que tengan la etiqueta simplemente la perderán. Eliminar una etiqueta que ya no existe (o que nunca existió) devuelve `200` con `deleted: 0` en lugar de un `404`, ya que no hay nada que enumerar.

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

**Respuesta**

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

### Eliminar varias etiquetas a la vez

`DELETE /tags`

| Campo | Descripción |
|---|---|
| `tagIds` | Matriz de IDs de etiquetas a eliminar (máximo 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"] }'
```

**Respuesta**

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

Los IDs que no existen o que pertenecen a otra cuenta se omiten silenciosamente y no se cuentan en `deleted`.

---

## Establecer una bandera de forma masiva

`POST /contacts/bulk-flag`

Establece una bandera booleana en muchos contactos a la vez. Hasta 500 IDs de contacto por solicitud. Los IDs que no existen en su cuenta se omiten y se cuentan en `skipped`.

| Campo | Descripción |
|---|---|
| `contactIds` | Matriz de IDs de contacto a actualizar (máx. 500). |
| `field` | Qué bandera establecer. Uno de `bot_active` (asistente de IA activado/desactivado), `dnd` (pausar alcance automatizado), `spam`, `private`. |
| `value` | El valor booleano al que establecer la bandera. |

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

**Respuesta**

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

---

## Importar contactos de forma masiva

`POST /contacts/import`

Crea hasta 500 contactos en una sola llamada desde una matriz JSON. Cada registro necesita un `phone_number` en formato internacional; todo lo demás es opcional. Los registros con números de teléfono no válidos o canales no admitidos se **omiten** (no se crean), y cada registro omitido se informa con su índice y motivo, para que pueda corregir solo los fallos y volver a intentarlo.

Los números de teléfono que ya existen en su cuenta se omiten como `duplicate` de forma predeterminada. Envíe `updateExisting: true` para **actualizar** esos contactos en su lugar: los campos presentes en el registro sobrescriben los del contacto (`first_name`, `last_name`, `email`, `lead_profile` y `custom_fields` se combinan clave por clave), se añaden `tags` y el contacto se añade a `listId`. El canal, el número de teléfono y las banderas de bot nunca se cambian en un contacto existente.

Opcionalmente, puede añadir cada contacto importado (o actualizado) a una lista con `listId`, establecer un `defaultChannel` para los registros que no especifiquen uno, y etiquetar los registros con `tags` (nombres de etiquetas: las etiquetas que faltan se crean, las existentes se comparan sin distinguir entre mayúsculas y minúsculas).

**Campos de nivel superior**

| Campo | Requerido | Descripción |
|---|---|---|
| `contacts` | Sí | Matriz de registros de contacto (máx. 500). |
| `listId` | No | Lista a la que añadir cada contacto importado (y actualizado). Debe ser una lista en su cuenta. |
| `defaultChannel` | No | Canal aplicado a los registros que omiten `channel`. Uno de `whatsapp`, `sms`, `whatsapp_web`. El valor predeterminado es `whatsapp`. |
| `updateExisting` | No | `true` para actualizar los contactos cuyo número de teléfono ya existe en lugar de omitirlos como `duplicate`. El valor predeterminado es `false`. |

**Campos por registro**

| Campo | Requerido | Descripción |
|---|---|---|
| `phone_number` | Sí | Número de teléfono en formato internacional (se añade un `+` inicial si falta). |
| `first_name` | No | Nombre. |
| `last_name` | No | Apellido. |
| `email` | No | Dirección de correo electrónico. |
| `channel` | No | Uno de `whatsapp`, `sms`, `whatsapp_web`. Recurre a `defaultChannel`. |
| `is_bot_active` | No | Si el asistente de IA responde. El valor predeterminado es `true`. |
| `is_private` | No | Marcar como privado. El valor predeterminado es `false`. |
| `lead_profile` | No | Notas de cliente potencial en texto libre. |
| `custom_fields` | No | Objeto de claves y valores de campos personalizados. |
| `tags` | No | Matriz de nombres de etiquetas (también funciona una sola cadena `"a; b"`). Las etiquetas que no existen se crean; las existentes se comparan ignorando mayúsculas y 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'])}")
```

**Respuesta**

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

Si algunos registros no se pueden crear, aparecen en `skipped` con el motivo (aquí sin `updateExisting`, por lo que se omite el número existente):

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

Con `updateExisting: true`, la misma solicitud informa del contacto existente bajo `updated` / `updated_contact_ids` en su lugar.

Posibles motivos de omisión: `invalid_record`, `missing_phone_number`, `invalid_phone_number`, `invalid_channel`, `duplicate_in_request`, `duplicate`, `contact_limit_reached`, `create_failed`.

> **Límites del plan.** Si el límite de contactos de su plan no permite esta cantidad de nuevos contactos, toda la solicitud se rechaza de antemano con un `403`. Si se alcanza el límite a mitad del proceso, los registros restantes se devuelven como omitidos con el motivo `contact_limit_reached`.

---

## Importar contactos desde un archivo CSV

Para importaciones más grandes de lo que permite la [importación masiva](#bulk-import-contacts) (hasta aproximadamente 50,000 filas), encole un trabajo de importación asíncrono para un archivo CSV que ya se encuentre en el almacenamiento de su cuenta y, a continuación, realice sondeos hasta que se complete.

### Iniciar la importación

`POST /contacts/import-csv`

| Campo | Obligatorio | Descripción |
|---|---|---|
| `csvStoragePath` | Sí | Ruta de almacenamiento del archivo CSV, bajo `users/{your account id}/imports/`, que termina en `.csv`. |
| `listName` | Sí | Crea (o reutiliza) una lista con este nombre y añade a ella cada contacto importado. |
| `existingListRefs` | No | Matriz de IDs de listas existentes a las que también se añadirá cada contacto importado. |
| `defaultChannel` | No | Canal aplicado a las filas que no especifican uno. |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "csvStoragePath": "users/abc123/imports/leads.csv",
    "listName": "Webinar signups"
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    csvStoragePath: "users/abc123/imports/leads.csv",
    listName: "Webinar signups",
  }),
});
const data = await res.json();
console.log(data.job_id);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/import-csv",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "csvStoragePath": "users/abc123/imports/leads.csv",
        "listName": "Webinar signups",
    },
)
job_id = res.json()["job_id"]
```

**Respuesta** (`202` — la importación está en cola, aún no ha finalizado)

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

> **Cómo llevar el archivo al almacenamiento.** Este endpoint inicia y realiza el seguimiento del trabajo de importación; no acepta una carga por sí mismo. El archivo CSV debe estar ya en `csvStoragePath` antes de llamarlo; el propio importador de CSV del panel de control realiza esto como primer paso.

### Consultar el trabajo de importación

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

**Respuesta**

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

`status` avanza a través de `queued` → `processing` → `completed`, o `failed` con el motivo en `error_message`. Un `jobId` que no existe en su cuenta devuelve un `404`.

---

## Exportar contactos

Inicia una exportación CSV asíncrona de sus contactos y devuelve un trabajo que debe sondear para verificar su finalización.

### Iniciar la exportación

`POST /contacts/export`

| Campo | Obligatorio | Descripción |
|---|---|---|
| `listId` | No | Exportar solo los contactos que pertenecen a esta lista. |
| `contactIds` | No | Exportar solo estos ID de contacto específicos. |

Si deja ambos campos vacíos, se exportarán todos los contactos de su cuenta.

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

**Respuesta** (`202` — la exportación está en cola)

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

### Consultar el estado del trabajo de exportación

`GET /contacts/export/{jobId}`

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

**Respuesta**

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

> Una vez que `status` esté `"completed"`, recibirá `export_id` y `contact_count`. La descarga del archivo CSV generado se realiza desde la página de Exportaciones de su panel de control.

---

## Enviar un mensaje a un contacto

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

Envía un mensaje a un contacto existente en el canal en el que ya se encuentre. El mensaje se pone en cola y se entrega en segundo plano; la respuesta confirma que fue aceptado, no que ya se haya entregado.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `body` | Sí | El texto del mensaje a enviar. |
| `mediaUrl` | No | URL de un archivo multimedia para adjuntar. |
| `mediaContentType` | No | Tipo MIME del archivo multimedia adjunto (p. ej., `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"])
```

**Respuesta**

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

> **¿No puedes enviar mensajes ahora?** Si el contacto tiene activado el modo no molestar o privado, o no se encuentra en un canal que pueda recibir mensajes salientes, la solicitud se rechaza con un `422` y un `error` explicativo.

Para enviar mensajes mediante número de teléfono, ID de Instagram u otra identidad de canal en lugar de un ID de contacto —y para obtener más información sobre mensajería en general—, consulta la [API de mensajes](messages.md).

---

## Asignar un agente de IA a un contacto

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

Mueve una conversación existente a un agente de IA diferente, a partir del siguiente mensaje. Es lo mismo que **Asignar agente de IA** en el menú de un chat, y el mismo paso que utiliza la acción **Asignar agente de IA o campaña** en las Automatizaciones.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `agentId` | Sí | El ID del agente de IA que debe tomar el control, o `null` para borrar la asignación y que la conversación vuelva a la bandeja de entrada de su equipo. |
| `triggerAIResponse` | No | `true` hace que el agente recién asignado responda de inmediato a los últimos mensajes sin respuesta del contacto. El valor predeterminado es `false`. |

> **Cuidado con `triggerAIResponse: true`**: envía un mensaje al contacto en ese mismo momento, así que úsalo solo cuando quieras que reciban el mensaje ahora. En Messenger e Instagram, ese mensaje fallará si el contacto te escribió por última vez hace más 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"])
```

**Respuesta**

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

> El agente debe pertenecer a la misma cuenta que el contacto; de lo contrario, la solicitud será rechazada con un `404` o `403`. Encuentre los ID de los agentes en la página de Agentes de IA (la URL de cada agente termina con su ID).

---

## Asignar un agente de IA a muchos contactos

`POST /contacts/bulk-assign-agent`

Mueve muchas conversaciones a un agente de IA diferente en una sola llamada, o borra la asignación para todos ellos con `null`. Es puramente un cambio de enrutamiento: **no se envía ningún mensaje y el agente no responde a nadie**. Cada contacto simplemente recibe el nuevo agente la próxima vez que escribe. (Por eso no hay `triggerAIResponse` aquí).

| Field | Required | Description |
|---|---|---|
| `agentId` | Yes | The AI agent that should take over, or `null` to clear the assignment. |
| `contactIds` | One of the three | Up to 500 contact IDs to move. |
| `filter` | One of the three | Pick the contacts on the server instead of listing them, newest first. Takes the same keys as the count endpoint's filters: `agentId` (or `none`), `channel`, `tag`, `listId`, `botActive`, `status`. |
| `rules` | One of the three | A smart-list rules object — see [The `smart_rules` shape](#the-smart_rules-shape). |
| `limit` | No | How many contacts to move in this call when you select with `filter` or `rules`. 1 to 500, defaults to 500. |

Envía exactamente uno de `contactIds`, `filter` o `rules`.

**cURL**

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agentId": "agent_xyz789",
    "filter": { "agentId": "agent_abc123", "channel": "messenger" }
  }'
```

**JavaScript**

```javascript
const res = await fetch("<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    agentId: "agent_xyz789",
    filter: { agentId: "agent_abc123", channel: "messenger" },
  }),
});
const data = await res.json();
console.log(data.updated, data.remaining);
```

**Python**

```python
import requests

res = requests.post(
    "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/bulk-assign-agent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "agentId": "agent_xyz789",
        "filter": {"agentId": "agent_abc123", "channel": "messenger"},
    },
)
data = res.json()
print(data["updated"], data["remaining"])
```

**Respuesta**

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

`matched` es cuántos contactos encontró la selección en total, `updated` cuántos fueron movidos por esta llamada, `skipped` cuántos de los IDs que enviaste no se encontraron en tu cuenta y `remaining` cuántos siguen coincidiendo ahora que la llamada ha terminado.

**Mover a todos.** Debido a que una llamada mueve como máximo 500 contactos, un grupo grande requiere varias llamadas. Usa un filtro que deje de coincidir con un contacto una vez que se haya movido (por ejemplo, `filter: { "agentId": "agent_abc123" }` mientras asignas a `agent_xyz789`) y repite exactamente la misma llamada hasta que `remaining` devuelva `0`. Cuando pasas `contactIds` en su lugar, `remaining` es siempre `0`.

---

## Asignar un contacto a un departamento

`POST /contacts/{contactId}/department`

"Asignar este cliente potencial a Ventas": archiva un contacto en un departamento específico y, de forma predeterminada, se lo entrega a la persona de ese departamento que actualmente tenga menos contactos. Esto es independiente de [asignar un agente de IA](#assign-an-ai-agent-to-a-contact): un departamento responde a "qué equipo es el propietario de esto", un agente responde a "qué IA responde a esto", y configurar uno nunca elimina el otro.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `department_id` | Sí | El departamento en el que archivar el contacto. Pase `null` para borrarlo. |
| `hand_to_member` | No | También entregar el contacto a la persona con menos carga de trabajo en ese departamento. El valor predeterminado es `true`. Nunca reasigna un contacto que ya pertenece a alguien. |

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

**Respuesta**

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

`assigned_to` es `null` cuando el contacto ya pertenecía a alguien, o si pasó `hand_to_member: false`.

---

## Vincular un contacto a través de canales

"Continuar en WhatsApp" (o SMS) busca o crea el contacto de esta persona en otro canal basado en teléfono y vincula ambos, de modo que el resto de la aplicación los reconozca como la misma persona.

### Vincular a otro canal

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

| Campo | Obligatorio | Descripción |
|---|---|---|
| `channel` | Sí | El canal al que vincular. Uno de `whatsapp`, `whatsapp_web`, `sms`. |
| `phoneNumber` | No | Número de teléfono a utilizar en el nuevo canal. Por defecto, utiliza el número del contacto de origen. |

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

**Respuesta**

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

`created` le indica si se creó un nuevo contacto para el canal de destino o si se encontró y vinculó uno existente. Llamar a esto una segunda vez es seguro: devuelve el mismo `contact_id` con `created: false` en lugar de crear un duplicado.

Un `422` significa que la cuenta no puede realizar este vínculo en este momento: el contacto ya está en esa familia de canales, no tiene número de teléfono que utilizar o no hay ningún remitente conectado para el canal de destino. Un `409` significa que los dos contactos ya están vinculados a dos personas diferentes; primero debe desvincular uno.

### Listar las conversaciones vinculadas de un contacto

`GET /contacts/{contactId}/linked`

Devuelve las otras conversaciones que corresponden a la misma persona que este contacto. Un contacto no vinculado devuelve una matriz vacía, no un `404`; "esta persona no tiene otros canales" es un estado normal.

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

**Respuesta**

```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 un contacto

`DELETE /contacts/{contactId}/link`

Elimina este contacto de su persona, de forma unilateral; cualquier otro contacto que siga vinculado a esa persona mantiene su vínculo, por lo que desvincular uno de tres no disuelve el grupo.

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

**Respuesta**

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

---

## Obtener la foto de perfil de un contacto

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

Obtiene (y almacena en caché) la foto de perfil de WhatsApp o Meta del contacto bajo demanda; la misma foto que se devuelve como `avatarUrl` en [Obtener un contacto](#get-a-contact-by-phone-or-email), actualizada.

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

**Respuesta**

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

`cached: true` significa que la URL proviene de una obtención reciente en lugar de una búsqueda nueva en el proveedor; las imágenes se almacenan en caché durante 7 días, y un contacto del que el proveedor informa que no tiene una foto accesible se almacena en caché como no disponible durante 24 horas. Cuando no hay ninguna imagen que obtener, se omite `avatar_url` y `message` explica el motivo.

---

## Etiquetado automático de contactos con IA

Ejecuta las reglas de etiquetas de su cuenta sobre el historial completo de conversaciones de uno o más contactos y aplica (o elimina) etiquetas exactamente igual que el etiquetado en tiempo real que se ejecuta durante un chat en vivo: mismas reglas, mismo coste de crédito por etiqueta.

### Iniciar una ejecución

`POST /contacts/auto-tag`

| Campo | Obligatorio | Descripción |
|---|---|---|
| `scope` | Sí | `"contacts"` para etiquetar contactos específicos, o `"agent"` para etiquetar cada conversación gestionada actualmente por un agente de IA. |
| `contact_ids` | Obligatorio cuando `scope` es `"contacts"` | Matriz de IDs de contacto, de 1 a 500. |
| `agent_id` | Obligatorio cuando `scope` es `"agent"` | El agente de IA cuyas conversaciones se van a etiquetar. Cuando `scope` es `"contacts"`, esto es opcional y solo restringe qué reglas de etiqueta del agente se ejecutan. |

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/contacts/auto-tag?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "contacts", "contact_ids": ["contact_abc123", "contact_def456"] }'
```

Un **único** contacto se ejecuta en línea y devuelve el resultado inmediatamente:

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

**Dos o más** contactos (o `scope: "agent"`) se ejecutan como un trabajo en segundo plano y devuelven `202` inmediatamente:

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

### Consultar una ejecución

`GET /contacts/auto-tag/run`

Devuelve la ejecución actual (o más reciente) de la cuenta, para que pueda consultar el progreso sin tener que realizar el seguimiento de `run_id` usted mismo.

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

**Respuesta**

```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` es `null` cuando la cuenta nunca ha iniciado una. `status` pasa de `"running"` a `"completed"` o `"failed"`.

Solo puede haber una ejecución masiva en curso por cuenta a la vez; iniciar una segunda mientras otra está en ejecución devuelve `409` con `error_code: "auto_tag_run_in_progress"`. Quedarse sin créditos en una ejecución de un solo contacto devuelve `402` con `error_code: "insufficient_credits"`; una ejecución masiva, en cambio, se detiene antes de tiempo e informa de hasta dónde llegó en `run`.

---

## Eliminar un contacto

`DELETE /contacts/{contactId}`

Elimina permanentemente un contacto por ID, junto con su historial de mensajes. **Esto no se puede deshacer.** Para eliminar varios contactos en una sola llamada, utilice [Eliminar contactos](#delete-contacts) a continuación.

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

**Respuesta**

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

Un ID de contacto que no existe en su cuenta, o que pertenece a una cuenta diferente, devuelve un `404`.

---

## Eliminar contactos

`DELETE /contacts`

Elimina permanentemente uno o varios contactos por ID en una sola llamada (hasta 500 ID). Los ID que no existen en tu cuenta se omiten y se contabilizan en `skipped`. **Esta acción no se puede deshacer.**

| Campo | Descripción |
|---|---|
| `contactIds` | Matriz de ID 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']}")
```

**Respuesta**

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

---

## Eliminar un campo personalizado

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

Elimina una clave de campo personalizado de **todos** los contactos de su cuenta. Utilice esto para realizar una limpieza después de renombrar o retirar un campo personalizado. La clave solo puede contener letras, números, guiones bajos y guiones. Devuelve cuántos contactos fueron actualizados. **Esta acción no se puede deshacer.**

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

**Respuesta**

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

::: note
**Nota:** Una clave de campo con caracteres no admitidos devuelve un `400`.
:::


---

## Listas

Las listas agrupan contactos. Una lista puede ser **estática** (tú decides quién está en ella) o **inteligente** (la pertenencia se calcula a partir de reglas y se mantiene actualizada automáticamente; consulta [Organización de listas y contactos](../get-started/list-and-contact-management.md#smart-lists-auto-updating)).

| Campo | Descripción |
|---|---|
| `name` | Obligatorio al crear. Hasta 100 caracteres. |
| `status` | `live` (predeterminado) o `draft`. En minúsculas. |
| `contact_ids` | Matriz de IDs de contacto para incluir en la lista. **Solo listas estáticas.** |
| `type` | `static` (predeterminado) o `smart`. |
| `smart_rules` | El conjunto de reglas: obligatorio cuando `type` es `smart`. Ver más abajo. |

### Crear una lista

`POST /lists`

```bash
curl -X POST "<span data-t="apiBaseUrl">https://api.youraiconnector.com</span>/v1/lists?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Hot leads (active)",
        "type": "smart",
        "smart_rules": {
          "match": "all",
          "conditions": [
            { "field": "tags", "op": "has_any", "value": ["tagHotLead"] },
            { "field": "last_activity_at", "op": "within_last", "value": { "amount": 90, "unit": "days" } }
          ]
        }
      }'
```

**Respuesta**

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

Una lista inteligente se evalúa **en línea**, en la misma solicitud, por lo que `evaluation` te indica exactamente quién terminó en ella. En una lista estática, `evaluation` es `null`.

### Actualizar una lista

`PUT /lists/{listId}`

Envía solo los campos que vas a cambiar. Cambiar `smart_rules` vuelve a evaluar la lista inmediatamente y devuelve el mismo 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"] } ] } }'
```

Puedes cambiar una lista entre los dos tipos:

- **Estática → inteligente**: envía `{ "type": "smart", "smart_rules": { … } }`. Las reglas se aplican al instante.
- **Inteligente → estática**: envía `{ "type": "static" }`. Las reglas se eliminan y quien esté en la lista permanece en ella.

### La estructura 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 las condiciones deben ser verdaderas) o `any` (al menos una).
- `conditions` — de 1 a 20 condiciones, cada una con un máximo de 100 valores, cadenas de hasta 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 (**solo listas estáticas**: una lista inteligente no puede crearse a partir de otra lista inteligente) |
| `channel` | `is_any`, `is_none` | matriz de canales |
| `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" }` |
| mismos campos de fecha | `before`, `after` | fecha ISO (`"2026-01-01"`, comparada como días completos) o fecha y hora ISO completa (`"2026-01-01T14:30:00Z"`, comparada con el momento exacto) |
| mismos campos de fecha | `is_set`, `not_set` | — |
| `has_interacted_with_ai` | `is` | `true` / `false` — `true` coincide con los contactos a los que la IA ha enviado mensajes al menos una vez (alguna 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` | cadena para los formularios `contains` |
| `current_campaign_id`, `assigned_agent` | `is_any`, `is_none`, `is_set`, `not_set` | matriz de IDs para los formularios `is_any` / `is_none` |
| `custom_field` (más un `key`) | `eq`, `neq`, `contains`, `not_contains`, `is_set`, `not_set` | cadena para los formularios de valor |

`not_within_last` también coincide con contactos para los que nunca se estableció la fecha ("hace más de N, **o nunca**"), y las comparaciones de texto ignoran las mayúsculas y minúsculas.

**Interacción de la IA.** `has_interacted_with_ai` es la marca de tiempo de por vida: `true` para cada contacto al que su IA haya enviado al menos un mensaje, `false` para todos los demás (incluidos los contactos a los que solo su equipo ha respondido). Se marca en el primer mensaje de la IA a un contacto y nunca se borra, por lo que desactivar las respuestas de la IA del contacto o moverlo a otra campaña no la restablece. Para un *período* — "los contactos que mi IA gestionó este mes", la pregunta de facturación habitual — utilice el rango `last_ai_interaction_at` en su lugar:

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

No confunda ninguno de los dos con `is_bot_active` (la IA tiene *permiso* para responder, no que lo haya hecho) o `has_ever_responded` (el *contacto* respondió, a cualquiera). Las mismas dos marcas se devuelven en cada contacto como `first_ai_interaction_at` / `last_ai_interaction_at`, y todo el conjunto de reglas también funciona en `GET /contacts?rules=`, por lo que puede contar las coincidencias sin crear una lista.

### Previsualizar un conjunto de reglas

`POST /lists/preview`

Cuenta y muestra una muestra de los contactos que coincidirían con un conjunto de reglas, sin crear ni cambiar nada. Úselo para verificar la coherencia de las reglas antes de guardarlas.

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

**Respuesta**

```json
{
  "success": true,
  "count": 3,
  "sample": [
    {
      "id": "contact_abc123",
      "first_name": "Sofia",
      "last_name": "Martinez",
      "phone_number": "+31600000000",
      "email": "sofia@example.com",
      "channel": "whatsapp"
    }
  ]
}
```

`sample` contiene hasta 10 contactos, ordenados por actividad más reciente primero.

### Volver a ejecutar una lista inteligente ahora

`POST /lists/{listId}/evaluate`

Fuerza una reevaluación inmediata (lo mismo que hace **Actualizar ahora** en el panel de control). Las listas inteligentes ya se actualizan cuando un contacto cambia y cada 15 minutos para las reglas basadas en tiempo, por lo que esto solo es necesario cuando desea el resultado *ahora mismo*.

**Respuesta**

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

`evaluation.skipped: true` significa que ya se estaba ejecutando otra evaluación de la misma lista y esta llamada no hizo nada.

### Las listas inteligentes rechazan miembros seleccionados manualmente

Los endpoints de membresía devuelven **`409`** con `"This is a smart list — its members are computed from its rules. Edit the rules instead."` cuando la lista de destino es inteligente. Esto cubre `POST /contacts/lists`, `DELETE /contacts/lists`, `POST /contacts/lists/batch`, `contact_ids` en `POST /lists` y `PUT /lists/{listId}`, además de elegir una lista inteligente como destino de importación CSV. Cambie las reglas en su lugar.

Llamar a `POST /lists/{listId}/evaluate` en una lista **estática** también es un `409`: no tiene reglas que ejecutar.

---

## Errores de la API de contactos

Los endpoints de contactos devuelven el sobre de error estándar:

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

Algunos endpoints también incluyen `error_code`, que generalmente coincide con el estado HTTP; la única excepción es el caso de contacto duplicado a continuación, donde el estado HTTP es `200` y solo `error_code` lleva el `409`. Los códigos específicos para los endpoints de contacto:

| Código | Cuándo ocurre en un endpoint de contacto |
|---|---|
| `400` | Solicitud incorrecta: un campo faltante/inválido, cuerpo vacío, cursor incorrecto o más de 500 IDs en un lote. |
| `402` | No hay suficientes créditos para completar una ejecución de etiquetado con IA en un contacto (`error_code: "insufficient_credits"`). |
| `404` | El contacto, la lista o la etiqueta no se encontraron en su cuenta. |
| `409` | Ya existe un contacto con ese número de teléfono (al crear). Se devuelve como `error_code` en el cuerpo con un estado HTTP de `200`, así que bifurque en `error_code` aquí. También se devuelve cuando una ejecución de etiquetado automático masivo ya está en curso (`error_code: "auto_tag_run_in_progress"`), o cuando vincular un contacto a otro canal uniría dos contactos que ya están vinculados a dos personas diferentes. |
| `422` | El contacto no puede recibir un mensaje en este momento (no molestar, privado o canal no compatible). En el endpoint de vinculación de canal, también cubre la falta de número de teléfono, un emparejamiento de canal no compatible o la falta de un remitente conectado para el canal de destino. |

Un `403` en un endpoint de contacto también puede significar un problema de límite de contactos o de permiso de lista en lugar de acceso al plan. Los códigos compartidos que puede devolver cualquier endpoint — `401`, `403` (su plan no incluye acceso a la API), `429` (límite de tasa) y `500` — se enumeran con orientación sobre reintentos en [Errores y paginación](errors-and-pagination.md).

---

## Próximos pasos

- [API de mensajes](messages.md) — envíe mensajes por identidad de canal y gestione conversaciones.
- [Referencia de la API](reference.md) — lista completa de endpoints, incluyendo etiquetas y listas.
- [Acceso a la API](../integrations/api-access.md) — autenticación, límites de tasa y manejo de errores.
