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
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
| Field | Type | Description |
|---|---|---|
| qrequired | string | Search text, matched fuzzily against message bodies, visitor name, and visitor email. |
| limitoptional | integer | Max items to return. Default and maximum 50. |
| statusoptional | enum | One of open, pending, solved, spam. |
| channeloptional | string | Filter by channel. |
| tagoptional | string | Filter to conversations carrying this tag. |
| fromoptional | ISO datetime | Only conversations updated on or after this time. |
| tooptional | ISO datetime | Only conversations updated on or before this time. |
Response shape
{
"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)
The one list endpoint that does not require a search term: recently closed conversations, for the dashboard's Archive tab.
Code samples
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'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();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"]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
$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.
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
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.
{
"status": "solved"
}Allowed transitions
| From | To |
|---|---|
open | pending, solved, spam |
pending | open, solved, spam |
solved | open |
spam | open |
closed | open, solved |
Assign to an agent
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.
| Field | Type | Description |
|---|---|---|
| agentIdrequired | uuid | Target agent. |
| noteoptional | string | Optional context note shown to the receiving agent. |
| forceoptional | boolean | Skip the handshake and assign immediately. |
| expectedVersionrequired | integer | Version returned by a prior read. Stale versions return 409. |
Assign to a department
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.
| Field | Type | Description |
|---|---|---|
| departmentIdrequired | uuid | Department (group) to route the conversation to. |
| expectedVersionoptional | integer | Optimistic-lock version, checked when present. |
Resolving a pending handshake
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
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
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 | On assign/transfer only: expectedVersion does not match the current version. Re-fetch and retry. |
409 illegal_status_transition | Status 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
expectedVersionon 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.