Messages Live
Messages live inside conversations. Messages include senderType, body, attachments and delivery metadata. Internal notes are a separate resource, not a message kind.
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
Paginated, but not with the cursor convention used elsewhere on this site: the param here is before (an ISO timestamp, exclusive), and the response returns messages oldest-first within the page so the dashboard can render top-to-bottom without re-sorting. nextCursor in the response is the timestamp to pass as before on the next call.
Code samples
curl 'https://app.message.com/api/v1/conversations/:id/messages?limit=50' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT'const res = await fetch(`https://app.message.com/api/v1/conversations/${conversationId}/messages?limit=50`, {
headers: { Authorization: 'Bearer ' + token }
});
const { messages, nextCursor } = await res.json();import requests
r = requests.get(
f"https://app.message.com/api/v1/conversations/{conversation_id}/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/#{conversation_id}/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"]
]);
$url = "https://app.message.com/api/v1/conversations/{$conversationId}/messages?limit=50";
$body = file_get_contents($url, false, $ctx);
$messages = json_decode($body, true)["messages"];Message shape
{
"id": "uuid",
"conversationId": "uuid",
"senderType": "agent",
"senderId": "uuid",
"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", "name": "refund-policy.pdf", "size": 53120 }
],
"deliveryStatus": null,
"readAt": null,
"createdAt": "2026-05-13T15:30:00Z"
}senderType values
| Value | 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). |
ai | The AI sent it directly (auto-reply), not a human-edited draft. |
system | Status change, assignment, transfer, AI engagement note. Visible to agents, sometimes to visitors. |
There is no internal_note message kind. Internal notes are a separate resource entirely, not a message with a special senderType. See below.
Send a message
Requires an authenticated agent session, with access to the conversation’s channel and department. Returns HTTP 201 with { message: { ... } }. On a ticket conversation, this action sends the reply as email and changes its status to pending. The email-transcript action sends a separate history copy; it is not the reply endpoint.
| Field | Type | Description |
|---|---|---|
| bodyrequired | string | Plain text body, 1 to 10,000 characters. |
| attachmentsoptional | object[] | Up to 20 items: { url, name?, size? }. URLs come from the Uploads API. |
{
"body": "Plain text body",
"attachments": [
{ "url": "https://cdn.message.com/abc.pdf", "name": "abc.pdf", "size": 53120 }
]
}There is no client-facing Idempotency-Key support (see REST API conventions). A retried POST after a timeout can create a duplicate message.
Internal notes are a different resource
This is not a message send with a hidden kind flag. Notes live in their own table. See Notes API for the full surface, including the PATCH endpoint's upsert-your-own-latest-note behavior and the note-level DELETE.
{ "body": "Internal note: customer is a Series A founder, escalate." }There is no message edit or delete endpoint. This API does not edit sent message bodies. Separate conversation retention and administrator privacy-erasure actions can remove stored history; this is not a promise of permanent retention.
Errors
| Code | When |
|---|---|
400 invalid_body | Body validation failed. Empty body is not allowed. |
403 channel_not_assigned | Agent's RBAC channels don't include this conversation's channel. |
404 not_found | Conversation does not exist, belongs to another workspace, or is outside the agent's department scope. |
409 conversation_archived | Conversation status is spam or solved. Reopen it first. |
Common pitfalls
- Passing
bodyHtmlon send. There is no such input field; only plainbodyis accepted.bodyHtmlon a returned message is server-rendered for display, not something you set. - Polling for new messages. Use the Socket.io agent namespace. Polling the list endpoint will burn through rate limits without delivering sub-second freshness.
- Expecting to edit or delete a sent message. Neither exists. Treat sending as an append operation and handle retention separately.