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
Returns a cursor-paginated list of conversations in the authenticated workspace. Filters can be combined.
Query parameters
| Field | Type | Description |
|---|---|---|
| cursoroptional | string | Pagination cursor returned by a prior response. Omit on the first call. |
| limitoptional | integer | Max items to return. Default 50, maximum 200. |
| statusoptional | enum | One of open, pending, solved, closed, spam. |
| channeloptional | enum | One of chat, ticket, call. |
| assigneeIdoptional | uuid | Filter by assigned agent. Pass null for unassigned. |
| departmentIdoptional | uuid | Filter by department (also known as group). |
| visitorIdoptional | uuid | Filter to a specific visitor. |
Response shape
{
"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 -X GET 'https://app.message.com/api/v1/conversations?status=open&limit=50' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT' \
-H 'Content-Type: application/json'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();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"]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
$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
Returns a single conversation with its full message timeline. See Messages for the message payload shape.
Change status
Move a conversation through its status state machine. Requires expectedVersion for optimistic locking. Illegal transitions return 409 illegal_status_transition.
{
"status": "solved",
"expectedVersion": 4
}Allowed transitions
| From | To |
|---|---|
open | pending, solved, spam |
pending | open, solved, spam |
solved | open |
spam | open |
Assign to an agent
Assigns the conversation. Triggers a 30-second pending handshake: the recipient agent has 30 seconds to accept or it reverts to the previous assignee.
| Field | Type | Description |
|---|---|---|
| agentIdrequired | uuid | Target agent. |
| expectedVersionrequired | integer | Version returned by the prior GET. Stale versions return 409. |
Transfer
Agent-to-agent transfer (with the same 30s handshake) or department transfer (instant). Pass either an agentId or a departmentId, not both.
| Field | Type | Description |
|---|---|---|
| agentIdoptional | uuid | Agent-to-agent transfer target. |
| departmentIdoptional | uuid | Department to route the conversation to. |
| noteoptional | string | Optional context note shown to the receiving agent. |
| expectedVersionrequired | integer | Optimistic-lock version. |
Spam helpers
Convenience wrappers around the status PATCH. They take no body and skip the version check (idempotent within a 5-second window).
Send a message
Sends an agent message into the conversation. See Messages for the payload shape, delivery statuses, and attachment handling.
Errors
| Code | When |
|---|---|
400 invalid_body | Request body failed validation. |
401 unauthorized | Missing or invalid bearer token. |
403 forbidden | The authenticated agent lacks access to this conversation (channel or department scope). |
404 not_found | Conversation does not exist or belongs to a different workspace. |
409 stale_version | expectedVersion does not match the current version. GET the conversation and retry. |
409 illegal_status_transition | Status change is not allowed from the current status. |
429 rate_limited | Per-agent rate limit hit. See retryAfter field. |
Common pitfalls
- Always pass
expectedVersionon 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:newevent 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.