## Create **post** `/v1/feedback-records` Creates a new feedback record data point ### Body Parameters - `field_id: string` Identifier for the question/field. NULL bytes not allowed. - `field_type: "text" or "categorical" or "nps" or 6 more` Field type: text (enrichable), categorical, nps, csat, ces, rating, number, boolean, date - `"text"` - `"categorical"` - `"nps"` - `"csat"` - `"ces"` - `"rating"` - `"number"` - `"boolean"` - `"date"` - `source_type: string` Type of feedback source (e.g., survey, review, feedback_form). NULL bytes not allowed. - `submission_id: string` Identifier for the logical submission this record belongs to (tenant-scoped). Required. Enables grouping multi-field submissions and idempotent ingestion. Unique per (tenant_id, submission_id, field_id). If a record has no logical submission, use e.g. field_id. - `tenant_id: string` Tenant/organization identifier for multi-tenancy. Required. - `collected_at: optional string` When the feedback was collected (defaults to now). Must be between 1970-01-01 and 2080-12-31. - `field_group_id: optional string` Stable identifier grouping related fields (for ranking, matrix, grid questions). NULL bytes not allowed. - `field_group_label: optional string` Human-readable question text for the group - `field_label: optional string` The actual question text - `language: optional string` ISO language code. NULL bytes not allowed. - `metadata: optional map[unknown]` Arbitrary context for this record, stored as JSON and returned with equivalent values (key order and number formatting are normalized): the dimensions you want to slice a dashboard by, such as channel, device, browser, OS, country, referrer, campaign, plan or tags. Values may be strings, numbers, booleans, null, or nested objects and arrays. Use snake_case keys and keep them stable across records, since a key is what a dashboard groups by: `customer_tier`, not a mix of `customerTier` and `tier`. Numbers keep full precision in storage (unlike `value_number`, which is a float64), so a large integer id round-trips exactly through this API — though a JavaScript client will still lose precision above 2^53 when it parses the response. Nothing here is redacted. Metadata is **not** sent to the LLM or embedding enrichment providers, unlike `value_text` — but it **is** included in the `feedback_record.created` and `feedback_record.updated` webhook payloads, so it reaches whatever URL a tenant has configured. Erasure covers metadata, with one precondition worth knowing: `DELETE /v1/feedback-records` matches on `user_id` and hard-deletes the whole row, metadata included — so a record written **without** a `user_id` cannot be reached that way. Set `user_id` on anything you may later have to erase. Three kinds of content are rejected with 400, because none of them can be stored: invalid UTF-8; NULL bytes or unpaired UTF-16 surrogates, in keys or values; and numbers outside the storable numeric range. - `source_id: optional string` Reference to survey/form/ticket ID - `source_name: optional string` Human-readable name - `user_id: optional string` User ID (e.g., anonymous ID or email hash) - `value_boolean: optional boolean` For yes/no questions - `value_date: optional string` For date responses. Must be between 1970-01-01 and 2080-12-31. - `value_id: optional string` Stable id of the selected option in the source system (e.g. a survey choice id or matrix column id), stored alongside value_text so selected-choice answers keep a durable identity across label edits and languages. Opaque to Hub (not validated). Null for free-text/"other" answers, non-choice fields, and sources without option ids. NULL bytes not allowed when present. - `value_number: optional number` For ratings, NPS scores, numeric responses. Must be between -1e15 and +1e15. - `value_text: optional string` For open-ended text responses. Omit or null if not applicable. NULL bytes not allowed when present. ### Returns - `FeedbackRecordData = object { id, collected_at, created_at, 24 more }` - `id: string` UUIDv7 primary key - `collected_at: string` When the feedback was collected - `created_at: string` When this record was created - `field_id: string` Identifier for the question/field - `field_type: "text" or "categorical" or "nps" or 6 more` Type of field - `"text"` - `"categorical"` - `"nps"` - `"csat"` - `"ces"` - `"rating"` - `"number"` - `"boolean"` - `"date"` - `source_type: string` Type of feedback source - `submission_id: string` Identifier for the logical submission this record belongs to (required). - `tenant_id: string` Tenant/organization identifier. NULL bytes not allowed. - `updated_at: string` When this record was last updated - `emotions: optional array of "joy" or "anger" or "sadness" or 3 more` Emotions inferred from value_text (emotion enrichment); multi-label from a fixed set. Read-only; absent until the record is enriched, and never an empty array. - `"joy"` - `"anger"` - `"sadness"` - `"fear"` - `"surprise"` - `"disgust"` - `field_group_id: optional string` Stable identifier grouping related fields (for ranking, matrix, grid questions) - `field_group_label: optional string` Human-readable question text for the group - `field_label: optional string` The actual question text - `language: optional string` ISO language code. NULL bytes not allowed. - `metadata: optional unknown or array of unknown or string or 2 more` Arbitrary context stored with this record, returned with equivalent values (key order is normalized, and so are number exponents — `1e2` reads back as `100`). 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 request schemas accept. The wider union is for reading: the column stores whatever JSON was written, so a record created before that convention can return an array or a scalar, and an object-only response schema would make a generated client reject it. - `UnionMember0 = unknown` Arbitrary context stored with this record, returned with equivalent values (key order is normalized, and so are number exponents — `1e2` reads back as `100`). 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 request schemas accept. The wider union is for reading: the column stores whatever JSON was written, so a record created before that convention can return an array or a scalar, and an object-only response schema would make a generated client reject it. - `UnionMember1 = array of unknown` Arbitrary context stored with this record, returned with equivalent values (key order is normalized, and so are number exponents — `1e2` reads back as `100`). 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 request schemas accept. The wider union is for reading: the column stores whatever JSON was written, so a record created before that convention can return an array or a scalar, and an object-only response schema would make a generated client reject it. - `UnionMember2 = string` Arbitrary context stored with this record, returned with equivalent values (key order is normalized, and so are number exponents — `1e2` reads back as `100`). 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 request schemas accept. The wider union is for reading: the column stores whatever JSON was written, so a record created before that convention can return an array or a scalar, and an object-only response schema would make a generated client reject it. - `UnionMember3 = number` Arbitrary context stored with this record, returned with equivalent values (key order is normalized, and so are number exponents — `1e2` reads back as `100`). 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 request schemas accept. The wider union is for reading: the column stores whatever JSON was written, so a record created before that convention can return an array or a scalar, and an object-only response schema would make a generated client reject it. - `UnionMember4 = boolean` Arbitrary context stored with this record, returned with equivalent values (key order is normalized, and so are number exponents — `1e2` reads back as `100`). 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 request schemas accept. The wider union is for reading: the column stores whatever JSON was written, so a record created before that convention can return an array or a scalar, and an object-only response schema would make a generated client reject it. - `sentiment: optional "very_negative" or "negative" or "neutral" or 3 more` Sentiment polarity inferred from value_text (sentiment enrichment). Read-only; absent until the record is enriched. - `"very_negative"` - `"negative"` - `"neutral"` - `"positive"` - `"very_positive"` - `"mixed"` - `sentiment_score: optional number` Signed sentiment polarity from -1.0 (very negative) to 1.0 (very positive) (sentiment enrichment). Read-only; absent until the record is enriched. - `source_id: optional string` Reference to survey/form/ticket ID - `source_name: optional string` Human-readable name - `translation_lang_key: optional string` BCP-47 target locale that value_text_translated was produced in (language enrichment). Read-only; absent until the record is enriched. - `user_id: optional string` User ID (e.g., anonymous ID or email hash) - `value_boolean: optional boolean` Boolean response - `value_date: optional string` Date response - `value_id: optional string` Stable id of the selected option in the source system (e.g. a survey choice id), stored alongside value_text for durable option identity. Opaque to Hub. Absent for free-text/non-choice answers. NULL bytes not allowed. - `value_number: optional number` Numeric response - `value_text: optional string` Text response. NULL bytes not allowed. - `value_text_translated: optional string` value_text translated into the tenant's configured target language (language enrichment). Read-only; absent until the record is enriched. ### Example ```http curl http://localhost:8080/v1/feedback-records \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $HUB_API_KEY" \ -d '{ "field_id": "q1", "field_type": "rating", "source_type": "survey", "submission_id": "550e8400-e29b-41d4-a716-446655440000", "tenant_id": "org-123" }' ```