package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.formbricks.com/api/v3/feedbackRecords/search/semantic"
payload := strings.NewReader("{\n \"query\": \"complaints about waiting times at the gates\",\n \"tenant_id\": \"ph7zv3w2u1x5k9d8m4q6c0bn\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
HttpResponse<String> response = Unirest.post("https://app.formbricks.com/api/v3/feedbackRecords/search/semantic")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"query\": \"complaints about waiting times at the gates\",\n \"tenant_id\": \"ph7zv3w2u1x5k9d8m4q6c0bn\"\n}")
.asString();
{
"data": [
{
"feedback_record_id": "018e1234-5678-7abc-8ef0-123456789abd",
"score": 0.71,
"field_label": "What could we improve?",
"value_text": "Queues at the north gate were far too long."
},
{
"feedback_record_id": "018e1234-5678-7abc-8ef0-1234567890aa",
"score": 0.64,
"field_label": "Anything else?",
"value_text": "Took 40 minutes to get in. Please open more turnstiles."
}
],
"limit": 10
}
V3 Feedback Records
Search feedback records semantically
Embeds query and returns the ids of the records in the dataset whose text is closest to it,
with cosine similarity scores. Only records with a non-empty value_text are embedded, so only
those can match. The gateway reads tenant_id from the JSON body to authorize the call.
Requires an API key with read permission (or higher) on a workspace the dataset is
assigned to.
Results carry ids and the embedded text only — fetch a match with
GET /api/v3/feedbackRecords/{id} for the full record. Answers 503 on instances without
an embedding model configured.
POST
/
api
/
v3
/
feedbackRecords
/
search
/
semantic
Search feedback records semantically
curl --request POST \
--url https://app.formbricks.com/api/v3/feedbackRecords/search/semantic \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"query": "complaints about waiting times at the gates",
"tenant_id": "ph7zv3w2u1x5k9d8m4q6c0bn"
}
'
import requests
url = "https://app.formbricks.com/api/v3/feedbackRecords/search/semantic"
payload = {
"query": "complaints about waiting times at the gates",
"tenant_id": "ph7zv3w2u1x5k9d8m4q6c0bn"
}
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)
const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
query: 'complaints about waiting times at the gates',
tenant_id: 'ph7zv3w2u1x5k9d8m4q6c0bn'
})
};
fetch('https://app.formbricks.com/api/v3/feedbackRecords/search/semantic', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.formbricks.com/api/v3/feedbackRecords/search/semantic"
payload := strings.NewReader("{\n \"query\": \"complaints about waiting times at the gates\",\n \"tenant_id\": \"ph7zv3w2u1x5k9d8m4q6c0bn\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
HttpResponse<String> response = Unirest.post("https://app.formbricks.com/api/v3/feedbackRecords/search/semantic")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"query\": \"complaints about waiting times at the gates\",\n \"tenant_id\": \"ph7zv3w2u1x5k9d8m4q6c0bn\"\n}")
.asString();
{
"data": [
{
"feedback_record_id": "018e1234-5678-7abc-8ef0-123456789abd",
"score": 0.71,
"field_label": "What could we improve?",
"value_text": "Queues at the north gate were far too long."
},
{
"feedback_record_id": "018e1234-5678-7abc-8ef0-1234567890aa",
"score": 0.64,
"field_label": "Anything else?",
"value_text": "Took 40 minutes to get in. Please open more turnstiles."
}
],
"limit": 10
}
Page size, at most 100. Unlike the limit on GET /api/v3/feedbackRecords, this one is never rejected: the search endpoints clamp instead of answering 400. A value above 100 is reduced to 100, and 0, a negative number or a non-numeric value falls back to the default of 10. Read the limit in the response to see what was actually applied.
Only matches with score >= min_score are returned. Like limit, this value is clamped rather than rejected: above 1 it becomes 1, below 0 it becomes 0, and a non-numeric value falls back to the default of 0.7. That default is conservative — on the embedding model we measured, query→record similarities for genuinely relevant text often landed around 0.55–0.7 — so if a good query returns nothing, lower it (e.g. 0.5) and filter on the returned value_text. Score distributions differ per model, so treat those numbers as an observation rather than a guarantee.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Documentation - Formbricks","colors":{"primary":"#00C4B8","light":"#00C4B8","dark":"#00C4B8"},"logo":{"light":"https://mintcdn.com/formbricks/wqEbm3FT1QK_6-vL/images/logo-light.svg?fit=max\u0026auto=format\u0026n=wqEbm3FT1QK_6-vL\u0026q=85\u0026s=365e5fb767b72b4fbbb2016860fd92ba","dark":"https://mintcdn.com/formbricks/wqEbm3FT1QK_6-vL/images/logo-dark.svg?fit=max\u0026auto=format\u0026n=wqEbm3FT1QK_6-vL\u0026q=85\u0026s=763690d005584ee5f4b4014615c3b98c"},"favicon":"https://mintcdn.com/formbricks/wqEbm3FT1QK_6-vL/images/favicon.svg?fit=max\u0026auto=format\u0026n=wqEbm3FT1QK_6-vL\u0026q=85\u0026s=01910ab43e3a3acc0aca5471235b90c5","navbar":{"links":[{"href":"https://github.com/formbricks/formbricks/discussions","label":"Support"}],"primary":{"type":"button","label":"Go to app","href":"https://app.formbricks.com"}},"footer":{"socials":{"github":"https://github.com/formbricks/formbricks","linkedin":"https://linkedin.com/company/formbricks","x":"https://x.com/formbricks"}},"integrations":{"posthog":{"apiKey":"phc_zJO1n3rjHlhqKiJ0gqLDwT4qvfWF1wzHDq2gfl0moQm","apiHost":"https://eu.i.posthog.com"}},"errors":{"404":{"redirect":true}},"contextual":{"options":["copy","view","chatgpt","claude","perplexity"],"display":"header"},"styling":{"latex":false},"navigation":{"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"formbricks","hasAudioNarration":false,"actualSubdomain":"formbricks","customDomains":["formbricks.com"],"gitSource":{"type":"github","owner":"formbricks","repo":"formbricks","deployBranch":"release/6.1","contentDirectory":"docs","isPrivate":false,"mountPath":"","label":""},"chroma":{"liveDeploymentHistoryId":"6ad8c05da69b8ab3962ea10d","collectionId":"cbc1ed51-3c6a-4b1f-8507-84eb7d51eb9b","schemaVersion":"v3","latestBuiltDeploymentHistoryId":"6ad8c05da69b8ab3962ea10d"},"feedback":{"thumbs":true,"issues":true,"edits":true,"agentFeedback":false},"entitlements":{"AI_CHAT":{"status":"DISABLED"}},"buildId":"6ad8c05da69b8ab3962ea10d","clientVersion":"0.0.3971","searchFilterCounts":[{"language":"default","tag":"Surveys (Ask)","count":87},{"language":"default","tag":"Survey Features","count":71},{"language":"default","tag":"Self Hosting","count":53},{"language":"default","tag":"API v2 Reference (Beta)","count":41},{"language":"default","tag":"API v1 Reference","count":35},{"language":"default","tag":"Platform","count":28},{"language":"default","tag":"Configuration","count":28},{"language":"default","tag":"Platform Features","count":23},{"language":"default","tag":"Integrations","count":22},{"language":"default","tag":"Development","count":21}]},"assistantConfig":{},"basePath":"/docs","hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[],"isAtlas":false,"googleFontsInlined":false},"customCss":[],"page":{"pagePath":"api-v3-reference/v3-feedback-records/search-feedback-records-semantically","slug":"/api-v3-reference/v3-feedback-records/search-feedback-records-semantically","pageMetadata":{"title":"Search feedback records semantically","description":"Embeds `query` and returns the ids of the records in the dataset whose text is closest to it,\nwith cosine similarity scores. Only records with a non-empty `value_text` are embedded, so only\nthose can match. The gateway reads `tenant_id` from the JSON body to authorize the call.\nRequires an API key with **read** permission (or higher) on a workspace the dataset is\nassigned to.\n\nResults carry ids and the embedded text only — fetch a match with\n`GET /api/v3/feedbackRecords/{id}` for the full record. Answers **503** on instances without\nan embedding model configured.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/feedbackRecords/search/semantic","href":"/api-v3-reference/v3-feedback-records/search-feedback-records-semantically","autogeneratedByOpenApi":true,"hidden":true},"description":{"compiledSource":"\"use strict\";\nconst {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n code: \"code\",\n p: \"p\",\n strong: \"strong\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsxs(_Fragment, {\n children: [_jsxs(_components.p, {\n children: [\"Embeds \", _jsx(_components.code, {\n children: \"query\"\n }), \" and returns the ids of the records in the dataset whose text is closest to it,\\nwith cosine similarity scores. Only records with a non-empty \", _jsx(_components.code, {\n children: \"value_text\"\n }), \" are embedded, so only\\nthose can match. The gateway reads \", _jsx(_components.code, {\n children: \"tenant_id\"\n }), \" from the JSON body to authorize the call.\\nRequires an API key with \", _jsx(_components.strong, {\n children: \"read\"\n }), \" permission (or higher) on a workspace the dataset is\\nassigned to.\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"Results carry ids and the embedded text only — fetch a match with\\n\", _jsx(_components.code, {\n children: \"GET /api/v3/feedbackRecords/{id}\"\n }), \" for the full record. Answers \", _jsx(_components.strong, {\n children: \"503\"\n }), \" on instances without\\nan embedding model configured.\"]\n })]\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){return _jsx(_Fragment,{})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Search feedback records semantically","description":"Embeds `query` and returns the ids of the records in the dataset whose text is closest to it,\nwith cosine similarity scores. Only records with a non-empty `value_text` are embedded, so only\nthose can match. The gateway reads `tenant_id` from the JSON body to authorize the call.\nRequires an API key with **read** permission (or higher) on a workspace the dataset is\nassigned to.\n\nResults carry ids and the embedded text only — fetch a match with\n`GET /api/v3/feedbackRecords/{id}` for the full record. Answers **503** on instances without\nan embedding model configured.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/feedbackRecords/search/semantic","href":"/api-v3-reference/v3-feedback-records/search-feedback-records-semantically","autogeneratedByOpenApi":true,"hidden":true}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[],"codeExamples":{}},"jsLinks":["/docs/_mintlify/custom/formbricks/fd84136b00b6b9ed/formbricks.js"],"openApiReferenceData":{"operation":{"operationId":"searchFeedbackRecordsV3","summary":"Search feedback records semantically","description":"Embeds `query` and returns the ids of the records in the dataset whose text is closest to it,\nwith cosine similarity scores. Only records with a non-empty `value_text` are embedded, so only\nthose can match. The gateway reads `tenant_id` from the JSON body to authorize the call.\nRequires an API key with **read** permission (or higher) on a workspace the dataset is\nassigned to.\n\nResults carry ids and the embedded text only — fetch a match with\n`GET /api/v3/feedbackRecords/{id}` for the full record. Answers **503** on instances without\nan embedding model configured.\n","tags":["V3 Feedback Records"],"parameters":["d920773f-c7d3-4dab-8691-de87efc7d94e","e7a2fdb6-6b3d-490b-b984-17dc286b1051","1b8727ba-a549-4e0b-995e-d9b74f9afc3e"],"requestBody":"2f339f69-b0b9-4338-ad22-4e1ec61326ed","responses":{"200":"c8c3ff0d-d7d9-465a-8ea0-9e6ca99f9f2a","400":"cf33b4ca-a78e-408d-9c43-525cf5b1e529","401":"af222372-c795-458d-a935-c8d7901a287c","403":"a734894c-0075-4876-9f7b-a0309eef11ee","413":"3873ea3d-6f69-4185-b603-c6250f075aad","500":"9936a3d5-ab1d-4695-a44a-adb72f486abc","503":"5c5b6bbf-3e91-46f0-9606-f56d5906728c"},"security":["a203df75-4118-42ac-bf45-74c3f740f595"],"method":"post","type":"path","servers":["5a1d53ae-163d-4303-ac56-7a378a4d43bc"],"path":"/api/v3/feedbackRecords/search/semantic","baseUrlOptions":["https://app.formbricks.com"],"title":"Search feedback records semantically","codeSamples":[],"requiredOnlyExamples":false,"requestExampleType":"generated","responseExampleType":"generated"},"dependencies":{"parameters":{"d920773f-c7d3-4dab-8691-de87efc7d94e":{"in":"query","name":"limit","schema":{"type":"integer","default":10,"description":"Page size, at most 100. Unlike the `limit` on `GET /api/v3/feedbackRecords`, this one is never rejected: the search endpoints **clamp** instead of answering 400. A value above 100 is reduced to 100, and `0`, a negative number or a non-numeric value falls back to the default of 10. Read the `limit` in the response to see what was actually applied.","uniqueKey":"","typeLabel":"integer"},"description":"Page size, at most 100. Unlike the `limit` on `GET /api/v3/feedbackRecords`, this one is never rejected: the search endpoints **clamp** instead of answering 400. A value above 100 is reduced to 100, and `0`, a negative number or a non-numeric value falls back to the default of 10. Read the `limit` in the response to see what was actually applied."},"e7a2fdb6-6b3d-490b-b984-17dc286b1051":{"in":"query","name":"cursor","schema":{"type":"string","description":"Opaque keyset cursor returned as `next_cursor` by the previous page. Omit on the first request. A malformed cursor answers **400**.","uniqueKey":"","typeLabel":"string"},"description":"Opaque keyset cursor returned as `next_cursor` by the previous page. Omit on the first request. A malformed cursor answers **400**."},"1b8727ba-a549-4e0b-995e-d9b74f9afc3e":{"in":"query","name":"min_score","schema":{"type":"number","format":"float","default":0.7,"description":"Only matches with `score >= min_score` are returned. Like `limit`, this value is **clamped rather than rejected**: above `1` it becomes `1`, below `0` it becomes `0`, and a non-numeric value falls back to the default of `0.7`. That default is conservative — on the embedding model we measured, query→record similarities for genuinely relevant text often landed around `0.55–0.7` — so if a good query returns nothing, lower it (e.g. `0.5`) and filter on the returned `value_text`. Score distributions differ per model, so treat those numbers as an observation rather than a guarantee.","uniqueKey":"","typeLabel":"number\u003cfloat>"},"description":"Only matches with `score >= min_score` are returned. Like `limit`, this value is **clamped rather than rejected**: above `1` it becomes `1`, below `0` it becomes `0`, and a non-numeric value falls back to the default of `0.7`. That default is conservative — on the embedding model we measured, query→record similarities for genuinely relevant text often landed around `0.55–0.7` — so if a good query returns nothing, lower it (e.g. `0.5`) and filter on the returned `value_text`. Score distributions differ per model, so treat those numbers as an observation rather than a guarantee."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["query","tenant_id"],"properties":{"query":{"type":"string","minLength":1,"description":"Natural-language search text. It is embedded and compared to record embeddings by cosine similarity.","isRequired":true,"uniqueKey":"query","typeLabel":"string"},"tenant_id":{"type":"string","format":"cuid2","description":"The feedback dataset to search (the dataset id). Results never cross datasets.","isRequired":true,"uniqueKey":"tenant_id","typeLabel":"string\u003ccuid2>"}},"uniqueKey":"","typeLabel":"object"},"example":{"query":"complaints about waiting times at the gates","tenant_id":"ph7zv3w2u1x5k9d8m4q6c0bn"},"examples":{"Example":{"title":"Example","value":{"query":"complaints about waiting times at the gates","tenant_id":"ph7zv3w2u1x5k9d8m4q6c0bn"}}},"exampleType":"examples"}},"exampleType":"examples"},"responses":{"c8c3ff0d-d7d9-465a-8ea0-9e6ca99f9f2a":{"description":"Matches retrieved","content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["data","limit"],"properties":{"data":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["feedback_record_id","score","field_label","value_text"],"properties":{"feedback_record_id":{"type":"string","format":"uuid","description":"The matching record. Fetch it with `GET /api/v3/feedbackRecords/{id}` for the full payload.","isRequired":true,"uniqueKey":"data.items.[INDEX].feedback_record_id","typeLabel":"string\u003cuuid>"},"score":{"type":"number","format":"double","minimum":0,"maximum":1,"description":"Cosine similarity, `0` unrelated to `1` identical.","isRequired":true,"uniqueKey":"data.items.[INDEX].score","typeLabel":"number\u003cdouble>"},"field_label":{"type":"string","description":"The record's question text (part of what was embedded). Empty when the record has no label.","isRequired":true,"uniqueKey":"data.items.[INDEX].field_label","typeLabel":"string"},"value_text":{"type":"string","description":"The record's text answer (the text that was embedded). Empty when it has since been cleared — only records with a non-empty `value_text` are embedded in the first place.","isRequired":true,"uniqueKey":"data.items.[INDEX].value_text","typeLabel":"string"}},"uniqueKey":"data.items.[INDEX]","typeLabel":"object"},"description":"Matches ordered by descending `score`.","isRequired":true,"uniqueKey":"data","typeLabel":"object[]"},"limit":{"type":"integer","description":"The page size that was applied.","isRequired":true,"uniqueKey":"limit","typeLabel":"integer"},"next_cursor":{"type":"string","description":"Opaque keyset cursor for the next page. Present only when a full page was returned and there may be more results. Pass it back unchanged as `cursor`.","uniqueKey":"next_cursor","typeLabel":"string"}},"uniqueKey":"","typeLabel":"object"},"examples":{"matches":{"title":"matches","value":{"data":[{"feedback_record_id":"018e1234-5678-7abc-8ef0-123456789abd","score":0.71,"field_label":"What could we improve?","value_text":"Queues at the north gate were far too long."},{"feedback_record_id":"018e1234-5678-7abc-8ef0-1234567890aa","score":0.64,"field_label":"Anything else?","value_text":"Took 40 minutes to get in. Please open more turnstiles."}],"limit":10}}},"exampleType":"examples"}},"exampleType":"examples"},"cf33b4ca-a78e-408d-9c43-525cf5b1e529":{"description":"Bad Request. From the **gateway** (`text/plain`): a missing or malformed `tenant_id`; a request body that is not a single JSON object; a body whose top-level keys can be read more than one way (a repeated key, a key with non-ASCII characters, or a key that differs from `tenant_id` only in letter case); a `tenant_id` query parameter given more than once; or a path under `/api/v3/feedbackRecords` that is not one of the documented operations. A record id that is not a UUID and a path such as `/count` both fall in that last case and share the one `Unsupported FeedbackRecords route` body, so the two cannot be told apart from the response. From the **feedback store** (`application/problem+json`): schema validation failures — an unknown enum value, an inverted range, a malformed or re-ordered cursor, or a string with NULL bytes or invalid UTF-8.","content":{"text/plain":{"schema":{"type":"string","uniqueKey":"","typeLabel":"string"},"examples":{"missingTenant":{"title":"missingTenant","value":"Invalid or missing tenant_id"},"invalidBody":{"title":"invalidBody","value":"Invalid request body"},"ambiguousTenant":{"title":"ambiguousTenant","value":"Ambiguous tenant_id"},"unsupportedRoute":{"title":"unsupportedRoute","value":"Unsupported FeedbackRecords route"}},"exampleType":"examples"},"application/problem+json":{"schema":{"type":"object","additionalProperties":false,"description":"RFC 9457 problem details (`application/problem+json`) as produced by the feedback store behind the API gateway. Branch on the stable `code` member rather than on `title` or `type`. Note the member names differ from the rest of v3 (`request_id`, not `requestId`) — the payload is passed through unchanged.","required":["title","status","code","request_id"],"properties":{"title":{"type":"string","description":"Short, human-readable summary of the problem type; stable across occurrences.","isRequired":true,"uniqueKey":"title","typeLabel":"string"},"status":{"type":"integer","description":"HTTP status code.","isRequired":true,"uniqueKey":"status","typeLabel":"integer"},"code":{"type":"string","description":"Stable, machine-readable error code. `service_unavailable` means a feature or dependency is temporarily unavailable and the request may be retried later; `tenant_write_conflict` means the write collided with an in-progress dataset purge and may be retried unchanged. Every other code is terminal until the request itself changes.","enum":["validation","bad_request","unauthorized","forbidden","not_found","conflict","tenant_write_conflict","method_not_allowed","content_too_large","service_unavailable","internal_server_error"],"isRequired":true,"uniqueKey":"code","typeLabel":"enum\u003cstring>"},"request_id":{"type":"string","description":"Identifier for this request, also returned in the `X-Request-ID` response header; include it when reporting a problem.","isRequired":true,"uniqueKey":"request_id","typeLabel":"string"},"type":{"type":"string","format":"uri","description":"A URI reference identifying the problem type.","uniqueKey":"type","typeLabel":"string\u003curi>"},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence.","uniqueKey":"detail","typeLabel":"string"},"instance":{"type":"string","description":"URI reference identifying this occurrence — the request path **as the feedback store saw it** (`/v1/feedback-records/...`), not the `/api/v3/feedbackRecords/...` path you called.","uniqueKey":"instance","typeLabel":"string"},"details":{"type":"object","additionalProperties":true,"description":"Optional structured context, e.g. `resource_type` on a not-found problem.","uniqueKey":"details","typeLabel":"object"},"invalid_params":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"description":"A single invalid request parameter (RFC 9457 `invalid_params` extension member).","required":["name","reason"],"properties":{"name":{"type":"string","description":"Dotted path to the offending request field, e.g. `field_type` or `items[0].field_type`.","isRequired":true,"uniqueKey":"invalid_params.items.[INDEX].name","typeLabel":"string"},"reason":{"type":"string","description":"Human- and agent-readable explanation of why the value is invalid, naming allowed values or constraints so the request can be corrected without guessing.","isRequired":true,"uniqueKey":"invalid_params.items.[INDEX].reason","typeLabel":"string"}},"uniqueKey":"invalid_params.items.[INDEX]","typeLabel":"object"},"description":"Field-level validation failures. Present on `validation` problems.","uniqueKey":"invalid_params","typeLabel":"object[]"}},"uniqueKey":"","typeLabel":"object"},"examples":{"invertedRange":{"title":"invertedRange","value":{"type":"https://hub.formbricks.com/problems/validation","title":"Validation Failed","status":400,"code":"validation","detail":"One or more request parameters are invalid","request_id":"0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b","invalid_params":[{"name":"sentiment_score_min","reason":"must be less than or equal to sentiment_score_max"}]}}},"exampleType":"examples"}},"exampleType":"examples"},"af222372-c795-458d-a935-c8d7901a287c":{"description":"Unauthorized — no credentials, or an API key that does not exist or is revoked. Answered by the gateway as `text/plain`, before the request reaches the feedback store.","content":{"text/plain":{"schema":{"type":"string","uniqueKey":"","typeLabel":"string"},"example":"Unauthorized","examples":{"Example":{"title":"Example","value":"Unauthorized"}},"exampleType":"examples"}},"exampleType":"examples"},"a734894c-0075-4876-9f7b-a0309eef11ee":{"description":"Forbidden. Answered by the gateway as `text/plain` whenever the request cannot be authorized: the dataset does not exist, is archived, or is not assigned to a workspace the API key has the required permission on; the organization lacks the Unify Feedback entitlement; a mutation was attempted with an API key on a dataset shared by more than one workspace; or the record id does not exist. Existence is not leaked: an unknown dataset or record is refused here rather than with a 404. The single-record operations are the one exception, and only for a minute after a delete — see their `404`.","content":{"text/plain":{"schema":{"type":"string","uniqueKey":"","typeLabel":"string"},"example":"Forbidden","examples":{"Example":{"title":"Example","value":"Forbidden"}},"exampleType":"examples"}},"exampleType":"examples"},"3873ea3d-6f69-4185-b603-c6250f075aad":{"description":"Payload Too Large. The gateway buffers the request body in order to authorize it, and the proxy in front of it stops reading at 1 MiB — Envoy's `maxRequestBytes` on Formbricks Cloud, Traefik's `maxbodysize` in the Docker install. The refusal is therefore generated by that proxy rather than by the application, so neither its media type nor its body is part of this contract; branch on the status alone.","content":{"_mintlify/placeholder":{"schema":{"type":"object","description":""}}}},"9936a3d5-ab1d-4695-a44a-adb72f486abc":{"description":"Internal Server Error from the feedback store (`application/problem+json`, `code: internal_server_error`).","content":{"application/problem+json":{"schema":{"type":"object","additionalProperties":false,"description":"RFC 9457 problem details (`application/problem+json`) as produced by the feedback store behind the API gateway. Branch on the stable `code` member rather than on `title` or `type`. Note the member names differ from the rest of v3 (`request_id`, not `requestId`) — the payload is passed through unchanged.","required":["title","status","code","request_id"],"properties":{"title":{"type":"string","description":"Short, human-readable summary of the problem type; stable across occurrences.","isRequired":true,"uniqueKey":"title","typeLabel":"string"},"status":{"type":"integer","description":"HTTP status code.","isRequired":true,"uniqueKey":"status","typeLabel":"integer"},"code":{"type":"string","description":"Stable, machine-readable error code. `service_unavailable` means a feature or dependency is temporarily unavailable and the request may be retried later; `tenant_write_conflict` means the write collided with an in-progress dataset purge and may be retried unchanged. Every other code is terminal until the request itself changes.","enum":["validation","bad_request","unauthorized","forbidden","not_found","conflict","tenant_write_conflict","method_not_allowed","content_too_large","service_unavailable","internal_server_error"],"isRequired":true,"uniqueKey":"code","typeLabel":"enum\u003cstring>"},"request_id":{"type":"string","description":"Identifier for this request, also returned in the `X-Request-ID` response header; include it when reporting a problem.","isRequired":true,"uniqueKey":"request_id","typeLabel":"string"},"type":{"type":"string","format":"uri","description":"A URI reference identifying the problem type.","uniqueKey":"type","typeLabel":"string\u003curi>"},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence.","uniqueKey":"detail","typeLabel":"string"},"instance":{"type":"string","description":"URI reference identifying this occurrence — the request path **as the feedback store saw it** (`/v1/feedback-records/...`), not the `/api/v3/feedbackRecords/...` path you called.","uniqueKey":"instance","typeLabel":"string"},"details":{"type":"object","additionalProperties":true,"description":"Optional structured context, e.g. `resource_type` on a not-found problem.","uniqueKey":"details","typeLabel":"object"},"invalid_params":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"description":"A single invalid request parameter (RFC 9457 `invalid_params` extension member).","required":["name","reason"],"properties":{"name":{"type":"string","description":"Dotted path to the offending request field, e.g. `field_type` or `items[0].field_type`.","isRequired":true,"uniqueKey":"invalid_params.items.[INDEX].name","typeLabel":"string"},"reason":{"type":"string","description":"Human- and agent-readable explanation of why the value is invalid, naming allowed values or constraints so the request can be corrected without guessing.","isRequired":true,"uniqueKey":"invalid_params.items.[INDEX].reason","typeLabel":"string"}},"uniqueKey":"invalid_params.items.[INDEX]","typeLabel":"object"},"description":"Field-level validation failures. Present on `validation` problems.","uniqueKey":"invalid_params","typeLabel":"object[]"}},"uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"title":"\u003cstring>","status":123,"code":"validation","request_id":"\u003cstring>","type":"\u003cstring>","detail":"\u003cstring>","instance":"\u003cstring>","details":{},"invalid_params":[{"name":"\u003cstring>","reason":"\u003cstring>"}]}}},"exampleType":"generated"}},"exampleType":"generated"},"5c5b6bbf-3e91-46f0-9606-f56d5906728c":{"description":"Service Unavailable. From the **gateway** (`text/plain`): the record's dataset could not be resolved because the feedback store did not answer, the authorization service itself was unreachable, or the record's stored `tenant_id` is not a dataset id this API can address. From the **feedback store** (`application/problem+json`, `code: service_unavailable`): the operation needs a capability that is not configured on this instance — semantic search and similar records require an embedding model.","content":{"text/plain":{"schema":{"type":"string","uniqueKey":"","typeLabel":"string"},"example":"Feedback record lookup failed","examples":{"Example":{"title":"Example","value":"Feedback record lookup failed"}},"exampleType":"examples"},"application/problem+json":{"schema":{"type":"object","additionalProperties":false,"description":"RFC 9457 problem details (`application/problem+json`) as produced by the feedback store behind the API gateway. Branch on the stable `code` member rather than on `title` or `type`. Note the member names differ from the rest of v3 (`request_id`, not `requestId`) — the payload is passed through unchanged.","required":["title","status","code","request_id"],"properties":{"title":{"type":"string","description":"Short, human-readable summary of the problem type; stable across occurrences.","isRequired":true,"uniqueKey":"title","typeLabel":"string"},"status":{"type":"integer","description":"HTTP status code.","isRequired":true,"uniqueKey":"status","typeLabel":"integer"},"code":{"type":"string","description":"Stable, machine-readable error code. `service_unavailable` means a feature or dependency is temporarily unavailable and the request may be retried later; `tenant_write_conflict` means the write collided with an in-progress dataset purge and may be retried unchanged. Every other code is terminal until the request itself changes.","enum":["validation","bad_request","unauthorized","forbidden","not_found","conflict","tenant_write_conflict","method_not_allowed","content_too_large","service_unavailable","internal_server_error"],"isRequired":true,"uniqueKey":"code","typeLabel":"enum\u003cstring>"},"request_id":{"type":"string","description":"Identifier for this request, also returned in the `X-Request-ID` response header; include it when reporting a problem.","isRequired":true,"uniqueKey":"request_id","typeLabel":"string"},"type":{"type":"string","format":"uri","description":"A URI reference identifying the problem type.","uniqueKey":"type","typeLabel":"string\u003curi>"},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence.","uniqueKey":"detail","typeLabel":"string"},"instance":{"type":"string","description":"URI reference identifying this occurrence — the request path **as the feedback store saw it** (`/v1/feedback-records/...`), not the `/api/v3/feedbackRecords/...` path you called.","uniqueKey":"instance","typeLabel":"string"},"details":{"type":"object","additionalProperties":true,"description":"Optional structured context, e.g. `resource_type` on a not-found problem.","uniqueKey":"details","typeLabel":"object"},"invalid_params":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"description":"A single invalid request parameter (RFC 9457 `invalid_params` extension member).","required":["name","reason"],"properties":{"name":{"type":"string","description":"Dotted path to the offending request field, e.g. `field_type` or `items[0].field_type`.","isRequired":true,"uniqueKey":"invalid_params.items.[INDEX].name","typeLabel":"string"},"reason":{"type":"string","description":"Human- and agent-readable explanation of why the value is invalid, naming allowed values or constraints so the request can be corrected without guessing.","isRequired":true,"uniqueKey":"invalid_params.items.[INDEX].reason","typeLabel":"string"}},"uniqueKey":"invalid_params.items.[INDEX]","typeLabel":"object"},"description":"Field-level validation failures. Present on `validation` problems.","uniqueKey":"invalid_params","typeLabel":"object[]"}},"uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"title":"\u003cstring>","status":123,"code":"validation","request_id":"\u003cstring>","type":"\u003cstring>","detail":"\u003cstring>","instance":"\u003cstring>","details":{},"invalid_params":[{"name":"\u003cstring>","reason":"\u003cstring>"}]}}},"exampleType":"generated"}},"exampleType":"examples"}},"security":{"a203df75-4118-42ac-bf45-74c3f740f595":{"apiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Management API key; must include **workspaceId** as an allowed workspace with read, write, or manage permission.\n","scopes":[],"schemeName":"apiKeyAuth"}}},"servers":{"5a1d53ae-163d-4303-ac56-7a378a4d43bc":{"url":"https://app.formbricks.com","description":"Formbricks Cloud. Paths in this spec already include the `/api/v3` prefix; self-hosted instances substitute their own origin."}},"processedSecurityOptions":[{"schemes":[{"scheme":{"type":"apiKey","in":"header","name":"x-api-key","description":"Management API key; must include **workspaceId** as an allowed workspace with read, write, or manage permission.\n","scopes":[],"schemeName":"apiKeyAuth"},"schema":{"title":"x-api-key","type":"string","description":"Management API key; must include **workspaceId** as an allowed workspace with read, write, or manage permission.\n","isRequired":true,"typeLabel":"string","deprecated":false},"schemeName":"apiKeyAuth","name":"x-api-key"}],"title":"apiKeyAuth"}]},"originalFileLocation":"api-v3-reference/openapi.yml"},"subdomain":"formbricks","siteName":"Documentation - Formbricks","title":"Search feedback records semantically - Documentation - Formbricks","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-10-20T07:04:41.813Z","originalFileLocation":"api-v3-reference/v3-feedback-records/search-feedback-records-semantically.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Embeds query and returns the ids of the records in the dataset whose text is closest to it, with cosine similarity scores. Only records with a non-empty value_text are embedded, so only those can match. The gateway reads tenant_id from the JSON body to authorize the call. Requires an API key with read permission (or higher) on a workspace the dataset is assigned to. Results carry ids and the embedded text only — fetch a match with GET /api/v3/feedbackRecords/{id} for the full record. Answers 503 on instances without an embedding model configured.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Documentation - Formbricks\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003cmeta data-mint-head name=\"robots\" content=\"noindex\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Search feedback records semantically - Documentation - Formbricks\">\u003cmeta data-mint-head property=\"og:description\" content=\"Embeds query and returns the ids of the records in the dataset whose text is closest to it, with cosine similarity scores. Only records with a non-empty value_text are embedded, so only those can match. The gateway reads tenant_id from the JSON body to authorize the call. Requires an API key with read permission (or higher) on a workspace the dataset is assigned to. Results carry ids and the embedded text only — fetch a match with GET /api/v3/feedbackRecords/{id} for the full record. Answers 503 on instances without an embedding model configured.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Documentation - Formbricks\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://formbricks.mintlify.app/_mintlify/api/og/v2?division=V3+Feedback+Records\u0026amp;title=Search+feedback+records+semantically\u0026amp;description=Embeds+%60query%60+and+returns+the+ids+of+the+records+in+the+dataset+whose+text+is+closest+to+it%2C%0Awith+cosine+similarity+scores.+Only+records+with+a+non-empty+%60valu\u0026amp;theme=5f66c2cdb60ad4d135e6b4e5\u0026amp;signature=c77ede51773a42076987b291fb4a2e20291dd818d5f4160a88d04ae365bc666c\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Search feedback records semantically - Documentation - Formbricks\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Embeds query and returns the ids of the records in the dataset whose text is closest to it, with cosine similarity scores. Only records with a non-empty value_text are embedded, so only those can match. The gateway reads tenant_id from the JSON body to authorize the call. Requires an API key with read permission (or higher) on a workspace the dataset is assigned to. Results carry ids and the embedded text only — fetch a match with GET /api/v3/feedbackRecords/{id} for the full record. Answers 503 on instances without an embedding model configured.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://formbricks.mintlify.app/_mintlify/api/og/v2?division=V3+Feedback+Records\u0026amp;title=Search+feedback+records+semantically\u0026amp;description=Embeds+%60query%60+and+returns+the+ids+of+the+records+in+the+dataset+whose+text+is+closest+to+it%2C%0Awith+cosine+similarity+scores.+Only+records+with+a+non-empty+%60valu\u0026amp;theme=5f66c2cdb60ad4d135e6b4e5\u0026amp;signature=c77ede51773a42076987b291fb4a2e20291dd818d5f4160a88d04ae365bc666c\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/docs/mintlify-assets/_mintlify/favicons/formbricks/GlZ4IqkvkQlhFmAc/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Documentation - Formbricks\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#00C4B8\">\u003clink data-mint-head rel=\"canonical\" href=\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/docs/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://formbricks.com/#organization\",\"name\":\"Documentation - Formbricks\",\"url\":\"https://formbricks.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/formbricks/wqEbm3FT1QK_6-vL/images/logo-light.svg?fit=max\u0026auto=format\u0026n=wqEbm3FT1QK_6-vL\u0026q=85\u0026s=365e5fb767b72b4fbbb2016860fd92ba\"}},{\"@type\":\"WebSite\",\"@id\":\"https://formbricks.com/docs#website\",\"name\":\"Documentation - Formbricks\",\"url\":\"https://formbricks.com/docs\",\"publisher\":{\"@id\":\"https://formbricks.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically#webpage\",\"url\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically\",\"name\":\"Search feedback records semantically\",\"description\":\"Embeds query and returns the ids of the records in the dataset whose text is closest to it, with cosine similarity scores. Only records with a non-empty value_text are embedded, so only those can match. The gateway reads tenant_id from the JSON body to authorize the call. Requires an API key with read permission (or higher) on a workspace the dataset is assigned to. Results carry ids and the embedded text only — fetch a match with GET /api/v3/feedbackRecords/{id} for the full record. Answers 503 on instances without an embedding model configured.\",\"dateModified\":\"2026-10-20T07:04:41.813Z\",\"isPartOf\":{\"@id\":\"https://formbricks.com/docs#website\"},\"breadcrumb\":{\"@id\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"V3 Feedback Records\",\"item\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/list-feedback-records\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Search feedback records semantically\",\"item\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically\"}]},{\"@type\":[\"TechArticle\",\"APIReference\"],\"@id\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically#apireference\",\"headline\":\"Search feedback records semantically\",\"name\":\"Search feedback records semantically\",\"description\":\"Embeds query and returns the ids of the records in the dataset whose text is closest to it, with cosine similarity scores. Only records with a non-empty value_text are embedded, so only those can match. The gateway reads tenant_id from the JSON body to authorize the call. Requires an API key with read permission (or higher) on a workspace the dataset is assigned to. Results carry ids and the embedded text only — fetch a match with GET /api/v3/feedbackRecords/{id} for the full record. Answers 503 on instances without an embedding model configured.\",\"url\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically\",\"mainEntityOfPage\":{\"@id\":\"https://formbricks.com/docs/api-v3-reference/v3-feedback-records/search-feedback-records-semantically#webpage\"},\"dateModified\":\"2026-10-20T07:04:41.813Z\",\"publisher\":{\"@id\":\"https://formbricks.com/#organization\"},\"isPartOf\":{\"@id\":\"https://formbricks.com/docs#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Documentation - Formbricks\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"Documentation - Formbricks","nav":{"global":null,"tabs":[{"tab":"Platform","pages":[{"title":"Platform","href":"/platform/introduction"}]},{"tab":"Surveys (Ask)","pages":[{"title":"Surveys (Ask)","href":"/surveys/overview"}]},{"tab":"Unify Feedback (Analyze)","pages":[{"title":"Unify Feedback (Analyze)","href":"/unify-feedback/overview"}]},{"tab":"Workflows (Act)","pages":[{"title":"Workflows (Act)","href":"/workflows/overview"}]},{"tab":"Self Hosting","pages":[{"title":"Self Hosting","href":"/self-hosting/overview"}]},{"tab":"Development","pages":[{"title":"Development","href":"/development/overview"}]},{"tab":"MCP","pages":[{"title":"MCP","href":"/platform/mcp/overview"}]},{"tab":"API v1 Reference","pages":[{"title":"API v1 Reference","href":"/api-reference/rest-api"}]},{"tab":"API v2 Reference (Beta)","pages":[{"title":"API v2 Reference (Beta)","href":"/api-v2-reference/introduction"}]},{"tab":"API v3 Reference (Private Beta)","hidden":true,"groups":[{"group":"API v3 Documentation (Private Beta)","pages":[{"title":"API v3 Reference (Private Beta)","description":"The next generation of the Formbricks management API, including the Feedback Records API","icon":"code-compare","href":"/api-v3-reference/introduction"}]},{"group":"V3 Surveys","pages":[{"title":"List surveys","description":"Returns the surveys in the workspace **that the caller may read**. A restricted survey is listed only\nto its owner and to the organisation's owners and managers; to everyone else, API keys included, it\ndoes not exist here. `meta.totalCount` and `meta.workspaceSurveyCount` count that same readable set,\nso neither number reveals a restricted survey to someone who cannot see it. The visibility predicate\nis applied in the query and the cursor, never after the fact: `limit` keeps its meaning and\n`meta.nextCursor` being `null` remains the only end-of-collection signal.\n\nSession cookie, `x-api-key`, or a Bearer token.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/surveys","href":"/api-v3-reference/v3-surveys/list-surveys"},{"title":"Create a survey","description":"Creates a block-based survey template from one strict survey document. The endpoint accepts\nmultilingual authoring maps keyed by real locale codes and converts them to Formbricks'\ninternal default-language representation. Non-default locale keys in translated content\nmust be declared in `languages`; undeclared locale keys return `unsupported_locale` in\n`invalid_params` instead of silently mutating workspace languages.\n\n`blocks[].id` and `variables[].id` are stable public identifiers. They may be omitted on\ncreate, in which case the server generates cuid2 ids. If the same create request needs to\nreference a block or variable from logic, provide explicit valid ids and use those references\nconsistently.\n\nFor normal sequential surveys, omit `logic` and `logicFallback`. `logicFallback` is only valid\nwhen the same block has at least one `logic` rule; otherwise the API returns\n`invalid_reference`.\n\n`type` may be omitted or set to `link` or `app`. App surveys additionally accept the `distribution` (display options + triggers) and `targeting` (contact segment filters) objects; both are rejected for link surveys. Trigger ids must reference existing workspace action classes (discover them via `GET /api/v3/action-classes`), and `targeting.filters` references (contact-attribute keys and segments) must exist in the workspace. Unsupported fields are rejected instead of ignored.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/surveys","href":"/api-v3-reference/v3-surveys/create-a-survey"},{"title":"Create a survey draft payload with AI","description":"Creates a strict v3 `POST /api/v3/surveys` create payload from a creator prompt and returns\nvalidation metadata. This endpoint is single-purpose: it does **not** persist the survey.\nClients should validate the returned payload with `/api/v3/surveys/validate` or use the\nembedded validation metadata, then create the draft through `POST /api/v3/surveys`.\n\nThe generated payload always sets `status: draft`. Both `type: link` and `type: app` are\nsupported; for `app`, the payload includes a default `distribution` (display once, no triggers,\nno targeting) that you finish configuring in the editor or via the create request.\n\nPrompt privacy: prompt text is sent to the configured AI provider to create the payload, but\nthis endpoint does not store prompts and server logs should use request ids and failure\ncategories instead of raw prompt content.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/surveys/generate","href":"/api-v3-reference/v3-surveys/create-a-survey-draft-payload-with-ai"},{"title":"Validate a survey document","description":"Validates a future create or patch survey payload without writing data, creating languages,\nor creating survey versions. Invalid survey documents return `200` with `data.valid=false`\nso agents and MCP clients can repair payloads from structured `invalid_params`.\n\nUse `400` only for malformed validation envelopes, unsupported operations, invalid `surveyId`,\nor unsupported query parameters.\n\n**Custom CSS.** A create or patch payload that carries `customCss` is also run through the custom CSS processor; the result adds the compiled `customCss` and `warnings`, or `errors` and `valid=false`. `operation: customCss` validates CSS alone for a live preview — with `scope: workspace | survey` and, optionally, an existing `surveyId` in `workspaceId` — and returns the compiled CSS and warnings. Validation never saves, versions, touches timestamps or invalidates caches, needs only read access, and grants nothing: the write itself checks permissions and the plan again. Requests that process custom CSS also count against a separate budget of 60 per minute per user or API key, shared with the workspace custom CSS endpoint, with survey creates and updates that add or edit custom CSS, and with the MCP tools; beyond it they return **429**.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/surveys/validate","href":"/api-v3-reference/v3-surveys/validate-a-survey-document"},{"title":"Retrieve a survey","description":"Returns the public v3 survey management resource for one survey. By default, translatable\nfields are returned as stable multilingual maps keyed by the language codes emitted in\n`languages[].code`. Use `lang` to filter those maps to one or more requested language selectors.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/surveys/{surveyId}","href":"/api-v3-reference/v3-surveys/retrieve-a-survey"},{"title":"Delete a survey","description":"Deletes a survey by id. Session cookie, `x-api-key`, or a Bearer token.","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml delete /api/v3/surveys/{surveyId}","href":"/api-v3-reference/v3-surveys/delete-a-survey"},{"title":"Update a survey","description":"Updates a survey through a strict top-level partial document. Omitted top-level fields are\npreserved. Provided top-level objects and arrays replace that whole subtree; this endpoint\ndoes not deep-merge nested objects and does not implement JSON Patch.\n\nThis write endpoint accepts the canonical multilingual authoring document only. Translatable\nfields must be locale-code maps, not strings. `PATCH ?lang=...` and `?version=...` are not\nsupported in Scope 1.\n\nFor MCP and AI-agent workflows, fetch the current survey first, modify only the intended\ntop-level fields, call `/api/v3/surveys/validate` with `operation: patch` for preflight, then\nsubmit the same patch here. Missing ids in replaced arrays are treated as deletions. Variable\nname renames are allowed but may be destructive for downstream exports or integrations.\nElement id changes are rejected once the survey and element are no longer drafts.\n\nServer-owned fields that `GET` returns (`id`, `workspaceId`, `type`, `createdAt`, `archivedAt`, `defaultLanguage`, `languages[].alias`) are accepted and verified, so the output of a `GET` can be sent straight back. Echoing the value you were given is a no-op; changing one is a **422** `read_only_field`.\n\nA round-tripped `updatedAt` acts as an optimistic-concurrency precondition: the write is applied only if the survey has not changed since your read, and is otherwise rejected with **409**. Omit it and overlapping writes remain last-write-wins. Header-based preconditions (`ETag` / `If-Match`) are still future work.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml patch /api/v3/surveys/{surveyId}","href":"/api-v3-reference/v3-surveys/update-a-survey"},{"title":"Edit survey blocks","description":"Edits a survey's blocks in place — update, insert or remove whole blocks without resending the ones you are not changing. Operations apply in order against a single snapshot and are applied atomically: either all of them land or none do.\n\nBlocks are addressed by id, never by position, so a concurrent edit cannot silently redirect an operation onto a different block. An `update` replaces the target block entirely, the same whole-subtree rule `PATCH /api/v3/surveys/{surveyId}` uses, and the replacement must carry every language the survey has configured.\n\nPass `expectedUpdatedAt` (the `updatedAt` from your last read) to make the write conditional; see the **409** response. Session cookie or x-api-key.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml patch /api/v3/surveys/{surveyId}/blocks","href":"/api-v3-reference/v3-surveys/edit-survey-blocks"},{"title":"Set survey block order","description":"Replaces the order of a survey's blocks. The body lists every current block id exactly once, in the order you want; block content is untouched.\n\nIdempotent: the same `order` always yields the same survey, and an order that already matches performs no write at all, so `updatedAt` does not move and other callers' preconditions stay valid. A stale `expectedUpdatedAt` is still rejected with **409** on that path — the precondition is evaluated before the method, so a no-op outcome never reports success to a stale caller. Because the list must be a permutation, a dropped or duplicated block is rejected rather than silently applied — the failure mode that resending the whole `blocks` array cannot catch.\n\nPass `expectedUpdatedAt` (the `updatedAt` from your last read) to make the write conditional; see the **409** response. Session cookie or x-api-key.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml put /api/v3/surveys/{surveyId}/blocks/order","href":"/api-v3-reference/v3-surveys/set-survey-block-order"},{"title":"Archive a survey","description":"Archives (soft-deletes) a survey by id. Archived surveys are hidden from the default list, stop collecting responses, and become read-only until restored — a subsequent `PATCH` returns **422**. Works on any survey, including legacy question-based surveys, so this endpoint returns a minimal lifecycle acknowledgement rather than the full survey resource. Session cookie, `x-api-key`, or a Bearer token.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/surveys/{surveyId}/archive","href":"/api-v3-reference/v3-surveys/archive-a-survey"},{"title":"Restore a survey","description":"Restores a previously archived (soft-deleted) survey by id, clearing its archive marker so it reappears in the default list and becomes editable again. Works on any survey, including legacy question-based surveys, so this endpoint returns a minimal lifecycle acknowledgement rather than the full survey resource. Session cookie, `x-api-key`, or a Bearer token.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/surveys/{surveyId}/restore","href":"/api-v3-reference/v3-surveys/restore-a-survey"},{"title":"Read a survey's visibility and the impact of changing it","description":"Returns the survey's enforced visibility, its owner, the caller's access, and what a change would do: `blockers[]` lists the outbound connections that currently make `restricted` refusable (webhooks, integrations, active feedback sources, and draft or enabled workflows), `impact` carries the numbers the confirmation dialogues show, `allowedTargets[]` the values a `POST` would act on right now, and `pending` / `version` describe a change still in flight. Read this before offering \"Make restricted\"; the `POST` answers **409** while `blockers[]` is non-empty.\n\nRequires `survey.change_visibility`, the same permission as the `POST`: the survey's owner while they still hold read access to the workspace, or an organisation owner/manager, with the feature enabled. Everyone else — read or write members, team-level managers, every API key, and anyone who cannot see the survey at all — answers **403** with the same body as an unknown id. Connection names and ids are configuration, not survey content, and only the people who may act on them get to read them.\n\nWhile `pending` is non-null the survey is restricted for everyone but its owner (within their workspace access: read to view, write to edit) and the organisation's owners and managers, whichever direction is queued — the application fails closed until the graph acknowledges the change — so `visibility` reads `restricted` in that state and `allowedTargets` includes the value that cancels the pending change.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/surveys/{surveyId}/visibility","href":"/api-v3-reference/v3-surveys/read-a-surveys-visibility-and-the-impact-of-changing-it"},{"title":"Change a survey's visibility","description":"Sets the survey to `restricted` or `workspace`.\n\n**Restrictions take effect at once.** A `workspace → restricted` request stores the flag, a new `version` and a pending marker in one transaction. From that instant the survey is restricted on every path — single reads, lists and counts, responses, exports — for everyone but the owner and the organisation's owners and managers, whether or not the graph has caught up (fail closed). The owner keeps only the access their workspace role gives them — read to view, write to edit; an owner who loses workspace access loses the survey too. The relationships are then projected; the answer is **200** either way, with `pending: null` when the graph acknowledged this `version` inside the request and `pending: \"restricted\"` when the outbox will finish it.\n\n**Grants take effect when enforced.** A `restricted → workspace` request answers **200** only once the graph has acknowledged this exact `version`; until then the survey stays restricted. If the in-request projection fails, the answer is **503** `projection_pending`: the change is stored and queued, `GET …/visibility` shows `pending: \"workspace\"`, and retrying is safe.\n\n**Fencing.** Every accepted change increments the survey's `version`. Projection work carries the version it was issued for, and a projector holding an older version never writes, so a worker leased before a newer change cannot overtake it — including one that crashed before its verification pass. A **200** for a grant means the graph holds this exact version.\n\n**No-ops and cancellation.** Requesting the value the survey already enforces, with nothing pending, is a **200** no-op: nothing is written or audited and `changedAt` / `changedBy` are unchanged. Requesting the enforced value while the *opposite* is pending **cancels** the pending change: a new version is stored, the queued work is superseded, and the answer is **200** with `pending: null`. Requesting the pending value again re-attempts the projection (200 or 503).\n\nWhen `restricted` is refused for two reasons at once — no owner **and** outbound connections — the **422** wins: the missing owner cannot be fixed, so listing connections to remove would send the caller down a dead end.\n\nAllowed for the survey's owner (while they still hold read access to the workspace) and for the organisation's owners and managers. **Not** for workspace members with write or team-level manage rights, and never for API keys. Every successful change is audit-logged with the actor.\n\nGoing `workspace → restricted` is refused with **409** while any outbound connection depends on the survey; resolve them first. It is refused with **422** when the survey has no owner, because \"restricted\" would then mean \"administrators only\" — duplicate the survey for a restricted copy. Going `restricted → workspace` has no preconditions.\n\nThis is the only way to change visibility: `PATCH /api/v3/surveys/{surveyId}` accepts `visibility`, `owner` and `access` only as the exact values `GET` returned (so a fetched survey round-trips), and rejects any other value as an unsupported field. Publishing does not change visibility; the publish dialogue chains this call before the status change.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/surveys/{surveyId}/visibility","href":"/api-v3-reference/v3-surveys/change-a-surveys-visibility"}]},{"group":"V3 Responses","pages":[{"title":"List responses","description":"Returns responses for a workspace, newest first. Session cookie, `x-api-key`, or a Bearer token, with read access.\n\n**Scoping.** `workspaceId` is required, as on every v3 collection read. Add `surveyId` to narrow to one survey — the common case, and the one the storage layer is indexed for (`(surveyId, createdAt)`). A workspace-wide list has to reach responses through their surveys and is correspondingly slower, so pass `surveyId` whenever you know it. `contactId` narrows to one respondent.\n\n**Pagination is keyset**, on `(createdAt, id)`. The `cursor` is opaque and bound to the sort order and the filter set it was issued for; changing either invalidates it. `meta.nextCursor` being `null` is the only end-of-collection signal — do not infer the end from a short page.\n\n`id` is there purely as a tie-breaker, and it makes the order **total**, so no row is ever skipped or repeated across pages. What it is not is a finer-grained clock: `createdAt` is stored to millisecond precision, so responses sharing a millisecond — everything written in one bulk insert, for instance — come back in an order that is stable and repeatable but is not their true creation order. Do not read the sequence within a millisecond as meaningful.\n\n**Filters are allow-listed**, and each one on its own is either served by an index or bounded by one. `filter[finished][eq]` and `filter[language][in]` additionally require `surveyId` or `contactId`, because both are heap residuals that are only bounded when the scan is anchored to one survey or one contact; sending either without such a scope returns **400**.\n\nBe aware that **combining** a `filter[createdAt]` bound with `filter[finished][eq]` is the one case no single index covers: the ordered scan uses `(surveyId, createdAt)` and `finished` stays a heap residual, so a page whose matches are sparse reads far more rows than it returns. Prefer narrowing by `surveyId` and a tight `createdAt` window, and treat that combination as the slow path until a covering index exists.\n\n**What this endpoint deliberately does not filter on:** individual answers, hidden-field values, submission metadata (`source`, `url`, `country`, `browser`, `os`, `deviceType`), contact attributes, quotas, and tags. Each is a JSON path or an unindexed join on the largest table in the database, where the honest outcome is a timeout rather than a slow query. The in-app dashboard does offer these, so this endpoint is knowingly behind it; closing the gap needs index work and is tracked separately rather than quietly omitted.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/responses","href":"/api-v3-reference/v3-responses/list-responses"},{"title":"Create a response","description":"Records a survey response. Requires read-write access to the workspace, which is resolved from `surveyId` rather than supplied by the caller.\n\n**This performs a real submission.** It runs the full response pipeline — webhooks, integrations, follow-ups, quota evaluation, and Hub ingestion once the response is finished — and it consumes one metered monthly response. That is the same power a v1/v2 management create has today, and the same power an API key with write access already carries; it is called out because an agent holding that key can create genuine submissions. Use `POST /api/v3/responses/validate` to check a payload without any of that happening.\n\n`createdAt` cannot be supplied. Bulk historical loading is therefore not what this endpoint is for — see `ResponseListItem.createdAt` for why, and use v2 while a dedicated import path is outstanding.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/responses","href":"/api-v3-reference/v3-responses/create-a-response"},{"title":"Count responses","description":"Returns how many responses match a scope and filter set, without fetching any of them.\n\nThis exists as its own operation because \"how many match this?\" is the question asked most often and it should not require paging through data to answer — particularly for agent clients, where a page of payloads to obtain a single integer is pure waste. It takes the same scope and filter parameters as `GET /api/v3/responses`, minus pagination and sorting, so a count and a page are always answering the same question.\n\nThe list endpoint's `meta.totalCount` covers the other case: a caller already paging that also wants the total. That one is opt-in; this one is the direct route.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/responses/count","href":"/api-v3-reference/v3-responses/count-responses"},{"title":"Validate a response payload","description":"Checks a create or patch payload and reports what a real write would do — **without writing anything**. Never has side effects: no response is created, no pipeline fires, nothing is metered.\n\nDeliberately the same shape as `POST /api/v3/surveys/validate`: same dual auth, a request discriminated on `operation` carrying the body the real call would take, and a result of `valid`, `operation` and `invalid_params` with document-relative paths. The value is that the two endpoints behave alike, so a client can validate uniformly. Answers are checked against the survey's own validation rules, so a payload this endpoint accepts is one the real write accepts.\n\n**It reports effects, not just a verdict.** When `valid` is `true`, `effects` names the language that would be stamped, the contact and display that would be linked, the quotas that would count, whether the pipeline would fire, and whether the write would be metered. The language is the field that motivated this endpoint: an importer can see that a payload produces language-less responses *before* writing thousands of them.\n\nAuthorization is the same as the operation being validated — read-write for `create` and `patch` — so this cannot be used as a cheaper cross-tenant existence probe than the write itself. An inaccessible `responseId` on a patch validation answers **403**, as the patch would.\n\n**What it does not do:** an endpoint lets a caller check; it cannot stop a caller writing a bad record anyway. Consistency across the write paths comes from all of them calling the same validator — this endpoint is that validator's public face, not a gate in front of it.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/responses/validate","href":"/api-v3-reference/v3-responses/validate-a-response-payload"},{"title":"Delete responses in bulk","description":"Deletes up to 100 responses in one transaction. Requires `manage` access to the workspace.\n\n**A POST custom method, not a `DELETE` on the collection.** A collection-level `DELETE` that loses its filters — through a client bug, a proxy stripping a query string, a copied curl command — degrades into erasing every response in scope. A distinct path cannot be reached by accident. AIP-165 blesses `batchDelete` for exactly this reason.\n\n**It scope-filters rather than rejecting.** Ids belonging to another workspace, and ids that no longer exist, are ignored; the ids in the caller's own workspace are deleted and `deleted` reports how many that was. Rejecting the whole call on a foreign id would do two bad things at once: leak that the id exists somewhere, and break retries — after a partially-applied call, an already-deleted id is indistinguishable from a foreign one. Scope-filtering is idempotent and leaks nothing, so a client can safely retry the same batch.\n\nBecause `deleted` can therefore be lower than the number of ids sent — or zero — do not treat a shortfall as an error. Deletion cascades to uploaded files exactly as the single-response delete does — and, exactly as it does, leaves the Hub copy in place. See that operation for why.\n\n**`workspaceId` is the scope, and it is required.** Unlike the single-response delete — which resolves the workspace *from* the response so a caller cannot pair someone else's id with a scope it happens to hold — a batch has no single response to resolve from, and scope-filtering is only meaningful against a known workspace. Supplying it is safe here because the same value is both what `manage` is checked against and what the delete is filtered by: the two cannot diverge, so a foreign id matches nothing rather than being deleted under a scope the caller does hold.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/responses/batch-delete","href":"/api-v3-reference/v3-responses/delete-responses-in-bulk"},{"title":"Get a response","description":"Returns one response in the detailed view — everything the list view carries, plus `contact`, `displayId`, `singleUseId`, and the stored answer map `data`.\n\nThe workspace is resolved from the response itself, so the caller cannot choose which scope it is authorized against and no `workspaceId` parameter is accepted. An unknown or inaccessible `responseId` answers **403**, not **404**, so response ids cannot be probed for existence.\n\n**Round-tripping.** This read returns `data` in the stored, write-shaped form, so the simplest correction is to take it, change one entry and `PATCH` it back. Embedded Data round-trips through `embeddedData[]` instead: the `key` you read is the `key` a write accepts. `answers[]` is the typed view of the same information, offered alongside. Independently of that, a client working from a *list* row — which omits the maps — can still rebuild the write-shaped payload: `answers[]`, `embeddedData[]` and `unresolved[]` together carry every stored key and value **verbatim**, with no need to refetch the survey. Two rules make that guarantee hold, and both are contractual:\n\n`rawKey`, `rawValue`, `valueText`, `valueDate`, `fileUrls` and composite slot values are always the stored bytes, never resolved or reformatted — the resolved forms live in the separate `elementLabel`, `optionId`, `optionLabel`, `rowId` and `columnId` fields. And **empty-but-present values are returned, not omitted**: the `\"\"` of an unchecked consent, the `\"\"` of a selected but unfilled Other, a blank composite slot. That second rule matters because `PATCH` replaces `data` wholesale, so an omitted key is a deleted answer rather than an unchanged one.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/responses/{responseId}","href":"/api-v3-reference/v3-responses/get-a-response"},{"title":"Delete a response","description":"Permanently deletes a response. Requires `manage` access; the workspace is resolved from the response, and an unknown or inaccessible `responseId` answers **403**.\n\nDeletion cascades to any files uploaded with the response, which are removed from storage after the row is committed. No `responseDeleted` webhook is emitted — no such trigger exists.\n\n**The Hub copy is not removed.** A submission forwarded to the Hub stays there and stays searchable through the feedback-record endpoints after this returns 204. That cascade is ENG-2855 and lands after the Hub release; until then, an erasure workflow has to delete the feedback record itself. Stated because a caller cannot cheaply verify deletion semantics on a destructive endpoint.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml delete /api/v3/responses/{responseId}","href":"/api-v3-reference/v3-responses/delete-a-response"},{"title":"Update a response","description":"Partially updates a response. Requires read-write access; the workspace is resolved from the response. An unknown or inaccessible `responseId` answers **403**.\n\n`PATCH` is the only update verb — there is no `PUT`. Within it, `data` is replaced wholesale rather than deep-merged, so send the complete answer map; `embeddedData` merges by field name. Unknown top-level keys are rejected with **400** rather than ignored.\n\nEvery patch emits `responseUpdated` and runs the response pipeline, including Hub ingestion. Transitioning `finished` to `true` additionally emits `responseFinished`; patching a response that is already finished does not re-emit it.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml patch /api/v3/responses/{responseId}","href":"/api-v3-reference/v3-responses/update-a-response"}]},{"group":"V3 Workflows","pages":[{"title":"List workflows","description":"Returns workflow list items for a workspace, sorted by `sortBy` (default `updatedAt`,\nnewest first). Workflows are workspace-scoped and exposed through the same v3\nsession-or-API-key authorization model as surveys.\n\nList items are intentionally slim: they include the derived `triggerType` and `surveyId`\nplus a `lastRun` summary, but not the full `definition`. Use\n`GET /api/v3/workflows/{workflowId}` to read the definition.\n\n`archived` workflows are excluded unless explicitly requested with\n`filter[status][in]=archived`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/workflows","href":"/api-v3-reference/v3-workflows/list-workflows"},{"title":"Create a workflow","description":"Creates a `draft` workflow. The workflow definition is the public source of truth used by\nthe dashboard and external clients, following the document-first pattern established by the\nv3 Survey API. Runtime persistence, revision creation, and version snapshotting are\nimplementation details behind this contract.\n\nWorkflows are always created as drafts; use `POST /api/v3/workflows/{workflowId}/enable` to\nmake them respond to trigger events. Graph reference failures (duplicate node ids, edges\npointing at missing nodes, `entryNodeId` not referencing the trigger, unknown `surveyId` or\n`endingCardIds` in the workspace) are rejected with **400** and structured `invalid_params`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows","href":"/api-v3-reference/v3-workflows/create-a-workflow"},{"title":"List workflow runs","description":"Returns workflow run summaries for a workspace, newest first (`createdAt` descending). Use\n`workflowId` to narrow the list to one workflow, or `responseId` to find the runs a specific\nsurvey response produced.\n\nList items are summaries without `triggerPayload`, `data`, or `logs`; fetch\n`GET /api/v3/workflows/runs/{runId}` for the full debug-oriented payload. This keeps\nresponse-level data out of list reads by default.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/workflows/runs","href":"/api-v3-reference/v3-workflows/list-workflow-runs"},{"title":"Retrieve a workflow run","description":"Returns one workflow run by globally unique run id, including the full debug payload: the\ncaptured `triggerPayload`, the run `data` (per-step results), the persisted `logs` trace,\nand retry metadata (`attempt`, `nextAttemptAt`, `lastErrorAt`). Scope 1 deliberately exposes\nthese as JSON dumps so users and agents can understand exactly what happened in a run.\n\nPoll this endpoint after `POST /api/v3/workflows/{workflowId}/test`: runs execute\nasynchronously and transition `queued` → `running` → `completed`/`failed`/`canceled`.\n\nUnknown or inaccessible run ids return `403` rather than `404` to avoid leaking\ncross-workspace resource existence.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/workflows/runs/{runId}","href":"/api-v3-reference/v3-workflows/retrieve-a-workflow-run"},{"title":"Retrieve a workflow","description":"Returns one workflow definition by id. Unknown or inaccessible workflow ids return `403`\nrather than `404` to avoid leaking cross-workspace resource existence.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/workflows/{workflowId}","href":"/api-v3-reference/v3-workflows/retrieve-a-workflow"},{"title":"Delete a workflow","description":"Permanently deletes a workflow by id. Deletion cascades to the workflow's versions, runs,\nand run logs so no response-derived data is left behind (GDPR requirement). This is\nirreversible; use `POST /api/v3/workflows/{workflowId}/archive` for a reversible\nsoft-delete that keeps history. Returns `204 No Content`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml delete /api/v3/workflows/{workflowId}","href":"/api-v3-reference/v3-workflows/delete-a-workflow"},{"title":"Update a workflow","description":"Updates human-facing workflow metadata and/or the workflow definition. Status changes are\nhandled by lifecycle endpoints (`enable`, `disable`, `archive`, `unarchive`) so the state\nmachine remains explicit. Provided fields replace the corresponding stored fields; this\nendpoint does not implement JSON Patch.\n\n`name` and `description` can be updated in any state except `archived`. The `definition`\ncan only be updated while the workflow is `draft` or `disabled`: the live version of an\n`enabled` workflow is an immutable snapshot, so definition changes require disabling (or\nmoving to draft) first and re-enabling afterwards. Violations return **422** with code\n`invalid_workflow_state`. A structurally invalid definition returns **400** with structured `invalid_params`; the referenced survey and ending cards are not verified here — that existence check runs when the workflow is enabled.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml patch /api/v3/workflows/{workflowId}","href":"/api-v3-reference/v3-workflows/update-a-workflow"},{"title":"Duplicate a workflow","description":"Creates a new `draft` workflow by copying an existing workflow's definition, name (or the\nprovided override), and description. The duplicate receives its own id and timestamps and\nstarts with empty run and version history; the source workflow is unchanged. Archived\nworkflows can be duplicated (the copy starts as a normal draft).\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/duplicate","href":"/api-v3-reference/v3-workflows/duplicate-a-workflow"},{"title":"Enable a workflow","description":"Moves a workflow from `draft` or `disabled` to `enabled`. Only enabled workflows respond to\ntrigger events.\n\nEnabling validates that the definition is executable — the trigger has exactly one outgoing\nedge (trigger-only drafts cannot be enabled), the graph is acyclic, every node is\nreachable from the trigger, only Scope 1 node types are present, the referenced survey\nand ending cards still exist, and every `send_email` action addressing a *literal* email address\ntargets someone who can access this workspace (an element-id `to` resolves to the respondent's own\naddress and is exempt) — and then snapshots an immutable workflow version. Runs\nreference that snapshot via `workflowVersionId`, so later draft edits never change what an\nenabled workflow executes. Validation failures return **422** with code\n`workflow_not_executable` and structured `invalid_params`; calling enable in a state other\nthan `draft` or `disabled` returns **422** with code `invalid_workflow_state`. Two concurrent enable requests race to publish the next version; the loser is rejected with **409** code `conflict` and can safely retry.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/enable","href":"/api-v3-reference/v3-workflows/enable-a-workflow"},{"title":"Disable a workflow","description":"Moves an `enabled` workflow to `disabled`. Disabled workflows keep their configuration but\ndo not respond to trigger events; runs that are already queued or running are not canceled.\nCalling disable in any other state returns **422** with code `invalid_workflow_state`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/disable","href":"/api-v3-reference/v3-workflows/disable-a-workflow"},{"title":"Archive a workflow","description":"Soft-deletes a workflow by moving it from any non-archived state to `archived`. Archived\nworkflows stop responding to trigger events immediately, become read-only, and are excluded\nfrom list responses unless requested with `filter[status][in]=archived`. Their runs, version\nhistory, and definition are preserved, and the workflow can be restored with\n`POST /api/v3/workflows/{workflowId}/unarchive`. Archiving an already archived workflow\nreturns **422** with code `invalid_workflow_state`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/archive","href":"/api-v3-reference/v3-workflows/archive-a-workflow"},{"title":"Unarchive a workflow","description":"Restores an `archived` workflow to `draft`. Unarchiving never re-enables a workflow\nautomatically: re-enable explicitly with `POST /api/v3/workflows/{workflowId}/enable`, which\nre-validates the definition (the referenced survey or ending cards may have changed while\nthe workflow was archived). Calling unarchive on a non-archived workflow returns **422**\nwith code `invalid_workflow_state`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/unarchive","href":"/api-v3-reference/v3-workflows/unarchive-a-workflow"},{"title":"Dry-run (validate) a workflow","description":"Validates a workflow without running it or causing any side effect. It checks that the workflow's current definition is executable, that the trigger's referenced survey and ending cards still exist in the workspace, and that every `send_email` action addressing a *literal* email address targets someone who can access this workspace. **No** workflow run is created and **no** email or webhook is sent.\n\nThe response is always `200` with `{ data: { workflowId, ok, problems } }`. `data.ok` is `true` when the workflow would execute; otherwise `data.problems` lists every issue found — each with a machine-readable `code` and the offending `field` — so they can be fixed in a single pass.\n\nDrafts are testable — validating the setup before going live is the point of a dry run. Only an `archived` workflow is rejected, with **422** and code `invalid_workflow_state`.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/workflows/{workflowId}/test","href":"/api-v3-reference/v3-workflows/dry-run-validate-a-workflow"}]},{"group":"V3 Action Classes","pages":[{"title":"List action classes","description":"Returns the action classes (user actions that can trigger an app survey) for a workspace. Use\nthe returned ids as `distribution.triggers[].actionClassId` when creating or updating app\nsurveys. Read-only: this v3 endpoint lists action classes but does not create them — action\nclasses are managed in the Formbricks app (Actions settings) or via the v1/v2 management API.\n\nAn empty `data` array means the workspace has no action classes yet. Triggers are optional, so\nyou can still create an app survey with `distribution.triggers: []` (or omit `distribution`\nentirely) and attach triggers later via `PATCH` once an action class exists. Session cookie, `x-api-key`, or a Bearer token.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/action-classes","href":"/api-v3-reference/v3-action-classes/list-action-classes"}]},{"group":"V3 Contact Attribute Keys","pages":[{"title":"List contact attribute keys","description":"Returns the contact attribute keys for a workspace. Use the returned `key` values as\n`targeting.filters[].root.contactAttributeKey` when targeting app surveys; `dataType` hints which\noperators apply. Session cookie, `x-api-key`, or a Bearer token. (Applying targeting filters requires the Contacts\nentitlement; this discovery endpoint does not.)\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/contact-attribute-keys","href":"/api-v3-reference/v3-contact-attribute-keys/list-contact-attribute-keys"}]},{"group":"V3 Workspaces","pages":[{"title":"Read workspace custom CSS","description":"Returns the workspace's shared survey CSS as editable **source** (light and dark), the one recoverable previous revision, whether the stored CSS is being delivered to respondents (`status`), and whether this caller may change it (`canEdit`, `planAllowed`). Compiled output is never returned here. Requires read access to the workspace. Session cookie, `x-api-key`, or a Bearer token.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/workspaces/{workspaceId}/custom-css","href":"/api-v3-reference/v3-workspaces/read-workspace-custom-css"},{"title":"Save or clear workspace custom CSS","description":"Saves or clears the workspace's shared survey CSS. `customCss` replaces both fields; `null` clears all of it. The source is processed by the same processor every save path uses: constructs it removes come back as top-level `warnings`, and CSS it rejects is a **422** that leaves the saved CSS in effect. The replaced value becomes `previous`, the one recoverable revision — restore it by sending its source back. Unchanged source writes nothing.\n\nWorkspace CSS reaches every survey in the workspace, so writing it is narrower than writing a survey: **organization owners and managers** for signed-in users (session or OAuth), and a **`manage`** grant on this workspace for API keys — a Read \u0026 write grant is refused with **403**. On Formbricks Cloud, adding or editing needs the Scale plan (**403** `custom_css_plan_required`); clearing never does. Successful changes refresh the cached configuration the SDKs receive (they pick it up on their next configuration fetch). Saves that add or edit CSS count against a custom CSS budget of 60 per minute per user or API key, shared with custom CSS validation and with survey writes that add or edit custom CSS; beyond it they return **429**. Re-saving the same CSS and clearing it are never counted.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml patch /api/v3/workspaces/{workspaceId}/custom-css","href":"/api-v3-reference/v3-workspaces/save-or-clear-workspace-custom-css"}]},{"group":"V3 Feedback Records","pages":[{"title":"List feedback records","description":"Lists the feedback records of one dataset, newest first, with optional filters and keyset\npagination. Served by the API gateway, which authorizes the call against `tenant_id` and\nforwards it to the feedback store — see the introduction for how the two layers answer.\n\nRequires an API key with **read** permission (or higher) on a workspace the dataset is assigned\nto. The organization must hold the Unify Feedback entitlement.\n\nMulti-value filters are OR-ed within a parameter and AND-ed across parameters. Repeat the\nparameter to pass several values (`?field_type=text\u0026field_type=rating`); comma-separated values\nare **not** split. Filters on enrichment members (`sentiment`, `emotions`, `sentiment_score_*`)\nnever match records that have not been enriched — use `has_sentiment=false` / `has_emotions=false`\nto find those.\n\n**Incremental extraction.** To keep a copy of a dataset in sync, filter on `updated_since` and\npage with `sort=updated_at\u0026order=asc`, bounded by `updated_until`: every page is then one index\nrange, and a record that changes while you page normally comes back later in the run. Start each\nrun with a `limit=1` request and read its `Date` header: that value is this run's `updated_until`\nand, minus a few minutes, the next run's `updated_since` — both bounds from the same value, or\nrecords between them are never read. Do not use the newest `updated_at` received instead: it\nskips a write that was still committing when you read past its timestamp.\nUpsert by `id` and let the most recent fetch win. Deleted records leave nothing for\n`updated_since` to match, so reconcile deletions separately: periodically list every record id\nwith no `updated_since`, and remove the local records that were not returned and were created\nbefore that listing's start (its first response's `Date` header) minus the overlap. Run one\nsync at a time.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/feedbackRecords","href":"/api-v3-reference/v3-feedback-records/list-feedback-records"},{"title":"Create a feedback record","description":"Stores one feedback record in the dataset named by `tenant_id` in the body. The gateway reads\n`tenant_id` from the JSON body to authorize the call — a body that is not JSON, or has no\n`tenant_id`, answers **400** before anything is stored.\n\nRequires an API key with **write** permission (or higher) on a workspace the dataset is\nassigned to. Creating is allowed on shared datasets (it is ordinary workspace work, like a CSV\nimport); only changing and deleting existing records is restricted to unshared ones.\n\nIngestion is idempotent per (`tenant_id`, `submission_id`, `field_id`): a second record with the\nsame triple answers **409** rather than creating a duplicate. Send exactly one `value_*` member,\nmatching `field_type`. A `text` record is enriched asynchronously — `sentiment`, `emotions` and\nthe translation members appear on the record once enrichment completes, and it becomes\nsearchable once embedded.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/feedbackRecords","href":"/api-v3-reference/v3-feedback-records/create-a-feedback-record"},{"title":"Delete a user's feedback records","description":"Permanently deletes every record in the dataset whose `user_id` matches — the erasure operation\nfor GDPR Article 17 requests. Derived embeddings are removed with the records. Idempotent:\nrepeating the call answers `deleted_count: 0`.\n\n`tenant_id` is **required**: the gateway scopes erasure to one dataset. (The underlying store\ncan erase a user across every dataset when `tenant_id` is omitted; that form is not reachable\nthrough this API.) Records written without a `user_id` cannot be reached this way — delete them\nindividually.\n\nDeletion is unrecoverable, so it requires an API key with **manage** permission on a workspace\nthe dataset is assigned to, and the dataset must be assigned to **exactly one** workspace: on a\nshared dataset no workspace permission can say whose records these are, so API-key mutations\nare refused with **403**.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml delete /api/v3/feedbackRecords","href":"/api-v3-reference/v3-feedback-records/delete-a-users-feedback-records"},{"title":"Search feedback records semantically","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml post /api/v3/feedbackRecords/search/semantic","href":"/api-v3-reference/v3-feedback-records/search-feedback-records-semantically"},{"title":"Get a feedback record","description":"Returns one feedback record by id, with its read-only assignment in the active directory\ntaxonomy. The assignment reflects the current visible path in the active run; editing a record\ndoes not reclassify it until another run. `taxonomy` is null if its lookup is unavailable.\nThe gateway looks the record up to learn its dataset and\nauthorizes against that, so no `tenant_id` is sent. Requires an API key with **read**\npermission (or higher) on a workspace the record's dataset is assigned to.\n\nAn unknown id, or one in a dataset you cannot read, answers **403** rather than 404, so ids\ncannot be probed. The exception is the minute after a delete: the gateway still has the\nrecord's dataset cached, authorizes the call, and the store answers **404**.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/feedbackRecords/{id}","href":"/api-v3-reference/v3-feedback-records/get-a-feedback-record"},{"title":"Delete a feedback record","description":"Permanently deletes one record and its derived embedding. Unrecoverable, so it requires an API\nkey with **manage** permission on a workspace the record's dataset is assigned to, and the\ndataset must be assigned to **exactly one** workspace: on a shared dataset no workspace\npermission can say whose record this is, so API-key mutations are refused with **403**.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml delete /api/v3/feedbackRecords/{id}","href":"/api-v3-reference/v3-feedback-records/delete-a-feedback-record"},{"title":"Update a feedback record","description":"Changes the answer, `language`, `user_id` or `metadata` of one record. Only the members sent\nare changed; provenance (`tenant_id`, `source_*`, `field_*`, `submission_id`, `collected_at`)\nis immutable. Requires an API key with **write** permission (or higher) on a workspace the\nrecord's dataset is assigned to, and the dataset must be assigned to **exactly one** workspace\n— see the delete operation for why.\n\nTwo behaviours are silent and worth knowing: `metadata` is **replaced**, not merged; and\nchanging `value_text` clears the enrichment members (`sentiment`, `sentiment_score`,\n`emotions`, `value_text_translated`, `translation_lang_key`) and queues re-enrichment and\nre-embedding — the response shows them absent until that completes. Changing `language` clears\nand re-queues the translation pair only.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml patch /api/v3/feedbackRecords/{id}","href":"/api-v3-reference/v3-feedback-records/update-a-feedback-record"},{"title":"Find similar feedback records","description":"Returns the records in the same dataset whose embedded text is closest to the given record's,\nwith cosine similarity scores. The anchor record itself is excluded. The gateway looks the\nrecord up to learn its dataset and authorizes against that. Requires an API key with **read**\npermission (or higher) on a workspace the record's dataset is assigned to.\n\nAnswers **403** for an unknown or inaccessible id. Answers **404** when the record exists but has\nno embedding — it has no text, or was created moments ago and is not embedded yet. Answers\n**503** on instances without an embedding model configured.\n","deprecated":null,"version":null,"openapi":"/api-v3-reference/openapi.yml get /api/v3/feedbackRecords/{id}/similar","href":"/api-v3-reference/v3-feedback-records/find-similar-feedback-records"}]}]}]},"title":"Search feedback records semantically - Documentation - Formbricks","buildId":"dpl_GvhK4qVXq51DpfYJLCtPpYNN4YHN","deploymentId":"dpl_GvhK4qVXq51DpfYJLCtPpYNN4YHN","dynamics":["c2aa737661","2b790eda92","b0f84f53ff","a9bb46feb3"],"chunks":["OpenApiReferenceProvider-Bg4Z7D_G.js","utils-Biz734EK.js","cloneDeep-izs3Hzxb.js","_Uint8Array-BVIqJ90l.js","isObjectLike-C8vmzPEN.js","isArray-Dcu1dQ5t.js","utils-C6F9YHs6.js","toString-BX-iyh8G.js","isSymbol-CSH9U2Gm.js","addUniqueKeysToSchema-BoJjY_VP.js","snippetPresets-CnZ-mOFh.js","style-to-object-BY3JEAdk.js","v4-DIgz8zy6.js","ApiExamples-DC_SB2pp.js","isEqual-BfaiBbcl.js","usePlaygroundInputsStore-BYKtOiIP.js","shallow-COAac_UD.js","getApiPlaygroundDisplay-BV4Viqi2.js","CodeSnippet-DDgW-JXl.js","CodeSnippets-CP1LpNhG.js","Page-BeiUfucV.js","getPastedJson-Cvgvbfmv.js","icon-D-H368xH.js","MethodPill-CXSxH1VW.js","DuplicateIcon-JcbBzF1x.js","createIcon-BDt_aHZM.js","DynamicMarkdown-DO1Fz7ar.js","SectionHeading-mwKBArs3.js","axios-vUo_bRtp.js","Markdown-C3KUTGJN.js","clsx-C6CAjViH.js","lib--scc4DXi.js","lib-B8HbPgcC.js","html-void-elements-mCSPHezU.js","dist-C3vb5JQZ.js","katex-YMM-FMeD.js","marked.esm-CZWythHk.js","AssistantPageLink-C8Hisf3g.js"]}