message.comDevelopers

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

GET/api/v1/conversations/:id/messagesAuth: Bearer

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
curl 'https://app.message.com/api/v1/conversations/:id/messages?limit=50' \
  -H 'Authorization: Bearer YOUR_WORKSPACE_JWT'
JavaScript
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();
Python
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"]
Ruby
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
<?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

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

ValueMeaning
visitorThe customer sent it (chat) or it arrived as inbound email / voicemail transcript.
agentAn agent sent it. Visible to the visitor on chat / ticket; muted on call (used for in-call note pinning).
aiThe AI sent it directly (auto-reply), not a human-edited draft.
systemStatus 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

POST/api/v1/conversations/:id/messagesAuth: Bearer

Sends an agent message into the conversation. Returns the created message. For ticket replies that should go out as email, use the tickets email-transcript action.

FieldTypeDescription
bodyrequiredstringPlain text body, 1 to 10,000 characters.
attachmentsoptionalobject[]Up to 20 items: { url, name?, size? }. URLs come from the Uploads API.
Body
{
  "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

POST/api/v1/conversations/:id/notesAuth: Bearer

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
{ "body": "Internal note: customer is a Series A founder, escalate." }

There is no message edit or delete endpoint. Once sent, a message's body is permanent; there is no editedAt or soft-delete on the messages resource.

Errors

CodeWhen
400 invalid_bodyBody validation failed. Empty body is not allowed.
403 channel_not_assignedAgent's RBAC channels don't include this conversation's channel.
404 not_foundConversation does not exist, belongs to another workspace, or is outside the agent's department scope.
409 conversation_archivedConversation status is spam or solved. Reopen it first.

Common pitfalls

  • Passing bodyHtml on send. There is no such input field; only plain body is accepted. bodyHtml on 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. Plan your integration around append-only messages.