Conversations, tickets, and messages
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Conversations, tickets, and messages
The thread model — how conversations differ from tickets, when each is the right tool, and how messages compose into both.
Three nested objects make up the core of Chatly's chat surface: conversation, ticket, and message. Knowing which one to reach for — and when to use both together — is one of the bigger productivity wins for support teams migrating from a less structured tool.
Info — Conversation vs ticket, in one sentence
A conversation is a live thread you have. A ticket is a piece of work you track. A conversation may produce zero, one, or many tickets — and a ticket may bundle several conversations on the same underlying issue.
Message
A message is a single utterance inside a conversation.
Message kinds (the message_kind enum — these five, exactly)
Name | Type | Description |
|---|---|---|
|
| Normal user-visible message. The default. |
|
| Structured payload — buttons, cards, provider templates. Rendering depends on what the channel adapter declares in its |
|
| Internal note in the conversation, seen only by agents (gated on |
|
| "Assigned to Mia", "Closed by automation". Auto-generated by the platform. |
|
| Attachments with no accompanying body text. |
Warning — There is no separate
attachment,card,templateorvoice_segmentkindAttachments ride on the
attachmentsarray of any message —attachment_onlyis the case where there is no body text with them. Rich and templated payloads are bothrich. Voice call markers aresystem_event. Sending an unrecognisedkindis a 400.
Every message has:
id,conversationId,createdAt— bare UUIDs, unprefixedauthorType—agent|contact|bot|systemauthorId— when applicablebody(string, max 10,000 chars),attachments(array, max 10)kind(above)metadata— channel-specific data (Twilio SID, WhatsApp message ID, etc.)deliveredAt,readAt,editedAt,replyToMessageId
Info — No AI-extracted
entitiesfieldMessages carry no
entitiescolumn. AI-derived facts about a person live on contact memories instead (GET /v1/contacts/{id}/memories), not on the message row.
Conversation
A conversation is the ongoing thread between a contact and your team on a particular channel.
Conversation fields
Name | Type | Description |
|---|---|---|
|
| A bare UUID. No |
|
| Who's talking. Required. |
|
| Which kind of channel it arrived on. Required. |
|
| The specific channel row, when one still exists. |
|
| Required, not optional — every workspace gets a default brand at creation, so every conversation has one. |
|
| Which agent owns it. |
|
| Which team owns it. |
|
|
|
|
|
|
|
| Thread subject, where the channel has one (email). |
|
| Free-form tags from the workspace taxonomy. |
|
| Denormalised for inbox sorting and the list row. |
|
| Per-side unread counters. |
|
| When a human agent first replied visibly. A bot reply and an internal note both leave it NULL — that's what makes it usable as the unattended predicate. |
|
| When it was closed. There is no separate |
|
| If |
Warning — No custom attributes on conversations
Conversations have no
attributes/ custom-fields column. Custom workspace-defined fields exist on contacts (attributes) and on tickets (customFields), not on the conversation row. Use tags, or hang the data off the contact.
An AI summary is available as autoSummary (with autoSummaryGeneratedAt), but it is not part of the conversation DTO returned by the list and detail endpoints.
Lifecycle
┌─────────┐
│ open │ ◄──────────┐ (the default on creation)
└──┬───┬──┘ │
│ │ snooze │ resurface (snoozeUntil passes,
│ ▼ │ or the customer replies)
│ ┌──────────┐ │
│ │ snoozed │ ────┘
│ └──────────┘
│
│ waiting on the customer
▼
┌─────────┐
│ pending │ ──── customer replies ──► open
└────┬────┘
│ close
▼
┌─────────┐
│ closed │ ──── reopen (customer replies) ──► open
└─────────┘
┌─────────┐
│ merged │ terminal — set by POST /v1/conversations/{winner}/merge/{loser}
└─────────┘ on the LOSING thread; mergedIntoId points at the survivorWarning — There is no
newstatus and noarchivedstatusA conversation is created directly as
open. And archiving is a label on a bulk action, not a state: the inbox's "Archive" button resolves tostatus = 'closed', becauseconversation_statushas noarchivedvalue. Nothing auto-archives on an idle timer either — the only status timer in the platform is the snooze waker.
Transitions are recorded in the audit log.
Ticket
A ticket is a wrapper around one or more conversations for long- lived work.
Tip — Use a ticket when the work outlives the chat
A refund request resolved within 30 minutes? Just a conversation. An engineering bug report that takes engineering 2 weeks to fix? That's a ticket — keeps the work visible to the team even when the customer has moved on with their day.
Ticket fields
Name | Type | Description |
|---|---|---|
|
| Human-friendly increment. |
|
| Short label + longer body. |
|
|
|
|
|
|
|
| Ticket type; defaults to |
|
| Who owns the work. |
|
| For sub-tickets. |
|
| Hard deadline for SLA reporting. |
|
| Which SLA policy governs; drives |
|
| Free-form tags. |
|
| Workspace-defined custom fields. This is where per-object custom data lives — conversations have no equivalent. |
Conversations are linked to a ticket through a join table, not through a field on the ticket — there is no linkedConversationIds array to write:
POST /v1/tickets/{id}/conversations/{conversationId} — Bearer token
GET /v1/tickets/{id}/conversations — Bearer token
DELETE /v1/tickets/{id}/conversations/{conversationId} — Bearer token
Warning — No
externalRefsfield on ticketsThere is no column for a Linear issue ID, Jira key, or GitHub issue URL on the ticket row, and no two-way sync driven from one. Track the outbound link in
customFieldsif you need it.
Tickets are visible only to agents, never to customers. Customers hear about a ticket only as text in a conversation reply ("We've filed ticket #1247 about this — we'll update you when it's resolved").
Conversation vs ticket — pick guide
Common scenarios
Name | Type | Description |
|---|---|---|
|
| Reply, close. No ticket. |
|
| File one ticket, link each conversation to it; mass-update when resolved. |
|
| Reply, file a feature-request ticket linked to your roadmap tool. Close conversation; ticket stays open in eng tracker. |
|
| Sales context. No ticket. |
|
| Reply with status; ticket tracks the approval workflow. |
|
| No ticket — use conversation custom attributes to track ongoing state. |
Routing on creation
When a conversation is created, routing rules fire. The actions available to them are a fixed set of primitives:
Assign to a specific agent, or to a team (which then applies the inbox's strategy —
round_robin,load_balanced,skills_based,sticky, ormanual)Transfer / unassign
Set status (
open|pending|snoozed|closed), close, or snoozeTag / untag
Set priority
Add an internal note ("VIP — see CRM for context")
Warning — There is no set-a-custom-attribute action
Conversations have no attributes column, so no routing or workflow action can write one. If you need per-thread state beyond tags and priority, put it on the contact's
attributesor on a linked ticket'scustomFields.
Each of these is permission-gated at the primitive (assignment needs conversations:assign, closing needs conversations:close, and so on), so a workflow cannot do on your behalf what you could not do yourself. Routing decisions write to the audit log.
Multi-channel threading
A contact can have multiple open conversations on different channels simultaneously — web chat + WhatsApp + email — all in the dashboard's unified inbox view. Agents see them side-by-side and can reply on whichever channel makes sense.
SLAs
Per workspace you can define SLA targets:
sla_policies — every target is in MINUTES, not seconds
Name | Type | Description |
|---|---|---|
|
| Policy label. Snapshotted onto each conversation's SLA row so a deleted policy still names itself in a breach report. |
|
| How quickly must a human agent first respond. Required. |
|
| Target for every subsequent agent reply. Without it, "answered once inside the window then went quiet for two days" satisfies the SLA. |
|
| How quickly must the conversation be closed. Required. |
|
| When true the clock only runs during the workspace's configured business hours. Defaults to |
|
| Per-priority overrides of |
Each conversation gets a conversation_slas row carrying the resolved firstResponseDueAt / resolutionDueAt and the …WarnedAt / …BreachedAt stamps the SLA watchdog writes.
CSAT + surveys
After a conversation is closed, you can fire a survey to ask CSAT. Survey responses attach to the conversation and roll up into the reports dashboard.
API
GET /v1/conversations?status=open&limit=50 — Bearer token
GET /v1/conversations/{id} — Bearer token
GET /v1/conversations/{id}/messages — Bearer token
POST /v1/conversations/{id}/messages — Bearer token
POST /v1/conversations/{id}/close — Bearer token
POST /v1/conversations/{id}/assign — Bearer token
POST /v1/conversations/{id}/escalate — Bearer token
POST /v1/conversations/{id}/internal-notes — Bearer token
POST /v1/conversations/bulk — Bearer token
GET /v1/conversations/unattended — Bearer token
Danger — You cannot create a conversation over the REST API
There is no
POST /v1/conversations. Conversations come into existence when a message arrives — through a channel adapter's inbound webhook, or throughPOST /v1/widget/conversationson the visitor surface. There is likewise noPOST /v1/conversations/{id}/snooze: snooze a thread throughPOST /v1/conversations/bulkwithaction: "snooze".
Filtering the list
Filters are flat query parameters, not bracketed ones — ?status=open, not ?filter[status]=open:
status, assignee, unread, mentioned, channel, tags, from, to, segment, priority, team, createdFrom, createdTo, unattended, unattendedFor, sla, brand, plus sort, cursor, limit.
Repeatable values are comma-separated (?tags=vip,refund). An unrecognised value narrows or is dropped rather than 400ing — these parameters arrive from saved views and shared URLs that outlive the vocabularies they name. The one exception is ?filter=, which carries the whole filter as a JSON group tree when it needs OR or nesting, and supersedes the flat parameters when present.
Sending a message
POST /v1/conversations/{id}/messages takes body (string, ≤10,000 chars), kind (defaults to text), attachments (≤10), and metadata. conversationId in the body is optional and ignored — the path wins.
Warning —
closeandassignare not API-key accessibleAn API key can read conversations and post messages, but
/closeand/assignrequire a signed-in user and reject ack_…key with a 403. See Authentication.
See REST API for the full surface and the Node SDK for typed clients.