API REST et webhooks
Une seule API derrière tout : l'inbox, Telegram, Discord, le serveur MCP et la CLI en sont tous des clients.
Principes
- Adresse de base :
https://app.inngame.app/v1(l'anciennehttps://inngame.meffysto.workers.dev/v1marche toujours) - Authentification :
Authorization: Bearer ig_sk_…(une clé d'API). La clé détermine l'organisation. - Format : JSON en entrée et en sortie. Les dates sont en millisecondes Unix. Les identifiants sont des chaînes préfixées (
cnv_…,msg_…,eu_…pour les joueurs,bug_…). - Erreurs : un statut hors 2xx avec
{"error": "code", "message": "…"}. Codes courants :invalid_api_key(401),requires_reply/requires_admin(403),human_approval_required/human_session_required(403 : réservé à une personne connectée, quel que soit le droit de la clé),conversation_not_found,bug_not_found,app_not_found(404 ; aussi pour tout ce qui appartient à une autre organisation),invalid_request(422). - Droits :
read<reply<all. Dans les tableaux ci-dessous,allcorrespond aux droits d'admin. Chaque écriture est notée dans le journal d'actions. - Limites : 300 requêtes par minute par clé d'API ; pour les joueurs, 20 messages ou envois de fichiers et 60 événements par minute. Au-delà, vous recevez
429 rate_limited: attendez une minute et réessayez. - Pagination : les listes renvoient jusqu'à 50 éléments (100 au plus avec
limit) et un curseur : passeznextBeforecommebeforepour les conversations, ou le plus ancienseqcommebeforepour les messages.
curl "https://app.inngame.app/v1/conversations?status=open&unread=1" \
-H "Authorization: Bearer $INNGAME_API_KEY"
Conversations et messages
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| GET | /conversations | read | Filtres : status (open, closed, snoozed, all), app, unread=1, assignee (me, none, identifiant d'utilisateur), tag, lang (langue du joueur : pt trouve aussi pt-BR ; und = inconnue), q (nom, e-mail, SteamID, texte des messages, traductions), before, limit. Les plus urgentes d'abord. |
| GET | /conversations/:id | read | La conversation, ses 50 derniers messages (avec traductions, contexte, brouillons, notes), la fiche du joueur (customer), les bugs sous lesquels elle est rangée (bugs : id, title, status, fixedInVersion, players) et hasMore. |
| GET | /conversations/:id/messages | read | Messages plus anciens : ?before=<seq>. |
| POST | /conversations/:id/messages | reply | {body, note?, attachments?, card?, lang?}. Traduit pour le joueur. note: true pour une note interne. Rouvre une conversation fermée. |
| POST | /conversations/:id/drafts/:msgId/approve | reply | Envoyer un brouillon de l'IA, éventuellement avec un body retouché. |
| DELETE | /conversations/:id/drafts/:msgId | reply | Jeter un brouillon de l'IA. |
| POST | /conversations/:id/messages/:msgId/retract | reply | Retirer un message envoyé au joueur (équipe, IA ou automatisation). Il quitte aussitôt le chat du joueur et reste barré pour l'équipe. Un objet déjà récupéré ne peut pas être repris (409 card_already_claimed). Une clé d'API peut retirer les messages de l'IA, des automatisations et des agents, pas ceux d'une personne. |
| POST | /conversations/:id/ai-suggest | reply | Faire écrire tout de suite un brouillon par l'IA intégrée, quel que soit le mode de l'IA (rien ne part sans validation). Renvoie la decision de l'IA (reply ou handoff). |
| PATCH | /conversations/:id | reply | {status, snoozeUntil, assigneeId, aiMode, priority}. aiMode : inherit, auto, draft, off. priority : de 0 à 2. snoozeUntil (ms) est obligatoire avec status: "snoozed". Régler auto (ou inherit quand l'app répond seule) demande une personne connectée : les clés d'API reçoivent 403 human_approval_required. |
| POST | /conversations/:id/read | read | Marquer comme lue par l'équipe. |
| POST | /conversations/:id/tags | reply | {name} |
| DELETE | /conversations/:id/tags/:name | reply | |
| GET | /tags | read | Les étiquettes et leur nombre d'utilisations. |
| GET | /saved-replies | read | Réponses enregistrées ; POST, PATCH et DELETE demandent reply. |
| POST | /uploads | reply | file en multipart ; renvoie une pièce jointe à mettre dans attachments. |
| GET | /stats | read | days (365 au plus), app : volumes, délai de première réponse, part de l'IA, satisfaction, séries par jour, conversations par langue du joueur (byLanguage). |
Cartes
Ajoutez une card à un message pour afficher un bouton dans le chat :
{
"body": "Sorry about that! Here's a little something.",
"card": {
"kind": "item", // review | offer | link | screen | item | refund | restore
"label": "Claim",
"item": { "id": "gems_500", "name": "500 gems", "quantity": 1 }
}
}
Autres champs : url (lien, repli pour l'avis), screen et value (écran), offerId (offre). Une carte item ne se récupère qu'une fois ; vous recevez un webhook item.claimed pour donner l'objet sur votre serveur. Seule une personne connectée peut envoyer une carte item : les clés d'API reçoivent 403 human_approval_required.
Omettez label sur les cartes review et refund pour afficher le texte du SDK dans la langue du joueur (« Laisser un avis », « Demander un remboursement »). Les cartes envoyées par Inngame lui-même portent le libellé anglais par défaut plus "defaultLabel": true ; les SDK 0.2.2 et suivants le remplacent par le texte traduit.
Joueurs
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| GET | /users | read | app, q, before. Vus le plus récemment d'abord. |
| GET | /users/:id | read | Fiche, appareils, métadonnées (dont le statut d'abonnement venu de votre intégration d'abonnements) et conversations. |
| PATCH | /users/:id | reply | {teamName, blocked}. teamName est un nom que seule votre équipe voit. |
| POST | /users/:id/messages | reply | {body, card?}. Écrire en premier à un joueur : démarre sa conversation, ou poursuit la conversation en cours. Renvoie conversationId. |
| GET | /users/:id/export | read | Tout ce qui est conservé sur le joueur dans un fichier JSON, pour les demandes d'accès : fiche, appareils, conversations avec messages, traductions, contexte et étiquettes, signalements de bugs. Photos sous forme de liens valables 7 jours. Aucun jeton. Noté user.export dans le journal. |
| DELETE | /users/:id | human admin | Supprimer le joueur, ses conversations et ses fichiers. Les clés d'agent reçoivent 403 human_approval_required. |
Apps, organisation et clés
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| GET | /org | read | Organisation, formule, fournisseur d'IA, consommation du mois, et qui vous êtes (nom et droit de la clé). |
| PATCH | /org | admin humain | {name, aiProvider, aiKey, aiModel, locale} : voir votre propre clé ; locale (en ou fr) est la langue de Telegram, Discord et des notes d'Inngame. Les clés d'API reçoivent 403 human_approval_required. |
| DELETE | /org | propriétaire, connecté | {confirm} avec le nom exact de l'organisation. Supprime tout ce que contient l'organisation (apps, joueurs, conversations, fichiers, clés, intégrations) et résilie son abonnement. Impossible avec une clé d'API. |
| GET | /apps | read | Les apps avec leurs clés publiques et leurs réglages. |
| GET | /apps/:id | read | Une app. Le secret d'identité n'est inclus que pour les admins. |
| POST | /apps | all | Créer une app. |
| PATCH | /apps/:id | all | Renommer ou changer settings (fusion en profondeur) : mode et pause de l'IA, traduction, modération, apparence (textes ; couleurs light et dark en #RRGGBB : accent, background, text, header, incoming, outgoing), identifiants des boutiques pour les avis, vérifiés seulement, retention.closedDays (0 = garder pour toujours, sinon de 7 à 3650), reviews.offerOnResolve (envoyer la carte d'avis sur la boutique à chaque joueur dont la conversation est fermée, quelle que soit sa note de satisfaction ; une fois par joueur, jamais en mode mineurs), csat.enabled (demander aux joueurs de noter les conversations fermées de 1 à 5, pour vos statistiques seulement)… Modération, identité et conservation demandent un admin humain. |
| DELETE | /apps/:id | admin humain | Supprime l'app avec ses joueurs, ses conversations et ses fichiers. |
| POST | /apps/:id/rotate-identity-secret | admin humain | {unverify?} : true marque aussi non vérifiés les joueurs vérifiés par l'ancien secret (ceux vérifiés par Steam le restent). Renvoie le nouveau identitySecret. |
| POST | /apps/:id/logo | all | Envoyer le logo du chat : champ multipart file, PNG ou JPEG, 512 Ko au plus. Renvoie {logoUrl}, une adresse publique qui change à chaque envoi (à mettre en cache sans limite). DELETE le retire. Les joueurs le reçoivent sous appearance.logoUrl dans la config du SDK. |
| GET | /members | read | Membres de l'équipe et invitations en attente. |
| POST | /invitations | admin humain | {email, role}, rôle admin ou member. Envoie l'invitation par e-mail (se connecter avec cette adresse fait rejoindre l'organisation ; expire au bout de 14 jours). Renvoie {id, link, emailed} : partagez link vous-même si emailed vaut false. DELETE /invitations/:id l'annule. |
| PATCH | /members/:userId | admin humain | {role}. DELETE /members/:userId retire un membre (chacun peut se retirer lui-même). Une organisation garde toujours un admin : si le dernier part, le membre le plus ancien devient admin. |
| GET | /api-keys | admin humain | POST {name, scopes} crée une clé (affichée une fois) ; DELETE la révoque. Les clés d'agent ne peuvent pas gérer les clés. |
| GET | /audit | all | Journal d'actions : app, actor, target, before. Chaque entrée indique si elle est undoable ou déjà undone. |
| POST | /audit/:id/undo | admin, connecté | Annuler une entrée : retirer les messages envoyés (réponses, réponses de l'IA, cartes, automatisations), retirer l'étiquette, remettre la conversation comme avant, ou débloquer le joueur. Une fois par entrée. Impossible avec une clé d'API. |
| GET | /billing | read | Formule, fin de l'essai, consommation et quota. Le paiement (POST /billing/checkout) et le portail de facturation (POST /billing/portal) sont réservés aux admins humains connectés. |
| GET | /apps/:id/integrations | read | Les intégrations branchées. Les membres connectés et les clés all reçoivent aussi leurs réglages non secrets (groupe Telegram, salon Discord, dépôt GitHub…) ; les clés read et reply voient seulement lesquelles sont branchées. Les brancher, les mettre en pause et les retirer est réservé aux admins humains connectés : voir Intégrations. |
Bugs
Les mêmes bugs que sur la page Bugs de l'inbox. Chaque bug a une url vers cette page, que vos coéquipiers peuvent ouvrir depuis Telegram, Discord, le point hebdomadaire ou le résumé d'un agent.
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| GET | /bugs | read | app, status (open, fixed, released, wontfix ; séparés par des virgules pour en prendre plusieurs), q (recherche), sort (players, newest, recent), limit (200 au plus). Chaque bug vient avec reports (joueurs), reports24h, notified, firstSeen, lastSeen, versions, platforms et url ; plus counts par statut. |
| GET | /bugs/counts | read | {open, fixed, closed, all}, app facultatif. |
| GET | /bugs/:id | read | Le bug, les joueurs sur 30 jours (timeline), la répartition (breakdown) par version, plateforme, appareil et OS, les versions actuelles des joueurs, des extraits de journaux avec la dernière erreur (samples), les conversations liées (reports), si GitHub est branché, et un aperçu du corps du ticket. |
| POST | /bugs | reply | {conversationId, bugId} pour lier une conversation, ou {conversationId, title, summary} pour créer un bug. |
| PATCH | /bugs/:id | reply | {title, summary, status, fixedInVersion}. Renseigner fixedInVersion prévient les joueurs qui l'ont signalé et ont déjà cette version, et les autres quand ils mettent à jour. Renvoie notified. |
| POST | /bugs/:id/github-issue | reply | Ouvre le ticket dans le dépôt branché. Renvoie url. |
| POST | /bugs/:id/merge | reply | {into} : déplace les signalements vers un autre bug de la même app et supprime celui-ci. Un joueur présent dans les deux n'est compté qu'une fois. |
| POST | /bugs/:id/request-fix | all | Commente le ticket pour demander une PR à l'agent de code du dépôt (@claude par défaut). Ne fusionne jamais. |
Remboursements
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| POST | /conversations/:id/refund-proposal | reply | {provider, reference, amountCents?, reason}, provider : stripe, googleplay, apple. Laisse une note pour un admin ; renvoie 202 awaiting_human_approval. |
| POST | /conversations/:id/refund | human admin | Les clés d'API reçoivent 403 human_approval_required. Stripe {paymentId, amountCents?}, Google Play {orderId, revoke}, Apple {transactionId} (envoie la feuille de remboursement d'Apple au joueur). |
FAQ et automatisations
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| GET | /apps/:id/faq | read | Articles. |
| POST | /apps/:id/faq | reply | {title, body, published}. PATCH et DELETE sur /apps/:id/faq/:articleId. |
| POST | /apps/:id/faq/import | all | {url, replace} : une page https ou un sitemap (30 pages au plus). |
| POST | /apps/:id/faq/generate | all | Écrit jusqu'à 15 brouillons d'articles non publiés à partir des conversations passées. |
| GET | /apps/:id/automations | read | Les automatisations avec leurs exécutions sur 30 jours, les événements d'abonnement et des événements de jeu suggérés. |
| POST | /apps/:id/automations | all | Créer. Renvoie l'extrait Track pour Unity et Swift. PUT et DELETE sur /apps/:id/automations/:automationId. Une automatisation qui donne un objet ne peut être créée, modifiée ou réactivée que par une personne connectée (les clés d'API reçoivent 403 human_approval_required), et elle n'envoie l'objet qu'une fois par joueur. |
Une automatisation a un triggerEvent (un événement du jeu ou un événement d'abonnement comme trial_started, subscription_cancelled, billing_issue), une frequency, une audience (règles sur les métadonnées du joueur), un delay (now, minutes ou local_time) et des actions : message, card (offre, lien, écran, objet), review, open_chat, wait.
Routes du SDK
Les SDK appellent /v1/sdk/* avec la clé publique de l'app dans X-App-Key et un jeton d'appareil en bearer. Ne les utilisez directement que si vous écrivez votre propre client (un jeu web, un autre moteur).
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| POST | /sdk/session | app key | {device, identity?}. Renvoie un token d'appareil (à garder), le joueur, l'identifiant de conversation, le nombre de non-lus et la config à distance. La config contient availability: {online, replyTime, nextOpenAt} : replyTime vaut "minutes", "hours", "day" ou null (historique trop court, ou masqué) ; nextOpenAt (ms Unix) est renseigné quand l'équipe est hors de ses horaires. |
| GET | /sdk/config | app key | La config à distance seule (apparence, logo, annonce, disponibilité, FAQ, identifiants des boutiques), ?lang= pour les textes traduits. Les SDK la rechargent quand l'équipe change les réglages de l'app. |
| POST | /sdk/identify | device | {userId, name, email, token, steamTicket, metadata} |
| GET | /sdk/messages | device | ?before=<seq> |
| POST | /sdk/messages | device | {body, clientId, attachments, context}. clientId rend les nouvelles tentatives sûres ; contexte de 64 Ko au plus. |
| POST | /sdk/uploads | device | file en multipart. Les images sont modérées ; celles refusées renvoient 422 photo_rejected et ne sont jamais stockées. |
| POST | /sdk/read | device | Marquer les messages de l'équipe comme lus. |
| GET | /sdk/unread | device | Nombre de non-lus (pour l'interrogation périodique). |
| POST | /sdk/push | device | {token, kind}, kind : apns, apns_sandbox, fcm. null désinscrit. |
| POST | /sdk/events | device | {name, props, opensChat}. Peut renvoyer openChat: true. |
| POST | /sdk/cards/:messageId/action | device | {action, nonce?, rating?} : tapped, failed, claimed, dismissed, rated. Pour rated (cartes de satisfaction seulement), envoyez rating de 1 à 5 ; seule la première note compte et la réponse la renvoie. Pour claimed, envoyez un nonce aléatoire et gardez-le pour les nouvelles tentatives : la réponse n'a granted: true que pour la demande qui l'a emporté (ou sa nouvelle tentative avec le même nonce). Ne donnez rien sinon. 404 si l'équipe a retiré la carte. |
| GET | /sdk/faq | app key | ?q= cherche dans les articles publiés. |
| POST | /sdk/logout | device | L'appareil continue avec un nouveau joueur anonyme. |
| POST | /sdk/delete-data | device | Effacer le joueur. |
| GET | /sdk/ws | device | WebSocket pour les messages en temps réel (app_key et token en paramètres de requête). |
Webhooks
Les webhooks envoient des événements à votre serveur : donner les objets récupérés, garder une trace des remboursements, prévenir quelqu'un lors d'une vague de bugs. Gérez-les dans l'inbox (Réglages → Webhooks) ou par l'API, connecté en admin. Les clés d'agent ne peuvent ni créer ni lire de webhooks, même avec le droit all : un webhook peut envoyer chaque message vers n'importe quelle adresse, c'est donc une personne qui le met en place.
| Méthode | Chemin | Droit | Remarques |
|---|---|---|---|
| GET | /webhooks | admin | Vos webhooks, l'état du dernier envoi et la liste des événements. |
| POST | /webhooks | admin | {url, appId?, events?}. https uniquement. events vaut ["*"] par défaut. Renvoie le secret (whsec_…) une seule fois. |
| POST | /webhooks/:id/test | admin | Envoie un événement ping et renvoie le résultat. |
| DELETE | /webhooks/:id | admin |
Chaque envoi est un POST avec un corps JSON, un user-agent Inngame-Webhooks/1 et un en-tête inngame-signature. Répondez en 2xx sous 8 secondes. Les envois ratés (erreurs réseau, délais dépassés, 408, 429 et 5xx) sont retentés après environ 1 et 5 minutes, puis 30 minutes, 2, 6 et 12 heures : sept tentatives sur à peu près une journée. Un autre 4xx vaut refus et n'est pas retenté. Les nouvelles tentatives gardent le même id d'événement : rendez votre traitement idempotent. Le dernier résultat figure sur le webhook (last_status, last_error, tentatives pending). POST /webhooks/:id/test n'envoie un ping qu'à ce webhook.
{
"id": "evt_8sd7f6g5h4j3k2l1m0n9",
"type": "item.claimed",
"created": 1793452800000,
"appId": "app_...",
"data": {
"messageId": "msg_...",
"conversationId": "cnv_...",
"player": { "id": "eu_...", "externalId": "player-1842", "steamId": null },
"card": { "kind": "item", "label": "Claim", "item": { "id": "gems_500", "name": "500 gems", "quantity": 1 }, "claimedAt": 1793452800000 },
"action": "claimed"
}
}
Liste des événements
| Événement | Quand | data |
|---|---|---|
message.created | Un message est envoyé dans une conversation (joueur, équipe ou IA ; pas les brouillons ni les notes). | conversationId, playerId, message |
item.claimed | Un joueur récupère une carte d'objet (une fois par carte). | messageId, conversationId, player, card, action |
card.action | Un joueur touche, ferme ou n'arrive pas à ouvrir toute autre carte. | Comme ci-dessus. |
refund.issued | Un admin a remboursé ou envoyé la feuille de remboursement Apple. | conversationId, playerId, provider, id, status |
bug.spike | Un bug atteint 5 joueurs en 24 heures (une fois par bug). | bugId, title, reports24h, url |
conversation.closed | Une conversation est fermée (inbox, API, Telegram ou Discord). | conversationId, playerId, by |
player.blocked | Un joueur est bloqué. | playerId, by |
message.retracted | L'équipe a retiré un message. | conversationId, messageId, playerId, by |
conversation.rated | Un joueur a noté une conversation fermée (satisfaction activée). | conversationId, messageId, rating (de 1 à 5), player |
digest.weekly | Le point du lundi sur la semaine écoulée (appId vaut null : les webhooks limités à une app ne le reçoivent pas). | apps[], suggestions[] |
ping | Envoyé par la route de test. | hello |
Vérifier les signatures
L'en-tête ressemble à ceci :
inngame-signature: t=1793452800,v1=pZ3f…Kq8
test l'heure Unix en secondes de l'envoi de l'événement.v1est le HMAC-SHA256 de la chaînet + "." + corps brut, avec votre secret de webhook comme clé, encodé en base64url (sans remplissage).- Calculez-le sur le corps brut de la requête, avant tout parsing JSON. Comparez en temps constant, et refusez les événements dont le
tdate de plus de 5 minutes.
// Node.js (Express)
import crypto from "node:crypto";
app.post("/inngame-webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("inngame-signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
const body = req.body.toString("utf8");
const expected = crypto
.createHmac("sha256", process.env.INNGAME_WEBHOOK_SECRET)
.update(`${t}.${body}`)
.digest("base64url");
const ok =
parts.v1 &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - t) < 300;
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(body);
if (event.type === "item.claimed") grantItem(event.data.player, event.data.card.item);
res.sendStatus(200);
});
# Python
import base64, hashlib, hmac, time
def verify(header: str, body: bytes, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = parts.get("t", "")
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).digest()
expected = base64.urlsafe_b64encode(mac).rstrip(b"=").decode()
return hmac.compare_digest(parts.get("v1", ""), expected) and abs(time.time() - int(t)) < 300