message.comDevelopers

Conversations Live

The unified-inbox model. Every customer contact, regardless of channel, is a conversation. Chat, tickets, and phone calls all share the same primitive, only the channel field changes.

If you are new to this model, read What is a unified inbox first. It explains why we collapse channels into one resource.

Find conversations

GET/api/v1/conversations/searchAuth: Bearer

There is no generic "list all conversations" endpoint. The live queue is a Socket.io concern (see the agent namespace); over REST, this is a required-query full-text search over message bodies and visitor name/email, with optional filters layered on top. q is required, so it is not a substitute for browsing the whole inbox. For recently closed conversations without a query, see Archived below.

Query parameters

FieldTypeDescription
qrequiredstringSearch text, matched fuzzily against message bodies, visitor name, and visitor email.
limitoptionalintegerMax items to return. Default and maximum 50.
statusoptionalenumOne of open, pending, solved, spam.
channeloptionalstringFilter by channel.
tagoptionalstringFilter to conversations carrying this tag.
fromoptionalISO datetimeOnly conversations updated on or after this time.
tooptionalISO datetimeOnly conversations updated on or before this time.

Response shape

200 OK
{
  "rows": [
    {
      "conversationId": "uuid",
      "channel": "chat",
      "status": "open",
      "subject": "Refund for order #4567",
      "ticketNumber": 4567,
      "updatedAt": "2026-05-13T15:30:00Z",
      "matchedBody": "...refund my last order...",
      "visitorName": "Alice",
      "visitorEmail": "[email protected]",
      "tags": ["billing", "vip"]
    }
  ],
  "total": 1
}

Archived (recently closed, no query required)

GET/api/v1/conversations/archivedAuth: Bearer

The one list endpoint that does not require a search term: recently closed conversations, for the dashboard's Archive tab.

Code samples

cURL
curl -X GET 'https://app.message.com/api/v1/conversations/search?q=refund&status=open&limit=50' \
  -H 'Authorization: Bearer YOUR_WORKSPACE_JWT' \
  -H 'Content-Type: application/json'
JavaScript
const res = await fetch('https://app.message.com/api/v1/conversations/search?q=refund&status=open&limit=50', {
  headers: {
    Authorization: 'Bearer ' + token,
    'Content-Type': 'application/json',
  },
});
const { rows } = await res.json();
Python
import requests

r = requests.get(
    "https://app.message.com/api/v1/conversations/search",
    headers={"Authorization": f"Bearer {token}"},
    params={"q": "refund", "status": "open", "limit": 50},
)
rows = r.json()["rows"]
Ruby
require "net/http"
require "json"

uri = URI("https://app.message.com/api/v1/conversations/search")
uri.query = URI.encode_www_form(q: "refund", status: "open", 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
data = JSON.parse(res.body)
PHP
<?php
$ctx = stream_context_create([
  "http" => [
    "method" => "GET",
    "header" => "Authorization: Bearer $token\r\nAccept: application/json",
  ],
]);
$url = "https://app.message.com/api/v1/conversations/search?q=refund&status=open&limit=50";
$body = file_get_contents($url, false, $ctx);
$data = json_decode($body, true);
$rows = $data["rows"];

Get one conversation

There is no single endpoint that returns the conversation record by itself.

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

This returns its message timeline (paginated, oldest-first), which is what the dashboard actually loads to render a conversation. See Messages for the payload shape.

Change status

PATCH/api/v1/conversations/:id/statusAuth: Bearer

Move a conversation through its status state machine. No version check on this endpoint (unlike assign/transfer below). Illegal transitions return 409 illegal_status_transition.

Body
{
  "status": "solved"
}

Allowed transitions

FromTo
openpending, solved, spam
pendingopen, solved, spam
solvedopen
spamopen
closedopen, solved

Assign to an agent

POST/api/v1/conversations/:id/assign-to-agentAuth: Bearer

Assigns the conversation. Triggers a 30-second pending handshake: the recipient agent has 30 seconds to accept or decline before it reverts to the previous assignee.

FieldTypeDescription
agentIdrequireduuidTarget agent.
noteoptionalstringOptional context note shown to the receiving agent.
forceoptionalbooleanSkip the handshake and assign immediately.
expectedVersionrequiredintegerVersion returned by a prior read. Stale versions return 409.

Assign to a department

POST/api/v1/conversations/:id/assign-to-departmentAuth: Bearer

Routes to a department (instant, no handshake). There is no unified "transfer" endpoint that takes either an agent or a department: use this for department routing and assign-to-agent above for agent-to-agent.

FieldTypeDescription
departmentIdrequireduuidDepartment (group) to route the conversation to.
expectedVersionoptionalintegerOptimistic-lock version, checked when present.

Resolving a pending handshake

POST/api/v1/conversations/:id/transfer/acceptAuth: Bearer
POST/api/v1/conversations/:id/transfer/declineAuth: Bearer
POST/api/v1/conversations/:id/transfer/cancelAuth: Bearer

The other side of an assign-to-agent handshake. accept and decline: the pending recipient, or an admin/supervisor. cancel: the agent who initiated the transfer, or an admin/supervisor. All three take an optional expectedVersion and an optional reason string.

Spam helpers

POST/api/v1/conversations/:id/spamAuth: Bearer
POST/api/v1/conversations/:id/unspamAuth: Bearer

Convenience wrappers around the status change: no body, no version check. Naturally idempotent since they just set status to spam or open.

Send a message

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

Sends an agent message into the conversation. See Messages for the payload shape, delivery statuses, and attachment handling.

Errors

CodeWhen
400 invalid_bodyRequest body failed validation.
401 unauthorizedMissing or invalid bearer token.
403 forbiddenThe authenticated agent lacks access to this conversation (channel or department scope).
404 not_foundConversation does not exist or belongs to a different workspace.
409 stale_versionOn assign/transfer only: expectedVersion does not match the current version. Re-fetch and retry.
409 illegal_status_transitionStatus change is not allowed from the current status.

Common pitfalls

  • Assuming a general list-all endpoint exists. It doesn't. Search requires a query string; only Archive and Socket.io give you an unqueried feed.
  • Pass expectedVersion on assign-to-agent. It's required there (optional elsewhere it appears). Stale-version errors are how we prevent two agents stomping each other on a handoff.
  • Don't poll for new conversations. Subscribe to call/chat events on the agent Socket.io namespace instead.
  • Cross-workspace requests return 404. We do not leak resource existence with 403. If you see 404 on a known-good ID, double-check the auth token belongs to the same workspace.