The messages inside a conversation, and the internal notes agents leave beside them. The timeline merges both, newest first. Replies you send go out through the channel the conversation arrived on, so a WhatsApp conversation sends over WhatsApp.
Endpoints
GET/conversations/{conversationId}/timelineList a conversation’s timelinePOST/conversations/{conversationId}/messagesSend a messagePOST/conversations/{conversationId}/notesAdd a private notePOST/conversations/{conversationId}/seenMark a conversation as seenPUT/conversations/{conversationId}/languageSet a conversation’s languageThe message object
One entry of the timeline: a customer message, an agent or bot reply, a system event or a private note.
Attributes
idintegerMessage ID. Messages and private notes are numbered independently, so a note and a message in one conversation can share anid— tell them apart withprivateNote.conversationIdintegerThe conversation this entry belongs to.senderTypestringWho wrote it.One of:CONTACTAGENTBOTSYSTEMCAMPAIGNmessageTypestringThe kind of content:TEXT,IMAGE,AUDIO,VIDEO,DOCUMENT,STICKER,LOCATION,CONTACTS,INTERACTIVE,REACTIONorTEMPLATE. Private notes usePRIVATE_NOTE.channelTypestringnullableChannel it travelled on, e.g.WHATSAPP.contentstringnullableThe text, when the message has any.contentJsonstringnullableStructured content for non-text messages, as a serialised JSON string. Parse it before use. (Requests take a real JSON value instead.)statusstringnullableDelivery state.PENDINGmeans queued but not yet accepted by the channel.One of:RECEIVEDPENDINGSENTDELIVEREDREADFAILEDproviderMessageIdstringnullableThe channel provider’s own ID for the message.privateNotebooleanAlways present:falseon a message,trueon an internal note. Notes are never delivered to the customer. A note hassenderTypeAGENT,channelTypeINTERNAL,contentJson"{}"andstatusSENT.originTypestringnullableWhat produced the entry, such asAGENTfor a note orHANDOFFfor a hand-over message. Free-form; treat unknown values as possible.originIdintegernullableThe ID of that origin, when it has one.originKindstringnullableSet when a macro, an automation rule or auto-resolve wrote the entry; otherwisenull.One of:MACROAUTOMATION_RULEAUTO_RESOLVEsourceIdintegernullableThe ID of the macro, rule or other source named byoriginKind.errorMessagestringnullableWhy delivery failed, whenstatusisFAILED.noteKindstringnullableTRANSFER_NOTEfor a handover note posted with a transfer; otherwisenull.One of:TRANSFER_NOTEtransferEventIdintegernullableThe transfer aTRANSFER_NOTEbelongs to.replyToMessageIdintegernullableThe message this one quotes, for both an agent’s quote-reply and a customer’s WhatsApp reply.campaignIdintegernullableThe campaign that sent it, forCAMPAIGNmessages.attachmentsarray<object>Files on the message, in one shape for every channel.createdAtstringnullableWhen it was written (ISO 8601).
{
"id": 90417,
"conversationId": 4388,
"senderType": "CONTACT",
"messageType": "TEXT",
"channelType": "WHATSAPP",
"content": "مرحبا، أريد تغيير موعد الفاتورة",
"contentJson": null,
"status": "RECEIVED",
"providerMessageId": "wamid.HBgLOTYyNzkxMjM0NTY3FQIAEhgg",
"privateNote": false,
"originType": null,
"originId": null,
"originKind": null,
"sourceId": null,
"errorMessage": null,
"noteKind": null,
"transferEventId": null,
"replyToMessageId": null,
"campaignId": null,
"attachments": [],
"createdAt": "2026-11-06T09:41:12Z"
}
List a conversation’s timeline
Returns messages and private notes, newest first, as a plain array. Keyset paginated: pass the smallest id of the page you have as afterId to get older entries. Notes and messages are merged by time, so a page can hold fewer messages than size. Notes are numbered separately from messages, so take that id from the oldest entry that is not a private note.
liveagent.conversations:read Keyset paginatedPath parameters
conversationIdintegerrequired
Query parameters
afterIdintegerReturn entries older than this ID. Pass theidof the oldest entry that is not a private note (privateNote: false), because note IDs come from a separate sequence.0or omitted returns the newest.sizeintegerEntries per page. Defaults to50, clamped to 1–100.
Returns
200The message object.
Errors
- 404
CONVERSATION_NOT_FOUNDNo conversation with this ID in your organisation.
curl -X GET "https://api.cx.na6iq.com/api/v1/conversations/42/timeline?size=25" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/conversations/42/timeline?size=25', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/conversations/42/timeline?size=25",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
[
{
"id": 90418,
"conversationId": 4388,
"senderType": "AGENT",
"messageType": "TEXT",
"channelType": "WHATSAPP",
"content": "أهلاً ليلى، يسعدني مساعدتك. أي موعد تفضّلين؟",
"contentJson": null,
"status": "SENT",
"providerMessageId": "wamid.HBgLOTYyNzkxMjM0NTY3FQIAEhggQ0E",
"privateNote": false,
"originType": "AGENT",
"originId": 214,
"originKind": null,
"sourceId": null,
"errorMessage": null,
"noteKind": null,
"transferEventId": null,
"replyToMessageId": null,
"campaignId": null,
"attachments": [],
"createdAt": "2026-11-06T09:42:03Z"
},
{
"id": 90417,
"conversationId": 4388,
"senderType": "CONTACT",
"messageType": "TEXT",
"channelType": "WHATSAPP",
"content": "مرحبا، أريد تغيير موعد الفاتورة",
"contentJson": null,
"status": "RECEIVED",
"providerMessageId": "wamid.HBgLOTYyNzkxMjM0NTY3FQIAEhgg",
"privateNote": false,
"originType": null,
"originId": null,
"originKind": null,
"sourceId": null,
"errorMessage": null,
"noteKind": null,
"transferEventId": null,
"replyToMessageId": null,
"campaignId": null,
"attachments": [],
"createdAt": "2026-11-06T09:41:12Z"
}
]
Send a message
Sends an agent reply. The reply is stored first and then handed to the channel, so the response status is usually PENDING (the message is sent after the response) and may later become SENT or FAILED. The response also carries assignedToOther: { agentId, displayName } when another agent holds the ticket, else null. Send an idempotencyKey (one is generated when blank, but then a retry cannot be de-duplicated) and reuse the same one when you retry — the backend de-duplicates per organisation, so a retry never sends twice. TEXT needs non-blank content; media types need contentJson.link or contentJson.id. To send an approved WhatsApp template, pass template with no content or contentJson.
liveagent.conversations:writePath parameters
conversationIdintegerrequired
Request body
idempotencyKeystringRecommended. A unique key for this send, e.g. a UUID. Reuse it on retry. Generated when blank.messageTypestringThe kind of message. Defaults toTEXT.One of:TEXTIMAGEAUDIOVIDEODOCUMENTSTICKERLOCATIONCONTACTSINTERACTIVEREACTIONTEMPLATEcontentstringThe text of aTEXTmessage.contentJsonobjectStructured content, as a real JSON value (not a string). For media:{ "type": "image", "link": "https://…", "caption": "…" }.replyToMessageIdintegernullableQuote an earlier message from the same conversation.templateobjectSend an approved WhatsApp template.
Returns
201The message object.
Errors
- 400
VALIDATION_FAILEDA required field is missing, or `replyToMessageId` is not in this conversation. - 404
CONVERSATION_NOT_FOUNDNo conversation with this ID in your organisation. - 404
INBOX_NOT_FOUNDThe conversation’s queue no longer exists. - 409
NOT_ASSIGNEEThe queue lets only the assignee reply, and you are not the assignee. `error.data` is `{ assignedAgentId, assignedAgentName, version }`. - 409
DUPLICATE_REPLYA colleague just sent the same text. `error.data` is `{ outboundMessageId, messageId, sentByUserId, sentByName, sentAt }`. - 409
TEMPLATE_REQUIREDThe WhatsApp 24-hour window is closed; send an approved template instead. `error.data` is `{ channelType, lastInboundAt, freeFormUntil }`. - 502
CHANNEL_SEND_FAILEDThe channel provider refused the send.
curl -X POST "https://api.cx.na6iq.com/api/v1/conversations/42/messages" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "2f6b8f0e-5c2a-4e53-9d5c-7a9f0b1a6c11",
"messageType": "TEXT",
"content": "أهلاً ليلى، يسعدني مساعدتك. أي موعد تفضّلين؟"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/conversations/42/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"idempotencyKey": "2f6b8f0e-5c2a-4e53-9d5c-7a9f0b1a6c11",
"messageType": "TEXT",
"content": "أهلاً ليلى، يسعدني مساعدتك. أي موعد تفضّلين؟"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/conversations/42/messages",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"idempotencyKey": "2f6b8f0e-5c2a-4e53-9d5c-7a9f0b1a6c11",
"messageType": "TEXT",
"content": "أهلاً ليلى، يسعدني مساعدتك. أي موعد تفضّلين؟"
},
)
res.raise_for_status()
data = res.json()
{
"id": 90418,
"conversationId": 4388,
"senderType": "AGENT",
"messageType": "TEXT",
"content": "أهلاً ليلى، يسعدني مساعدتك. أي موعد تفضّلين؟",
"contentJson": null,
"status": "PENDING",
"providerMessageId": null,
"createdAt": "2026-11-06T09:42:03Z",
"replyToMessageId": null,
"assignedToOther": null
}
Add a private note
Leaves an internal note for your team on the conversation. Notes are stored and shown in the timeline, and are never delivered to the customer.
liveagent.conversations:writePath parameters
conversationIdintegerrequired
Request body
contentstringrequiredThe note text.
Returns
201The message object.
Errors
- 400
VALIDATION_FAILED`content` is blank. - 404
CONVERSATION_NOT_FOUNDNo conversation with this ID in your organisation.
curl -X POST "https://api.cx.na6iq.com/api/v1/conversations/42/notes" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Customer is on the Gold plan — approve the date change."
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/conversations/42/notes', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"content": "Customer is on the Gold plan — approve the date change."
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/conversations/42/notes",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"content": "Customer is on the Gold plan — approve the date change."
},
)
res.raise_for_status()
data = res.json()
{
"id": 733,
"conversationId": 4388,
"ticketId": 5120,
"authorUserId": 214,
"content": "Customer is on the Gold plan — approve the date change.",
"originKind": null,
"sourceId": null,
"createdAt": "2026-11-06T09:43:40Z"
}
Mark a conversation as seen
Records the calling agent’s last-seen point, which clears the conversation’s unread count for that agent. No request body.
liveagent.conversations:readPath parameters
conversationIdintegerrequired
Returns
200The new read position. `lastSeenMessageId` is `null` when the conversation has no messages.
Errors
- 404
CONVERSATION_NOT_FOUNDNo conversation with this ID in your organisation. - 404
AGENT_NOT_FOUNDThe caller has no agent profile.
curl -X POST "https://api.cx.na6iq.com/api/v1/conversations/42/seen" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/conversations/42/seen', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/conversations/42/seen",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
{
"conversationId": 4388,
"lastSeenAt": "2026-11-06T09:44:10Z",
"lastSeenMessageId": 90418,
"unreadCount": 0
}
Set a conversation’s language
Overrides the language detected for the conversation. Send null to clear the override so detection takes over again. Routing rules read this language, so it is a routing correction, not only a label.
liveagent.conversations:writePath parameters
conversationIdintegerrequired
Request body
languagestringnullablear,enorother(case-insensitive).nullor blank clears the override.
Returns
200The message object.
Errors
- 400
INVALID_REQUEST`language` is not `ar`, `en` or `other`. - 404
CONVERSATION_NOT_FOUNDNo conversation with this ID in your organisation.
curl -X PUT "https://api.cx.na6iq.com/api/v1/conversations/42/language" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"language": "ar"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/conversations/42/language', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"language": "ar"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.put(
"https://api.cx.na6iq.com/api/v1/conversations/42/language",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"language": "ar"
},
)
res.raise_for_status()
data = res.json()
{
"conversationId": 4388,
"language": "ar",
"languageSource": "MANUAL"
}