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.

List conversations

GET/api/v1/conversationsAuth: Bearer

Returns a cursor-paginated list of conversations in the authenticated workspace. Filters can be combined.

Query parameters

FieldTypeDescription
cursoroptionalstringPagination cursor returned by a prior response. Omit on the first call.
limitoptionalintegerMax items to return. Default 50, maximum 200.
statusoptionalenumOne of open, pending, solved, closed, spam.
channeloptionalenumOne of chat, ticket, call.
assigneeIdoptionaluuidFilter by assigned agent. Pass null for unassigned.
departmentIdoptionaluuidFilter by department (also known as group).
visitorIdoptionaluuidFilter to a specific visitor.

Response shape

200 OK
{
  "conversations": [
    {
      "id": "uuid",
      "version": 4,
      "channel": "chat",
      "status": "open",
      "subject": "Refund for order #4567",
      "visitorId": "uuid",
      "assignedAgentId": "uuid",
      "groupId": "uuid",
      "lastMessageAt": "2026-05-13T15:30:00Z",
      "createdAt": "2026-05-13T14:00:00Z",
      "tags": ["billing", "vip"],
      "unread": false
    }
  ],
  "nextCursor": "2026-05-13T14:00:00Z|uuid"
}

Code samples

cURL
curl -X GET 'https://app.message.com/api/v1/conversations?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?status=open&limit=50', {
  headers: {
    Authorization: 'Bearer ' + token,
    'Content-Type': 'application/json',
  },
});
const { conversations, nextCursor } = await res.json();
Python
import requests

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

uri = URI("https://app.message.com/api/v1/conversations")
uri.query = URI.encode_www_form(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?status=open&limit=50";
$body = file_get_contents($url, false, $ctx);
$data = json_decode($body, true);
$conversations = $data["conversations"];
$next_cursor = $data["nextCursor"];

Get one conversation

GET/api/v1/conversations/:idAuth: Bearer

Returns a single conversation with its full message timeline. See Messages for the message payload shape.

Change status

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

Move a conversation through its status state machine. Requires expectedVersion for optimistic locking. Illegal transitions return 409 illegal_status_transition.

Body
{
  "status": "solved",
  "expectedVersion": 4
}

Allowed transitions

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

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 it reverts to the previous assignee.

FieldTypeDescription
agentIdrequireduuidTarget agent.
expectedVersionrequiredintegerVersion returned by the prior GET. Stale versions return 409.

Transfer

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

Agent-to-agent transfer (with the same 30s handshake) or department transfer (instant). Pass either an agentId or a departmentId, not both.

FieldTypeDescription
agentIdoptionaluuidAgent-to-agent transfer target.
departmentIdoptionaluuidDepartment to route the conversation to.
noteoptionalstringOptional context note shown to the receiving agent.
expectedVersionrequiredintegerOptimistic-lock version.

Spam helpers

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

Convenience wrappers around the status PATCH. They take no body and skip the version check (idempotent within a 5-second window).

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_versionexpectedVersion does not match the current version. GET the conversation and retry.
409 illegal_status_transitionStatus change is not allowed from the current status.
429 rate_limitedPer-agent rate limit hit. See retryAfter field.

Common pitfalls

  • Always pass expectedVersion on PATCH / POST mutations. Stale-version errors are how we prevent two agents stomping each other.
  • Don't poll this endpoint for new conversations. Subscribe to the conversation:new event 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.