Your AI Connector Docs

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 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 — 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.

Both also only find contacts that already exist. Neither call creates one: an ID or an identity nothing matches comes back as a 404, it does not become a new contact. So if you’re messaging someone for the first time, either create the contact first with POST /contacts, or, on WhatsApp Web, use Send to a WhatsApp Web number directly below, which creates the contact in the same call.

Send by contact ID

POST /contacts/{contactId}/send-message

Field Required Description
body Yes The message text to send, up to 65,536 characters. A body longer than the channel allows in one message (1,500 on the WhatsApp Business API and SMS, 1,000 on Instagram, 2,000 on Messenger, 4,096 on Telegram and LinkedIn, 5,000 on LINE, 6,000 on TikTok, 7,000 on Viber, 65,536 on WhatsApp Web; no limit on email, the chat widget and custom channels) is delivered as several messages in order, split at paragraph or sentence breaks. The response then carries messageIds for every part and autoSplit: true.
mediaUrl No URL of a media file (image, audio, video, document) to attach.
mediaContentType No MIME type of the attached media, e.g. image/jpeg. Worked out from the URL’s file extension when omitted (.mp3 → audio/mpeg, .pdf → application/pdf); set it for a URL without an extension.
sendAsVoiceNote No WhatsApp Web only, audio only: deliver the audio as a voice note (the play bubble). Audio that is not OGG/Opus is converted at send time. Defaults to false.
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.
clearIncompleteReply No true discards a half-finished bot reply so it doesn’t resume after your message.
human_reply No true marks the message as written by a person on your team rather than by an integration. The AI then treats it as a teammate’s reply in the conversation, and a reply the AI had queued for the contact’s last message is cancelled so the contact doesn’t get two answers. It does not pause the AI; use pauseBot for that. Defaults to false.

On LinkedIn, a message you send this way, or from the inbox, is delivered even while a person is handling the thread on the LinkedIn side. The AI’s own replies and its follow-ups are held back on such a thread. A held reply is removed from the chat rather than shown as failed, because nothing was sent and no credit is used. The AI’s last reply before it hands a thread to a person is still delivered.

cURL

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

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

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):

{
  "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, up to 65,536 characters. A body longer than the channel allows in one message (1,500 on the WhatsApp Business API and SMS, 1,000 on Instagram, 2,000 on Messenger, 4,096 on Telegram and LinkedIn, 5,000 on LINE, 6,000 on TikTok, 7,000 on Viber, 65,536 on WhatsApp Web; no limit on email, the chat widget and custom channels) is delivered as several messages in order, split at paragraph or sentence breaks. The response then carries messageIds for every part and autoSplit: true.
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)

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

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

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):

{
  "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.

Sending on WhatsApp Web: variations and burst protection

Two things apply to body when the contact is on WhatsApp Web.

Spintax braces are expanded per send. A body of {Hi|Hello|Hey} there, quick question goes out as one of the three greetings, chosen for that send — the braces never reach the contact. Use it to give every contact a different opening message from the same integration code. Text without braces is sent exactly as written.

Bursts of identical opening messages are refused. If ten near-identical opening messages go out from one WhatsApp Web number within an hour, to contacts who have never written to you, further ones are not delivered: the send is accepted as usual (you still get a messageId), and the message is then left with status: "failed" and error_code: "opener_burst", with a customer-readable reason on it. Retrying the same text does not help — vary the text, wait an hour, and send again. Replies inside existing conversations are never affected. The reasoning is in Identical openers get numbers restricted.

Send to a WhatsApp Web number directly

POST /whatsapp-web/send

Send to a phone number on WhatsApp Web without looking a contact up first. If the number isn’t a contact yet, the contact is created in the same call, with the AI switched off on it, so the message reads as one you sent by hand and no assistant takes the conversation over afterwards. An existing contact is reused as it is.

Field Required Description
phoneNumber Yes The number to message, in international format (+31612345678).
message No The text to send. Either message or mediaUrl must be there.
mediaUrl No Public URL of a file to send. Either message or mediaUrl must be there.
mediaContentType No MIME type of that file, for example image/jpeg. Worked out from the URL when you leave it off.
sendAsVoiceNote No true sends an audio file as a voice note rather than as an attachment.
fromPhoneNumber No Which of your connected WhatsApp Web numbers to send from, in international format. Defaults to the number the contact already belongs to.
campaignId No Take the active hours from this Agent instead of the account’s default. Must be an Agent on the same account.
ignoreActiveHours No true sends straight away, whatever the active hours say.
sub_account_id No For agencies: send on behalf of one of your client accounts.
curl -X POST "https://api.youraiconnector.com/v1/whatsapp-web/send?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumber": "+31612345678",
    "message": "Hi Alex, your quote is ready."
  }'

A successful call answers 202:

{ "success": true, "queued": true, "contactId": "abc123" }

202 means accepted, not delivered yet. Outside active hours the send is held and goes out in the next allowed slot, unless you passed ignoreActiveHours. Read the message’s own status back with List a contact’s messages using the contactId you got here.

When it doesn’t go through:

Status Code What happened
400 INVALID_PHONE_FORMAT The number isn’t in international format, or phoneNumber is missing, or both message and mediaUrl are.
400 CONTACT_DND That contact is on Do not disturb, so nothing outbound is sent to them.
400 CONTACT_PRIVATE That contact is marked private.
404 CAMPAIGN_NOT_FOUND No Agent matches the campaignId you passed.
403 CAMPAIGN_FORBIDDEN That Agent belongs to another account.
402 The account has no credits left, or no WhatsApp Web number is connected.

A second call for the same contact while one held send is still waiting answers 409, so a queue never stacks up behind one person.


Log a note into a conversation

POST /contacts/notes

Adds a line to a contact’s conversation without sending it. Use it for context your team and the AI should have but the contact should never receive: a summary of a phone call your voice agent just made, a remark from your CRM, or a note from a colleague. It works on every channel (WhatsApp, SMS, Instagram, Messenger, Telegram, the chat widget, custom channels and so on).

What happens to a note:

  • It shows in the chat in the inbox as a yellow Note card, with the author if you sent one.
  • The AI reads it as part of the conversation history, marked as an internal note, so its next reply takes it into account.
  • It is never sent to the contact, costs no message credits, and triggers nothing: no AI reply, no automation, no webhook, no notification, no unread badge. It does not pause the AI and does not count as a reply from your team.

Find the contact the same way as Send by contact identity: either its ID, or a channel plus the matching identifier. A note only goes to a contact that already exists; nothing matching is a 404.

Field Required Description
contact_id One of the two The contact’s ID.
channel + identifier One of the two channel plus phone_number (WhatsApp, WhatsApp Web, SMS), instagram_id, messenger_id or telegram_user_id, as in Send by contact identity.
body Yes The note text, up to 4,000 characters. Line breaks are kept.
author No Who or what wrote it, up to 80 characters, for example Voice agent or Sarah (sales). Shown on the card and to the AI.
timestamp No When it happened, in epoch milliseconds. Defaults to now. Use the call’s end time so the note sits in the right place in the thread. It cannot be more than 5 minutes in the future; a later time is rejected with a 400.
external_id No Your own ID for this note, up to 200 characters, for example the call ID. Sending the same external_id for the same contact again does not add a second note; the response then carries duplicate: true. Use it so a retried webhook cannot log the same call twice.

cURL

curl -X POST "https://api.youraiconnector.com/v1/contacts/notes" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "whatsapp",
    "phone_number": "+31612345678",
    "author": "Voice agent",
    "body": "Voice call, 3:12. Asked about knee surgery waiting times, prefers a call back on Tuesday morning.",
    "external_id": "call_8f2k1"
  }'

Response (201, or 200 with duplicate: true when the external_id was already logged)

{
  "success": true,
  "messageId": "note_4c1f0e...",
  "contactId": "contact123"
}

A note appears in List a contact’s messages with type: "note".

Generate a reply draft with the AI

POST /contacts/{contactId}/draft-reply

Asks the AI agent that handles a conversation to write the next reply, and gives you the text back. Nothing is sent. It is the same draft the Generate Reply button in the inbox writes: the agent uses its setup, its knowledge base and the conversation so far, and answers in the contact’s language. Use it to put a human (or your own app) in between the AI and the contact: fetch the draft, show it for approval, then send it with Send by contact ID, changed or as it is.

  • The draft is written by the agent assigned to the contact. A contact that has no agent yet gets the agent that would answer its next message.
  • While drafting, the agent does not run any tools or functions, so no booking, tag, hand off or other action happens.
  • Nothing is stored in the conversation, and the contact sees nothing.
  • A draft costs the same credits as one AI reply from that agent in this conversation, and nothing when your own AI key is connected.
  • The conversation needs at least one message.

All body fields are optional; an empty body (or no body) drafts the next reply.

Field Required Description
instruction No A one-time direction for this draft, up to 2,000 characters, the same as long-pressing Generate Reply in the inbox. For example Offer Tuesday at 3pm or Ask for their email address.
campaign_id No Draft under this campaign instead of the contact’s agent.
purpose No chat_reply (default) writes a message to the contact. faq_answer writes a short answer to the contact’s question that works on its own, for a knowledge base FAQ.

cURL

curl -X POST "https://api.youraiconnector.com/v1/contacts/contact123/draft-reply" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "instruction": "Offer Tuesday at 3pm for the consultation" }'

Response (200)

{
  "success": true,
  "contact_id": "contact123",
  "draft": "Hi Sara, Tuesday at 3pm works for the consultation. Shall I put you down for it?"
}

A 400 means the conversation has no messages yet, your credits ran out, or the contact has no agent or campaign to draft from (error says which). A 404 means the contact does not exist, or a team member’s key is limited to contacts it cannot see. The key or team member needs permission to edit messages.

Using an AI client over MCP? The same action is available as the generate_reply_draft tool.

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

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

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

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):

{
  "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. For a message with an attachment, body starts with a bracketed note of what arrived, followed by any caption, for example [THE CONTACT UPLOADED AN IMAGE] Here is my CV or [THE CONTACT UPLOADED A VOICE NOTE] - [TRANSCRIPTION: ...]. Voice notes and videos include the transcription.
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. This link stops working 7 days after the message arrived, on every channel (WhatsApp Business API, WhatsApp Web, Instagram, Messenger, Telegram, chat widget and the rest). Get a fresh one with Download a message’s attachment.
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.
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.
faq_ids_used IDs of the Knowledge Base FAQs the AI had in front of it when it wrote this reply: the ones picked for the latest message plus the ones kept from earlier in the conversation. The same list is on every message of a reply that was sent in several parts. Always an array, empty for inbound messages, messages sent by people, automatic follow-ups and AI replies sent before 25 September 2026.
faqs_used The same FAQs with their question: each entry has id and question. question is null when the FAQ has been deleted since.

Download a message’s attachment

POST /chat-exports/media/download-url

Gives you a fresh download link for the file attached to a message: an image, voice note, video or document. The link works for 1 hour. Use it when you want to save attachments somewhere else (for example, filing CVs or voice notes a contact sent), or when the media_url on a message or a New Message webhook no longer opens.

To find messages with attachments, list a contact’s messages with filter=media. Each message’s id and the contact’s ID make up the messagePath.

Body field Required Description
messagePath Yes Which message, in the form contacts/{contactId}/messages/{messageId}.

cURL

curl -X POST "https://api.youraiconnector.com/v1/chat-exports/media/download-url" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messagePath": "contacts/contact123/messages/aB3dE5fG7hI9jK1lM2nO"}'

JavaScript

const res = await fetch("https://api.youraiconnector.com/v1/chat-exports/media/download-url", {
  method: "POST",
  headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ messagePath: "contacts/contact123/messages/aB3dE5fG7hI9jK1lM2nO" }),
});
const { data } = await res.json();
console.log(data.url); // download the file within the hour

Python

import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/chat-exports/media/download-url",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={"messagePath": "contacts/contact123/messages/aB3dE5fG7hI9jK1lM2nO"},
)
file_url = res.json()["data"]["url"]

Response (200 OK):

{
  "success": true,
  "data": {
    "url": "https://storage.googleapis.com/...",
    "expiresInMs": 3600000
  }
}
Field Description
data.url The download link. Fetch the file from it directly, no API key needed.
data.expiresInMs How long the link works, in milliseconds (3600000 = 1 hour). Call the endpoint again for a new link.
Status When
400 messagePath is missing or not in the contacts/{contactId}/messages/{messageId} form, or the message has no attachment that can be downloaded this way. Some older attachments are stored at the channel provider and cannot be re-issued, and a shared Instagram reel or post is a link to Instagram rather than a file.
403 The contact does not belong to your account.
404 No message with that ID exists on that contact.

Using an AI client over MCP? The same action is available as the get_chat_media_download_url tool.


List chat sessions

A chat session is one conversation window with a contact. It opens on the first incoming message when no session is open, and it closes automatically 48 hours after the contact’s last incoming message (every new incoming message pushes the close out by another 48 hours). The AI concluding a chat does not close the session; it only fires the Chat Concluded webhook. 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

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

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

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):

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

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

Response (200 OK):

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

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

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

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):

{
  "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 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 (text messages only), 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 no longer than the channel allows in one message (the same per-channel limits as Send by contact ID; 4,096 when the message has no channel).

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, the message has an attachment that the channel cannot edit (media_not_editable: on WhatsApp Web only text messages can be edited, not the text under a photo, video or file), or something else went wrong.

cURL

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

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

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):

{
  "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:

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

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

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

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):

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

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

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

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):

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

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

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

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):

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

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

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

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):

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

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

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

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):

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

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

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

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):

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

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

Response (200 OK):

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

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

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):

{
  "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:

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

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

Response (200 OK):

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

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):

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

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

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

import requests

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

Response

{
  "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 these 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.
human_reply true marks the message as a teammate’s reply and cancels a reply the AI had queued. Pausing still needs pauseBot.
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,
    "human_reply": true
  }'

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

Flagging a contact as private with POST /contacts/bulk-flag also pauses the bot for them. See Contacts for the full field list.


Building your own inbox

Everything an inbox needs is on this page and in Contacts:

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 rather than polling this API on a timer.


Messages API errors

Message endpoints return the standard error envelope:

{
  "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.


Next steps

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