List, search, detail, message timeline, find-or-create, and bulk edit.
List conversations
Paginated chat list backing the mobile inbox screen. Ordered by last_message_at DESC NULLS LAST, created_at DESC.
Filter combinations mirror the chatmo-dashboard /api/conversations endpoint, so partners who already integrated against the dashboard see identical query semantics here. channel_type matches every inbox of a brand (e.g. all WhatsApp Official channels in the org), while inbox_id picks a single inbox row.
Caps: page β€ 500, limit β€ 100. The composite index (organization_id, assignment_status, last_message_at DESC) keeps deep pages fast even at the cap.
query Parameters
tabMaps to conversations.assignment_status.
inbox_idFilter by a specific channel instance (c.channel_id).
channel_typeFilter by channel brand. Matches every inbox of that brand β pair with inbox_id when you want one specific inbox.
Wire enum identifying the channel brand. Pass to ?channel_type= on GET /conversations.
agent_idFilter by assigned_agent_id.
ai_agent_idFilter by ai_agent_id. Use with agent_id to pivot between AI-handled and agent-handled conversations.
campaign_idFilter by broadcast_id (campaigns persist as broadcasts).
is_unreadSQL mirror of the Redis unread counter β useful for show-only-unread without paging through everything.
qSubstring on contact name OR phone (case-insensitive ILIKE).
statusComma-separated. conversations.status IN (...). Allowed: ACTIVE,OPEN,RESOLVED,PENDING,ARCHIVED.
label_idsComma-separated UUIDs β conversation matches if it has ANY of the labels (OR via junction).
date_fromYYYY-MM-DD or RFC 3339. Inclusive lower bound on created_at.
date_toYYYY-MM-DD or RFC 3339. Inclusive upper bound on created_at. Bare dates are auto-bumped to end-of-day.
page1-indexed page number. Capped at 500 to bound deep-scan cost.
limitList conversations βΊ Responses
Page of conversations.
successcodeFind or create a conversation
Find-or-create the conversation row keyed on (channel_id, country_code, phone). Idempotent β re-posting the same triple returns the existing row, so the New Chat composer can call this as a precondition for the conversation.subscribe + message.send WebSocket flow without worrying about duplicate rows.
The response carries the full enriched conversation shape so the caller can prepend it to local state without a follow-up GET /conversations/{id}.
201 is returned for newly-created rows; 200 for an existing row. Branch on is_new_conversation in the body if you need the distinction.
Find or create a conversation βΊ Request Body
channel_idMust be a WHATSAPP_OFFICIAL or WHATSAPP_UNOFFICIAL channel in your organization.
country_codeE.164 dial code with leading +.
phoneLocal digits only β country code lives in the field above.
nameContact display name. Used when find-or-create creates a new contact; ignored when the contact already exists.
Find or create a conversation βΊ Responses
Existing conversation returned (find-or-create matched).
successcodeSearch conversations by contact + message content
Unified contact + message search powering the chat-list search bar. The response is a paginated list of conversations whose contact name or phone OR most-recent matching message content matches q. Each row carries match_type plus the matched-message preview so the client can render a WhatsApp-style snippet with text highlighting in one round-trip.
Unlike /conversations, this endpoint adds a LATERAL join onto messages and depends on the idx_messages_content_trgm GIN index β kept separate so the regular chat-list hot path does not pay for it. The index is shared with the dashboard's /api/messages/search, so both surfaces hit one index.
Results are ordered COALESCE(matched_message.sent_at, conversation.last_message_at) DESC NULLS LAST, conversation.created_at DESC β freshly-matched message threads bubble to the top even when the parent conversation has not seen unrelated activity since.
query Parameters
qSearch query, 2-200 chars. Matched against contacts.name, contacts.phone, and messages.content via case-insensitive ILIKE.
tabWhen omitted, search spans every conversation β matches the dashboard's "search overrides tab" behavior.
inbox_idchannel_typeWire enum identifying the channel brand. Pass to ?channel_type= on GET /conversations.
agent_idai_agent_idcampaign_idis_unreadstatuslabel_idsdate_fromYYYY-MM-DD or RFC 3339. Inclusive lower bound on created_at.
date_toYYYY-MM-DD or RFC 3339. Inclusive upper bound on created_at. Bare dates are auto-bumped to end-of-day.
page1-indexed page number. Capped at 500 to bound deep-scan cost.
limitSearch conversations by contact + message content βΊ Responses
Matched conversations with optional matched-message preview per row.
successApply multiple actions to multiple conversations
Multi-conversation editor β one request applies any subset of four orthogonal actions across up to 50 conversation_ids:
add_collaboratorsβ additive; existing actives reported asskipped.assignβ replace; provide exactly one ofassigned_agent_idorassigned_team_id.add_labelsβ additive; existing labels per conversation areskipped.update_statusβ replace; per-row transition rules fromPATCH /conversations/{id}/statusstill apply.
Results live under their own action slot so the Bulk Edit modal can render a per-section summary (Handled By 5/5, Labels 4/5 (1 skipped)). At least one action MUST be set; an empty actions bag returns 400.
Internally fans the matching single-action usecases out per row, so every per-conversation WebSocket conversation_history event (JOIN, ASSIGNED, STATUS_CHANGE) fires exactly as if the agent had clicked each action individually.
This endpoint accepts the dashboard JWT in addition to the regular OpenAPI JWT / x-api-key (HTTPAuthEither middleware) β the same route serves chatmo-dashboard and chatmo-app.
Apply multiple actions to multiple conversations βΊ Request Body
conversation_idsTargets. Capped at 50 β fan-out runs the matching single-action usecase per row.
Apply multiple actions to multiple conversations βΊ Responses
Bulk update applied β each action slot reports per-row outcomes.
successcodemessageGet a single conversation
Returns one conversation with the same row shape as the list endpoint, wrapped under data.conversation. Use for the chat-detail screen header (contact, channel, agent) when you deep-linked into the screen without going through the list.
A 404 conversation not found is returned when the conversation does not exist OR belongs to a different organization β the two cases are intentionally indistinguishable to prevent ID enumeration.
path Parameters
conversation_idUUID of the conversation. Cross-tenant access returns 404 conversation not found.
Get a single conversation βΊ Responses
The conversation row.
successList messages in a conversation
Messages ordered DESC by sent_at (newest first). The mobile UI renders these as chat bubbles and paginates older messages as the user scrolls up.
Private messages (AI summaries and other internal-only types) are filtered out server-side β the wire shape never contains them. The response envelope's conversation field carries {id, contact_name, status} so deep-link navigation (notification tap) can render the chat header without a separate GET /conversations/{id} call.
reply_to carries media_url, thumbnail_url, and file_name for the parent message β WhatsApp-style inline thumbnails and DOCUMENT filenames render without a second lookup.
path Parameters
conversation_idUUID of the conversation. Cross-tenant access returns 404 conversation not found.
query Parameters
page1-indexed page number. Capped at 500 to bound deep-scan cost.
limitList messages in a conversation βΊ Responses
Page of messages.
success