
# Mensajes y conversaciones

La API de Mensajes le permite enviar un mensaje a cualquier contacto, leer una conversación, corregir o eliminar un mensaje que ya envió, reaccionar a uno, extraer un hilo completo de sesión de chat, exportar una transcripción y marcar chats como leídos o no leídos, todo sin abrir la bandeja de entrada.

Todas las rutas en esta página son relativas a la URL base `https://api.youraiconnector.com/v1`. Cada solicitud requiere su clave de API; consulte [Autenticación](authentication.md) para ver la lista completa de formas de enviarla. Los ejemplos a continuación utilizan el encabezado `X-API-Key`, y un ejemplo de cURL muestra también el formato de consulta `?apiKey=`.

> **Cómo funciona la entrega:** El envío de un mensaje **no** espera a que llegue. La API acepta su mensaje, responde inmediatamente con un ID de mensaje y luego lo entrega en segundo plano a través del canal del contacto (WhatsApp, SMS, Instagram, etc.). Para realizar un seguimiento de si un mensaje se entregó o leyó realmente, escuche las actualizaciones de estado con [Webhooks](webhooks.md); no realice sondeos (polling). La respuesta de envío solo confirma que el mensaje fue aceptado.

---

## Enviar un mensaje

Hay dos formas de enviar. Elija la que mejor se adapte a cómo identifica actualmente al contacto:

- **Enviar por ID de contacto**: usted ya conoce el ID del contacto (por ejemplo, creó el contacto a través de la API u lo obtuvo de un webhook). Use `POST /contacts/{contactId}/send-message`.
- **Enviar por identidad de contacto**: usted conoce el número de teléfono, el ID de Instagram, etc., del contacto, pero no su ID interno. Use `POST /contacts/send` y deje que la plataforma encuentre el contacto correcto.

Ambos ponen el mensaje en cola de la misma manera y lo entregan en el canal en el que se encuentre el contacto. Usted no elige el transporte; la plataforma enruta los contactos de WhatsApp a través de WhatsApp, los contactos de SMS a través de SMS, y así sucesivamente.

### Enviar por ID de contacto

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

| Campo | Obligatorio | Descripción |
|---|---|---|
| `body` | Sí | El texto del mensaje a enviar. |
| `mediaUrl` | No | URL de un archivo multimedia (imagen, documento, etc.) para adjuntar. |
| `mediaContentType` | No | Tipo MIME del archivo multimedia adjunto, p. ej. `image/jpeg`. |
| `pauseBot` | No | `true` pausa la IA para este contacto a medida que se envía el mensaje, para cuando un humano toma el control. Consulta [Pausar o reanudar la IA](#pause-or-resume-the-ai-for-one-contact). |
| `clearIncompleteReply` | No | `true` descarta una respuesta del bot a medio terminar para que no se reanude después de tu mensaje. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/send-message" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi! Your appointment is confirmed for tomorrow at 10:00."
  }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/send-message",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      body: "Hi! Your appointment is confirmed for tomorrow at 10:00.",
    }),
  }
);
const data = await res.json();
console.log(data.messageId);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/contact123/send-message",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"body": "Hi! Your appointment is confirmed for tomorrow at 10:00."},
)
print(res.json()["messageId"])
```

**Respuesta** (`200 OK`):

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

### Enviar por identidad de contacto

`POST /contacts/send`

Utilice esto cuando no tenga el ID interno del contacto. Proporcione el `body` del mensaje más **o bien** un `contact_id`, **o bien** un `channel` junto con el campo de identidad que coincida con ese canal.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `body` | Sí | El texto del mensaje a enviar. |
| `contact_id` | No | ID de un contacto existente. Cuando se establece, los campos de identidad a continuación no son necesarios. |
| `channel` | No | Canal a través del cual enviar. Obligatorio cuando no se proporciona `contact_id`. Uno de los 14 canales de envío saliente: `whatsapp`, `whatsapp_web`, `sms`, `instagram`, `instagram_private`, `messenger`, `telegram`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin`, `viber`. |
| `phone_number` | No | Número de teléfono del contacto en formato internacional. Se utiliza con `whatsapp`, `whatsapp_web` y `sms`. |
| `instagram_id` | No | ID de usuario de Instagram del contacto. Se utiliza con `instagram`. |
| `messenger_id` | No | ID de usuario de Messenger del contacto. Se utiliza con `messenger`. |
| `telegram_user_id` | No | ID de usuario de Telegram del contacto. Se utiliza con `telegram`. |
| `media_url` | No | URL de un archivo multimedia para adjuntar. |
| `media_content_type` | No | Tipo MIME del archivo multimedia adjunto, p. ej., `image/jpeg`. |

**Qué canales pueden resolverse mediante identidad.** Solo seis de los 14 aceptan un campo de identidad en lugar de un `contact_id`: `whatsapp`, `whatsapp_web` y `sms` se buscan mediante `phone_number`, `instagram` mediante `instagram_id`, `messenger` mediante `messenger_id`, y `telegram` mediante `telegram_user_id`. Los otros ocho — `instagram_private`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin` y `viber` — no tienen una identidad pública que buscar, por lo que enviar a través de esos canales requiere `contact_id`; pasar solo `channel` devuelve un `400` indicando que se requiere `contact_id`.

**cURL** (usando el formato de consulta `?apiKey=`)

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/send?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "phone_number": "+31612345678",
    "body": "Hi! Your appointment is confirmed."
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/contacts/send", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    channel: "whatsapp",
    phone_number: "+31612345678",
    body: "Hi! Your appointment is confirmed.",
  }),
});
const data = await res.json();
console.log(data.message_id, data.channel);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/send",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "channel": "whatsapp",
        "phone_number": "+31612345678",
        "body": "Hi! Your appointment is confirmed.",
    },
)
data = res.json()
print(data["message_id"], data["channel"])
```

**Respuesta** (`201 Created`):

```json
{
  "success": true,
  "message_id": "aB3dE5fG7hI9jK1lM2nO",
  "contact_id": "contact123",
  "channel": "whatsapp"
}
```

> **Por qué un mensaje podría ser rechazado:** Un contacto con el modo "no molestar" o privado activado no puede recibir mensajes salientes; la solicitud falla con un `422`. Si ningún contacto coincide con el ID o la identidad que proporcionó, obtendrá un `404`.

---

## Listar los mensajes de un contacto

`GET /contacts/{contactId}/messages`

Devuelve los mensajes de un contacto, del más reciente al más antiguo, con paginación basada en cursor.

| Parámetro de consulta | Obligatorio | Descripción |
|---|---|---|
| `limit` | No | Tamaño de página. Predeterminado `50`, máximo `100`. |
| `cursor` | No | El valor `next_cursor` de una respuesta anterior. Devuelve mensajes anteriores al cursor. |
| `filter` | No | Filtrar por tipo de contenido: `all` (predeterminado), `text`, `media` o `tool_use`. |
| `direction` | No | Filtrar por dirección: `all` (predeterminado), `inbound` (recibido del contacto) o `outbound` (enviado por usted). |

> **Nota sobre el filtrado y la paginación:** Los filtros `filter` y `direction` se aplican a cada página después de ser leída, por lo que una página filtrada puede contener menos elementos que `limit`. El `next_cursor` sigue avanzando a través de la conversación completa, así que continúe paginando hasta que `next_cursor` sea `null`.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/contacts/contact123/messages?limit=50&direction=inbound" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const params = new URLSearchParams({ limit: "50", direction: "inbound" });
const res = await fetch(
  `https://api.youraiconnector.com/v1/contacts/contact123/messages?${params}`,
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.messages, data.next_cursor);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"limit": 50, "direction": "inbound"},
)
data = res.json()
print(data["messages"], data["next_cursor"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "messages": [
    {
      "id": "aB3dE5fG7hI9jK1lM2nO",
      "body": "Hi! Thanks for reaching out.",
      "direction": "inbound",
      "channel": "whatsapp",
      "status": "delivered",
      "type": null,
      "timestamp": "2026-06-01T10:00:00.000Z",
      "media_url": null,
      "media_content_type": null,
      "bot_reply": false
    }
  ],
  "next_cursor": "cD4eF6gH8iJ0kL2mN3oP"
}
```

### Campos del mensaje

| Campo | Descripción |
|---|---|
| `id` | ID único del mensaje. |
| `body` | Contenido de texto del mensaje. |
| `direction` | `inbound` (recibido del contacto) o `outbound` (enviado por su cuenta). |
| `channel` | Canal por el que se envió o recibió el mensaje (p. ej., `whatsapp`, `sms`, `instagram`). |
| `status` | Estado de entrega actual, p. ej., `Created`, `sent`, `delivered`, `read`, `failed`. |
| `type` | Tipo de mensaje. Los mensajes de texto sin formato tienen un tipo `null`; la actividad de la herramienta de asistente automatizado se marca como `tool_use`. |
| `timestamp` | Hora ISO 8601 en la que se creó el mensaje. |
| `media_url` | URL de un archivo multimedia adjunto, si existe. |
| `media_content_type` | Tipo MIME del archivo multimedia adjunto, si existe. |
| `bot_reply` | `true` cuando el mensaje fue generado por el asistente de IA. |
| `score` | Su calificación del mensaje: `1` pulgar arriba, `-1` pulgar abajo, `0` cuando no ha sido calificado. Consulte [Calificar o destacar un mensaje](#rate-or-star-a-message). |
| `is_important` | `true` cuando el mensaje ha sido destacado. |
| `is_deleted` | `true` cuando el mensaje ha sido eliminado. Los mensajes eliminados permanecen en la lista pero su `body` y `media_url` están vacíos. |
| `reactions` | Reacciones con emojis en el mensaje, de ambas partes. Siempre es una matriz; vacía cuando no hay ninguna. Cada entrada tiene `emoji`, `from_phone_number`, `from_me` (`true` cuando la reacción es suya) y `reacted_at`. |

---

## Listar sesiones de chat

Una sesión de chat es una ventana de conversación con un contacto: se abre cuando empiezan a hablar y se cierra cuando la conversación concluye. Las sesiones son la forma en que usted pagina un historial largo en conversaciones legibles en lugar de una lista interminable.

### Sesiones recientes de todos los contactos

`GET /chat-sessions/recent`

Devuelve las sesiones que comenzaron en las últimas X horas, de la más reciente a la más antigua, en todos los contactos de la cuenta.

| Parámetro de consulta | Requerido | Descripción |
|---|---|---|
| `hours` | Sí | Cuántas horas mirar hacia atrás. Debe ser un número entero positivo. |
| `status` | No | Solo devolver sesiones con este estado: `ChatSessionOpened` o `ChatSessionClosed`. |
| `limit` | No | Número máximo de sesiones a devolver. Predeterminado `100`, máximo `100`. |
| `includeMessages` | No | `true` añade una matriz `messages` a cada sesión. Desactivado por defecto porque hace que la respuesta sea mucho más grande. |

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/chat-sessions/recent?hours=24&status=ChatSessionClosed" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const params = new URLSearchParams({ hours: "24", status: "ChatSessionClosed" });
const res = await fetch(`https://api.youraiconnector.com/v1/chat-sessions/recent?${params}`, {
  headers: { "X-API-Key": "YOUR_API_KEY" },
});
const data = await res.json();
console.log(data.data.total_sessions, data.data.sessions);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/chat-sessions/recent",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"hours": 24, "status": "ChatSessionClosed"},
)
data = res.json()["data"]
print(data["total_sessions"], data["sessions"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "hours_ago": 24,
    "total_sessions": 2,
    "sessions": [
      {
        "session_id": "session456",
        "contact_id": "contact123",
        "contact_name": "Jane Doe",
        "contact_phone": "+31612345678",
        "contact_email": "jane@example.com",
        "start_date_time": "2026-06-01T09:55:00.000Z",
        "end_date_time": "2026-06-01T10:20:00.000Z",
        "status": "ChatSessionClosed",
        "tag": "Booking enquiry"
      }
    ]
  }
}
```

### Todas las sesiones de un contacto

`GET /chat-sessions/{contactId}`

Devuelve todas las sesiones de chat de un solo contacto. Los mismos parámetros `status`, `limit` y `includeMessages` que arriba; `hours` no se aplica aquí.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/chat-sessions/contact123?limit=20" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "contact_id": "contact123",
    "contact_name": "Jane Doe",
    "total_sessions": 2,
    "sessions": [
      {
        "id": "session456",
        "start_date_time": "2026-06-01T09:55:00.000Z",
        "end_date_time": "2026-06-01T10:20:00.000Z",
        "status": "ChatSessionClosed",
        "tag": "Booking enquiry"
      }
    ]
  }
}
```

> **Los nombres de los campos de ID de sesión difieren entre los dos endpoints.** La lista de sesiones recientes lo llama `session_id` (también contiene los detalles del contacto, ya que las sesiones provienen de muchos contactos); la lista por contacto lo llama `id`. Cualquiera de los dos valores es lo que usted pasa como `{sessionId}` al recuperar el hilo completo a continuación.

Cuando `includeMessages=true`, cada sesión obtiene una matriz `messages` cuyas entradas contienen `id`, `body`, `direction`, `timestamp`, `type`, `channel` y `status`.

---

## Obtener un hilo de sesión de chat

`GET /contacts/{contactId}/chat-sessions/{sessionId}/messages`

Una sesión de chat agrupa los mensajes de un contacto en una única ventana de conversación. Este endpoint devuelve el hilo completo de una sola sesión, **del más antiguo al más reciente**, junto con los metadatos de la sesión. Puede encontrar los ID de sesión de un contacto a través de los endpoints de sesiones de chat.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/contacts/contact123/chat-sessions/session456/messages" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/chat-sessions/session456/messages",
  { headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.session, data.messages);
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/contacts/contact123/chat-sessions/session456/messages",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
print(data["session"], data["messages"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "session": {
    "id": "session456",
    "status": "ChatSessionClosed",
    "start_date_time": "2026-06-01T09:55:00.000Z",
    "end_date_time": "2026-06-01T10:20:00.000Z",
    "tag": "Booking enquiry"
  },
  "messages": [
    {
      "id": "aB3dE5fG7hI9jK1lM2nO",
      "body": "Hi! Thanks for reaching out.",
      "direction": "inbound",
      "channel": "whatsapp",
      "status": "delivered",
      "type": null,
      "timestamp": "2026-06-01T09:55:00.000Z",
      "media_url": null,
      "media_content_type": null,
      "bot_reply": false
    }
  ]
}
```

El objeto `session` informa sobre `status` (`ChatSessionOpened` mientras está activa, `ChatSessionClosed` una vez finalizada), `start_date_time`, `end_date_time` y un `tag` legible por humanos. La matriz `messages` utiliza los mismos [campos de mensaje](#message-fields) que el endpoint de lista.

---

## Editar, eliminar y reaccionar a mensajes

Estos endpoints cambian un mensaje después de haber sido enviado. Dos de ellos se comunican con el canal del contacto además de con su propia copia, así que lea la introducción de la sección antes de configurarlos; lo que es posible depende totalmente del canal en el que se encuentre la conversación.

**Qué permite cada canal**

| Acción | Canales que pueden cambiar la copia del contacto | Límite de tiempo |
|---|---|---|
| Editar un mensaje enviado | Widget de chat, WhatsApp Web, Telegram, LinkedIn | Ninguno en el widget de chat, 15 minutos en WhatsApp Web, 48 horas en Telegram, 60 minutos en LinkedIn |
| Eliminar para todos | Widget de chat, WhatsApp Web, Telegram, LinkedIn | 60 minutos en LinkedIn; los demás no tienen un límite publicado |
| Reaccionar con un emoji | WhatsApp Web, Telegram | Ninguno |

En cualquier otro canal —la API de WhatsApp Business, SMS, Instagram, Messenger, correo electrónico, LINE, canales personalizados—, una eliminación sigue quitando el mensaje de su bandeja de entrada, pero el contacto conserva su copia, y editar o reaccionar no es posible en absoluto.

### Editar un mensaje

`POST /contacts/{contactId}/messages/{messageId}/edit`

Reescribe un mensaje que ya envió, tanto en el dispositivo del contacto como en su copia.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `body` | Sí | El nuevo texto del mensaje. No debe estar vacío y puede tener un máximo de 4096 caracteres. |

A diferencia de la eliminación, esto **falla de forma notoria** cuando el canal lo rechaza: obtiene un `409` y su copia se queda exactamente igual a como la tiene el contacto, porque mostrar una edición que nunca recibieron desincronizaría ambas partes. El campo `edit_reason` le indica el motivo: la ventana de edición del canal ha expirado, el canal está desconectado o algo salió mal.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1/edit" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Sorry - I meant Thursday at 3pm." }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1/edit",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ body: "Sorry - I meant Thursday at 3pm." }),
  }
);
const data = await res.json();
console.log(data.edited, data.edit_reason);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1/edit",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"body": "Sorry - I meant Thursday at 3pm."},
)
data = res.json()
print(data.get("edited"), data.get("edit_reason"))
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "message_id": "msg_1",
  "edited": true,
  "edit_reason": "edit_dispatched"
}
```

Si el canal no acepta la edición, obtendrá un `409` en su lugar y nada habrá cambiado:

```json
{
  "success": false,
  "error": "The message could not be edited",
  "edit_reason": "channel_disconnected"
}
```

Un mensaje que ya ha sido eliminado, un canal que no permite editar en absoluto y un mensaje demasiado antiguo para su canal devuelven `400`: la solicitud nunca llega al canal.

### Eliminar un mensaje

`DELETE /contacts/{contactId}/messages/{messageId}`

Elimina el mensaje de su conversación y, cuando el canal lo permite, también retira la copia del contacto. No requiere cuerpo de solicitud.

Esto siempre responde `200` cuando el mensaje existía, incluso si la copia del contacto no pudo ser retirada; su copia **sí** ha desaparecido, por lo que un error sería engañoso. Lea los tres campos en la respuesta para informar al usuario de lo que realmente sucedió:

| Campo | Descripción |
|---|---|
| `revoke_supported` | Si este canal puede retirar mensajes en absoluto. |
| `revoked` | Si se eliminó la copia en el dispositivo del contacto. |
| `revoke_reason` | Por qué no se eliminó, cuando `revoked` es `false`; por ejemplo, `revoke_window_closed` o `already_deleted`. |

**cURL**

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1",
  { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.revoked, data.revoke_reason);
```

**Python**

```python
import requests

res = requests.delete(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
data = res.json()
print(data["revoked"], data["revoke_reason"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "message_id": "msg_1",
  "revoke_supported": true,
  "revoked": true,
  "revoke_reason": "revoke_dispatched"
}
```

> Los mensajes eliminados no se borran del historial de la conversación. Permanecen en `GET /contacts/{contactId}/messages` con `is_deleted: true` y un `body` y `media_url` vacíos.

### Eliminar varios mensajes a la vez

`POST /contacts/{contactId}/messages/bulk-delete`

Borra un lote de mensajes solo de tu lado. Los cuerpos y archivos adjuntos se vacían, pero **no se retira nada en el dispositivo del contacto**; para retirar también un mensaje, elimínalo uno a uno con el endpoint de mensaje único anterior.

| Campo | Requerido | Descripción |
|---|---|---|
| `message_ids` | Sí | Una matriz no vacía de IDs de mensaje, hasta 500 por solicitud. `messageIds` se acepta como alias. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/messages/bulk-delete" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message_ids": ["msg_1", "msg_2"] }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/messages/bulk-delete",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ message_ids: ["msg_1", "msg_2"] }),
  }
);
console.log((await res.json()).deleted);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages/bulk-delete",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"message_ids": ["msg_1", "msg_2"]},
)
print(res.json()["deleted"])
```

**Respuesta** (`200 OK`):

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

### Reaccionar a un mensaje

`POST /contacts/{contactId}/messages/{messageId}/react`

Coloca tu propia reacción con emoji en un mensaje, o retírala enviando una cadena vacía. Las reacciones del propio contacto nunca se ven afectadas.

| Campo | Requerido | Descripción |
|---|---|---|
| `emoji` | Sí | El emoji con el que reaccionar, o `""` para eliminar tu reacción. Debe ser una cadena única sin espacios, de máximo 16 caracteres. |

Al igual que la edición, esto falla en lugar de mostrar una reacción que el contacto nunca recibió, y el error te indica si vale la pena reintentarlo:

- `422` — nunca se puede entregar en esta conversación: el canal no admite reacciones, el mensaje no tiene un ID del lado del canal, o el emoji está fuera del conjunto permitido por ese canal.
- `409` — el canal no estuvo disponible momentáneamente. Un reintento podría funcionar.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1/react" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "emoji": "👍" }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1/react",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ emoji: "👍" }),
  }
);
const data = await res.json();
console.log(data.reactions);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1/react",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"emoji": "👍"},
)
print(res.json()["reactions"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "message_id": "msg_1",
  "reaction_supported": true,
  "reaction_reason": "reaction_dispatched",
  "reactions": [
    {
      "emoji": "👍",
      "from_phone_number": "+31612345678",
      "from_me": true,
      "reacted_at": "2026-06-01T10:05:00.000Z"
    }
  ]
}
```

La matriz `reactions` es el conjunto completo de reacciones que hay ahora en el mensaje, las tuyas y las del contacto. En un `409` o `422` se devuelve sin cambios, por lo que un cliente que renderice directamente desde ella nunca mostrará una reacción que no se haya entregado.

### Calificar o destacar un mensaje

`PATCH /contacts/{contactId}/messages/{messageId}`

Califica un mensaje con pulgar arriba o pulgar abajo y/o lo marca como importante. Esto es contabilidad solo de tu lado; no se envía nada al contacto.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `score` | No | `1` pulgar hacia arriba, `-1` pulgar hacia abajo, `0` borra la calificación. |
| `is_important` | No | `true` marca el mensaje con una estrella, `false` le quita la estrella. Debe ser un booleano real, no la cadena `"true"`. |

Envíe al menos uno de los dos, o recibirá un `400`. Solo se escribe lo que usted envía, por lo que marcar un mensaje con estrella nunca borra su calificación y viceversa; además, la respuesta solo devuelve los campos que usted envió.

**cURL**

```bash
curl -X PATCH "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "score": 1, "is_important": true }'
```

**JavaScript**

```javascript
await fetch("https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1", {
  method: "PATCH",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ score: 1, is_important: true }),
});
```

**Python**

```python
import requests

requests.patch(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages/msg_1",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"score": 1, "is_important": True},
)
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "message_id": "msg_1",
  "score": 1,
  "is_important": true
}
```

---

## Marcar mensajes como leídos

Puede borrar el estado de no leído ya sea para mensajes específicos o para toda la conversación.

### Marcar mensajes específicos como leídos

`POST /contacts/{contactId}/messages/mark-read`

Pase los ID de los mensajes que desea marcar como leídos.

| Campo | Requerido | Descripción |
|---|---|---|
| `message_ids` | Sí | Una matriz no vacía de ID de mensajes (hasta 500 por solicitud). |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/messages/mark-read" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message_ids": ["aB3dE5fG7hI9jK1lM2nO", "cD4eF6gH8iJ0kL2mN3oP"]
  }'
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/messages/mark-read",
  {
    method: "POST",
    headers: {
      "X-API-Key": "YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      message_ids: ["aB3dE5fG7hI9jK1lM2nO", "cD4eF6gH8iJ0kL2mN3oP"],
    }),
  }
);
const data = await res.json();
console.log(data.marked_read);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/contact123/messages/mark-read",
    headers={
        "X-API-Key": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={"message_ids": ["aB3dE5fG7hI9jK1lM2nO", "cD4eF6gH8iJ0kL2mN3oP"]},
)
print(res.json()["marked_read"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123",
  "marked_read": 2
}
```

### Marcar todo el chat como leído

`POST /contacts/{contactId}/mark-read`

Borra el distintivo de no leído de toda la conversación del contacto en la bandeja de entrada. No se requiere cuerpo de solicitud.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/mark-read" \
  -H "X-API-Key: YOUR_API_KEY"
```

**JavaScript**

```javascript
const res = await fetch(
  "https://api.youraiconnector.com/v1/contacts/contact123/mark-read",
  { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY" } }
);
const data = await res.json();
console.log(data.success);
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/contacts/contact123/mark-read",
    headers={"X-API-Key": "YOUR_API_KEY"},
)
print(res.json()["success"])
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123"
}
```

### Marcar todo el chat como no leído

`POST /contacts/{contactId}/mark-unread`

Vuelve a colocar la insignia de no leído en la conversación; es útil cuando alguien de su equipo abrió un chat pero lo está devolviendo. No se requiere cuerpo de solicitud.

Este es un indicador exclusivo de la bandeja de entrada: **no** cambia cuándo se leyó la conversación por última vez, por lo que no se envía ningún recibo de lectura al contacto en los canales que los admiten.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/mark-unread" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "contact_id": "contact123"
}
```

---

## Exportar una conversación

Las exportaciones le ofrecen una conversación completa como una transcripción legible, en lugar de pasar páginas de mensajes. Cada punto final de exportación acepta un `filter` de `all` (predeterminado), `text`, `media` o `tool_use`, que coincide con el filtro de la lista de mensajes.

### Exportar el chat de un contacto

`GET /chat-exports/{contactId}`

| Parámetro de consulta | Obligatorio | Descripción |
|---|---|---|
| `format` | No | `txt` (predeterminado) devuelve un enlace de descarga a una transcripción en texto plano. `json` devuelve los mensajes como datos estructurados en la respuesta. |
| `filter` | No | `all` (predeterminado), `text`, `media` o `tool_use`. |

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/chat-exports/contact123?format=json" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/chat-exports/contact123",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={"format": "json"},
)
print(res.json()["data"]["messages"])
```

**Respuesta con `format=json`** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "contact": {
      "id": "contact123",
      "name": "Jane Doe",
      "phone": "+31612345678",
      "email": "jane@example.com"
    },
    "messages": [
      {
        "body": "Hi! I have a question about my order.",
        "direction": "inbound",
        "timestamp": "2026-06-01T09:55:00.000Z",
        "type": "text",
        "media_url": null,
        "media_content_type": null,
        "name": null,
        "args": null
      }
    ]
  }
}
```

Con `format=txt` (el predeterminado), `data` es en su lugar un enlace de descarga al archivo de transcripción generado:

```json
{
  "success": true,
  "data": "https://storage.googleapis.com/.../chat-export-contact123-....txt"
}
```

> **El enlace de descarga tiene una duración limitada.** Obtenga el archivo tan pronto como reciba el enlace en lugar de almacenarlo; solicite una nueva exportación cuando necesite la transcripción nuevamente.

### Exportar todas las conversaciones recientes

`GET /chat-exports/recent`

Exporta las conversaciones de todos los contactos que estuvieron activos en las últimas X horas, en una sola llamada.

| Parámetro de consulta | Obligatorio | Descripción |
|---|---|---|
| `hours` | Sí | Cuántas horas de actividad revisar hacia atrás. Debe ser un número entero positivo. |
| `format` | No | `json` (predeterminado) devuelve una entrada por contacto. `txt` devuelve un único archivo de texto descargable con todas las conversaciones. |
| `limit` | No | Número máximo de contactos a exportar. Predeterminado `50`, máximo `100`. |
| `filter` | No | `all` (predeterminado), `text`, `media` o `tool_use`. |

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/chat-exports/recent?hours=24&limit=25" \
  -H "X-API-Key: YOUR_API_KEY"
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "hours_ago": 24,
    "total_contacts": 2,
    "exports": [
      {
        "contactId": "contact123",
        "contactName": "Jane Doe",
        "phoneNumber": "+31612345678",
        "email": "jane@example.com",
        "messageCount": 12,
        "chatExport": "Acme Export - Jane Doe\nPhone: +31612345678\n..."
      }
    ]
  }
}
```

Con `format=txt` la respuesta es el archivo de texto en sí, enviado como una descarga en lugar de JSON.

> Esta llamada obtiene el historial completo de cada contacto coincidente, así que mantenga `hours` y `limit` moderados en cuentas con mucha actividad.

### Enviar una transcripción por correo electrónico al contacto

`POST /chat-exports/{contactId}/email`

Envía al contacto su propia transcripción de la conversación por correo electrónico: el flujo "enviarme este chat por correo", gestionado desde su propio sistema.

| Campo | Obligatorio | Descripción |
|---|---|---|
| `recipient_email` | No | Dónde enviarlo. De forma predeterminada, utiliza la dirección de correo electrónico almacenada del contacto. |
| `via` | No | `auto` (predeterminado) elige la mejor ruta, `transactional` lo envía como un correo electrónico del sistema, `email_channel` lo envía desde su canal de correo electrónico conectado. |
| `note` | No | Una breve línea suya que se muestra sobre la transcripción. Hasta 1000 caracteres. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/chat-exports/contact123/email" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "note": "Here is a copy of our chat, as promised." }'
```

**Respuesta** (`200 OK`):

```json
{
  "success": true,
  "data": {
    "via": "transactional",
    "recipientEmail": "jane@example.com",
    "messageCount": 42,
    "omittedCount": 0
  }
}
```

`omittedCount` le indica cuántos de los mensajes más antiguos se omitieron para mantener el correo electrónico con una longitud razonable. Un `200` significa que la transcripción se generó y se puso en cola para su envío, no que haya llegado a la bandeja de entrada todavía.

---

## Pausar o reanudar la IA para un contacto

`PUT /contacts/{contactId}`

Establece `is_bot_active` en `false` para detener las respuestas de la IA a un contacto, y vuelve a `true` para devolver la conversación. Este es el interruptor de control que necesitas cuando un humano interviene en una conversación: los mensajes salientes que envías con la API se siguen entregando mientras el bot está pausado.

**cURL**

```bash
curl -X PUT "https://api.youraiconnector.com/v1/contacts/contact123" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_bot_active": false }'
```

**JavaScript**

```javascript
await fetch("https://api.youraiconnector.com/v1/contacts/contact123", {
  method: "PUT",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ is_bot_active: false }),
});
```

**Python**

```python
import requests

requests.put(
    "https://api.youraiconnector.com/v1/contacts/contact123",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"is_bot_active": False},
)
```

**Respuesta**

```json
{
  "success": true,
  "contact_id": "contact123"
}
```

**Pausar como parte de la respuesta**

Si un humano toma el control enviando una respuesta, puedes pausar el bot en la misma solicitud en lugar de realizar una segunda llamada. `POST /contacts/{contactId}/send-message` acepta dos indicadores opcionales:

| Campo | Descripción |
|---|---|
| `pauseBot` | `true` pausa la IA para este contacto a medida que se envía el mensaje. |
| `clearIncompleteReply` | `true` descarta una respuesta del bot a medio terminar para que no se reanude después. |

```bash
curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/send-message" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi, Sarah here - taking over from the assistant.",
    "pauseBot": true,
    "clearIncompleteReply": true
  }'
```

La respuesta incluye `"botPaused": true` cuando se aplicó la pausa.

> Marcar un contacto como privado con [`POST /contacts/bulk-flag`](contacts.md) también pausa el bot para él. Consulta [Contactos](contacts.md) para ver la lista completa de campos.

---

## Construyendo tu propia bandeja de entrada

Todo lo que necesita una bandeja de entrada está en esta página y en [Contactos](contacts.md):

| Lo que necesita | Endpoint |
|---|---|
| Listar conversaciones | `GET /contacts` |
| Leer una conversación | `GET /contacts/{contactId}/messages` |
| Listar las sesiones de chat de un contacto | `GET /chat-sessions/{contactId}` |
| Ver lo que ha llegado recientemente | `GET /chat-sessions/recent` |
| Leer una sesión de chat | `GET /contacts/{contactId}/chat-sessions/{sessionId}/messages` |
| Enviar una respuesta manual | `POST /contacts/{contactId}/send-message` |
| Corregir una respuesta que acaba de enviar | `POST /contacts/{contactId}/messages/{messageId}/edit` |
| Eliminar un mensaje | `DELETE /contacts/{contactId}/messages/{messageId}` |
| Borrar varios mensajes | `POST /contacts/{contactId}/messages/bulk-delete` |
| Reaccionar con un emoji | `POST /contacts/{contactId}/messages/{messageId}/react` |
| Calificar o marcar un mensaje | `PATCH /contacts/{contactId}/messages/{messageId}` |
| Marcar como leído | `POST /contacts/{contactId}/mark-read` |
| Devolver un chat al equipo | `POST /contacts/{contactId}/mark-unread` |
| Exportar una transcripción | `GET /chat-exports/{contactId}` |
| Pausar o reanudar la IA | `PUT /contacts/{contactId}` con `is_bot_active` |

Para actualizaciones en tiempo real, suscríbete a los eventos `New Message`, `Replies`, `Human Alerted` y `Chat Concluded` con [Webhooks](webhooks.md) en lugar de consultar esta API mediante un temporizador.

---

## Errores de la API de mensajes

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

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

| Estado | Cuándo ocurre en un endpoint de mensaje |
|---|---|
| `400` | Falta un campo obligatorio o un parámetro no es válido (un `limit`, `hours`, `filter`, `direction`, `status` incorrecto, una matriz `message_ids` vacía o de más de 500 elementos, un `cursor` no válido, una edición `body` vacía o demasiado larga, un `score` fuera de `-1`/`0`/`1`, o un emoji con espacios o más de 16 caracteres). También se devuelve cuando un mensaje no se puede editar en absoluto: fue eliminado, su canal no permite ediciones o ha pasado el periodo de edición de ese canal. |
| `404` | No se encontró el contacto, la sesión de chat o uno de los IDs de mensaje proporcionados. |
| `409` | El canal no aceptaría el cambio en este momento. No se escribió nada: en una edición, `edit_reason` explica por qué; en una reacción, el canal estaba momentáneamente inaccesible y un reintento podría funcionar. |
| `422` | El contacto no puede recibir mensajes salientes (no molestar, privado o un canal no compatible), o una reacción nunca se puede entregar en esta conversación (`reaction_reason` indica cuál). |

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

- [Webhooks](webhooks.md): reciba actualizaciones del estado de entrega en lugar de realizar sondeos.
- [Contactos](contacts.md): cree y busque los contactos a los que envía mensajes.
- [Citas](appointments.md): reserve y gestione citas para sus contactos.
