
# Messages et conversations

L'API Messages vous permet d'envoyer un message à n'importe quel contact, de relire une conversation, de corriger ou de supprimer un message déjà envoyé, d'y réagir, de récupérer l'intégralité d'un fil de discussion, d'exporter une transcription et de marquer des discussions comme lues ou non lues — tout cela sans ouvrir la boîte de réception.

Tous les chemins sur cette page sont relatifs à l'URL de base `https://api.youraiconnector.com/v1`. Chaque requête nécessite votre clé API — consultez [Authentification](authentication.md) pour obtenir la liste complète des méthodes pour l'envoyer. Les exemples ci-dessous utilisent l'en-tête `X-API-Key`, avec un exemple cURL montrant également le formulaire de requête `?apiKey=`.

> **Fonctionnement de la livraison :** L'envoi d'un message n'attend **pas** qu'il soit arrivé à destination. L'API accepte votre message, renvoie immédiatement un identifiant de message, puis le livre en arrière-plan sur le canal du contact (WhatsApp, SMS, Instagram, etc.). Pour savoir si un message a réellement été livré ou lu, écoutez les mises à jour de statut avec les [Webhooks](webhooks.md) — ne faites pas de polling. La réponse d'envoi confirme uniquement que le message a été accepté.

---

## Envoyer un message

Il existe deux façons d'envoyer un message. Choisissez celle qui correspond à la manière dont vous identifiez déjà le contact :

- **Envoyer par ID de contact** — vous connaissez déjà l'ID du contact (par exemple, vous avez créé le contact via l'API ou l'avez obtenu via un webhook). Utilisez `POST /contacts/{contactId}/send-message`.
- **Envoyer par identité de contact** — vous connaissez le numéro de téléphone, l'identifiant Instagram, etc. du contact, mais pas son ID interne. Utilisez `POST /contacts/send` et laissez la plateforme trouver le bon contact.

Les deux méthodes mettent le message en file d'attente de la même manière et le livrent sur le canal utilisé par le contact. Vous ne choisissez pas le transport — la plateforme achemine les contacts WhatsApp via WhatsApp, les contacts SMS via SMS, et ainsi de suite.

### Envoyer par ID de contact

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

| Champ | Requis | Description |
|---|---|---|
| `body` | Oui | Le texte du message à envoyer. |
| `mediaUrl` | Non | URL d'un fichier média (image, document, etc.) à joindre. |
| `mediaContentType` | Non | Type MIME du média joint, par ex. `image/jpeg`. |
| `pauseBot` | Non | `true` met l'IA en pause pour ce contact lors de l'envoi du message — pour une intervention humaine. Voir [Mettre en pause ou reprendre l'IA](#pause-or-resume-the-ai-for-one-contact). |
| `clearIncompleteReply` | Non | `true` annule une réponse du bot en cours de rédaction afin qu'elle ne reprenne pas après votre message. |

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

**Réponse** (`200 OK`) :

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

### Envoyer par identité de contact

`POST /contacts/send`

Utilisez cette méthode lorsque vous ne disposez pas de l'ID interne du contact. Fournissez le `body` du message ainsi que **soit** un `contact_id`, **soit** un `channel` accompagné du champ d'identité correspondant à ce canal.

| Champ | Requis | Description |
|---|---|---|
| `body` | Oui | Le texte du message à envoyer. |
| `contact_id` | Non | ID d'un contact existant. Lorsqu'il est défini, les champs d'identité ci-dessous ne sont pas nécessaires. |
| `channel` | Non | Canal utilisé pour l'envoi. Requis lorsque `contact_id` n'est pas fourni. L'un des 14 canaux d'envoi sortant : `whatsapp`, `whatsapp_web`, `sms`, `instagram`, `instagram_private`, `messenger`, `telegram`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin`, `viber`. |
| `phone_number` | Non | Numéro de téléphone du contact au format international. Utilisé avec `whatsapp`, `whatsapp_web` et `sms`. |
| `instagram_id` | Non | ID utilisateur Instagram du contact. Utilisé avec `instagram`. |
| `messenger_id` | Non | ID utilisateur Messenger du contact. Utilisé avec `messenger`. |
| `telegram_user_id` | Non | ID utilisateur Telegram du contact. Utilisé avec `telegram`. |
| `media_url` | Non | URL d'un fichier multimédia à joindre. |
| `media_content_type` | Non | Type MIME du média joint, par ex. `image/jpeg`. |

**Quels canaux peuvent être résolus par identité.** Seuls six des 14 acceptent un champ d'identité au lieu d'un `contact_id` : `whatsapp`, `whatsapp_web` et `sms` sont recherchés par `phone_number`, `instagram` par `instagram_id`, `messenger` par `messenger_id`, et `telegram` par `telegram_user_id`. Les huit autres — `instagram_private`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin` et `viber` — n'ont pas d'identité publique à rechercher, donc l'envoi sur ces canaux nécessite `contact_id` ; passer `channel` seul renvoie une `400` vous indiquant que `contact_id` est requis.

**cURL** (utilisant le formulaire de requête `?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"])
```

**Réponse** (`201 Created`) :

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

> **Pourquoi un message peut être rejeté :** Un contact ayant activé le mode « ne pas déranger » ou le mode privé ne peut pas recevoir de messages sortants — la requête échoue avec une erreur `422`. Si aucun contact ne correspond à l'ID ou à l'identité que vous avez fourni(e), vous recevrez une erreur `404`.

---

## Lister les messages d'un contact

`GET /contacts/{contactId}/messages`

Renvoie les messages d'un contact, du plus récent au plus ancien, avec une pagination basée sur un curseur.

| Paramètre de requête | Requis | Description |
|---|---|---|
| `limit` | Non | Taille de la page. Par défaut `50`, maximum `100`. |
| `cursor` | Non | La valeur `next_cursor` issue d'une réponse précédente. Renvoie les messages antérieurs au curseur. |
| `filter` | Non | Filtrer par type de contenu : `all` (par défaut), `text`, `media` ou `tool_use`. |
| `direction` | Non | Filtrer par direction : `all` (par défaut), `inbound` (reçu du contact) ou `outbound` (envoyé par vous). |

> **Remarque sur le filtrage et la pagination :** Les filtres `filter` et `direction` sont appliqués à chaque page après sa lecture ; une page filtrée peut donc contenir moins d'éléments que `limit`. Le `next_cursor` continue de progresser dans la conversation complète, donc continuez la pagination jusqu'à ce que `next_cursor` soit `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"])
```

**Réponse** (`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"
}
```

### Champs de message

| Champ | Description |
|---|---|
| `id` | ID unique du message. |
| `body` | Contenu textuel du message. |
| `direction` | `inbound` (reçu du contact) ou `outbound` (envoyé par votre compte). |
| `channel` | Canal sur lequel le message a été envoyé ou reçu (par ex. `whatsapp`, `sms`, `instagram`). |
| `status` | État de livraison actuel, par ex. `Created`, `sent`, `delivered`, `read`, `failed`. |
| `type` | Type de message. Les messages en texte brut ont un type `null` ; l'activité des outils d'assistance automatisés est marquée `tool_use`. |
| `timestamp` | Heure ISO 8601 de création du message. |
| `media_url` | URL d'un fichier multimédia joint, le cas échéant. |
| `media_content_type` | Type MIME du média joint, le cas échéant. |
| `bot_reply` | `true` lorsque le message a été généré par l'assistant IA. |
| `score` | Votre évaluation du message : `1` pouce levé, `-1` pouce baissé, `0` lorsqu'il n'a pas été évalué. Voir [Évaluer ou marquer un message](#rate-or-star-a-message). |
| `is_important` | `true` lorsque le message a été marqué d'une étoile. |
| `is_deleted` | `true` lorsque le message a été supprimé. Les messages supprimés restent dans la liste mais leurs champs `body` et `media_url` sont vides. |
| `reactions` | Réactions par émojis sur le message, des deux côtés. Toujours un tableau — vide s'il n'y en a pas. Chaque entrée contient `emoji`, `from_phone_number`, `from_me` (`true` lorsque la réaction est la vôtre) et `reacted_at`. |

---

## Lister les sessions de discussion

Une session de discussion est une fenêtre de conversation avec un contact : elle s'ouvre lorsqu'il commence à parler et se ferme lorsque la conversation est terminée. Les sessions vous permettent de diviser un long historique en conversations lisibles plutôt qu'en une liste interminable.

### Sessions récentes pour tous les contacts

`GET /chat-sessions/recent`

Renvoie les sessions qui ont commencé au cours des X dernières heures, de la plus récente à la plus ancienne, pour chaque contact du compte.

| Paramètre de requête | Requis | Description |
|---|---|---|
| `hours` | Oui | Combien d'heures remonter en arrière. Doit être un nombre entier positif. |
| `status` | Non | Ne renvoyer que les sessions avec ce statut : `ChatSessionOpened` ou `ChatSessionClosed`. |
| `limit` | Non | Nombre maximum de sessions à renvoyer. Par défaut `100`, maximum `100`. |
| `includeMessages` | Non | `true` ajoute un tableau `messages` à chaque session. Désactivé par défaut car cela alourdit considérablement la réponse. |

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

**Réponse** (`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"
      }
    ]
  }
}
```

### Toutes les sessions pour un contact

`GET /chat-sessions/{contactId}`

Renvoie toutes les sessions de discussion pour un seul contact. Mêmes paramètres `status`, `limit` et `includeMessages` que ci-dessus — `hours` ne s'applique pas ici.

**cURL**

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

**Réponse** (`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"
      }
    ]
  }
}
```

> **Les noms des champs d'ID de session diffèrent entre les deux points de terminaison.** La liste des sessions récentes l'appelle `session_id` (elle contient également les détails du contact, car les sessions proviennent de nombreux contacts) ; la liste par contact l'appelle `id`. L'une ou l'autre valeur est ce que vous transmettez en tant que `{sessionId}` lors de la récupération du fil complet ci-dessous.

Lorsque `includeMessages=true`, chaque session gagne un tableau `messages` dont les entrées contiennent `id`, `body`, `direction`, `timestamp`, `type`, `channel` et `status`.

---

## Récupérer un fil de discussion

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

Une session de chat regroupe les messages d'un contact dans une seule fenêtre de conversation. Ce point de terminaison renvoie le fil complet d'une session unique, **du plus ancien au plus récent**, ainsi que les métadonnées de la session. Vous pouvez trouver les identifiants de session d'un contact via les points de terminaison des sessions 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"])
```

**Réponse** (`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
    }
  ]
}
```

L'objet `session` rapporte `status` (`ChatSessionOpened` lorsqu'elle est active, `ChatSessionClosed` une fois terminée), `start_date_time`, `end_date_time`, et un `tag` lisible par l'homme. Le tableau `messages` utilise les mêmes [champs de message](#message-fields) que le point de terminaison de liste.

---

## Modifier, supprimer et réagir aux messages

Ces points de terminaison modifient un message après son envoi. Deux d'entre eux interagissent avec le canal du contact ainsi qu'avec votre propre copie. Lisez donc l'introduction de la section avant de les configurer — ce qui est possible dépend entièrement du canal sur lequel se déroule la conversation.

**Ce que chaque canal permet**

| Action | Canaux pouvant modifier la copie du contact | Délai |
|---|---|---|
| Modifier un message envoyé | Widget de chat, WhatsApp Web, Telegram, LinkedIn | Aucun pour le widget de chat, 15 minutes sur WhatsApp Web, 48 heures sur Telegram, 60 minutes sur LinkedIn |
| Supprimer pour tout le monde | Widget de chat, WhatsApp Web, Telegram, LinkedIn | 60 minutes sur LinkedIn ; les autres n'ont pas de limite publiée |
| Réagir avec un emoji | WhatsApp Web, Telegram | Aucun |

Sur tous les autres canaux — l'API WhatsApp Business, SMS, Instagram, Messenger, e-mail, LINE, canaux personnalisés — une suppression retire toujours le message de votre boîte de réception, mais le contact conserve sa copie, et la modification ou la réaction est totalement impossible.

### Modifier un message

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

Réécrit un message que vous avez déjà envoyé, sur l'appareil du contact et dans votre copie.

| Champ | Requis | Description |
|---|---|---|
| `body` | Oui | Le nouveau texte du message. Ne doit pas être vide et peut contenir au maximum 4096 caractères. |

Contrairement à la suppression, cette opération **échoue bruyamment** lorsque le canal refuse : vous obtenez une `409` et votre copie reste exactement telle que le contact l'a, car afficher une modification qu'il n'a jamais reçue créerait un décalage entre les deux parties. Le champ `edit_reason` vous indique pourquoi — la fenêtre de modification du canal est fermée, le canal est déconnecté ou une autre erreur est survenue.

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

**Réponse** (`200 OK`) :

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

Si le canal n'accepte pas la modification, vous obtenez une `409` à la place, et rien n'est modifié :

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

Un message déjà supprimé, un canal qui ne permet aucune modification et un message trop ancien pour son canal renvoient tous une `400` — la requête n'atteint jamais le canal.

### Supprimer un message

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

Supprime le message de votre conversation et, lorsque le canal le permet, retire également la copie du contact. Aucun corps de requête.

Ceci répond toujours `200` lorsque le message existait, même si la copie du contact n'a pas pu être retirée — votre copie **est** supprimée, donc une erreur serait trompeuse. Lisez les trois champs de la réponse pour indiquer à l'utilisateur ce qui s'est réellement passé :

| Champ | Description |
|---|---|
| `revoke_supported` | Indique si ce canal peut retirer des messages. |
| `revoked` | Indique si la copie sur l'appareil du contact a été supprimée. |
| `revoke_reason` | Pourquoi elle n'a pas été supprimée, lorsque `revoked` est `false` — par exemple `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"])
```

**Réponse** (`200 OK`) :

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

> Les messages supprimés ne sont pas retirés de l'historique de la conversation. Ils restent dans `GET /contacts/{contactId}/messages` avec `is_deleted: true` ainsi qu'un `body` et un `media_url` vides.

### Supprimer plusieurs messages à la fois

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

Efface un lot de messages de votre côté uniquement. Le corps et les pièces jointes sont vidés, mais **rien n'est rétracté sur l'appareil du contact** — pour également retirer un message, supprimez-le un par un avec le point de terminaison pour message unique ci-dessus.

| Champ | Requis | Description |
|---|---|---|
| `message_ids` | Oui | Un tableau non vide d'identifiants de message, jusqu'à 500 par requête. `messageIds` est accepté comme 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"])
```

**Réponse** (`200 OK`) :

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

### Réagir à un message

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

Ajoute votre propre réaction emoji sur un message, ou la retire en envoyant une chaîne vide. Les réactions du contact ne sont jamais modifiées.

| Champ | Requis | Description |
|---|---|---|
| `emoji` | Oui | L'emoji avec lequel réagir, ou `""` pour supprimer votre réaction. Doit être une chaîne unique sans espaces, de 16 caractères maximum. |

Tout comme pour la modification, cela échoue plutôt que d'afficher une réaction que le contact n'a jamais reçue, et l'échec vous indique si une nouvelle tentative en vaut la peine :

- `422` — il ne pourra jamais être délivré dans cette conversation : le canal ne prend pas en charge les réactions, le message n'a pas d'identifiant côté canal, ou l'emoji est en dehors de l'ensemble autorisé par ce canal.
- `409` — le canal était momentanément inaccessible. Une nouvelle tentative peut fonctionner.

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

**Réponse** (`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"
    }
  ]
}
```

Le tableau `reactions` est l'ensemble complet des réactions présentes sur le message, les vôtres et celles du contact. Sur un `409` ou un `422`, il est renvoyé inchangé, de sorte qu'un client effectuant le rendu directement à partir de celui-ci n'affiche jamais une réaction qui n'a pas été délivrée.

### Évaluer ou marquer un message

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

Évalue un message avec un pouce levé ou baissé et/ou le marque comme important. Il s'agit d'une gestion interne de votre côté uniquement — rien n'est envoyé au contact.

| Champ | Requis | Description |
|---|---|---|
| `score` | Non | `1` pouce levé, `-1` pouce baissé, `0` efface l'évaluation. |
| `is_important` | Non | `true` ajoute une étoile au message, `false` retire l'étoile. Doit être un booléen réel, pas la chaîne `"true"`. |

Envoyez au moins l'un des deux, sinon vous recevrez une `400`. Seul ce que vous envoyez est écrit, donc ajouter une étoile à un message n'efface jamais son évaluation et vice versa — et la réponse ne renvoie que les champs que vous avez envoyés.

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

**Réponse** (`200 OK`) :

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

---

## Marquer les messages comme lus

Vous pouvez supprimer l'état non lu soit pour des messages spécifiques, soit pour l'ensemble de la conversation.

### Marquer des messages spécifiques comme lus

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

Transmettez les identifiants des messages à marquer comme lus.

| Champ | Requis | Description |
|---|---|---|
| `message_ids` | Oui | Un tableau non vide d'identifiants de message (jusqu'à 500 par requête). |

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

**Réponse** (`200 OK`) :

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

### Marquer l'intégralité du chat comme lu

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

Supprime le badge de non-lu pour toute la conversation du contact dans la boîte de réception. Aucun corps de requête n'est requis.

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

**Réponse** (`200 OK`) :

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

### Marquer toute la conversation comme non lue

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

Remet le badge de non lu sur la conversation — pratique lorsqu'un membre de votre équipe a ouvert une discussion mais la transmet à quelqu'un d'autre. Aucun corps de requête n'est requis.

Il s'agit d'un indicateur propre à la boîte de réception : il ne modifie **pas** la date de dernière lecture de la conversation, donc aucun accusé de lecture n'est envoyé au contact sur les canaux qui les prennent en charge.

**cURL**

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

**Réponse** (`200 OK`) :

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

---

## Exporter une conversation

Les exportations vous fournissent une conversation entière sous forme de transcription lisible, plutôt que de parcourir les messages page par page. Chaque point de terminaison d'exportation accepte un `filter` de `all` (par défaut), `text`, `media` ou `tool_use`, correspondant au filtre de la liste des messages.

### Exporter la discussion d'un contact

`GET /chat-exports/{contactId}`

| Paramètre de requête | Requis | Description |
|---|---|---|
| `format` | Non | `txt` (par défaut) renvoie un lien de téléchargement vers une transcription en texte brut. `json` renvoie les messages sous forme de données structurées dans la réponse. |
| `filter` | Non | `all` (par défaut), `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"])
```

**Réponse avec `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
      }
    ]
  }
}
```

Avec `format=txt` (la valeur par défaut), `data` est plutôt un lien de téléchargement vers le fichier de transcription généré :

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

> **Le lien de téléchargement est éphémère.** Récupérez le fichier dès que vous obtenez le lien plutôt que de le stocker — demandez une nouvelle exportation lorsque vous avez à nouveau besoin de la transcription.

### Exporter toutes les conversations récentes

`GET /chat-exports/recent`

Exporte les conversations de tous les contacts ayant été actifs au cours des X dernières heures, en un seul appel.

| Paramètre de requête | Requis | Description |
|---|---|---|
| `hours` | Oui | Nombre d'heures d'activité à prendre en compte. Doit être un nombre entier positif. |
| `format` | Non | `json` (par défaut) renvoie une entrée par contact. `txt` renvoie un fichier texte unique téléchargeable contenant toutes les conversations. |
| `limit` | Non | Nombre maximum de contacts à exporter. Par défaut `50`, maximum `100`. |
| `filter` | Non | `all` (par défaut), `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"
```

**Réponse** (`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..."
      }
    ]
  }
}
```

Avec `format=txt`, la réponse est le fichier texte lui-même, envoyé en tant que téléchargement plutôt qu'au format JSON.

> Cet appel unique extrait l'historique complet de chaque contact correspondant, veillez donc à ce que `hours` et `limit` restent raisonnables sur les comptes très actifs.

### Envoyer une transcription par e-mail au contact

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

Envoie au contact sa propre transcription de conversation par e-mail — le flux « m'envoyer cette discussion par e-mail », piloté depuis votre propre système.

| Champ | Requis | Description |
|---|---|---|
| `recipient_email` | Non | Où l'envoyer. Par défaut, l'adresse e-mail enregistrée du contact. |
| `via` | Non | `auto` (par défaut) choisit le meilleur itinéraire, `transactional` l'envoie en tant qu'e-mail système, `email_channel` l'envoie depuis votre canal e-mail connecté. |
| `note` | Non | Une courte ligne de votre part affichée au-dessus de la transcription. Jusqu'à 1000 caractères. |

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

**Réponse** (`200 OK`) :

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

`omittedCount` vous indique combien des messages les plus anciens ont été omis pour conserver une longueur raisonnable à l'e-mail. Un `200` signifie que la transcription a été générée et mise en file d'attente pour l'envoi, et non qu'elle est déjà arrivée dans la boîte de réception.

---

## Mettre en pause ou reprendre l'IA pour un contact

`PUT /contacts/{contactId}`

Définissez `is_bot_active` sur `false` pour empêcher l'IA de répondre à un contact, et remettez-le sur `true` pour lui rendre la conversation. C'est l'interrupteur de prise en main que vous souhaitez utiliser lorsqu'un humain intervient dans une conversation : les messages sortants que vous envoyez via l'API sont toujours délivrés pendant que le bot est en pause.

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

**Réponse**

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

**Mise en pause lors d'une réponse**

Si un humain prend le relais en envoyant une réponse, vous pouvez mettre le bot en pause dans la même requête au lieu d'effectuer un second appel. `POST /contacts/{contactId}/send-message` accepte deux options facultatives :

| Champ | Description |
|---|---|
| `pauseBot` | `true` met l'IA en pause pour ce contact lors de l'envoi du message. |
| `clearIncompleteReply` | `true` annule une réponse du bot en cours de rédaction afin qu'elle ne reprenne pas par la suite. |

```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 réponse inclut `"botPaused": true` lorsque la pause a été appliquée.

> Marquer un contact comme privé avec [`POST /contacts/bulk-flag`](contacts.md) met également le bot en pause pour lui. Voir [Contacts](contacts.md) pour la liste complète des champs.

---

## Créer votre propre boîte de réception

Tout ce dont une boîte de réception a besoin se trouve sur cette page et dans [Contacts](contacts.md) :

| Ce dont vous avez besoin | Point de terminaison |
|---|---|
| Lister les conversations | `GET /contacts` |
| Lire une conversation | `GET /contacts/{contactId}/messages` |
| Lister les sessions de chat d'un contact | `GET /chat-sessions/{contactId}` |
| Voir ce qui est arrivé récemment | `GET /chat-sessions/recent` |
| Lire une session de chat | `GET /contacts/{contactId}/chat-sessions/{sessionId}/messages` |
| Envoyer une réponse manuelle | `POST /contacts/{contactId}/send-message` |
| Corriger une réponse que vous venez d'envoyer | `POST /contacts/{contactId}/messages/{messageId}/edit` |
| Supprimer un message | `DELETE /contacts/{contactId}/messages/{messageId}` |
| Effacer plusieurs messages | `POST /contacts/{contactId}/messages/bulk-delete` |
| Réagir avec un emoji | `POST /contacts/{contactId}/messages/{messageId}/react` |
| Noter ou marquer un message | `PATCH /contacts/{contactId}/messages/{messageId}` |
| Marquer comme lu | `POST /contacts/{contactId}/mark-read` |
| Rendre une discussion à l'équipe | `POST /contacts/{contactId}/mark-unread` |
| Exporter une transcription | `GET /chat-exports/{contactId}` |
| Suspendre ou reprendre l'IA | `PUT /contacts/{contactId}` avec `is_bot_active` |

Pour des mises à jour en temps réel, abonnez-vous aux événements `New Message`, `Replies`, `Human Alerted` et `Chat Concluded` avec les [Webhooks](webhooks.md) plutôt que d'interroger cette API de manière répétée.

---

## Erreurs de l'API Messages

Les points de terminaison de message renvoient l'enveloppe d'erreur standard :

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

| Statut | Quand cela se produit sur un point de terminaison de message |
|---|---|
| `400` | Un champ requis est manquant ou un paramètre est invalide (mauvais `limit`, `hours`, `filter`, `direction`, `status`, un tableau `message_ids` vide ou de plus de 500 éléments, un `cursor` invalide, une modification `body` vide ou trop longue, un `score` en dehors de `-1`/`0`/`1`, ou un emoji avec des espaces ou plus de 16 caractères). Également renvoyé lorsqu'un message ne peut pas être modifié du tout — il a été supprimé, son canal ne permet pas la modification, ou il est en dehors de la fenêtre de modification de ce canal. |
| `404` | Le contact, la session de chat ou l'un des identifiants de message fournis n'a pas été trouvé. |
| `409` | Le canal n'accepte pas la modification pour le moment. Rien n'a été écrit : lors d'une modification, `edit_reason` explique pourquoi ; lors d'une réaction, le canal était momentanément inaccessible et une nouvelle tentative peut fonctionner. |
| `422` | Le contact ne peut pas recevoir de messages sortants (ne pas déranger, privé ou canal non pris en charge), ou une réaction ne peut jamais être délivrée sur cette conversation (`reaction_reason` indique lequel). |

Les codes partagés que chaque point de terminaison peut renvoyer — `401`, `403` (votre forfait n'inclut pas l'accès à l'API), `429` (limite de débit) et `500` — sont répertoriés avec des conseils de nouvelle tentative dans [Erreurs et pagination](errors-and-pagination.md).

---

## Étapes suivantes

- [Webhooks](webhooks.md) — recevez des mises à jour sur le statut de livraison au lieu d'interroger le serveur.
- [Contacts](contacts.md) — créez et recherchez les contacts auxquels vous envoyez des messages.
- [Rendez-vous](appointments.md) — réservez et gérez les rendez-vous de vos contacts.
