Search posts
Search feedback posts with a structured filter AST, full-text search, sorting, and cursor pagination.
Two modes (combinable)
- Filter mode — pass a structured
queryAST (and/orsort) and the endpoint returns posts matching the filter, ordered bysort(defaultcreatedAt:desc). Same shape as/v2/conversations/search,/v2/companies/search,/v2/contacts/search. - Search mode — pass a top-level
searchstring and the endpoint runs a hybrid full-text + vector search (BM25 + embeddings, semanticRatio 0.5) over posttitleandcontent, returning matches ranked by relevance. Optional: combine withqueryto constrain the search to a board / status / time window, and/orsortto override relevance ranking with chronological order.
{
"search": "mobile dark mode",
"query": {
"operator": "AND",
"value": [
{ "field": "boardId", "operator": "IN", "value": ["507f1f77bcf86cd799439011"] }
]
},
"limit": 20
}
search is server-managed: no operators, no field selection, no special syntax — same plain-text contract as /v2/conversations/search. The string must be 1–500 characters; up to ~1024 BM25-tokenizer characters are forwarded to Turbopuffer (longer queries are truncated at a word boundary).
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:
{
"query": {
"operator": "AND",
"value": [
{ "field": "boardId", "operator": "IN", "value": ["507f1f77bcf86cd799439011"] },
{ "field": "upvotes", "operator": ">", "value": 10 }
]
},
"sort": "upvotes:desc",
"limit": 20
}
To list every post a contact has upvoted (the inverse of feedback.posts.voters.list(postId)):
{
"query": { "field": "voterId", "operator": "=", "value": "507f1f77bcf86cd799439011" },
"sort": "upvotes:desc",
"limit": 100
}
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 (board_id, status_id, tag_id, author_id, voter_id, assignee_id, company_id, created_at, updated_at, comment_count, monthly_spend, opportunity_amount, in_review, is_pinned) are still accepted for back-compat but are deprecated — please migrate to camelCase in new code.
| Field | Type | Operators |
|---|---|---|
boardId | id (categoryId) | =, !=, IN, NIN |
statusId | id (postStatus) | =, !=, IN, NIN |
tagId | id (postTags) | =, !=, IN, NIN |
authorId | string (user id) | =, !=, IN, NIN |
voterId | id (customer) | =, IN (use to list every post a contact has upvoted; only matches type: customer upvoters — guests/admins are not searchable here) |
assigneeId | id or null (admin) | =, !=, IN, NIN (use null for unassigned) |
companyId | string (external company id) | =, !=, IN, NIN |
createdAt, updatedAt, eta | unix seconds | =, !=, >, <, >=, <= |
upvotes, commentCount, monthlySpend, opportunityAmount | number | =, !=, >, <, >=, <= |
inReview, isPinned | boolean | =, != |
Use the top-level search field for full-text search across title and content (see “Search mode” above). The clause-level operators ~, !~, ^, $ are reserved for future use and currently return 400.
A filter-mode query (no search) consisting only of != or NIN clauses on unbounded fields is rejected with query_too_broad to prevent full-org scans. Combine the negation with at least one positive clause (=, IN, >, <) instead. Boolean fields (inReview, isPinned) are bounded so a standalone negation on them is allowed. Search mode (with search set) relaxes this — the FTS query itself acts as the positive constraint.
Sort
Allowed values:
createdAt:desc(default),createdAt:ascupdatedAt:desc,updatedAt:ascupvotes:desc,upvotes:asceta:desc,eta:ascmonthlySpend:desc,monthlySpend:asc(sum of upvoters’ monthly spend)opportunityAmount:desc,opportunityAmount:asc(linked HubSpot/Salesforce value)commentCount:desc,commentCount:asc
Snake_case sort axes (created_at:desc, monthly_spend:asc, etc.) are accepted for back-compat — the cursor encodes the canonical camelCase identity, so a caller can switch naming conventions mid-pagination without restarting.
When search is set, results are ranked by relevance unless sort is also set, in which case the matching posts are returned in the requested order. The chosen sort axis (or relevance) is encoded in the cursor; switching sort (or toggling search on/off) mid-pagination returns 400 invalid_cursor. Restart pagination without a cursor when changing the sort or search shape.
Pagination
Cursor-based, limit between 1 and 100 (default 10). In filter mode, totalCount is approximate and capped at 5000; totalCountCapped is true when the real count may be higher. In search mode, totalCount reflects the matching survivor set after the AST filter and is capped at 200 (the Turbopuffer top-K).
Server-side guards
These exclusions are always applied and CANNOT be disabled via the AST:
- Spam posts (
isSpam: true) are excluded. - Merged posts (those rolled into another post) are excluded — the canonical row is what’s returned.
- Support-board (ticket) categories are excluded — tickets have their own
/v2/ticketssurface.
Response
Returns a standard list envelope with post rows identical to GET /v2/posts/{id}.
Version Availability
This endpoint is only available in API version 2026-01-01.nova and newer.
Search posts
import Featurebase from 'featurebase-node';
const client = new Featurebase({
apiKey: process.env['FEATUREBASE_API_KEY'], // This is the default and can be omitted
});
const response = await client.feedback.posts.search();
console.log(response.data);{
"data": [
{
"id": "507f1f77bcf86cd799439011",
"access": {
"companyExternalIds": [
"string"
],
"userIds": [
"string"
]
},
"assigneeId": "507f1f77bcf86cd799439013",
"author": {
"id": "507f1f77bcf86cd799439011",
"email": "john@example.com",
"name": "John Doe",
"profilePicture": "https://cdn.example.com/avatars/john.png",
"type": "customer"
},
"boardId": "507f1f77bcf86cd799439011",
"commentCount": 5,
"content": "<p>It would be great to have a dark mode option for the dashboard.</p>",
"createdAt": "2023-12-12T00:00:00.000Z",
"customFields": {
"cf_priority": "bar",
"cf_effort": "bar"
},
"eta": "2025-01-01T00:00:00.000Z",
"features": {
"commentsEnabled": true
},
"inReview": false,
"integrations": {
"clickup": [
{
"id": "86a1b2c3d",
"title": "Add dark mode support",
"url": "https://app.clickup.com/t/86a1b2c3d"
}
],
"devops": [
{
"id": 1234,
"projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"projectName": "My Project",
"title": "Add dark mode support",
"url": "https://dev.azure.com/org/project/_workitems/edit/1234"
}
],
"github": [
{
"id": "1234567890",
"number": "42",
"repositoryFullName": "acme/backend",
"repositoryName": "backend",
"title": "Add dark mode support",
"url": "https://github.com/acme/backend/issues/42"
}
],
"hubspot": [
{
"dealAmount": 5000,
"dealClosed": false,
"objectId": 123456789,
"type": "TICKET"
}
],
"jira": [
{
"issueId": "10042",
"issueUrl": "https://myteam.atlassian.net/browse/PROJ-123"
}
],
"linear": [
{
"issueId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"issueUrl": "https://linear.app/team/issue/ENG-123"
}
],
"salesforce": [
{
"amount": 25000,
"isClosed": false,
"objectId": "006Dn00000Abcdef",
"objectType": "Opportunity"
}
]
},
"isPinned": false,
"object": "post",
"opportunityAmount": 30000,
"postUrl": "https://feedback.example.com/p/add-dark-mode-support",
"slug": "add-dark-mode-support",
"status": {
"id": "507f1f77bcf86cd799439011",
"color": "Blue",
"isDefault": false,
"name": "In Progress",
"object": "post_status",
"type": "active"
},
"tags": [
{
"id": "507f1f77bcf86cd799439011",
"color": "#FF5722",
"name": "bug"
}
],
"title": "Add dark mode support",
"updatedAt": "2023-12-13T00:00:00.000Z",
"upvotes": 42
}
],
"nextCursor": "eyJpZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMSJ9",
"object": "list",
"totalCount": 42,
"totalCountCapped": false
}Returns Examples
{
"data": [
{
"id": "507f1f77bcf86cd799439011",
"access": {
"companyExternalIds": [
"string"
],
"userIds": [
"string"
]
},
"assigneeId": "507f1f77bcf86cd799439013",
"author": {
"id": "507f1f77bcf86cd799439011",
"email": "john@example.com",
"name": "John Doe",
"profilePicture": "https://cdn.example.com/avatars/john.png",
"type": "customer"
},
"boardId": "507f1f77bcf86cd799439011",
"commentCount": 5,
"content": "<p>It would be great to have a dark mode option for the dashboard.</p>",
"createdAt": "2023-12-12T00:00:00.000Z",
"customFields": {
"cf_priority": "bar",
"cf_effort": "bar"
},
"eta": "2025-01-01T00:00:00.000Z",
"features": {
"commentsEnabled": true
},
"inReview": false,
"integrations": {
"clickup": [
{
"id": "86a1b2c3d",
"title": "Add dark mode support",
"url": "https://app.clickup.com/t/86a1b2c3d"
}
],
"devops": [
{
"id": 1234,
"projectId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"projectName": "My Project",
"title": "Add dark mode support",
"url": "https://dev.azure.com/org/project/_workitems/edit/1234"
}
],
"github": [
{
"id": "1234567890",
"number": "42",
"repositoryFullName": "acme/backend",
"repositoryName": "backend",
"title": "Add dark mode support",
"url": "https://github.com/acme/backend/issues/42"
}
],
"hubspot": [
{
"dealAmount": 5000,
"dealClosed": false,
"objectId": 123456789,
"type": "TICKET"
}
],
"jira": [
{
"issueId": "10042",
"issueUrl": "https://myteam.atlassian.net/browse/PROJ-123"
}
],
"linear": [
{
"issueId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"issueUrl": "https://linear.app/team/issue/ENG-123"
}
],
"salesforce": [
{
"amount": 25000,
"isClosed": false,
"objectId": "006Dn00000Abcdef",
"objectType": "Opportunity"
}
]
},
"isPinned": false,
"object": "post",
"opportunityAmount": 30000,
"postUrl": "https://feedback.example.com/p/add-dark-mode-support",
"slug": "add-dark-mode-support",
"status": {
"id": "507f1f77bcf86cd799439011",
"color": "Blue",
"isDefault": false,
"name": "In Progress",
"object": "post_status",
"type": "active"
},
"tags": [
{
"id": "507f1f77bcf86cd799439011",
"color": "#FF5722",
"name": "bug"
}
],
"title": "Add dark mode support",
"updatedAt": "2023-12-13T00:00:00.000Z",
"upvotes": 42
}
],
"nextCursor": "eyJpZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMSJ9",
"object": "list",
"totalCount": 42,
"totalCountCapped": false
}