Feedback über die API senden
Das Widget ist nicht der einzige Weg. Alles, was eine HTTP-Anfrage stellen kann, kann Feedback an Opini senden, mit eigenen Tags (Bug, Idee, Abrechnung) und jedem Kontext, den du neben der Nachricht sehen willst.
Wofür es da ist
- Dein eigenes Feedback-Formular. Du hast schon ein Formular oder einen Screen in der App und willst die Nachrichten in Opini.
- Dein Backend und deine Support-Tools. Leite Nachrichten aus einem Support-Postfach, einem Chatbot, App-Store-Bewertungen oder einer Kündigungsumfrage weiter.
- Spiele, Mobile- und Desktop-Apps. Sende Feedback direkt aus der App, mit Plattform, Build oder Level.
- Skripte und Importe. Hol Feedback, das du woanders gesammelt hast, oder sende es aus einem Kommandozeilen-Tool.
Welcher Schlüssel
- Von deinem Server oder einem Skript: ein privater Schlüssel (
sk_live_…), gesendet im HeaderAuthorization. Er darf jeden Tag des Projekts setzen. Behalt ihn auf dem Server; pack ihn nie in eine App zum Herunterladen oder in eine Webseite. - Aus einer App, die Leute installieren (Spiel, Mobile, Desktop): ein Live-Schlüssel (
pk_live_…) mit eingeschaltetem Apps und Spiele, gesendet im Body. Live-Schlüssel sind dafür gemacht, in deiner App ausgeliefert zu werden, und du bestimmst, welche Tags sie setzen dürfen. - Auf einer Website: das Widget mit einem Live-Schlüssel und der Adresse deiner Website in den erlaubten Origins.
Schlüssel anlegen
Leg zuerst die Tags, die du senden willst, unter Projekteinstellungen → Tags an, etwa bug, idee und abrechnung. Öffne dann Projekteinstellungen → API-Schlüssel und klick auf Schlüssel anlegen.
Für deinen Server oder ein Skript:
- Wähl Privat und gib ihm einen Namen, etwa support-postfach.
- Kopier den Schlüssel, wenn er angezeigt wird. Er erscheint nur einmal; erzeuge einen neuen, wenn du ihn verlierst.
Für eine App, die Leute installieren:
- Wähl Öffentlich und gib ihm einen Namen, etwa ios-app oder steam-build.
- Schalte Apps und Spiele ein. Die erlaubten Origins können leer bleiben, wenn nur deine App den Schlüssel nutzt.
- Wähl unter Tags, die dieser Schlüssel setzen darf die Tags, die Leute senden dürfen. Lass alle aus, wenn die App keine Tags setzen soll.
- Setz ein Tageslimit, das zur Zahl deiner App-Nutzer passt.
Nimm einen Schlüssel pro App, Plattform oder Integration. Dann kannst du einen erneuern oder widerrufen, ohne die anderen anzufassen, und die Schlüsseltabelle zeigt, wann jeder zuletzt genutzt wurde.
Feedback senden
Sende einen POST an https://opini.dev/api/v1/ingest mit JSON-Body. Ein privater Schlüssel gehört in den Header Authorization: Bearer sk_live_…; ein Live-Schlüssel in den Body als project_key.
{
"project_key": "pk_live_…", // live keys only; private keys go in the header
"text": "What the person wrote", // required: 1 to 4,000 characters
"email": "[email protected]", // optional: so you can reply
"name": "Sam", // optional
"tags": ["bug", "crash"], // optional: up to 10 tag names
"context": { "platform": "steam" } // optional: up to 16 KB of JSON
}Eine vollständige Anfrage:
curl https://opini.dev/api/v1/ingest \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"text": "Exports to CSV keep timing out for our larger workspaces.",
"email": "[email protected]",
"tags": ["bug"],
"context": {
"source": "support-inbox",
"plan": "business",
"ticket": "4821"
}
}'Ein 200 heißt, das Feedback ist gespeichert und schon in deiner Feedback-Liste:
{ "feedback_public_id": "7k2m9qxa4f", "attachments": { "enabled": true } }Beispiele
Aus deinem Backend, mit privatem Schlüssel (Node):
// Node, from your backend. The private key stays on the server.
await fetch("https://opini.dev/api/v1/ingest", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OPINI_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: survey.reason,
email: user.email,
tags: ["churn"],
context: { source: "cancel-survey", plan: user.plan },
}),
})Aus einer Desktop- oder Mobile-App, mit Live-Schlüssel (Electron, React Native):
// Any JavaScript runtime without a browser origin: Electron's main
// process, React Native, Node.
await fetch("https://opini.dev/api/v1/ingest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project_key: "pk_live_…",
text: "The export button does nothing on Linux.",
tags: ["bug"],
context: { platform: process.platform, version: app.getVersion() },
}),
})Aus einem Spiel, mit Live-Schlüssel (Unity, C#):
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
public class OpiniFeedback : MonoBehaviour
{
const string Url = "https://opini.dev/api/v1/ingest";
const string Key = "pk_live_…"; // a key with "Apps and games" on
[System.Serializable] class Context { public string platform; public string build; public string level; }
[System.Serializable] class Body { public string project_key; public string text; public string[] tags; public Context context; }
public IEnumerator Send(string text, string[] tags, string level)
{
var body = new Body {
project_key = Key,
text = text,
tags = tags,
context = new Context {
platform = Application.platform.ToString(),
build = Application.version,
level = level,
},
};
var json = JsonUtility.ToJson(body);
using var req = new UnityWebRequest(Url, "POST");
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
Debug.LogWarning($"Opini: {req.responseCode} {req.downloadHandler.text}");
}
}Aus einem Spiel, mit Live-Schlüssel (Godot, GDScript):
# Godot 4. Add an HTTPRequest node as a child named "Http".
const URL = "https://opini.dev/api/v1/ingest"
const KEY = "pk_live_…" # a key with "Apps and games" on
func send_feedback(text: String, tags: Array, level: String) -> void:
var body = {
"project_key": KEY,
"text": text,
"tags": tags,
"context": {
"platform": OS.get_name(),
"build": ProjectSettings.get_setting("application/config/version"),
"level": level,
},
}
$Http.request(URL, ["Content-Type: application/json"],
HTTPClient.METHOD_POST, JSON.stringify(body))Kontext
context ist ein beliebiges JSON-Objekt bis 16 KB. Opini braucht keine feste Form: Jeder Schlüssel erscheint unter Kontext, wenn du die Nachricht öffnest, und du kannst nach jedem filtern.
- Aus einem Backend:
source,plan,account_id,ticket. - Aus einer App oder einem Spiel:
platform,version,build,device,level. - Bleib möglichst flach (
{"platform": "steam"}). Verschachtelte Objekte gehen auch; gefiltert wird mit Punkten, etwadevice.gpu. - Nutze überall dieselben Schlüsselnamen, damit ein Filter alle Nachrichten findet.
- Sende keine Passwörter, Tokens oder anderes, wovon Leute nicht erwarten, dass du es speicherst. Dein Team kann jeden Wert lesen.
In Opini finden
In der Feedback-Liste zeigt der Filter Tag Nachrichten mit einem der gewählten Tags, und der Filter Kontext Nachrichten, deren Kontext einen Schlüssel mit einem Wert hat, etwa platform = steam oder level = 3. Werte werden als Text verglichen, 3 passt also auf die Zahl 3. Filter lassen sich miteinander und mit Status- und Datumsfiltern kombinieren und bleiben in der Adresse, damit du die Ansicht teilen kannst.
Dieselben Filter funktionieren in der API und in deinem KI-Tool. Das MCP-Tool list_feedback nimmt tags, context_key und context_value, du kannst also fragen: „Welche Bugs haben Business-Kunden diese Woche gemeldet?“
GET /api/v1/projects/{project_id}/feedback?tag=bug,crash&context_key=platform&context_value=steamAntworten und Fehler
| Status | Fehler | Was tun |
|---|---|---|
200 | keiner | Gespeichert. |
400 | missing_origin | Ein Live-Schlüssel wurde außerhalb einer Website genutzt und hat Apps und Spiele nicht eingeschaltet. Schalte es ein oder sende von deinem Server mit einem privaten Schlüssel. |
400 | invalid_text, invalid_email, context_too_large | Der Text muss 1 bis 4.000 Zeichen lang sein, die E-Mail eine echte Adresse ohne Wegwerf-Anbieter, und der Kontext unter 16 KB. |
401 | invalid_key | Der Schlüssel ist falsch oder wurde widerrufen. |
403 | origin_not_allowed | Die Anfrage kam von einer Website, die nicht in den erlaubten Origins des Schlüssels steht. |
422 | tags_not_allowed | Füge die genannten Tags dem Projekt oder den erlaubten Tags des Schlüssels hinzu, oder sende sie nicht mehr. |
429 | rate_limited, daily_cap_exceeded | Zu viele Nachrichten. Warte und versuch es erneut; beim Tageslimit sagt Retry-After, wann es zurückgesetzt wird (Mitternacht UTC). |
Schlägt eine Anfrage fehl, weil das Gerät offline ist, behalt die Nachricht und sende sie beim nächsten App-Start.
Schlüssel schützen
Ein privater Schlüssel kann Feedback mit jedem Tag senden, also behalt ihn auf deinem Server, in einer Umgebungsvariable oder einem Secret Store, und erneuere ihn, falls er durchsickert.
Alles, was in einer App ausgeliefert wird, kann jemand Entschlossenes auslesen, behandle einen Live-Schlüssel also als öffentlich. Deshalb kann er nur Feedback senden, und nur mit den erlaubten Tags. Um Missbrauch klein zu halten:
- Setz ein Tageslimit nahe an dem, was du erwartest. Überzählige Nachrichten bekommen ein 429, statt deine Liste zu füllen.
- Erlaube nur die Tags, die Leute wählen sollen. Interne Tags gehören nicht an den Schlüssel.
- Wird ein Schlüssel missbraucht, erneuere ihn und liefere den neuen mit dem nächsten Update aus. Widerrufen stoppt ihn sofort.
- Nutze pro App oder Plattform einen eigenen Schlüssel, damit ein Leck nicht den Rest betrifft.