---
title: "Create a feedback record"
method: POST
path: "/api/v3/feedbackRecords"
tags: ["V3 Feedback Records"]
---

# Create a feedback record

`POST /api/v3/feedbackRecords`

## Request

- Base URL: `https://app.formbricks.com`
- URL: `https://app.formbricks.com/api/v3/feedbackRecords`
- Auth: API key in header `x-api-key`

Stores one feedback record in the dataset named by `tenant_id` in the body. The gateway reads
`tenant_id` from the JSON body to authorize the call — a body that is not JSON, or has no
`tenant_id`, answers **400** before anything is stored.

Requires an API key with **write** permission (or higher) on a workspace the dataset is
assigned to. Creating is allowed on shared datasets (it is ordinary workspace work, like a CSV
import); only changing and deleting existing records is restricted to unshared ones.

Ingestion is idempotent per (`tenant_id`, `submission_id`, `field_id`): a second record with the
same triple answers **409** rather than creating a duplicate. Send exactly one `value_*` member,
matching `field_type`. A `text` record is enriched asynchronously — `sentiment`, `emotions` and
the translation members appear on the record once enrichment completes, and it becomes
searchable once embedded.

## Request body

- CreateFeedbackRecordRequest: A new feedback record. Exactly one `value_*` member should carry the answer, matching `field_type`. Strings must be valid UTF-8 without NULL bytes; violations answer **400**.
  - `tenant_id` string, cuid2, required: The feedback dataset to write into (the dataset id). The gateway authorizes the request against this dataset, so it must be one the API key's workspace is assigned to.
  - `source_type` string, required: Type of feedback source, e.g. `survey`, `review`, `feedback_form`.
  - `source_id` string, nullable: Reference to the survey / form / ticket the record came from.
  - `source_name` string, nullable: Human-readable name of the source.
  - `submission_id` string, required: Identifier of the logical submission this record belongs to. Groups multi-field submissions and makes ingestion idempotent: records are unique per (`tenant_id`, `submission_id`, `field_id`) and a duplicate answers **409**. If a record has no logical submission, reuse `field_id`.
  - `field_id` string, required: Identifier of the question / field.
  - `field_label` string, nullable: The question text.
  - `field_group_id` string: Stable identifier grouping related fields (ranking, matrix and grid questions).
  - `field_group_label` string, nullable: Human-readable question text for the group.
  - `field_type` 'text' | 'categorical' | 'nps' | 'csat' | 'ces' | 'rating' | 'number' | 'boolean' | 'date', required: The 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`.
  - `language` string: ISO language code the feedback was given in.
  - `user_id` string: End-user identifier (e.g. an anonymous id or an email hash). Set it on anything you may later have to erase: `DELETE /api/v3/feedbackRecords` matches on `user_id`.
  - `collected_at` string, date-time: When the feedback was given. Defaults to now. Must be between 1970-01-01 and 2080-12-31.
  - `value_text` string, nullable: Text answer. The only member that feeds sentiment, emotion, translation and embedding enrichment.
  - `value_id` string, nullable: Stable id of the selected option in the source system (e.g. a survey choice id or matrix column id), stored alongside `value_text`. Opaque, not validated. Omit for free-text, "other" and non-choice answers.
  - `value_number` number, double: Numeric answer (ratings, NPS scores, numbers). Must be between -1e15 and +1e15.
  - `value_boolean` boolean: Yes / no answer.
  - `value_date` string, date-time: Date answer. Must be between 1970-01-01 and 2080-12-31.
  - `metadata` object, nullable: Arbitrary context for this record, stored as JSON: the dimensions you want to slice a dashboard by, such as channel, device, country, campaign, plan or tags. Values may be strings, numbers, booleans, `null`, or nested objects and arrays. Use stable `snake_case` keys — a key is what a dashboard groups by. Numbers keep full precision in storage. Metadata is **not** sent to the enrichment providers, but it **is** included in the `feedback_record.created` / `feedback_record.updated` webhook payloads.

## Response `201`

Feedback record created

- FeedbackRecord: One piece of feedback in the normalized Feedback Record shape. Field names are `snake_case`: the payload is the Hub's, passed through the API gateway unchanged. Optional members are **absent** (not `null`) when unset, except `taxonomy` on the get-one response, which is null if its lookup fails.
  - `taxonomy` FeedbackRecordTaxonomy: Read-time view of the record's membership in the active directory taxonomy.
    - `status` 'classified' | 'unclassified' | 'no_active_taxonomy', required
    - `run_id` string, uuid, nullable, required
    - `path` object[], required: Currently visible non-root nodes in root-to-leaf order; empty unless classified.
      - `id` string, uuid, required
      - `label` string, required
      - `level` integer, required
      - `node_type` 'branch' | 'leaf', required
  - `id` string, uuid, required: UUIDv7 primary key.
  - `tenant_id` string, cuid2, required: The feedback dataset this record belongs to (the dataset id, as shown by the MCP `list_feedback_datasets` tool).
  - `collected_at` string, date-time, required: When the feedback was given.
  - `created_at` string, date-time, required: When the record was stored. Diverges from `collected_at` on a historical re-import.
  - `updated_at` string, date-time, required: When the record was last changed.
  - `source_type` string, required: Type of feedback source, e.g. `survey`, `review`, `feedback_form`.
  - `source_id` string: Reference to the survey / form / ticket the record came from.
  - `source_name` string: Human-readable name of the source.
  - `submission_id` string, required: Identifier of the logical submission this record belongs to (e.g. a response id). Records are unique per (`tenant_id`, `submission_id`, `field_id`).
  - `field_id` string, required: Identifier of the question / field.
  - `field_label` string: The question text.
  - `field_group_id` string: Stable identifier grouping related fields (ranking, matrix and grid questions).
  - `field_group_label` string: Human-readable question text for the group.
  - `field_type` 'text' | 'categorical' | 'nps' | 'csat' | 'ces' | 'rating' | 'number' | 'boolean' | 'date', required: The 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`.
  - `language` string: ISO language code the feedback was given in.
  - `user_id` string: End-user identifier (e.g. an anonymous id or an email hash).
  - `value_text` string: Text answer.
  - `value_id` string: Stable id of the selected option in the source system (e.g. a survey choice id), stored alongside `value_text` so choice answers keep their identity across label edits and languages. Opaque. Absent for free-text and non-choice answers.
  - `value_number` number, double: Numeric answer (ratings, NPS scores, numbers).
  - `value_boolean` boolean: Yes / no answer.
  - `value_date` string, date-time: Date answer.
  - `metadata` union: Arbitrary context stored with the record and returned with equivalent values (key order and number formatting are normalized). Omitted when the record has no metadata; a record explicitly stored with a JSON `null` returns `null`. An object is the supported shape and the only one the write operations accept; the wider union exists so records written before that convention still deserialize.
    - object
    - unknown[]
      - unknown
    - string
    - number
    - boolean
  - `sentiment` 'very_negative' | 'negative' | 'neutral' | 'positive' | 'very_positive' | 'mixed': Sentiment polarity label inferred from `value_text` by the sentiment enrichment: five ordinal levels plus a distinct `mixed`. Server-generated and read-only.
  - `sentiment_score` number, double: Signed sentiment polarity from `-1.0` (very negative) to `1.0` (very positive). Read-only; absent until the record is enriched.
  - `emotions` FeedbackRecordEmotion[]: Emotions inferred from `value_text`; multi-label from a fixed set. Read-only, and **never an empty array** — the column is either absent or has at least one label. Absent covers two different states: not yet classified, and classified with no emotion detected. Do not read absence as "not enriched yet"; `has_emotions=false` selects both, and the enrichment-status endpoint is what distinguishes them.
  - `value_text_translated` string: `value_text` translated into the dataset's configured target language. Read-only; absent until the record is enriched.
  - `translation_lang_key` string: BCP-47 locale that `value_text_translated` was produced in. Read-only; absent until the record is enriched.

## Other responses

- `400`: 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.
- `401`: 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.
- `403`: 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`.
- `409`: Conflict (`application/problem+json`). Either a **duplicate** — a record with this `tenant_id`, `submission_id` and `field_id` already exists (`code: conflict`, terminal; update the existing record instead) — or the dataset is being **purged** while you write to it (`code: tenant_write_conflict`, transient; retry unchanged once the purge completes).
- `413`: 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.
- `500`: Internal Server Error from the feedback store (`application/problem+json`, `code: internal_server_error`).
- `503`: 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.

## Changes

- **2026-10-01** `f917fab96782`: 1 info
  - added the optional property `taxonomy` to the response with the `201` status
- **2026-09-09** `b127aa22f81c`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/formbricks/apis/formbricks-api-v3/changes/api/v3/feedbackRecords/post.md)

---

[API](https://skmtc.dev/formbricks/apis/formbricks-api-v3.md) - [All operations](https://skmtc.dev/formbricks/apis/formbricks-api-v3/llms.txt) - [OpenAPI document](https://skmtc.dev/formbricks/apis/formbricks-api-v3/revisions/25335c959a13?raw)
