message.comDevelopers

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

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

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.

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

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. Treat sending as an append operation and handle retention separately.