curl --request GET \
--url https://app.formbricks.com/api/v3/feedbackRecords \
--header 'x-api-key: <api-key>'import requests
url = "https://app.formbricks.com/api/v3/feedbackRecords"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://app.formbricks.com/api/v3/feedbackRecords', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.formbricks.com/api/v3/feedbackRecords",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.formbricks.com/api/v3/feedbackRecords"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.formbricks.com/api/v3/feedbackRecords")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.formbricks.com/api/v3/feedbackRecords")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyList feedback records
Lists the feedback records of one dataset, newest first, with optional filters and keyset
pagination. Served by the API gateway, which authorizes the call against tenant_id and
forwards it to the feedback store — see the introduction for how the two layers answer.
Requires an API key with read permission (or higher) on a workspace the dataset is assigned to. The organization must hold the Unify Feedback entitlement.
Multi-value filters are OR-ed within a parameter and AND-ed across parameters. Repeat the
parameter to pass several values (?field_type=text&field_type=rating); comma-separated values
are not split. Filters on enrichment members (sentiment, emotions, sentiment_score_*)
never match records that have not been enriched — use has_sentiment=false / has_emotions=false
to find those.
Incremental extraction. To keep a copy of a dataset in sync, filter on updated_since and
page with sort=updated_at&order=asc, bounded by updated_until: every page is then one index
range, and a record that changes while you page normally comes back later in the run. Start each
run with a limit=1 request and read its Date header: that value is this run’s updated_until
and, minus a few minutes, the next run’s updated_since — both bounds from the same value, or
records between them are never read. Do not use the newest updated_at received instead: it
skips a write that was still committing when you read past its timestamp.
Upsert by id and let the most recent fetch win. Deleted records leave nothing for
updated_since to match, so reconcile deletions separately: periodically list every record id
with no updated_since, and remove the local records that were not returned and were created
before that listing’s start (its first response’s Date header) minus the overlap. Run one
sync at a time.
curl --request GET \
--url https://app.formbricks.com/api/v3/feedbackRecords \
--header 'x-api-key: <api-key>'import requests
url = "https://app.formbricks.com/api/v3/feedbackRecords"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://app.formbricks.com/api/v3/feedbackRecords', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.formbricks.com/api/v3/feedbackRecords",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.formbricks.com/api/v3/feedbackRecords"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.formbricks.com/api/v3/feedbackRecords")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.formbricks.com/api/v3/feedbackRecords")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_bodyAuthorizations
Management API key; must include workspaceId as an allowed workspace with read, write, or manage permission.
Query Parameters
The feedback dataset to operate on (the dataset id, as shown by the MCP list_feedback_datasets tool). Required by the gateway on this operation: a missing or malformed value answers 400 before the request reaches the feedback store, and a dataset the API key's workspace is not assigned to answers 403. The two POST operations take the same value in the request body instead, and the single-record operations derive it from the record.
Records belonging to any of these logical submissions (e.g. response ids).
100255Records from any of these source types, e.g. survey, review.
100255Records from any of these sources (e.g. survey ids).
100255Records whose source display name is any of these. Prefer source_id where records carry one: a name can be edited or translated, the id is stable.
100255Every answer to any of these questions.
100255Records in any of these field groups (ranking / matrix questions).
100255Records of any of these field types. An empty value is ignored, so ?field_type= equals omitting the filter.
9The type of a feedback field, which determines which value_* member carries the answer: text → value_text (the only enrichable type), categorical → value_text + value_id, nps / csat / ces / rating / number → value_number, boolean → value_boolean, date → value_date.
text, categorical, nps, csat, ces, rating, number, boolean, date Records whose selected option id is any of these (e.g. every pick of one survey choice).
100255Everything any of these end users submitted.
100255Records given in any of these languages (ISO codes).
10010collected_at >= since (ISO 8601, inclusive). Must be between 1970-01-01 and 2080-12-31.
collected_at <= until (ISO 8601, inclusive).
created_at >= created_since (ISO 8601, inclusive). created_at is when the record was stored; collected_at (see since) is when the feedback was given. They diverge on a historical re-import, so this is the filter for "what did this import bring in".
created_at <= created_until (ISO 8601, inclusive).
updated_at >= updated_since (ISO 8601, inclusive). updated_at is stamped when a record is created, corrected, or enriched (translation, sentiment, emotions), so this selects records created or changed since a point — the filter for incremental extraction. Deleted records are never returned; see "Incremental extraction" above for choosing the next value and for detecting deletions.
updated_at <= updated_until (ISO 8601, inclusive).
value_number >= value_number_min (inclusive), e.g. NPS promoters with value_number_min=9. Records without a numeric answer are excluded. A max below the min answers 400.
value_number <= value_number_max (inclusive). Paired with the min it selects a band: 9..10 promoters, 0..6 detractors.
value_date >= value_date_min (inclusive) — bounds the answer to a date question, not when the feedback was collected.
value_date <= value_date_max (inclusive).
Records carrying any of these sentiment labels. An empty value is ignored.
6Sentiment polarity label inferred from value_text by the sentiment enrichment: five ordinal levels plus a distinct mixed. Server-generated and read-only.
very_negative, negative, neutral, positive, very_positive, mixed Records tagged with any of these emotions: a record tagged {joy} and one tagged {joy, anger} both match ?emotions=joy&emotions=anger. There is no "all of them" form.
6A single emotion label inferred from value_text by the emotion enrichment (the six Ekman basic emotions). Emotions are multi-label, so a record carries zero or more of these; "mixed" is not a label, it is two or more present at once. Server-generated and read-only.
joy, anger, sadness, fear, surprise, disgust sentiment_score >= sentiment_score_min (inclusive). The score is continuous where the label is bucketed, so this is the filter for "the most negative feedback".
-1 <= x <= 1sentiment_score <= sentiment_score_max (inclusive).
-1 <= x <= 1true selects enriched records (a sentiment label is present), false the not-yet-enriched ones. Omit for no constraint.
true selects records carrying emotion labels, false those without. Note false covers both "not yet classified" and "classified, no emotion detected".
true selects translated records, false untranslated ones. Omit for no constraint.
Column to order by. collected_at and created_at never change. updated_at does, so it is accepted only with order=asc: a record that changes while you page then normally moves ahead of the cursor and is returned again (upsert it by id). Descending, or with no order (which defaults to desc), it answers 400. Even ascending, a write that started before the cursor's position and commits after it is missed by that run and picked up by the next, whose updated_since is the previous run's start minus an overlap. Use sort=updated_at&order=asc with updated_since and updated_until for incremental extraction.
collected_at, created_at, updated_at Sort direction. Rows tied on the sort column are ordered by id ascending.
asc, desc Page size. Out-of-range and non-numeric values are rejected — -5 and 1001 answer 400 — with one exception: limit=0 is indistinguishable from omitting the parameter, so it is accepted and the default of 100 applies. Send no limit rather than 0 if you are computing it. The search operations behave differently again: they clamp instead of rejecting.
1 <= x <= 1000Opaque keyset cursor returned as next_cursor by the previous page. Omit on the first request. A cursor is a position within one specific ordering: presenting it with a different sort or order answers 400 — restart without a cursor instead.
Response
Feedback records retrieved successfully
Was this page helpful?