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

curl "https://app.inngame.app/v1/conversations?status=open&unread=1" \
  -H "Authorization: Bearer $INNGAME_API_KEY"

Conversations et messages

MéthodeCheminDroitRemarques
GET/conversationsreadFiltres : 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/:idreadLa 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/messagesreadMessages plus anciens : ?before=<seq>.
POST/conversations/:id/messagesreply{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/approvereplyEnvoyer un brouillon de l'IA, éventuellement avec un body retouché.
DELETE/conversations/:id/drafts/:msgIdreplyJeter un brouillon de l'IA.
POST/conversations/:id/messages/:msgId/retractreplyRetirer 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-suggestreplyFaire é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/:idreply{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/readreadMarquer comme lue par l'équipe.
POST/conversations/:id/tagsreply{name}
DELETE/conversations/:id/tags/:namereply
GET/tagsreadLes étiquettes et leur nombre d'utilisations.
GET/saved-repliesreadRéponses enregistrées ; POST, PATCH et DELETE demandent reply.
POST/uploadsreplyfile en multipart ; renvoie une pièce jointe à mettre dans attachments.
GET/statsreaddays (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éthodeCheminDroitRemarques
GET/usersreadapp, q, before. Vus le plus récemment d'abord.
GET/users/:idreadFiche, appareils, métadonnées (dont le statut d'abonnement venu de votre intégration d'abonnements) et conversations.
PATCH/users/:idreply{teamName, blocked}. teamName est un nom que seule votre équipe voit.
POST/users/:id/messagesreply{body, card?}. Écrire en premier à un joueur : démarre sa conversation, ou poursuit la conversation en cours. Renvoie conversationId.
GET/users/:id/exportreadTout 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/:idhuman adminSupprimer le joueur, ses conversations et ses fichiers. Les clés d'agent reçoivent 403 human_approval_required.

Apps, organisation et clés

MéthodeCheminDroitRemarques
GET/orgreadOrganisation, formule, fournisseur d'IA, consommation du mois, et qui vous êtes (nom et droit de la clé).
PATCH/orgadmin 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/orgproprié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/appsreadLes apps avec leurs clés publiques et leurs réglages.
GET/apps/:idreadUne app. Le secret d'identité n'est inclus que pour les admins.
POST/appsallCréer une app.
PATCH/apps/:idallRenommer 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/:idadmin humainSupprime l'app avec ses joueurs, ses conversations et ses fichiers.
POST/apps/:id/rotate-identity-secretadmin 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/logoallEnvoyer 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/membersreadMembres de l'équipe et invitations en attente.
POST/invitationsadmin 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/:userIdadmin 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-keysadmin humainPOST {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/auditallJournal d'actions : app, actor, target, before. Chaque entrée indique si elle est undoable ou déjà undone.
POST/audit/:id/undoadmin, 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/billingreadFormule, 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/integrationsreadLes 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éthodeCheminDroitRemarques
GET/bugsreadapp, 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/countsread{open, fixed, closed, all}, app facultatif.
GET/bugs/:idreadLe 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/bugsreply{conversationId, bugId} pour lier une conversation, ou {conversationId, title, summary} pour créer un bug.
PATCH/bugs/:idreply{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-issuereplyOuvre le ticket dans le dépôt branché. Renvoie url.
POST/bugs/:id/mergereply{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-fixallCommente le ticket pour demander une PR à l'agent de code du dépôt (@claude par défaut). Ne fusionne jamais.

Remboursements

MéthodeCheminDroitRemarques
POST/conversations/:id/refund-proposalreply{provider, reference, amountCents?, reason}, provider : stripe, googleplay, apple. Laisse une note pour un admin ; renvoie 202 awaiting_human_approval.
POST/conversations/:id/refundhuman adminLes 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éthodeCheminDroitRemarques
GET/apps/:id/faqreadArticles.
POST/apps/:id/faqreply{title, body, published}. PATCH et DELETE sur /apps/:id/faq/:articleId.
POST/apps/:id/faq/importall{url, replace} : une page https ou un sitemap (30 pages au plus).
POST/apps/:id/faq/generateallÉcrit jusqu'à 15 brouillons d'articles non publiés à partir des conversations passées.
GET/apps/:id/automationsreadLes 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/automationsallCré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éthodeCheminDroitRemarques
POST/sdk/sessionapp 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/configapp keyLa 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/identifydevice{userId, name, email, token, steamTicket, metadata}
GET/sdk/messagesdevice?before=<seq>
POST/sdk/messagesdevice{body, clientId, attachments, context}. clientId rend les nouvelles tentatives sûres ; contexte de 64 Ko au plus.
POST/sdk/uploadsdevicefile en multipart. Les images sont modérées ; celles refusées renvoient 422 photo_rejected et ne sont jamais stockées.
POST/sdk/readdeviceMarquer les messages de l'équipe comme lus.
GET/sdk/unreaddeviceNombre de non-lus (pour l'interrogation périodique).
POST/sdk/pushdevice{token, kind}, kind : apns, apns_sandbox, fcm. null désinscrit.
POST/sdk/eventsdevice{name, props, opensChat}. Peut renvoyer openChat: true.
POST/sdk/cards/:messageId/actiondevice{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/faqapp key?q= cherche dans les articles publiés.
POST/sdk/logoutdeviceL'appareil continue avec un nouveau joueur anonyme.
POST/sdk/delete-datadeviceEffacer le joueur.
GET/sdk/wsdeviceWebSocket 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éthodeCheminDroitRemarques
GET/webhooksadminVos webhooks, l'état du dernier envoi et la liste des événements.
POST/webhooksadmin{url, appId?, events?}. https uniquement. events vaut ["*"] par défaut. Renvoie le secret (whsec_…) une seule fois.
POST/webhooks/:id/testadminEnvoie un événement ping et renvoie le résultat.
DELETE/webhooks/:idadmin

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énementQuanddata
message.createdUn message est envoyé dans une conversation (joueur, équipe ou IA ; pas les brouillons ni les notes).conversationId, playerId, message
item.claimedUn joueur récupère une carte d'objet (une fois par carte).messageId, conversationId, player, card, action
card.actionUn joueur touche, ferme ou n'arrive pas à ouvrir toute autre carte.Comme ci-dessus.
refund.issuedUn admin a remboursé ou envoyé la feuille de remboursement Apple.conversationId, playerId, provider, id, status
bug.spikeUn bug atteint 5 joueurs en 24 heures (une fois par bug).bugId, title, reports24h, url
conversation.closedUne conversation est fermée (inbox, API, Telegram ou Discord).conversationId, playerId, by
player.blockedUn joueur est bloqué.playerId, by
message.retractedL'équipe a retiré un message.conversationId, messageId, playerId, by
conversation.ratedUn joueur a noté une conversation fermée (satisfaction activée).conversationId, messageId, rating (de 1 à 5), player
digest.weeklyLe point du lundi sur la semaine écoulée (appId vaut null : les webhooks limités à une app ne le reçoivent pas).apps[], suggestions[]
pingEnvoyé par la route de test.hello

Vérifier les signatures

L'en-tête ressemble à ceci :

inngame-signature: t=1793452800,v1=pZ3f…Kq8
// 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