Aller au contenu

Envoyer des retours via l'API

Le widget n'est pas la seule porte d'entrée. Tout ce qui peut faire une requête HTTP peut envoyer des retours à Opini, avec vos propres tags (bug, idée, facturation) et tout le contexte que vous voulez voir à côté du message.

À quoi ça sert

  • Votre propre formulaire. Vous avez déjà un formulaire ou un écran dans l'app et voulez les messages dans Opini.
  • Votre backend et vos outils de support. Transférez les messages d'une boîte de support, d'un chatbot, des avis des stores ou d'un questionnaire de résiliation.
  • Jeux, apps mobiles et de bureau. Envoyez des retours directement depuis l'app, avec la plateforme, le build ou le niveau.
  • Scripts et imports. Importez des retours collectés ailleurs, ou envoyez-les depuis un outil en ligne de commande.

Quelle clé utiliser

  • Depuis votre serveur ou un script : une clé privée (sk_live_…), envoyée dans l'en-tête Authorization. Elle peut définir n'importe quel tag du projet. Gardez-la sur le serveur ; ne la mettez jamais dans une app téléchargeable ni dans une page web.
  • Depuis une app que les gens installent (jeu, mobile, bureau) : une clé live (pk_live_…) avec Apps et jeux activé, envoyée dans le corps. Les clés live sont faites pour être livrées dans votre app, et vous choisissez les tags qu'elles peuvent définir.
  • Sur un site web : le widget avec une clé live et l'adresse de votre site dans ses origines autorisées.

Créer la clé

Créez d'abord les tags que vous enverrez dans Paramètres du projet → Tags, par exemple bug, idée et facturation. Puis ouvrez Paramètres du projet → Clés d'API et cliquez sur Créer une clé.

Pour votre serveur ou un script :

  1. Choisissez Privée et donnez-lui un nom, comme boite-support.
  2. Copiez la clé quand elle s'affiche. Elle n'est montrée qu'une fois ; régénérez-la si vous la perdez.

Pour une app que les gens installent :

  1. Choisissez Publique et donnez-lui un nom, comme app-ios ou build-steam.
  2. Activez Apps et jeux. Vous pouvez laisser les origines autorisées vides si seule votre app utilise la clé.
  3. Dans Tags que cette clé peut définir, choisissez les tags que les gens peuvent envoyer. Laissez tout décoché si l'app ne doit pas définir de tags.
  4. Fixez un plafond quotidien adapté au nombre d'utilisateurs de l'app.

Utilisez une clé par app, plateforme ou intégration. Vous pourrez en régénérer ou en révoquer une sans toucher aux autres, et le tableau des clés indique quand chacune a servi pour la dernière fois.

Envoyer un retour

Envoyez un POST à https://opini.dev/api/v1/ingest avec un corps JSON. Une clé privée va dans l'en-tête Authorization: Bearer sk_live_… ; une clé live va dans le corps, en 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
}

Une requête complète :

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"
    }
  }'

Un 200 signifie que le retour est enregistré et déjà dans votre liste Feedback :

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

Exemples

Depuis votre backend, avec une clé privée (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 },
  }),
})

Depuis une app de bureau ou mobile, avec une clé live (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() },
  }),
})

Depuis un jeu, avec une clé live (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}");
    }
}

Depuis un jeu, avec une clé live (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

Les tags servent à trier ce qui arrive : vous pouvez filtrer la liste Feedback avec. Envoyez des noms de tags ; la casse n'a pas d'importance, donc "Bug" et "bug" sont le même tag.

  • Chaque tag doit déjà exister dans le projet. Envoyer un tag ne le crée jamais.
  • Une clé live ne peut utiliser que les tags de sa liste. Une clé privée peut utiliser tous les tags du projet.
  • Jusqu'à 10 tags par message.
  • Si un tag n'est pas autorisé, rien n'est enregistré et la réponse est une 422 qui nomme ces tags : vous le voyez pendant le développement au lieu de perdre des retours sans le savoir :
http
HTTP/1.1 422 Unprocessable Entity

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

Réservez les tags aux quelques critères de filtre et de tri, comme bug, idée, facturation ou crash. Si votre formulaire demande le type de retour, envoyez ce choix comme tag. Tout le reste va dans le contexte.

Sur un site, le widget envoie aussi des tags. Définissez-les sur la balise script ou changez-les depuis votre page :

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>

Contexte

context est n'importe quel objet JSON jusqu'à 16 Ko. Opini n'impose pas de forme : chaque clé apparaît sous Contexte quand vous ouvrez le message, et vous pouvez filtrer sur chacune.

  • Depuis un backend : source, plan, account_id, ticket.
  • Depuis une app ou un jeu : platform, version, build, device, level.
  • Restez à plat quand c'est possible ({"platform": "steam"}). Les objets imbriqués fonctionnent aussi ; on les filtre avec des points, comme device.gpu.
  • Utilisez les mêmes noms de clés partout où vous envoyez, pour qu'un seul filtre retrouve tous les messages.
  • N'envoyez pas de mots de passe, de jetons ni rien que les gens ne s'attendraient pas à vous voir garder. Votre équipe peut lire toutes les valeurs.

Les retrouver dans Opini

Dans la liste Feedback, le filtre Tag affiche les messages portant l'un des tags choisis, et le filtre Contexte ceux dont le contexte a une clé avec une valeur, comme platform = steam ou level = 3. Les valeurs sont comparées comme du texte, donc 3 correspond au nombre 3. Les filtres se combinent entre eux et avec les filtres d'état et de date, et restent dans l'adresse pour partager la vue.

Les mêmes filtres fonctionnent dans l'API et dans votre outil d'IA. L'outil MCP list_feedback accepte tags, context_key et context_value : vous pouvez demander « quels bugs les clients business ont-ils signalés cette semaine ? ».

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

Réponses et erreurs

StatutErreurQue faire
200aucuneEnregistré.
400missing_originUne clé live a été utilisée hors d'un site web et n'a pas Apps et jeux activé. Activez-le, ou envoyez depuis votre serveur avec une clé privée.
400invalid_text, invalid_email, context_too_largeLe texte doit faire de 1 à 4 000 caractères, l'e-mail doit être une vraie adresse non jetable, et le contexte doit faire moins de 16 Ko.
401invalid_keyLa clé est incorrecte ou a été révoquée.
403origin_not_allowedLa requête vient d'un site qui ne figure pas dans les origines autorisées de la clé.
422tags_not_allowedAjoutez les tags indiqués au projet ou aux tags autorisés de la clé, ou cessez de les envoyer.
429rate_limited, daily_cap_exceededTrop de messages. Patientez puis réessayez ; pour le plafond quotidien, Retry-After indique quand il se réinitialise (minuit UTC).

Si une requête échoue parce que l'appareil est hors ligne, gardez le message et envoyez-le au prochain démarrage de l'app.

Protéger vos clés

Une clé privée peut envoyer des retours avec n'importe quel tag : gardez-la sur votre serveur, dans une variable d'environnement ou un coffre à secrets, et régénérez-la en cas de fuite.

Tout ce qui est livré dans une app peut être lu par quelqu'un de déterminé : considérez une clé live comme publique. C'est pourquoi elle ne peut qu'envoyer des retours, et seulement avec les tags autorisés. Pour limiter les abus :

  • Fixez un plafond quotidien proche de ce que vous attendez. Les messages en trop reçoivent une 429 au lieu de remplir votre liste.
  • N'autorisez que les tags que les gens doivent choisir. Gardez les tags internes hors de la clé.
  • Si une clé est détournée, régénérez-la et livrez la nouvelle dans votre prochaine mise à jour. La révoquer l'arrête immédiatement.
  • Utilisez une clé distincte par app ou plateforme, pour qu'une fuite n'affecte pas le reste.