
# Messages & Conversations

The Messages API lets you send a message to any contact, read back a conversation, correct or remove a message you already sent, react to one, pull a full chat-session thread, export a transcript, and mark chats read or unread — all without opening the inbox.

All paths on this page are relative to the base URL `https://api.youraiconnector.com/v1`. Every request needs your API key — see [Authentication](authentication.md) for the full list of ways to send it. The examples below use the `X-API-Key` header, with one cURL example showing the `?apiKey=` query form too.

> **How delivery works:** Sending a message does **not** wait for it to arrive. The API accepts your message, returns immediately with a message ID, and then delivers it in the background on the contact's channel (WhatsApp, SMS, Instagram, and so on). To track whether a message was actually delivered or read, listen for status updates with [Webhooks](webhooks.md) — don't poll. The send response only confirms the message was accepted.

---

## Send a message

There are two ways to send. Pick whichever fits how you already identify the contact:

- **Send by contact ID** — you already know the contact's ID (for example, you created the contact through the API or got it from a webhook). Use `POST /contacts/{contactId}/send-message`.
- **Send by contact identity** — you know the contact's phone number, Instagram ID, etc., but not their internal ID. Use `POST /contacts/send` and let the platform find the right contact.

Both queue the message the same way and deliver it on whichever channel the contact is on. You don't pick a transport — the platform routes WhatsApp contacts over WhatsApp, SMS contacts over SMS, and so on.

### Send by contact ID

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

| Field | Required | Description |
|---|---|---|
| `body` | Yes | The message text to send. |
| `mediaUrl` | No | URL of a media file (image, document, etc.) to attach. |
| `mediaContentType` | No | MIME type of the attached media, e.g. `image/jpeg`. |
| `pauseBot` | No | `true` pauses the AI for this contact as the message is sent — for a human taking over. See [Pause or resume the AI](#pause-or-resume-the-ai-for-one-contact). |
| `clearIncompleteReply` | No | `true` discards a half-finished bot reply so it doesn't resume after your 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"])
```

**Response** (`200 OK`):

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

### Send by contact identity

`POST /contacts/send`

Use this when you don't have the contact's internal ID. Provide the message `body` plus **either** a `contact_id`, **or** a `channel` together with the identity field that matches that channel.

| Field | Required | Description |
|---|---|---|
| `body` | Yes | The message text to send. |
| `contact_id` | No | ID of an existing contact. When set, the identity fields below are not needed. |
| `channel` | No | Channel to send on. Required when `contact_id` is not given. One of the 14 outbound-sendable channels: `whatsapp`, `whatsapp_web`, `sms`, `instagram`, `instagram_private`, `messenger`, `telegram`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin`, `viber`. |
| `phone_number` | No | Contact's phone number in international format. Used with `whatsapp`, `whatsapp_web`, and `sms`. |
| `instagram_id` | No | Contact's Instagram user ID. Used with `instagram`. |
| `messenger_id` | No | Contact's Messenger user ID. Used with `messenger`. |
| `telegram_user_id` | No | Contact's Telegram user ID. Used with `telegram`. |
| `media_url` | No | URL of a media file to attach. |
| `media_content_type` | No | MIME type of the attached media, e.g. `image/jpeg`. |

**Which channels can be resolved by identity.** Only six of the 14 accept an identity field instead of a `contact_id`: `whatsapp`, `whatsapp_web` and `sms` are looked up by `phone_number`, `instagram` by `instagram_id`, `messenger` by `messenger_id`, and `telegram` by `telegram_user_id`. The other eight — `instagram_private`, `chat-widget`, `custom`, `email`, `line`, `imessage`, `linkedin` and `viber` — have no public identity to look up, so sending on those channels requires `contact_id`; passing `channel` alone returns a `400` telling you `contact_id` is required.

**cURL** (using the `?apiKey=` query form)

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

**Response** (`201 Created`):

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

> **Why a message might be rejected:** A contact with do-not-disturb or private mode turned on cannot receive outbound messages — the request fails with a `422`. If no contact matches the ID or identity you provided, you get a `404`.

---

## List a contact's messages

`GET /contacts/{contactId}/messages`

Returns a contact's messages, newest first, with cursor-based pagination.

| Query parameter | Required | Description |
|---|---|---|
| `limit` | No | Page size. Default `50`, maximum `100`. |
| `cursor` | No | The `next_cursor` value from a previous response. Returns messages older than the cursor. |
| `filter` | No | Filter by content type: `all` (default), `text`, `media`, or `tool_use`. |
| `direction` | No | Filter by direction: `all` (default), `inbound` (received from the contact), or `outbound` (sent by you). |

> **Note on filtering and pagination:** The `filter` and `direction` filters are applied to each page after it's read, so a filtered page can contain fewer items than `limit`. The `next_cursor` still advances through the full conversation, so keep paging until `next_cursor` is `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"])
```

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

### Message fields

| Field | Description |
|---|---|
| `id` | Unique ID of the message. |
| `body` | Text content of the message. |
| `direction` | `inbound` (received from the contact) or `outbound` (sent by your account). |
| `channel` | Channel the message was sent or received on (e.g. `whatsapp`, `sms`, `instagram`). |
| `status` | Current delivery status, e.g. `Created`, `sent`, `delivered`, `read`, `failed`. |
| `type` | Message type. Plain text messages have a `null` type; automated assistant tool activity is marked `tool_use`. |
| `timestamp` | ISO 8601 time the message was created. |
| `media_url` | URL of an attached media file, if any. |
| `media_content_type` | MIME type of the attached media, if any. |
| `bot_reply` | `true` when the message was generated by the AI assistant. |
| `score` | Your rating of the message: `1` thumbs up, `-1` thumbs down, `0` when it has not been rated. See [Rate or star a message](#rate-or-star-a-message). |
| `is_important` | `true` when the message has been starred. |
| `is_deleted` | `true` when the message was deleted. Deleted messages stay in the list but their `body` and `media_url` are empty. |
| `reactions` | Emoji reactions on the message, from either side. Always an array — empty when there are none. Each entry has `emoji`, `from_phone_number`, `from_me` (`true` when the reaction is yours) and `reacted_at`. |

---

## List chat sessions

A chat session is one conversation window with a contact: it opens when they start talking and closes when the conversation is concluded. Sessions are how you page a long history into readable conversations instead of one endless list.

### Recent sessions across all contacts

`GET /chat-sessions/recent`

Returns the sessions that started in the last X hours, newest first, across every contact on the account.

| Query parameter | Required | Description |
|---|---|---|
| `hours` | Yes | How many hours to look back. Must be a positive whole number. |
| `status` | No | Only return sessions with this status: `ChatSessionOpened` or `ChatSessionClosed`. |
| `limit` | No | Maximum number of sessions to return. Default `100`, maximum `100`. |
| `includeMessages` | No | `true` adds a `messages` array to every session. Off by default because it makes the response much larger. |

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

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

### All sessions for one contact

`GET /chat-sessions/{contactId}`

Returns every chat session for a single contact. Same `status`, `limit` and `includeMessages` parameters as above — `hours` does not apply here.

**cURL**

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

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

> **Session ID field names differ between the two endpoints.** The recent-sessions list calls it `session_id` (it also carries the contact's details, since sessions come from many contacts); the per-contact list calls it `id`. Either value is what you pass as `{sessionId}` when fetching the full thread below.

When `includeMessages=true`, each session gains a `messages` array whose entries carry `id`, `body`, `direction`, `timestamp`, `type`, `channel` and `status`.

---

## Fetch a chat-session thread

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

A chat session groups a contact's messages into one conversation window. This endpoint returns the full thread of a single session, **oldest first**, along with the session's metadata. You can find session IDs for a contact through the chat sessions endpoints.

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

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

The `session` object reports `status` (`ChatSessionOpened` while active, `ChatSessionClosed` once ended), `start_date_time`, `end_date_time`, and a human-readable `tag`. The `messages` array uses the same [message fields](#message-fields) as the list endpoint.

---

## Edit, delete and react to messages

These endpoints change a message after it has been sent. Two of them reach out to the contact's channel as well as your own copy, so read the section intro before wiring them up — what is possible depends entirely on the channel the conversation is on.

**What each channel allows**

| Action | Channels that can change the contact's copy | Time limit |
|---|---|---|
| Edit a sent message | Chat widget, WhatsApp Web, Telegram, LinkedIn | None on the chat widget, 15 minutes on WhatsApp Web, 48 hours on Telegram, 60 minutes on LinkedIn |
| Delete for everyone | Chat widget, WhatsApp Web, Telegram, LinkedIn | 60 minutes on LinkedIn; the others have no published limit |
| React with an emoji | WhatsApp Web, Telegram | None |

On every other channel — the WhatsApp Business API, SMS, Instagram, Messenger, email, LINE, custom channels — a delete still removes the message from your inbox, but the contact keeps their copy, and editing or reacting is not possible at all.

### Edit a message

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

Rewrites a message you already sent, on the contact's device and in your copy.

| Field | Required | Description |
|---|---|---|
| `body` | Yes | The new message text. Must not be empty and can be at most 4096 characters. |

Unlike deleting, this **fails loudly** when the channel refuses: you get a `409` and your copy is left exactly as the contact has it, because showing an edit they never received would put the two sides out of step. The `edit_reason` field tells you why — the channel's edit window has closed, the channel is disconnected, or something else went wrong.

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

**Response** (`200 OK`):

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

If the channel would not take the edit you get a `409` instead, and nothing was changed:

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

A message that has already been deleted, a channel that cannot edit at all, and a message that is too old for its channel all return `400` — the request never reaches the channel.

### Delete one message

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

Removes the message from your conversation and, where the channel allows it, pulls the contact's copy back too. No request body.

This always answers `200` when the message existed, even if the contact's copy could not be retracted — your copy **is** gone, so an error would be misleading. Read the three fields in the response to tell the user what actually happened:

| Field | Description |
|---|---|
| `revoke_supported` | Whether this channel can retract messages at all. |
| `revoked` | Whether the copy on the contact's device was removed. |
| `revoke_reason` | Why it was not removed, when `revoked` is `false` — for example `revoke_window_closed` or `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"])
```

**Response** (`200 OK`):

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

> Deleted messages are not removed from the conversation history. They stay in `GET /contacts/{contactId}/messages` with `is_deleted: true` and an empty `body` and `media_url`.

### Delete several messages at once

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

Clears a batch of messages from your side only. The bodies and attachments are emptied, but **nothing is retracted on the contact's device** — to also pull a message back, delete it one at a time with the single-message endpoint above.

| Field | Required | Description |
|---|---|---|
| `message_ids` | Yes | A non-empty array of message IDs, up to 500 per request. `messageIds` is accepted as an 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"])
```

**Response** (`200 OK`):

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

### React to a message

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

Puts your own emoji reaction on a message, or takes it back by sending an empty string. The contact's own reactions are never touched.

| Field | Required | Description |
|---|---|---|
| `emoji` | Yes | The emoji to react with, or `""` to remove your reaction. Must be a single string with no spaces, at most 16 characters. |

Like editing, this fails rather than showing a reaction the contact never got, and the failure tells you whether retrying is worth it:

- `422` — it can never be delivered on this conversation: the channel does not support reactions, the message has no channel-side ID, or the emoji is outside the set that channel allows.
- `409` — the channel was momentarily unreachable. A retry may work.

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

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

The `reactions` array is the full set of reactions now on the message, yours and the contact's. On a `409` or `422` it is returned unchanged, so a client rendering straight from it never shows a reaction that was not delivered.

### Rate or star a message

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

Rates a message thumbs up or thumbs down and/or stars it as important. This is bookkeeping on your side only — nothing is sent to the contact.

| Field | Required | Description |
|---|---|---|
| `score` | No | `1` thumbs up, `-1` thumbs down, `0` clears the rating. |
| `is_important` | No | `true` stars the message, `false` unstars it. Must be a real boolean, not the string `"true"`. |

Send at least one of the two, or you get a `400`. Only what you send is written, so starring a message never clears its rating and vice versa — and the response echoes back only the fields you sent.

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

**Response** (`200 OK`):

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

---

## Mark messages as read

You can clear the unread state either for specific messages or for the whole conversation.

### Mark specific messages as read

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

Pass the IDs of the messages to mark as read.

| Field | Required | Description |
|---|---|---|
| `message_ids` | Yes | A non-empty array of message IDs (up to 500 per request). |

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

**Response** (`200 OK`):

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

### Mark the whole chat as read

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

Clears the unread badge for the contact's entire conversation in the inbox. No request body is required.

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

**Response** (`200 OK`):

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

### Mark the whole chat as unread

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

Puts the unread badge back on the conversation — handy when someone on your team opened a chat but is handing it back. No request body is required.

This is an inbox-only flag: it does **not** change when the conversation was last read, so no read receipt is sent to the contact on channels that support them.

**cURL**

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

**Response** (`200 OK`):

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

---

## Export a conversation

Exports give you a whole conversation as a readable transcript, rather than paging through messages. Every export endpoint accepts a `filter` of `all` (default), `text`, `media` or `tool_use`, matching the filter on the message list.

### Export one contact's chat

`GET /chat-exports/{contactId}`

| Query parameter | Required | Description |
|---|---|---|
| `format` | No | `txt` (default) returns a download link to a plain-text transcript. `json` returns the messages as structured data in the response. |
| `filter` | No | `all` (default), `text`, `media` or `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"])
```

**Response with `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
      }
    ]
  }
}
```

With `format=txt` (the default), `data` is instead a download link to the generated transcript file:

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

> **The download link is short-lived.** Fetch the file as soon as you get the link rather than storing it — request a fresh export when you need the transcript again.

### Export every recent conversation

`GET /chat-exports/recent`

Exports the conversations of all contacts that were active in the last X hours, in one call.

| Query parameter | Required | Description |
|---|---|---|
| `hours` | Yes | How many hours of activity to look back over. Must be a positive whole number. |
| `format` | No | `json` (default) returns one entry per contact. `txt` returns a single downloadable text file with every conversation in it. |
| `limit` | No | Maximum number of contacts to export. Default `50`, maximum `100`. |
| `filter` | No | `all` (default), `text`, `media` or `tool_use`. |

**cURL**

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

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

With `format=txt` the response is the text file itself, sent as a download rather than JSON.

> This one call pulls the full history of every matching contact, so keep `hours` and `limit` modest on busy accounts.

### Email a transcript to the contact

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

Sends the contact their own conversation transcript by email — the "email me this chat" flow, driven from your own system.

| Field | Required | Description |
|---|---|---|
| `recipient_email` | No | Where to send it. Defaults to the contact's stored email address. |
| `via` | No | `auto` (default) picks the best route, `transactional` sends it as a system email, `email_channel` sends it from your connected email channel. |
| `note` | No | A short line from you shown above the transcript. Up to 1000 characters. |

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

**Response** (`200 OK`):

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

`omittedCount` tells you how many of the oldest messages were left out to keep the email a sensible length. A `200` means the transcript was built and queued for sending, not that it has landed in the inbox yet.

---

## Pause or resume the AI for one contact

`PUT /contacts/{contactId}`

Set `is_bot_active` to `false` to stop the AI replying to one contact, and back to `true` to hand the conversation back. This is the takeover switch you want when a human steps into a conversation: outgoing messages you send with the API still deliver while the bot is paused.

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

**Response**

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

**Pausing as part of the reply**

If a human is taking over by sending a reply, you can pause the bot in the same request instead of making a second call. `POST /contacts/{contactId}/send-message` accepts two optional flags:

| Field | Description |
|---|---|
| `pauseBot` | `true` pauses the AI for this contact as the message is sent. |
| `clearIncompleteReply` | `true` discards a half-finished bot reply so it does not resume afterwards. |

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

The response includes `"botPaused": true` when the pause was applied.

> Flagging a contact as private with [`POST /contacts/bulk-flag`](contacts.md) also pauses the bot for them. See [Contacts](contacts.md) for the full field list.

---

## Building your own inbox

Everything an inbox needs is on this page and in [Contacts](contacts.md):

| What you need | Endpoint |
|---|---|
| List conversations | `GET /contacts` |
| Read a conversation | `GET /contacts/{contactId}/messages` |
| List a contact's chat sessions | `GET /chat-sessions/{contactId}` |
| See what came in recently | `GET /chat-sessions/recent` |
| Read one chat session | `GET /contacts/{contactId}/chat-sessions/{sessionId}/messages` |
| Send a manual reply | `POST /contacts/{contactId}/send-message` |
| Correct a reply you just sent | `POST /contacts/{contactId}/messages/{messageId}/edit` |
| Remove a message | `DELETE /contacts/{contactId}/messages/{messageId}` |
| Clear several messages | `POST /contacts/{contactId}/messages/bulk-delete` |
| React with an emoji | `POST /contacts/{contactId}/messages/{messageId}/react` |
| Rate or star a message | `PATCH /contacts/{contactId}/messages/{messageId}` |
| Mark as read | `POST /contacts/{contactId}/mark-read` |
| Hand a chat back to the team | `POST /contacts/{contactId}/mark-unread` |
| Export a transcript | `GET /chat-exports/{contactId}` |
| Pause or resume the AI | `PUT /contacts/{contactId}` with `is_bot_active` |

For live updates, subscribe to the `New Message`, `Replies`, `Human Alerted` and `Chat Concluded` events with [Webhooks](webhooks.md) rather than polling this API on a timer.

---

## Messages API errors

Message endpoints return the standard error envelope:

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

| Status | When it happens on a message endpoint |
|---|---|
| `400` | A required field is missing or a parameter is invalid (bad `limit`, `hours`, `filter`, `direction`, `status`, an empty or over-500 `message_ids` array, an invalid `cursor`, an empty or over-long edit `body`, a `score` outside `-1`/`0`/`1`, or an emoji with spaces or over 16 characters). Also returned when a message cannot be edited at all — it was deleted, its channel has no edit, or it is past that channel's edit window. |
| `404` | The contact, chat session, or one of the supplied message IDs was not found. |
| `409` | The channel would not take the change right now. Nothing was written: on an edit, `edit_reason` says why; on a reaction, the channel was momentarily unreachable and a retry may work. |
| `422` | The contact cannot receive outbound messages (do-not-disturb, private, or an unsupported channel), or a reaction can never be delivered on this conversation (`reaction_reason` says which). |

The shared codes every endpoint can return — `401`, `403` (your plan does not include API access), `429` (rate limit) and `500` — are listed with retry guidance in [Errors & Pagination](errors-and-pagination.md).

---

## Next steps

- [Webhooks](webhooks.md) — get delivery-status updates pushed to you instead of polling.
- [Contacts](contacts.md) — create and look up the contacts you message.
- [Appointments](appointments.md) — book and manage appointments for your contacts.
