## Search conversations **post** `/v2/conversations/search` Search conversations with a structured filter AST and an optional full-text query. The endpoint has two modes that are picked automatically from the body: - **Filter mode** (`query` set, `search` omitted): structured filtering against indexed conversation fields. Sorted by `lastActivityAt` desc by default; set `sort` to `lastActivityAt:asc` for the inverse. - **Search mode** (`search` set, `query` optional): full-text search across conversation messages, then narrowed by the same `query` AST so the same filters apply. Results are ranked by relevance by default; set `sort` to order by `lastActivityAt` instead (the same query string still gates which conversations appear, only their ordering changes). ### Structured query AST Each clause has the shape `{ field, operator, value }`. You can pass a single clause or wrap up to 15 in a top-level `AND` group: ```json { "query": { "operator": "AND", "value": [ { "field": "state", "operator": "=", "value": "open" }, { "field": "adminAssigneeId", "operator": "=", "value": "507f1f77bcf86cd799439011" } ] } } ``` Top-level `OR` groups and nested groups are not supported in this version. ### Supported fields and operators Field names are camelCase, matching the rest of the v2 public API. Snake_case names that this endpoint originally shipped with (`admin_assignee_id`, `team_assignee_id`, `brand_id`, `tag_ids`, `mentioned_admin_ids`, `user_id`, `participant_id`, `company_id`, `created_at`) are still accepted for back-compat but are deprecated — please migrate to camelCase in new code. | Field | Type | Operators | | -------------------------- | --------------------------------------------------------------------------- | ---------------------- | | `state` | enum (`open` / `closed` / `snoozed`) | `=`, `!=`, `IN`, `NIN` | | `priority` | boolean | `=`, `!=` | | `adminAssigneeId` | id or null (unassigned) | `=`, `!=`, `IN`, `NIN` | | `teamAssigneeId` | id or null (unassigned) | `=`, `!=`, `IN`, `NIN` | | `brandId` | id or null | `=`, `!=`, `IN`, `NIN` | | `tagIds` | id (matches any conversation containing this tag) | `=`, `!=`, `IN`, `NIN` | | `userId` / `participantId` | id of a conversation participant | `=`, `!=`, `IN`, `NIN` | | `companyId` | company id (matches conversations whose participants belong to the company) | `=`, `!=`, `IN`, `NIN` | | `mentionedAdminIds` | admin id | `=`, `!=`, `IN`, `NIN` | | `createdAt` | unix seconds | `=`, `>`, `<` | For unbounded fields a query consisting only of `!=` or `NIN` clauses is rejected with `query_too_broad` to prevent full-org scans. Combine the negation with at least one positive clause (`=`, `IN`, `>`, `<`) instead. Bounded enum fields (`state`, `priority`) are exempt because their negation is naturally bounded. ### Pagination Cursor-based, `limit` between 1 and 100 (default 10). Pagination cursors are mode-specific - reusing a filter-mode cursor in search mode (or vice versa) returns `400`. `totalCount` is approximate and capped at 1000; `totalCountCapped` is `true` when the real count may be higher. ### Response Returns a slim conversation row optimised for inbox / list rendering - just the fields you need to render a row and link to the conversation. Fetch the full conversation via `GET /v2/conversations/{id}` when the user opens one. Search-specific additions on each row: - `conversationUser` - the customer / lead the conversation is with. Use this for the row identity. - `surroundingConversationParts` - context window of human messages around the matched message (≈15 before + 15 from the match onwards in search mode, sliding to first / last 30 if the match is near the conversation boundaries; falls back to the latest 30 messages in filter mode). Each `previewMarkdown` value is truncated server-side; fetch the full conversation for complete history and full bodies. - `matchingPartAuthor` - author of the specific message that matched the search query (search mode only). May be a teammate or bot. - `matchingPart` - reference to the matched message (search mode only). - `matchingBodyPreviewMarkdown` - plain-text / markdown preview of the matched body (search mode only). - `relevanceScore` - aggregate score (search mode only). ### Version Availability This endpoint is only available in API version 2026-01-01.nova and newer. ### Header Parameters - `"Featurebase-Version": optional "2026-01-01.nova" or "2025-12-12.clover"` - `"2026-01-01.nova"` - `"2025-12-12.clover"` ### Body Parameters - `cursor: optional string` An opaque cursor for pagination. Use the nextCursor value from a previous response to fetch the next page of results. - `limit: optional number` A limit on the number of objects to be returned, between 1 and 100. - `query: optional object { field, operator, value } or object { operator, value }` Structured filter AST. Either a single filter clause or one top-level AND group. Use this for filtering by `state`, `tagIds`, `adminAssigneeId`, etc. (Legacy snake_case names are accepted for back-compat — see the OpenAPI route description for the full alias map.) - `SearchFilter object { field, operator, value }` - `field: string` Field name to filter on (e.g. `state`, `tag_ids`, `created_at`) - `operator: "=" or "!=" or "IN" or 9 more` Comparison operator - `"="` - `"!="` - `"IN"` - `"NIN"` - `">"` - `"<"` - `">="` - `"<="` - `"~"` - `"!~"` - `"^"` - `"$"` - `value: string or number or boolean or array of string or number` Value to compare against (primitive or array of primitives) - `string` - `number` - `boolean` - `array of string or number` - `string` - `number` - `SearchFilterGroup object { operator, value }` - `operator: "AND" or "OR"` Group operator: AND (all match) or OR (any match) - `"AND"` - `"OR"` - `value: array of object { field, operator, value }` Array of filter clauses (1-15 entries) - `field: string` Field name to filter on (e.g. `state`, `tag_ids`, `created_at`) - `operator: "=" or "!=" or "IN" or 9 more` Comparison operator - `"="` - `"!="` - `"IN"` - `"NIN"` - `">"` - `"<"` - `">="` - `"<="` - `"~"` - `"!~"` - `"^"` - `"$"` - `value: string or number or boolean or array of string or number` Value to compare against (primitive or array of primitives) - `string` - `number` - `boolean` - `array of string or number` - `string` - `number` - `search: optional string` Plain-text full-text search across conversation messages. Server-managed - no operators, no field selection. - `sort: optional "lastActivityAt:desc" or "lastActivityAt:asc" or "last_activity_at:desc" or "last_activity_at:asc"` Result ordering. When omitted, filter mode defaults to `lastActivityAt:desc` and search mode defaults to relevance ranking. When set, both modes order by lastActivityAt in the requested direction (so you can ask a search query to return its hits in chronological order). Snake_case `last_activity_at:*` is accepted for back-compat — prefer camelCase in new code. - `"lastActivityAt:desc"` - `"lastActivityAt:asc"` - `"last_activity_at:desc"` - `"last_activity_at:asc"` ### Returns - `data: array of object { id, adminAssigneeId, brandId, 19 more }` Array of search results - `id: string` Unique conversation identifier - `adminAssigneeId: string` ID of the assigned admin - `brandId: string` ID of the brand associated with this conversation - `createdAt: string` ISO timestamp when conversation was created - `lastActivityAt: string` ISO timestamp of last activity - `object: "conversation"` Object type identifier - `"conversation"` - `participants: array of ConversationParticipant` Participants in this conversation - `id: string` Participant ID - `type: "customer" or "lead" or "admin" or 3 more` Type of participant - `"customer"` - `"lead"` - `"admin"` - `"bot"` - `"guest"` - `"integration"` - `priority: boolean` Whether this conversation is marked as priority - `prioritySetAt: string` ISO timestamp when priority was set - `state: "open" or "closed" or "snoozed"` Current state of the conversation - `"open"` - `"closed"` - `"snoozed"` - `tags: array of ConversationTag` Current tags applied anywhere in this conversation - `id: string` Unique tag identifier - `name: string` Current tag name - `type: "tag"` Object type identifier for a tag - `"tag"` - `teamAssigneeId: string` ID of the assigned team - `updatedAt: string` ISO timestamp when conversation was last updated - `awaitingCustomerReply: optional boolean` Whether we are awaiting a customer reply - `conversationUser: optional object { id, type, companies, 3 more }` The customer or lead the conversation is with. Use this for the row identity; use `matchingPartAuthor` if you specifically want the message author. - `id: string` Author id - `type: "admin" or "customer" or "lead" or 3 more` Author type - `"admin"` - `"customer"` - `"lead"` - `"bot"` - `"guest"` - `"integration"` - `companies: optional array of object { id, name }` Companies the author belongs to (customers only) - `id: string` - `name: optional string` - `email: optional string` Email when known - `name: optional string` Display name when known - `profilePicture: optional string` Profile picture URL when known - `matchingBodyPreviewMarkdown: optional string` Plain-text / markdown preview of the matching conversation part body (search mode only) - `matchingPart: optional object { id, createdAt, partType }` Reference to the conversation part that best matched the search query (search mode only) - `id: string` Conversation part id - `createdAt: optional string` ISO timestamp when the matching part was created - `partType: optional string` Conversation part type - `matchingPartAuthor: optional object { id, type, companies, 3 more }` The customer or lead the conversation is with. Use this for the row identity; use `matchingPartAuthor` if you specifically want the message author. - `id: string` Author id - `type: "admin" or "customer" or "lead" or 3 more` Author type - `"admin"` - `"customer"` - `"lead"` - `"bot"` - `"guest"` - `"integration"` - `companies: optional array of object { id, name }` Companies the author belongs to (customers only) - `id: string` - `name: optional string` - `email: optional string` Email when known - `name: optional string` Display name when known - `profilePicture: optional string` Profile picture URL when known - `relevanceScore: optional number` Aggregate relevance score for the conversation (search mode only). Higher is more relevant. - `source: optional object { bodyHtml, bodyMarkdown, channel, 4 more }` - `bodyHtml: string` Body of the initial message as HTML with signed image URLs - `bodyMarkdown: string` Body of the initial message as markdown - `channel: "unknown" or "desktop" or "android" or 2 more` Channel through which the conversation was initiated - `"unknown"` - `"desktop"` - `"android"` - `"ios"` - `"email"` - `author: optional ConversationParticipant` - `id: string` Participant ID - `type: "customer" or "lead" or "admin" or 3 more` Type of participant - `deliveredAs: optional "customer_initiated" or "admin_initiated"` How the conversation was initiated - `"customer_initiated"` - `"admin_initiated"` - `subject: optional string` Subject line for email conversations - `url: optional string` URL where the conversation was initiated - `surroundingConversationParts: optional array of object { id, previewMarkdown, previewMarkdownTruncated, 3 more }` Window of conversation messages around the matching part for lightweight context previews. In search mode this is up to 15 messages before plus 15 from the matching message onwards (sliding to first 30 / last 30 if the match is near the conversation boundaries). In filter mode (no matching message) this falls back to the latest 30 messages. Always capped and truncated by the server; fetch the full conversation for complete message history. - `id: string` Conversation part id - `previewMarkdown: string` Plain-text truncated preview of the part body - `previewMarkdownTruncated: boolean` True when previewMarkdown was truncated by the server - `author: optional object { id, type, companies, 3 more }` The customer or lead the conversation is with. Use this for the row identity; use `matchingPartAuthor` if you specifically want the message author. - `id: string` Author id - `type: "admin" or "customer" or "lead" or 3 more` Author type - `"admin"` - `"customer"` - `"lead"` - `"bot"` - `"guest"` - `"integration"` - `companies: optional array of object { id, name }` Companies the author belongs to (customers only) - `id: string` - `name: optional string` - `email: optional string` Email when known - `name: optional string` Display name when known - `profilePicture: optional string` Profile picture URL when known - `createdAt: optional string` ISO timestamp when the part was created - `partType: optional string` Conversation part type - `title: optional string` Conversation title - `nextCursor: string` Cursor for fetching the next page (null if no more results) - `object: "list"` Object type identifier - `"list"` - `totalCount: optional number` Total number of conversations matching the query, capped at 200. When the actual total is at or above the cap, `totalCountCapped` is true and the value is exactly the cap. - `totalCountCapped: optional boolean` True when totalCount is exactly the cap and the real count is `>= totalCount`. UI may render as "200+". ### Example ```http curl https://do.featurebase.app/v2/conversations/search \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $FEATUREBASE_API_KEY" \ -d '{ "cursor": "eyJpZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMSJ9", "limit": 10, "search": "refund stripe checkout", "sort": "lastActivityAt:desc" }' ``` #### Response ```json { "data": [ { "id": "12345", "adminAssigneeId": "507f1f77bcf86cd799439011", "brandId": "507f1f77bcf86cd799439011", "createdAt": "2025-01-15T10:30:00.000Z", "lastActivityAt": "2025-01-15T12:30:00.000Z", "object": "conversation", "participants": [ { "id": "676f0f6765bdaa7d7d760f88", "type": "customer" } ], "priority": false, "prioritySetAt": "2025-01-15T10:30:00.000Z", "state": "open", "tags": [ { "id": "67ec1234abcd5678ef901234", "name": "Churn", "type": "tag" } ], "teamAssigneeId": "507f1f77bcf86cd799439012", "updatedAt": "2025-01-15T12:30:00.000Z", "awaitingCustomerReply": true, "conversationUser": { "id": "676f0f6765bdaa7d7d760f88", "type": "customer", "companies": [ { "id": "id", "name": "name" } ], "email": "jane@example.com", "name": "Jane Doe", "profilePicture": "https://cdn.example.com/avatar.png" }, "matchingBodyPreviewMarkdown": "I would like to request a refund for my last invoice...", "matchingPart": { "id": "676f0f6765bdaa7d7d760f88", "createdAt": "2025-01-15T10:30:00.000Z", "partType": "user_msg" }, "matchingPartAuthor": { "id": "676f0f6765bdaa7d7d760f88", "type": "customer", "companies": [ { "id": "id", "name": "name" } ], "email": "jane@example.com", "name": "Jane Doe", "profilePicture": "https://cdn.example.com/avatar.png" }, "relevanceScore": 0.873, "source": { "bodyHtml": "
Hi, I have a question about your enterprise plan...
", "bodyMarkdown": "Hi, I have a question about your enterprise plan...", "channel": "desktop", "author": { "id": "676f0f6765bdaa7d7d760f88", "type": "customer" }, "deliveredAs": "customer_initiated", "subject": "Question about pricing", "url": "https://example.com/pricing" }, "surroundingConversationParts": [ { "id": "676f0f6765bdaa7d7d760f88", "previewMarkdown": "Just issued a refund for your other purchase.", "previewMarkdownTruncated": false, "author": { "id": "676f0f6765bdaa7d7d760f88", "type": "customer", "companies": [ { "id": "id", "name": "name" } ], "email": "jane@example.com", "name": "Jane Doe", "profilePicture": "https://cdn.example.com/avatar.png" }, "createdAt": "2025-01-15T10:30:00.000Z", "partType": "user_msg" } ], "title": "Question about pricing" } ], "nextCursor": "eyJpZCI6IjEyMzQ1In0=", "object": "list", "totalCount": 42, "totalCountCapped": false } ```