Zum Inhalt springen

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 Header Authorization. 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:

  1. Wähl Privat und gib ihm einen Namen, etwa support-postfach.
  2. 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:

  1. Wähl Öffentlich und gib ihm einen Namen, etwa ios-app oder steam-build.
  2. Schalte Apps und Spiele ein. Die erlaubten Origins können leer bleiben, wenn nur deine App den Schlüssel nutzt.
  3. 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.
  4. 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.

jsonc
{
  "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:

bash
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:

json
{ "feedback_public_id": "7k2m9qxa4f", "attachments": { "enabled": true } }

Beispiele

Aus deinem Backend, mit privatem Schlüssel (Node):

javascript
// 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):

javascript
// 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#):

csharp
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):

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))

Tags

Mit Tags sortierst du, was reinkommt: Du kannst die Feedback-Liste danach filtern. Sende Tag-Namen; Groß- und Kleinschreibung ist egal, "Bug" und "bug" sind also derselbe Tag.

  • Jeder Tag muss im Projekt schon existieren. Das Senden legt nie einen an.
  • Ein Live-Schlüssel darf nur die Tags auf seiner Liste nutzen. Ein privater Schlüssel darf alle Tags des Projekts nutzen.
  • Bis zu 10 Tags pro Nachricht.
  • Ist ein Tag nicht erlaubt, wird nichts gespeichert, und die Antwort ist ein 422 mit diesen Tags. So merkst du es beim Entwickeln, statt still Feedback zu verlieren:
http
HTTP/1.1 422 Unprocessable Entity

{ "error": "tags_not_allowed", "tags": ["internal"] }

Nutze Tags für die wenigen Dinge, nach denen du filterst und sortierst, etwa bug, idee, abrechnung oder crash. Fragt dein Formular nach der Art des Feedbacks, sende die Auswahl als Tag. Alles andere gehört in den Kontext.

Auf einer Website sendet auch das Widget Tags. Setz sie am Script-Tag oder änder sie aus deiner Seite heraus:

html
<script
  src="https://opini.dev/widget.js"
  data-opini-key="pk_live_…"
  data-opini-tags="web,beta"
  defer
></script>

<script>
  // Later, change the tags sent with the next message:
  window.opini.setTags(["web", "checkout"])
</script>

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, etwa device.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?“

http
GET /api/v1/projects/{project_id}/feedback?tag=bug,crash&context_key=platform&context_value=steam

Antworten und Fehler

StatusFehlerWas tun
200keinerGespeichert.
400missing_originEin 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.
400invalid_text, invalid_email, context_too_largeDer Text muss 1 bis 4.000 Zeichen lang sein, die E-Mail eine echte Adresse ohne Wegwerf-Anbieter, und der Kontext unter 16 KB.
401invalid_keyDer Schlüssel ist falsch oder wurde widerrufen.
403origin_not_allowedDie Anfrage kam von einer Website, die nicht in den erlaubten Origins des Schlüssels steht.
422tags_not_allowedFüge die genannten Tags dem Projekt oder den erlaubten Tags des Schlüssels hinzu, oder sende sie nicht mehr.
429rate_limited, daily_cap_exceededZu 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.