Messages Live
Messages live inside conversations. The same payload shape covers chat, ticket replies, internal notes, and system events; only the kind field distinguishes them.
For most uses the dashboard sends messages over the Socket.io agent namespace. The REST endpoint below exists for backend integrations, automation, and replay.
List messages in a conversation
Cursor-paginated list of messages ordered newest first. Use cursor + limit per REST API conventions.
Code samples
curl 'https://app.message.com/api/v1/conversations/abc/messages?limit=50' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT'const res = await fetch('https://app.message.com/api/v1/conversations/abc/messages?limit=50', {
headers: { Authorization: 'Bearer ' + token }
});
const { messages, nextCursor } = await res.json();import requests
r = requests.get(
"https://app.message.com/api/v1/conversations/abc/messages",
headers={"Authorization": f"Bearer {token}"},
params={"limit": 50},
)
messages = r.json()["messages"]require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/conversations/abc/messages?limit=50")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
http.request(req)
end
messages = JSON.parse(res.body)["messages"]<?php
$ctx = stream_context_create([
"http" => ["method" => "GET", "header" => "Authorization: Bearer $token"]
]);
$body = file_get_contents("https://app.message.com/api/v1/conversations/abc/messages?limit=50", false, $ctx);
$messages = json_decode($body, true)["messages"];Message shape
{
"id": "uuid",
"conversationId": "uuid",
"kind": "agent",
"agentId": "uuid",
"visitorId": null,
"body": "Hi, thanks for reaching out. We can issue a refund.",
"bodyHtml": "<p>Hi, thanks for reaching out. We can issue a refund.</p>",
"attachments": [
{ "url": "https://cdn.message.com/abc.pdf", "filename": "refund-policy.pdf", "size": 53120 }
],
"deliveryStatus": "delivered",
"createdAt": "2026-05-13T15:30:00Z",
"seenAt": "2026-05-13T15:30:12Z"
}Kinds
| Kind | Meaning |
|---|---|
visitor | The customer sent it (chat) or it arrived as inbound email / voicemail transcript. |
agent | An agent sent it. Visible to the visitor on chat / ticket; muted on call (used for in-call note pinning). |
system | Status change, assignment, transfer, AI engagement note. Visible to agents, sometimes to visitors. |
internal_note | Private agent commentary. Never sent to the visitor. Other agents in scope see it. |
Send a message
Sends an agent message into the conversation. Returns the created message. For ticket replies that should go out as email, use tickets /reply.
| Field | Type | Description |
|---|---|---|
| bodyrequired | string | Plain text body. Always required; HTML is optional. |
| bodyHtmloptional | string | Sanitized HTML. We strip scripts and on-* attributes server-side. |
| attachmentsoptional | object[] | URLs from the Uploads API plus filename and size. |
| kindoptional | enum | Defaults to agent. Pass internal_note for private commentary. |
{
"body": "Plain text body",
"bodyHtml": "<p>Plain text body</p>",
"attachments": [
{ "url": "https://cdn.message.com/abc.pdf", "filename": "abc.pdf", "size": 53120 }
]
}Always include an Idempotency-Key header on send. A retried POST without one will create a duplicate message in the visitor's inbox.
Post an internal note
Convenience wrapper around /messages with kind: internal_note hard-coded. See Notes API for the full surface (note editing, deletion).
{ "body": "Internal note: customer is a Series A founder, escalate." }Edit a message
Edits the body of an agent message within a 5-minute window. Visitor messages and notes are not editable. Edited messages carry an editedAt timestamp.
Delete a message
Soft-deletes the message. The row is preserved with deletedAt but the body returns as the literal string [message removed] to non-admins. Admins see the original body for audit.
Errors
| Code | When |
|---|---|
400 invalid_body | Body validation failed. Empty body is not allowed. |
403 forbidden | Agent lacks scope on the conversation. |
404 not_found | Conversation does not exist or belongs to another workspace. |
410 conversation_closed | Cannot send into a closed conversation. Reopen first. |
422 edit_window_expired | Edit attempted after the 5-minute window. |
Common pitfalls
- Sending HTML without sanitization. We sanitize server-side but you should still escape user-controlled fragments before constructing
bodyHtml. - Polling for new messages. Use the Socket.io agent namespace. Polling the list endpoint will burn through rate limits without delivering sub-second freshness.
- Editing visitor messages. Not allowed by design. If you need to redact, soft-delete instead.