API Submissions

Submit feedback securely from browsers, mobile apps, desktop apps, trusted backends, and AI agents.

For AI agents, LLM tools, and automations, start with the dedicated AI agents guide, which includes the machine-readable agent pack.

TellTide has two credential types. Choose the credential for the environment where your code runs.

CredentialExampleWhere it may be usedPermission
Publishable keytt_pub_…Browser, React, mobile, desktop, extensionSubmit feedback only
Private keytt_live_…_…Trusted backend or serverless functionExplicit configured scopes

Private keys are secrets. Never put one in browser JavaScript, a mobile or desktop binary, an extension, a public repository, or a NEXT_PUBLIC_/VITE_ environment variable. Environment variables do not protect values after they are bundled into client software.

All requests must use HTTPS. Credentials belong in headers, never URLs or query parameters.

Browser, React, mobile, and desktop

Use the app's publishable key:

POST https://telltide.com/api/v1/public/feedback
Content-Type: application/json
X-TellTide-Key: tt_pub_YOUR_KEY_ID
Idempotency-Key: submission-unique-id

The publishable key determines the destination app. appId is optional; if supplied, it must match the key's app. A publishable key cannot read, update, delete, moderate, or export feedback.

Do not set reserved feedback_data keys: activity_log, internal_notes, priority_level, or screenshot_id. Customer urgency on a feature request is requested_priority (low, medium, or high). Inbox priority is set later with PATCH { "priority": "high" }.

Browser JavaScript

await fetch('https://telltide.com/api/v1/public/feedback', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-TellTide-Key': 'tt_pub_YOUR_KEY_ID',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    type: 'feedback',
    name: 'Alex',
    email: 'alex@example.com',
    rating: 8,
    comment: 'The new workflow is much faster.',
    feedback_data: { feedback: 'The new workflow is much faster.', score: 8 },
  }),
});

React

async function submitFeedback(values) {
  const response = await fetch('https://telltide.com/api/v1/public/feedback', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-TellTide-Key': 'tt_pub_YOUR_KEY_ID',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify(values),
  });
  if (!response.ok) throw new Error((await response.json()).error.message);
  return response.json();
}

Electron or Tauri

Send the request from the Electron main process or Tauri native command. Pass the form data from the renderer over the framework's narrow IPC bridge. This avoids browser Origin: null behaviour; the publishable key is still intentionally public and safe to include in the binary:

await fetch('https://telltide.com/api/v1/public/feedback', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-TellTide-Key': 'tt_pub_YOUR_KEY_ID',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({
    type: 'bug',
    name: user.name,
    email: user.email,
    comment: 'Export stopped responding.',
    feedback_data: { screen: 'settings/export' },
  }),
});

Android (Kotlin)

val request = Request.Builder()
  .url("https://telltide.com/api/v1/public/feedback")
  .header("X-TellTide-Key", "tt_pub_YOUR_KEY_ID")
  .header("Idempotency-Key", UUID.randomUUID().toString())
  .post(jsonBody.toRequestBody("application/json".toMediaType()))
  .build()

iOS (Swift)

var request = URLRequest(url: URL(string: "https://telltide.com/api/v1/public/feedback")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("tt_pub_YOUR_KEY_ID", forHTTPHeaderField: "X-TellTide-Key")
request.setValue(UUID().uuidString, forHTTPHeaderField: "Idempotency-Key")
request.httpBody = try JSONSerialization.data(withJSONObject: payload)
let (_, response) = try await URLSession.shared.data(for: request)

Browser requests are also checked against the key's allowed websites. CORS and allowed websites reduce misuse but do not make a publishable key secret. Native requests do not send a browser Origin, so rate limits and application policy still apply. In the key settings, you can disable browser submissions or native/server submissions independently.

TellTide widget

The standard widget embed contains only the App ID:

<script
  src="https://telltide.com/src/widgets/widget.js"
  data-app-id="YOUR_APP_ID"
  async
></script>

The widget loads the app's current publishable key from TellTide and sends it in X-TellTide-Key. Do not add a private key, data-api-key, or credential query parameter to the embed. Configure the website in Apps → Manage → API credentials → Allowed websites. Replacing the publishable key does not require changing the standard embed.

Trusted backend

Create a private key with only the scopes the service requires. Send it using Bearer authentication:

curl https://telltide.com/api/v1/feedback \
  --request POST \
  --header "Authorization: Bearer $TELLTIDE_SECRET_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: order-8273-feedback" \
  --data '{
    "type": "bug",
    "name": "Alex",
    "email": "alex@example.com",
    "comment": "Checkout did not complete.",
    "feedback_data": {"screen": "checkout"}
  }'

Node.js or Next.js server

export async function POST(request) {
  const payload = await request.json();
  const response = await fetch('https://telltide.com/api/v1/feedback', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.TELLTIDE_SECRET_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify(payload),
  });
  return new Response(response.body, response);
}

TELLTIDE_SECRET_KEY must be a server-only environment variable. Never prefix it with NEXT_PUBLIC_.

Python backend

import os
import uuid
import requests

response = requests.post(
    "https://telltide.com/api/v1/feedback",
    headers={
        "Authorization": f"Bearer {os.environ['TELLTIDE_SECRET_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "type": "feature_request",
        "name": "Sam",
        "email": "sam@example.com",
        "comment": "Add scheduled exports.",
        "feedback_data": {"feature_title": "Scheduled exports"},
    },
    timeout=10,
)
response.raise_for_status()

Reading and managing feedback from a server

These endpoints require a private key with the corresponding scope:

EndpointScope
GET /api/v1/feedbackfeedback:read
GET /api/v1/feedback/:feedbackIdfeedback:read
PATCH /api/v1/feedback/:feedbackIdfeedback:update
DELETE /api/v1/feedback/:feedbackIdfeedback:delete

Every resource query is restricted to the app associated with the credential. Use separate keys as access roles—for example, a read-only agent receives only feedback:read, while a triage service receives feedback:read and feedback:update. TellTide checks these scopes from the database on every request.

Errors and rate limits

Errors use a stable structure:

{
  "error": {
    "code": "insufficient_scope",
    "message": "The supplied credential does not have the required permission.",
    "requestId": "req_..."
  }
}

Keep requestId when contacting support. A 429 response includes Retry-After. Retry timeouts and 5xx responses with exponential backoff and reuse the same Idempotency-Key. Reusing an idempotency key with different content returns 409.

Key operations

  • Create keys in Apps → Manage → API credentials.
  • Configure allowed websites on the publishable key.
  • Private key values are shown once.
  • Private keys default to a 90-day expiry; No expiry is available when operationally required.
  • Rotation creates a replacement and immediately revokes the old key.
  • Revocation takes effect immediately.
  • If a private key ever appeared in client code, a repository, a binary, a screenshot, or a support message, rotate it.

The old x-api-key, JSON-body key, and /api/feedback forms are deprecated compatibility paths. New integrations must use the endpoints and headers above.