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

Cursor-paginated list of messages ordered newest first. Use cursor + limit per REST API conventions.

Code samples

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

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

KindMeaning
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).
systemStatus change, assignment, transfer, AI engagement note. Visible to agents, sometimes to visitors.
internal_notePrivate agent commentary. Never sent to the visitor. Other agents in scope see it.

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 tickets /reply.

FieldTypeDescription
bodyrequiredstringPlain text body. Always required; HTML is optional.
bodyHtmloptionalstringSanitized HTML. We strip scripts and on-* attributes server-side.
attachmentsoptionalobject[]URLs from the Uploads API plus filename and size.
kindoptionalenumDefaults to agent. Pass internal_note for private commentary.
Body
{
  "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

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

Convenience wrapper around /messages with kind: internal_note hard-coded. See Notes API for the full surface (note editing, deletion).

Body
{ "body": "Internal note: customer is a Series A founder, escalate." }

Edit a message

PATCH/api/v1/messages/:idAuth: Bearer

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

DELETE/api/v1/messages/:idAuth: Bearer

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

CodeWhen
400 invalid_bodyBody validation failed. Empty body is not allowed.
403 forbiddenAgent lacks scope on the conversation.
404 not_foundConversation does not exist or belongs to another workspace.
410 conversation_closedCannot send into a closed conversation. Reopen first.
422 edit_window_expiredEdit 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.