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êteAuthorization. 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 :
- Choisissez Privée et donnez-lui un nom, comme boite-support.
- 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 :
- Choisissez Publique et donnez-lui un nom, comme app-ios ou build-steam.
- Activez Apps et jeux. Vous pouvez laisser les origines autorisées vides si seule votre app utilise la clé.
- 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.
- 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.
{
"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 :
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 :
{ "feedback_public_id": "7k2m9qxa4f", "attachments": { "enabled": true } }Exemples
Depuis votre backend, avec une clé privée (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 },
}),
})Depuis une app de bureau ou mobile, avec une clé live (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() },
}),
})Depuis un jeu, avec une clé live (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}");
}
}Depuis un jeu, avec une clé live (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))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, commedevice.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 ? ».
GET /api/v1/projects/{project_id}/feedback?tag=bug,crash&context_key=platform&context_value=steamRéponses et erreurs
| Statut | Erreur | Que faire |
|---|---|---|
200 | aucune | Enregistré. |
400 | missing_origin | Une 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. |
400 | invalid_text, invalid_email, context_too_large | Le 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. |
401 | invalid_key | La clé est incorrecte ou a été révoquée. |
403 | origin_not_allowed | La requête vient d'un site qui ne figure pas dans les origines autorisées de la clé. |
422 | tags_not_allowed | Ajoutez les tags indiqués au projet ou aux tags autorisés de la clé, ou cessez de les envoyer. |
429 | rate_limited, daily_cap_exceeded | Trop 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.