Feedback Ingestion API
A single POST endpoint that lets any user on your platform submit feedback directly into your Tensient workspace. Captures rich browser and device context automatically. Returns a tracking ID the user can save to follow their ticket.
Endpoint
POST
https://tensient.com/api/feedback/ingestAuthenticated via your Tensient API key. Generate keys in Settings → Developer.
Authentication
Pass your API key using either header format:
Option A — Bearer token
Authorization: Bearer tns_YOUR_KEY_HEREOption B — X-API-Key header
X-API-Key: tns_YOUR_KEY_HEREAPI keys are workspace-scoped. All submissions go into the workspace associated with the key used.
tns_pub_...Public key — safe for client-side use
Can be embedded in browser JavaScript bundles or mobile apps. Requires an origin allowlist — requests are only accepted from the registered domains you configure in Settings → Developer. Configure your domains before deploying.
tns_...Secret key — server-side only
Full access, no origin restriction. Must never be embedded in frontend code, mobile apps, or any environment where it could be extracted. Use only from your backend or server-side functions.
Categories
bug_reportBug Report
Something is not working as expected. Use this for broken features, errors, or unexpected behavior.
feature_requestFeature Request
A suggestion for something new or an improvement to existing functionality.
help_requestHelp Request
The user needs assistance or has a question about how something works.
urgent_issueUrgent Issue
A critical problem requiring immediate attention — data loss, security concerns, complete outages.
Request Payload
Required fields
categorystringOne of the four category values above.
subjectstringBrief title. Max 200 characters.
descriptionstringFull description of the issue or request. Max 10,000 characters.
submitter (optional object)
emailstringSubmitter's email address.
namestringSubmitter's display name.
externalIdstringYour platform's user ID. Useful for correlating with your own user data.
isAuthenticatedbooleanWhether the user was logged in on your platform when submitting.
typestring"human" (default) or "ai_agent" — use ai_agent when submitting programmatically from an AI system.
context (optional object — captured browser environment)
urlstringCurrent page URL.
referrerstringReferring page URL.
pageTitlestringdocument.title of the current page.
localestringnavigator.language — e.g. "en-US".
timezonestringIANA timezone — e.g. "America/New_York".
userAgentstringnavigator.userAgent. Captured server-side as fallback.
browserobject{ name, version, engine, engineVersion }
osobject{ name, version }
screenobject{ width, height, viewportWidth, viewportHeight, pixelRatio, colorDepth, orientation }
hardwareobject{ cpuCores, deviceMemory, maxTouchPoints, touchSupport, webdriver } — webdriver:true is a fraud signal.
networkobject{ effectiveType, downlink, rtt, online }
performanceobject{ memory, navigation, paint: { firstPaint, firstContentfulPaint } }
errorsarrayConsole errors: [{ message, source, lineno, colno, stack, timestamp }]
fingerprintobject{ canvasHash, webglRenderer, webglVendor } — for device clustering and fraud detection.
Other fields
customContextobjectArbitrary key-value metadata from your platform. Attach game session IDs, transaction IDs, feature flags, A/B test variants, etc.
tagsstring[]Free-form tags for filtering and grouping.
Ratings & Responses
Any submission can optionally include a rating and/or a structured set of multi-question responses. These fields are never required — they augment the core ticket model rather than replacing it.
rating (optional object — single primary rating)
valuenumberThe numeric rating. Required when sending a rating object.
scalenumberMaximum possible value (e.g. 10 for NPS, 5 for stars). Used for cross-type normalization.
typestring"nps" | "stars" | "csat" | "thumbs" | "likert" or any custom string.
labelstringHuman-readable question label, e.g. "Likely to recommend".
responses (optional array — multi-question survey payload)
questionIdstringStable identifier for the question, e.g. "nps", "q1", "overall_sat".
typestring"rating" | "nps" | "text" | "choice" | "multi_choice" | "boolean".
valueanyThe answer: number for rating/nps, string for text/choice, boolean for boolean, string[] for multi_choice.
scalenumberMax scale for numeric types.
labelstringHuman-readable label for the response value.
optionsstring[]For choice types: the full option set shown to the user.
Behavior
- ·If rating is sent, its value/scale/type are stored as indexed flat columns for fast SQL aggregations.
- ·If only responses is sent, the first numeric-typed entry is automatically promoted to the flat columns.
- ·Both can coexist — rating is a convenience alias for the primary response.
- ·category, subject, and description remain required. Rating is always additive.
NPS — single rating
{
"category": "help_request",
"subject": "Post-session NPS",
"description": "Automated NPS capture after game session",
"submitter": { "externalId": "usr_123", "isAuthenticated": true },
"rating": {
"value": 8,
"scale": 10,
"type": "nps",
"label": "How likely are you to recommend us?"
}
}5-star CSAT rating
{
"category": "help_request",
"subject": "Support ticket rating",
"description": "Post-resolution CSAT",
"submitter": { "externalId": "usr_456", "isAuthenticated": true },
"rating": { "value": 4, "scale": 5, "type": "stars" }
}Multi-question survey (NPS + follow-up text + choice)
{
"category": "help_request",
"subject": "Post-session survey",
"description": "Automated capture after game session ends",
"submitter": { "externalId": "usr_789", "isAuthenticated": true },
"rating": { "value": 8, "scale": 10, "type": "nps" },
"responses": [
{ "questionId": "nps", "type": "nps", "value": 8, "scale": 10 },
{ "questionId": "nps_reason", "type": "text",
"value": "Love the game variety but withdrawals are slow" },
{ "questionId": "primary_issue", "type": "choice",
"value": "withdrawal_speed",
"options": ["game_selection", "withdrawal_speed", "customer_support", "bonuses"] }
]
}Code Examples
curl
curl -X POST https://tensient.com/api/feedback/ingest \
-H "Authorization: Bearer tns_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"category": "bug_report",
"subject": "Payment fails at checkout",
"description": "Clicking Pay does nothing after entering card details. No error shown.",
"submitter": {
"email": "user@example.com",
"name": "Alex Smith",
"externalId": "usr_123",
"isAuthenticated": true
},
"context": {
"url": "https://mygame.com/checkout",
"pageTitle": "Checkout",
"locale": "en-US",
"timezone": "America/New_York",
"browser": { "name": "Chrome", "version": "124.0" },
"os": { "name": "macOS", "version": "14.4" },
"screen": { "width": 1440, "height": 900, "viewportWidth": 1440, "viewportHeight": 789 },
"hardware": { "webdriver": false }
},
"customContext": {
"gameSessionId": "sess_abc123",
"transactionId": "txn_xyz789"
},
"tags": ["checkout", "payments"]
}'JavaScript / TypeScript
const response = await fetch("https://tensient.com/api/feedback/ingest", {
method: "POST",
headers: {
"Authorization": "Bearer tns_YOUR_KEY_HERE",
"Content-Type": "application/json",
},
body: JSON.stringify({
category: "bug_report",
subject: "Payment fails at checkout",
description: "Clicking Pay does nothing after entering card details.",
submitter: {
email: "user@example.com",
externalId: "usr_123",
isAuthenticated: true,
},
context: {
url: window.location.href,
pageTitle: document.title,
locale: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
userAgent: navigator.userAgent,
screen: {
width: screen.width,
height: screen.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio,
},
hardware: {
cpuCores: navigator.hardwareConcurrency,
deviceMemory: (navigator as any).deviceMemory,
touchSupport: navigator.maxTouchPoints > 0,
webdriver: navigator.webdriver,
},
network: {
online: navigator.onLine,
effectiveType: (navigator as any).connection?.effectiveType,
},
},
customContext: {
gameSessionId: "sess_abc123",
},
}),
});
const result = await response.json();
// result.trackingId — save this to show the user their ticket link
// result.trackingUrl — e.g. https://tensient.com/feedback/track/fb_a1b2c3Python
import requests
response = requests.post(
"https://tensient.com/api/feedback/ingest",
headers={
"Authorization": "Bearer tns_YOUR_KEY_HERE",
"Content-Type": "application/json",
},
json={
"category": "bug_report",
"subject": "Payment fails at checkout",
"description": "Clicking Pay does nothing after entering card details.",
"submitter": {
"email": "user@example.com",
"externalId": "usr_123",
"isAuthenticated": True,
},
"context": {
"url": "https://mygame.com/checkout",
"pageTitle": "Checkout",
},
"customContext": {
"gameSessionId": "sess_abc123",
"transactionId": "txn_xyz789",
},
},
)
data = response.json()
tracking_id = data["trackingId"] # e.g. "fb_a1b2c3d4e5f6"
tracking_url = data["trackingUrl"] # share with the userResponses
Success — 201 Created
// HTTP 201
{
"id": "uuid-of-the-submission",
"trackingId": "fb_a1b2c3d4e5f6",
"trackingUrl": "https://tensient.com/feedback/track/fb_a1b2c3d4e5f6",
"status": "new",
"createdAt": "2026-03-28T10:00:00.000Z"
}Store trackingId or show trackingUrl to the submitter so they can follow their ticket status.
Error responses
// 400 — Validation error
{ "error": "category must be one of: bug_report, feature_request, help_request, urgent_issue" }
{ "error": "subject is required." }
{ "error": "description must be 10,000 characters or fewer." }
// 401 — Auth error
{ "error": "Missing API key. Use Authorization: Bearer tns_... or X-API-Key header." }
{ "error": "Invalid or revoked API key." }
// 429 — Rate limit
{ "error": "Rate limit exceeded. Max 60 submissions per minute per API key." }Rate Limits
60 submissions / minute10 submissions / minuteResponses that exceed the limit return HTTP 429. The limits reset on a rolling 60-second window.
CORS
CORS is controlled via the FEEDBACK_ALLOWED_ORIGINS environment variable on the Tensient server. Contact your Tensient workspace owner to add your domain to the allowed origins list.
For security, we recommend calling this endpoint from your backend rather than directly from browser JavaScript — this also keeps your API key off the client.
Server-Side Capture
Even without sending any context fields, Tensient automatically captures the following from every inbound request:
- ·IP address (from X-Forwarded-For)
- ·City, region, country (from Vercel geo headers)
- ·User-Agent (from request headers, as fallback)