
# Mensagens e Conversas

A API de Mensagens permite-lhe enviar uma mensagem a qualquer contacto, ler uma conversa, corrigir ou remover uma mensagem que já enviou, reagir a uma, obter um tópico completo de sessão de chat, exportar uma transcrição e marcar conversas como lidas ou não lidas — tudo isto sem abrir a caixa de entrada.

Todos os caminhos nesta página são relativos ao URL base `https://api.youraiconnector.com/v1`. Cada pedido necessita da sua chave de API — consulte [Autenticação](authentication.md) para obter a lista completa de formas de a enviar. Os exemplos abaixo utilizam o cabeçalho `X-API-Key`, com um exemplo cURL que mostra também o formulário de consulta `?apiKey=`.

> **Como funciona a entrega:** O envio de uma mensagem **não** aguarda pela sua chegada. A API aceita a sua mensagem, responde imediatamente com um ID de mensagem e, em seguida, entrega-a em segundo plano no canal do contacto (WhatsApp, SMS, Instagram, etc.). Para verificar se uma mensagem foi efetivamente entregue ou lida, utilize [Webhooks](webhooks.md) para receber atualizações de estado — não utilize polling. A resposta de envio apenas confirma que a mensagem foi aceite.

---

## Enviar uma mensagem

Existem duas formas de enviar. Escolha a que melhor se adapta à forma como já identifica o contacto:

- **Enviar por ID de contacto** — já conhece o ID do contacto (por exemplo, criou o contacto através da API ou obteve-o a partir de um webhook). Utilize `POST /contacts/{contactId}/send-message`.
- **Enviar por identidade de contacto** — conhece o número de telefone, ID de Instagram, etc., do contacto, mas não o seu ID interno. Utilize `POST /contacts/send` e deixe que a plataforma encontre o contacto correto.

Ambos colocam a mensagem na fila da mesma forma e entregam-na no canal em que o contacto se encontra. Não escolhe um transporte — a plataforma encaminha os contactos de WhatsApp via WhatsApp, os contactos de SMS via SMS, e assim sucessivamente.

### Enviar por ID de contacto

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

| Campo | Obrigatório | Descrição |
|---|---|---|
| `body` | Sim | O texto da mensagem a enviar. |
| `mediaUrl` | Não | URL de um ficheiro de multimédia (imagem, documento, etc.) a anexar. |
| `mediaContentType` | Não | Tipo MIME do ficheiro multimédia anexado, p. ex. `image/jpeg`. |
| `pauseBot` | Não | `true` suspende a IA para este contacto à medida que a mensagem é enviada — para uma intervenção humana. Consulte [Suspender ou retomar a IA](#pause-or-resume-the-ai-for-one-contact). |
| `clearIncompleteReply` | Não | `true` descarta uma resposta do bot incompleta para que esta não seja retomada após a sua mensagem. |

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

**Resposta** (`200 OK`):

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

### Enviar por identidade de contacto

`POST /contacts/send`

Utilize isto quando não tiver o ID interno do contacto. Forneça o `body` da mensagem mais **ou** um `contact_id`, **ou** um `channel` juntamente com o campo de identidade que corresponde a esse canal.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `body` | Sim | O texto da mensagem a enviar. |
| `contact_id` | Não | ID de um contacto existente. Quando definido, os campos de identidade abaixo não são necessários. |
| `channel` | Não | Canal através do qual enviar. Obrigatório quando `contact_id` não é fornecido. Um dos 14 canais de envio de saída: `whatsapp`, `whatsapp_web`, `sms`, `instagram`, `instagram_private`, `messenger`, `telegram`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin`, `viber`. |
| `phone_number` | Não | Número de telefone do contacto em formato internacional. Utilizado com `whatsapp`, `whatsapp_web` e `sms`. |
| `instagram_id` | Não | ID de utilizador do Instagram do contacto. Utilizado com `instagram`. |
| `messenger_id` | Não | ID de utilizador do Messenger do contacto. Utilizado com `messenger`. |
| `telegram_user_id` | Não | ID de utilizador do Telegram do contacto. Utilizado com `telegram`. |
| `media_url` | Não | URL de um ficheiro multimédia a anexar. |
| `media_content_type` | Não | Tipo MIME do ficheiro multimédia anexado, por exemplo, `image/jpeg`. |

**Que canais podem ser resolvidos por identidade.** Apenas seis dos 14 aceitam um campo de identidade em vez de um `contact_id`: `whatsapp`, `whatsapp_web` e `sms` são pesquisados por `phone_number`, `instagram` por `instagram_id`, `messenger` por `messenger_id` e `telegram` por `telegram_user_id`. Os outros oito — `instagram_private`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin` e `viber` — não têm identidade pública para pesquisa, pelo que o envio através desses canais requer `contact_id`; passar apenas `channel` devolve um `400` a indicar que `contact_id` é necessário.

**cURL** (utilizando o formulário 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"])
```

**Resposta** (`201 Created`):

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

> **Por que motivo uma mensagem pode ser rejeitada:** Um contacto com o modo "não incomodar" ou modo privado ativado não pode receber mensagens de saída — o pedido falha com um `422`. Se nenhum contacto corresponder ao ID ou identidade que forneceu, receberá um `404`.

---

## Listar as mensagens de um contacto

`GET /contacts/{contactId}/messages`

Devolve as mensagens de um contacto, da mais recente para a mais antiga, com paginação baseada em cursor.

| Parâmetro de consulta | Obrigatório | Descrição |
|---|---|---|
| `limit` | Não | Tamanho da página. Predefinição `50`, máximo `100`. |
| `cursor` | Não | O valor `next_cursor` de uma resposta anterior. Devolve mensagens mais antigas do que o cursor. |
| `filter` | Não | Filtrar por tipo de conteúdo: `all` (predefinição), `text`, `media` ou `tool_use`. |
| `direction` | Não | Filtrar por direção: `all` (predefinição), `inbound` (recebida do contacto) ou `outbound` (enviada por si). |

> **Nota sobre filtragem e paginação:** Os filtros `filter` e `direction` são aplicados a cada página após a sua leitura, pelo que uma página filtrada pode conter menos itens do que `limit`. O `next_cursor` continua a avançar pela conversa completa, por isso continue a paginar até que `next_cursor` seja `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"])
```

**Resposta** (`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 da mensagem

| Campo | Descrição |
|---|---|
| `id` | ID único da mensagem. |
| `body` | Conteúdo de texto da mensagem. |
| `direction` | `inbound` (recebida do contacto) ou `outbound` (enviada pela sua conta). |
| `channel` | Canal através do qual a mensagem foi enviada ou recebida (por exemplo, `whatsapp`, `sms`, `instagram`). |
| `status` | Estado de entrega atual, por exemplo, `Created`, `sent`, `delivered`, `read`, `failed`. |
| `type` | Tipo de mensagem. As mensagens de texto simples têm um tipo `null`; a atividade da ferramenta de assistente automatizado é marcada como `tool_use`. |
| `timestamp` | Hora ISO 8601 em que a mensagem foi criada. |
| `media_url` | URL de um ficheiro multimédia em anexo, se existir. |
| `media_content_type` | Tipo MIME do multimédia em anexo, se existir. |
| `bot_reply` | `true` quando a mensagem foi gerada pelo assistente de IA. |
| `score` | A sua classificação da mensagem: `1` polegar para cima, `-1` polegar para baixo, `0` quando não foi classificada. Consulte [Classificar ou marcar uma mensagem com estrela](#rate-or-star-a-message). |
| `is_important` | `true` quando a mensagem foi marcada com estrela. |
| `is_deleted` | `true` quando a mensagem foi eliminada. As mensagens eliminadas permanecem na lista, mas o seu `body` e `media_url` estão vazios. |
| `reactions` | Reações com emoji na mensagem, de ambos os lados. É sempre uma matriz — vazia quando não existem. Cada entrada tem `emoji`, `from_phone_number`, `from_me` (`true` quando a reação é sua) e `reacted_at`. |

---

## Listar sessões de chat

Uma sessão de chat é uma janela de conversa com um contacto: abre-se quando este começa a falar e fecha-se quando a conversa termina. As sessões são a forma de dividir um histórico longo em conversas legíveis, em vez de uma lista interminável.

### Sessões recentes de todos os contactos

`GET /chat-sessions/recent`

Devolve as sessões iniciadas nas últimas X horas, da mais recente para a mais antiga, de todos os contactos na conta.

| Parâmetro de consulta | Obrigatório | Descrição |
|---|---|---|
| `hours` | Sim | Quantas horas retroceder. Deve ser um número inteiro positivo. |
| `status` | Não | Devolver apenas sessões com este estado: `ChatSessionOpened` ou `ChatSessionClosed`. |
| `limit` | Não | Número máximo de sessões a devolver. O padrão é `100`, o máximo é `100`. |
| `includeMessages` | Não | `true` adiciona uma matriz `messages` a cada sessão. Desativado por padrão porque torna a resposta muito maior. |

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

**Resposta** (`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 as sessões de um contacto

`GET /chat-sessions/{contactId}`

Devolve todas as sessões de chat de um único contacto. Os mesmos parâmetros `status`, `limit` e `includeMessages` que acima — `hours` não se aplica aqui.

**cURL**

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

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

> **Os nomes dos campos de ID de sessão diferem entre os dois endpoints.** A lista de sessões recentes chama-lhe `session_id` (também contém os detalhes do contacto, uma vez que as sessões provêm de muitos contactos); a lista por contacto chama-lhe `id`. Qualquer um dos valores é o que deve passar como `{sessionId}` ao obter o tópico completo abaixo.

Quando `includeMessages=true`, cada sessão ganha uma matriz `messages` cujas entradas contêm `id`, `body`, `direction`, `timestamp`, `type`, `channel` e `status`.

---

## Obter um tópico de sessão de chat

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

Uma sessão de chat agrupa as mensagens de um contacto numa única janela de conversação. Este endpoint devolve o fio completo de uma única sessão, **da mais antiga para a mais recente**, juntamente com os metadados da sessão. Pode encontrar os IDs de sessão de um contacto através dos endpoints de sessões 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"])
```

**Resposta** (`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
    }
  ]
}
```

O objeto `session` reporta `status` (`ChatSessionOpened` enquanto ativo, `ChatSessionClosed` após terminar), `start_date_time`, `end_date_time`, e um `tag` legível por humanos. A matriz `messages` utiliza os mesmos [campos de mensagem](#message-fields) que o endpoint de listagem.

---

## Editar, eliminar e reagir a mensagens

Estes endpoints alteram uma mensagem após esta ter sido enviada. Dois deles contactam o canal do destinatário, bem como a sua própria cópia, por isso leia a introdução da secção antes de os configurar — o que é possível depende inteiramente do canal em que a conversa se encontra.

**O que cada canal permite**

| Ação | Canais que podem alterar a cópia do contacto | Limite de tempo |
|---|---|---|
| Editar uma mensagem enviada | Widget de chat, WhatsApp Web, Telegram, LinkedIn | Nenhum no widget de chat, 15 minutos no WhatsApp Web, 48 horas no Telegram, 60 minutos no LinkedIn |
| Eliminar para todos | Widget de chat, WhatsApp Web, Telegram, LinkedIn | 60 minutos no LinkedIn; os outros não têm limite publicado |
| Reagir com um emoji | WhatsApp Web, Telegram | Nenhum |

Em todos os outros canais — a API do WhatsApp Business, SMS, Instagram, Messenger, e-mail, LINE, canais personalizados — uma eliminação remove a mensagem da sua caixa de entrada, mas o contacto mantém a sua cópia, e não é possível editar ou reagir de todo.

### Editar uma mensagem

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

Reescreve uma mensagem que já enviou, no dispositivo do contacto e na sua cópia.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `body` | Sim | O novo texto da mensagem. Não pode estar vazio e pode ter no máximo 4096 caracteres. |

Ao contrário da eliminação, isto **falha de forma explícita** quando o canal recusa: recebe um `409` e a sua cópia permanece exatamente como o contacto a tem, porque mostrar uma edição que nunca receberam deixaria os dois lados dessincronizados. O campo `edit_reason` indica-lhe o motivo — a janela de edição do canal expirou, o canal está desligado ou algo correu 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"))
```

**Resposta** (`200 OK`):

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

Se o canal não aceitar a edição, recebe um `409` em vez disso, e nada foi alterado:

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

Uma mensagem que já foi eliminada, um canal que não permite edições de todo e uma mensagem demasiado antiga para o seu canal devolvem `400` — o pedido nunca chega ao canal.

### Eliminar uma mensagem

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

Remove a mensagem da sua conversa e, quando o canal o permite, retira também a cópia do contacto. Sem corpo de pedido.

Isto responde sempre `200` quando a mensagem existia, mesmo que a cópia do contacto não pudesse ser retirada — a sua cópia **foi** eliminada, pelo que um erro seria enganador. Leia os três campos na resposta para informar o utilizador sobre o que aconteceu realmente:

| Campo | Descrição |
|---|---|
| `revoke_supported` | Se este canal consegue retirar mensagens. |
| `revoked` | Se a cópia no dispositivo do contacto foi removida. |
| `revoke_reason` | Por que não foi removida, quando `revoked` é `false` — por exemplo `revoke_window_closed` ou `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"])
```

**Resposta** (`200 OK`):

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

> As mensagens eliminadas não são removidas do histórico da conversa. Permanecem em `GET /contacts/{contactId}/messages` com `is_deleted: true` e um `body` e `media_url` vazios.

### Eliminar várias mensagens de uma vez

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

Limpa um lote de mensagens apenas do seu lado. O conteúdo e os anexos são esvaziados, mas **nada é retirado no dispositivo do contacto** — para também recuperar uma mensagem, elimine-a uma a uma com o endpoint de mensagem única acima.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `message_ids` | Sim | Uma matriz não vazia de IDs de mensagem, até 500 por pedido. `messageIds` é aceite como um 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"])
```

**Resposta** (`200 OK`):

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

### Reagir a uma mensagem

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

Coloca a sua própria reação com emoji numa mensagem, ou retira-a enviando uma string vazia. As reações do próprio contacto nunca são alteradas.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `emoji` | Sim | O emoji com o qual reagir, ou `""` para remover a sua reação. Deve ser uma string única sem espaços, com no máximo 16 caracteres. |

Tal como a edição, isto falha em vez de mostrar uma reação que o contacto nunca recebeu, e a falha indica-lhe se vale a pena tentar novamente:

- `422` — nunca poderá ser entregue nesta conversa: o canal não suporta reações, a mensagem não tem um ID do lado do canal, ou o emoji está fora do conjunto permitido por esse canal.
- `409` — o canal estava momentaneamente inacessível. Uma nova tentativa poderá 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"])
```

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

A matriz `reactions` é o conjunto completo de reações atualmente na mensagem, as suas e as do contacto. Num `409` ou `422`, é devolvida inalterada, pelo que um cliente que faça a renderização diretamente a partir dela nunca mostrará uma reação que não tenha sido entregue.

### Classificar ou marcar uma mensagem com estrela

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

Classifica uma mensagem com um polegar para cima ou para baixo e/ou marca-a como importante. Isto é apenas para registo do seu lado — nada é enviado ao contacto.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `score` | Não | `1` polegar para cima, `-1` polegar para baixo, `0` limpa a classificação. |
| `is_important` | Não | `true` adiciona uma estrela à mensagem, `false` remove a estrela. Tem de ser um booleano real, não a string `"true"`. |

Envie pelo menos um dos dois, caso contrário receberá um `400`. Apenas o que enviar é escrito, por isso adicionar uma estrela a uma mensagem nunca limpa a sua classificação e vice-versa — e a resposta reflete apenas os campos que enviou.

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

**Resposta** (`200 OK`):

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

---

## Marcar mensagens como lidas

Pode limpar o estado de não lida tanto para mensagens específicas como para toda a conversação.

### Marcar mensagens específicas como lidas

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

Indique os IDs das mensagens a marcar como lidas.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `message_ids` | Sim | Uma matriz não vazia de IDs de mensagem (até 500 por pedido). |

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

**Resposta** (`200 OK`):

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

### Marcar todo o chat como lido

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

Limpa o indicador de não lidas de toda a conversação do contacto na caixa de entrada. Não é necessário um corpo de pedido.

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

**Resposta** (`200 OK`):

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

### Marcar toda a conversa como não lida

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

Coloca o emblema de não lido de volta na conversa — útil quando alguém na sua equipa abriu uma conversa, mas está a devolvê-la. Não é necessário um corpo de pedido.

Este é um sinalizador exclusivo da caixa de entrada: **não** altera a data em que a conversa foi lida pela última vez, pelo que não é enviado qualquer recibo de leitura ao contacto em canais que os suportem.

**cURL**

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

**Resposta** (`200 OK`):

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

---

## Exportar uma conversa

As exportações fornecem uma conversa completa como uma transcrição legível, em vez de percorrer as mensagens página a página. Cada endpoint de exportação aceita um `filter` de `all` (predefinição), `text`, `media` ou `tool_use`, correspondendo ao filtro na lista de mensagens.

### Exportar a conversa de um contacto

`GET /chat-exports/{contactId}`

| Parâmetro de consulta | Obrigatório | Descrição |
|---|---|---|
| `format` | Não | `txt` (predefinição) devolve uma ligação de transferência para uma transcrição em texto simples. `json` devolve as mensagens como dados estruturados na resposta. |
| `filter` | Não | `all` (predefinição), `text`, `media` ou `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"])
```

**Resposta com `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
      }
    ]
  }
}
```

Com `format=txt` (a predefinição), `data` é, em vez disso, uma ligação de transferência para o ficheiro de transcrição gerado:

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

> **A ligação de transferência tem uma duração curta.** Obtenha o ficheiro assim que receber a ligação em vez de o armazenar — solicite uma nova exportação quando precisar da transcrição novamente.

### Exportar todas as conversas recentes

`GET /chat-exports/recent`

Exporta as conversas de todos os contactos que estiveram ativos nas últimas X horas, numa única chamada.

| Parâmetro de consulta | Obrigatório | Descrição |
|---|---|---|
| `hours` | Sim | Quantas horas de atividade analisar. Deve ser um número inteiro positivo. |
| `format` | Não | `json` (predefinição) devolve uma entrada por contacto. `txt` devolve um único ficheiro de texto transferível com todas as conversas. |
| `limit` | Não | Número máximo de contactos a exportar. Predefinição `50`, máximo `100`. |
| `filter` | Não | `all` (predefinição), `text`, `media` ou `tool_use`. |

**cURL**

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

**Resposta** (`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..."
      }
    ]
  }
}
```

Com `format=txt`, a resposta é o próprio ficheiro de texto, enviado como uma transferência em vez de JSON.

> Esta chamada obtém o histórico completo de cada contacto correspondente, por isso mantenha `hours` e `limit` moderados em contas com muita atividade.

### Enviar uma transcrição por e-mail ao contacto

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

Envia ao contacto a sua própria transcrição da conversa por e-mail — o fluxo "enviar-me este chat por e-mail", gerido a partir do seu próprio sistema.

| Campo | Obrigatório | Descrição |
|---|---|---|
| `recipient_email` | Não | Para onde enviar. Por predefinição, utiliza o endereço de e-mail guardado do contacto. |
| `via` | Não | `auto` (predefinição) escolhe a melhor rota, `transactional` envia como um e-mail do sistema, `email_channel` envia a partir do seu canal de e-mail ligado. |
| `note` | Não | Uma breve linha da sua parte apresentada acima da transcrição. Até 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." }'
```

**Resposta** (`200 OK`):

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

`omittedCount` indica quantas das mensagens mais antigas foram omitidas para manter o e-mail com um tamanho razoável. Um `200` significa que a transcrição foi criada e colocada na fila de envio, não que já chegou à caixa de entrada.

---

## Suspender ou retomar a IA para um contacto

`PUT /contacts/{contactId}`

Defina `is_bot_active` como `false` para impedir que a IA responda a um contacto e volte a `true` para devolver a conversação. Este é o interruptor de controlo que pretende quando um humano assume uma conversação: as mensagens de saída que envia com a API continuam a ser entregues enquanto o bot está suspenso.

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

**Resposta**

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

**Suspender como parte da resposta**

Se um humano estiver a assumir o controlo ao enviar uma resposta, pode suspender o bot no mesmo pedido em vez de efetuar uma segunda chamada. `POST /contacts/{contactId}/send-message` aceita dois sinalizadores opcionais:

| Campo | Descrição |
|---|---|
| `pauseBot` | `true` suspende a IA para este contacto à medida que a mensagem é enviada. |
| `clearIncompleteReply` | `true` descarta uma resposta do bot incompleta para que esta não seja retomada posteriormente. |

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

A resposta inclui `"botPaused": true` quando a suspensão foi aplicada.

> Marcar um contacto como privado com [`POST /contacts/bulk-flag`](contacts.md) também suspende o bot para o mesmo. Consulte [Contactos](contacts.md) para obter a lista completa de campos.

---

## Criar a sua própria caixa de entrada

Tudo o que uma caixa de entrada precisa encontra-se nesta página e em [Contactos](contacts.md):

| O que precisa | Endpoint |
|---|---|
| Listar conversas | `GET /contacts` |
| Ler uma conversa | `GET /contacts/{contactId}/messages` |
| Listar sessões de chat de um contacto | `GET /chat-sessions/{contactId}` |
| Ver o que chegou recentemente | `GET /chat-sessions/recent` |
| Ler uma sessão de chat | `GET /contacts/{contactId}/chat-sessions/{sessionId}/messages` |
| Enviar uma resposta manual | `POST /contacts/{contactId}/send-message` |
| Corrigir uma resposta que acabou de enviar | `POST /contacts/{contactId}/messages/{messageId}/edit` |
| Remover uma mensagem | `DELETE /contacts/{contactId}/messages/{messageId}` |
| Limpar várias mensagens | `POST /contacts/{contactId}/messages/bulk-delete` |
| Reagir com um emoji | `POST /contacts/{contactId}/messages/{messageId}/react` |
| Classificar ou marcar uma mensagem | `PATCH /contacts/{contactId}/messages/{messageId}` |
| Marcar como lida | `POST /contacts/{contactId}/mark-read` |
| Devolver um chat à equipa | `POST /contacts/{contactId}/mark-unread` |
| Exportar uma transcrição | `GET /chat-exports/{contactId}` |
| Pausar ou retomar a IA | `PUT /contacts/{contactId}` com `is_bot_active` |

Para atualizações em tempo real, subscreva os eventos `New Message`, `Replies`, `Human Alerted` e `Chat Concluded` com [Webhooks](webhooks.md) em vez de consultar esta API periodicamente.

---

## Erros da API de Mensagens

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

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

| Estado | Quando acontece num endpoint de mensagem |
|---|---|
| `400` | Falta um campo obrigatório ou um parâmetro é inválido (`limit`, `hours`, `filter`, `direction`, `status` incorretos, uma matriz `message_ids` vazia ou com mais de 500 elementos, um `cursor` inválido, uma edição `body` vazia ou demasiado longa, um `score` fora de `-1`/`0`/`1`, ou um emoji com espaços ou mais de 16 caracteres). Também é devolvido quando uma mensagem não pode ser editada de todo — foi eliminada, o seu canal não permite edições ou já passou o período de edição desse canal. |
| `404` | O contacto, a sessão de chat ou um dos IDs de mensagem fornecidos não foi encontrado. |
| `409` | O canal não aceita a alteração neste momento. Nada foi escrito: numa edição, `edit_reason` diz o motivo; numa reação, o canal estava momentaneamente inacessível e uma nova tentativa poderá funcionar. |
| `422` | O contacto não pode receber mensagens de saída (não incomodar, privado ou um canal não suportado), ou uma reação nunca pode ser entregue nesta conversa (`reaction_reason` diz qual). |

Os códigos partilhados que qualquer endpoint pode devolver — `401`, `403` (o seu plano não inclui acesso à API), `429` (limite de taxa) e `500` — estão listados com orientações de repetição em [Erros e Paginação](errors-and-pagination.md).

---

## Próximos passos

- [Webhooks](webhooks.md) — receba atualizações do estado de entrega em vez de consultar periodicamente.
- [Contactos](contacts.md) — crie e procure os contactos para os quais envia mensagens.
- [Agendamentos](appointments.md) — marque e gira agendamentos para os seus contactos.
