Skip to content

SDK Appli Kappapps — mémo (API V2 réelle)

Tu écris une appli : un package web autonome (jeu ou outil) qu'un streamer installe sur son channel. Contrairement à un widget (éphémère, déclenché par un événement), une appli est persistante : elle est lancée par le streamer et vit jusqu'à ce qu'elle se termine elle-même (kapp.finish()) ou soit fermée.

Contrat d'exécution

  • index.html charge le SDK puis ton code. Pattern standard :
    html
    <script src="https://kappapps.app/overlay/app/appli-2.0.js"></script>
    <script>
      document.addEventListener('AppliLoaded', async () => {
        // injecte style.css + script.js (cache-busté tant que la version n'est pas soumise)
      });
    </script>
  • Ton script.js s'exécute après l'event AppliLoaded (le SDK est prêt, kapp existe). Forme habituelle : une IIFE (async function () { ... })();. Il n'y a PAS de window.executeAppFunction (ça, c'est le contrat widget — à ne pas confondre).
  • kapp.display() affiche la fenêtre de l'appli ; kapp.undisplay() la masque.
  • Fin explicite : await kapp.finish() termine proprement (envoie d'abord les rewards accumulées). kapp.finish(result) renvoie en plus une data à l'appli qui m'a lancé via kapp.apps.run (voir reference/app-to-app.md). kapp.abort('raison') interrompt. Hooks : kapp.onBeforeFinish(fn), kapp.onBeforeAbort(fn), kapp.onBeforeExit(status, fn).

Globaux injectés (NE PAS redéclarer)

  • kapp (alias k) — l'instance Appli, point d'entrée de tous les services. Immuable (Object.defineProperty non writable).
  • prefs — l'arbre complet des préférences streamer (= kapp.preferences.preferences). Lecture seule pratique, pas réactif.
  • text(key, args?) — i18n de l'appli (= kapp.textset.get(key, args)), résolu dans la langue du channel.
  • wait(ms)await wait(1000).
  • $ / jQuery.

Services propres à l'Appli (absents en widget)

kapp.<service>Rôle
kapp.preferencesConfig streamer persistante (get(path), set(changes)) ; global prefs
kapp.dataStorageÉtat global de l'appli (par app+channel) : get() / set(data) / delete()
kapp.userDataStorageÉtat par viewer : set(user_id, data) / reset(labels?) (ou user.getAppData() / user.setAppData())
kapp.logsStorageHistorique empilé : get(count?) / add(data) / clear()
kapp.rewardsRécompenses : winner(user) / loser(user) (envoyées au finish())
kapp.triggersCallbacks de préférences trigger persistantes : onTrigger(pref, callback) retourne un désabonnement local ; app déjà ouverte uniquement ; scope triggers:manage. Voir reference/persistent-triggers.md
kapp.dynamicTriggersTriggers à l'exécution : createFromConfig(config, cb?) conseillé (/v2, config manuelle ou WU dynamicTrigger) ; create(data) historique déprécié mais fonctionnel (/v1) ; remove(id) ; trigger.onTrigger(cb)
kapp.pluginPont overlay ↔ page viewers : emitViewers(users, ev, data) / emitStreamer(ev, data) / getViewersPluginUrl()
kapp.appsLance une appli oneshot (par slug) ou un widget (par id) et attend sa data → {resolved, data?, reason?}. Générique : run(target, args?, opts?)target = {type:'app', slug} ou {type:'widget', id} (forme produite par l'input de préférence runTarget, dispatch auto, invalid_target si malformé). Raccourcis : runApp(slug, …) / runWidget(id, …). Découverte : list() (unifié, tagué type), listApps(), listWidgets(). Args validés contre le schéma déclaré de la cible (type string_list dispo) ; appli : sortie déclarable via run-output/kind, unicité (busy) ; widget : id local non portable, sortie non typée, concurrence OK. Scope apps:run. Voir reference/app-to-app.md
kapp.textsetTextes traduits de l'appli : get(key, args)

Services partagés (identiques aux widgets — doc dans common/reference/)

kapp.chat, kapp.emotes, kapp.medias, kapp.announces, kapp.textToSpeech, kapp.users, kapp.twitch, kapp.poll / kapp.prediction, kapp.ai, kapp.dom, kapp.events, kapp.overlay.

« Je veux… → utilise »

ObjectifAPI
Lire une préférence streamerkapp.preferences.get('global.maClé') ou prefs.global.maClé
Persister l'état global de l'appliawait kapp.dataStorage.set({...}) / await kapp.dataStorage.get()
Persister l'état d'un viewerawait user.setAppData({...}) / await user.getAppData()
Empiler un historique (scores, logs)await kapp.logsStorage.add({...})
Récompenser des gagnants/perdantskapp.rewards.winner(user) / kapp.rewards.loser(user) puis kapp.finish()
Créer un trigger à l'exécution depuis une préférence streamerconst t = await kapp.dynamicTriggers.createFromConfig(prefs.viewerAction, callback) (t peut être null)
Créer manuellement un trigger avec le contrat conseilléconst t = await kapp.dynamicTriggers.createFromConfig({type: 'INTERVAL', title: 'Tick', interval_duration: 300})
Recevoir des arguments typés dans un callback dynamiqueEnrichir la config avec arguments: [{name:'Montant', key:'amount', type:'int', required:true}] ; lire request.args.amount, _raw, _config. Schéma propre au trigger, immuable, ajouté par le code. Player devient un User. Voir reference/dynamic-triggers.md.
Utiliser l'ancien contratconst t = await kapp.dynamicTriggers.create({...}) (déprécié, reste sur /v1)
Parler à la page des viewersawait kapp.plugin.emitViewers(null, 'event', {...})
Texte traduittext('maClé', { arg1: x })
Afficher / masquer l'applikapp.display() / kapp.undisplay()
Terminer l'appli (et payer les rewards)await kapp.finish()
Lister les apps oneshot appelables (+ leurs args)const apps = await kapp.apps.list()
Lancer une autre appli oneshot et attendre sa dataconst r = await kapp.apps.runApp('mon-slug', {…}); if (r.resolved) {…}
Laisser le streamer choisir la cible à lancerinput pref runTarget puis kapp.apps.run(prefs.cible) (voir reference/app-to-app.md)
Renvoyer une data à l'appli qui m'a lancéawait kapp.finish({ … })
Afficher les badges d'un viewer (abo tier/mois, modo, VIP…)await kapp.emotes.getBadges(ev) + ev.getSubscriberMonths() (voir common/reference/emotes.md)
Générer un QR code affichable dans une imageimg.src = await kapp.Utils.QRCode.toDataURL('https://…', { width: 280 }) (voir common/reference/qrcode.md)
Vérifier le sub/follow actuel d'un viewerawait user.getSubscriptionStatus() / await user.getFollowStatus() (live, scope channel-stats:read)
Laisser le streamer choisir un média (avec un son par défaut fourni)input pref mediaFile + default: { library: 'slug' } puis kapp.medias.playMedia(prefs.monSon) (voir reference/preferences.md)
Jouer un asset public Kappapps (soundboard fourni)await kapp.medias.playLibrary('slug', { volume: 70 }) (catalogue : kapp.medias.library())
Jouer un son / image / TTS / IA / chatservices partagés (voir common/reference/)
Lire/écrire les préférences en temps réel depuis un panneau custom (iframe sandboxée)window.KappappsPrefs dans un document HTML déclaré par prefs-panel-index (panneau global) ou wuOptions.source d'un WU customPanel (voir reference/prefs-panels.md)
Charger beaucoup d'images efficacementAction Créer un atlas dans Assets, puis await k.ImageAtlas.load('assets/atlas/game.json') (voir reference/image-atlases.md)

infos.json (manifeste de l'appli)

  • launch-mode : oneshot (lancée puis se termine), backtask (fond, sans UI obligatoire), external (popout interactive obligatoire, jamais overlay OBS).
  • externable : ajoute la popout interactive au mode overlay classique ; oneshot + externable: true autorise les deux.
  • Contexte courant : utilise kapp.overlay.isExternal(). La valeur brute displayMode vaut overlay ou external. Ne pas utiliser kapp.overlay.type === 'oneshot' : ce oneshot désigne le runtime popup historique, pas le launch-mode de l'app.
  • scopes : permissions Twitch requises (chat:read, chat:write, …).
  • services : services exposés par l'appli (voir common/reference/services.md).
  • run-output : format de sortie pour apps.run ({type, description}, types string/string_list/int/bool/object/void) ; kind : contrat standard clé-en-main (ex. pick-string). Voir reference/app-to-app.md.
  • app-index : index.html. textset : textset.js.
  • overlay-formats : ["16:9"] seulement si absent ; une liste explicite est respectée telle quelle, ["9:16"] seul est valide. Le format de la surface est choisi s'il est déclaré, sinon le premier de la liste. Déclarer seulement les formats adaptés et testés.
  • L'iframe remplit le cadre choisi (1920 × 1080 ou 1080 × 1920). window est accepté mais ignoré. Placez le contenu à l'intérieur, éventuellement via une préférence containerArea et kapp.overlay.resolveArea. kapp.overlay.layout et LAYOUT_CHANGED donnent les dimensions courantes. Lire reference/vertical.md.
  • viewers-plugin / streamer-plugin : pages d'extension (voir reference/plugins.md).
  • Validation agent : prepare_preview(surface: ...) → ouvrir immédiatement preview_url dans le navigateur/Playwright → attendre data-dev-preview-ready; open_managed_preview n'est que le repli. Pour un plugin, toujours garder le shell fourni, jamais l'iframe localhost brute.
  • viewers-optout : true si l'appli peut cibler un viewer sans qu'il l'ait demandé — il pourra alors se retirer lui-même, et devient invisible pour l'appli (voir reference/launch-modes.md).
  • prefs-panel / prefs-panel-index : document HTML custom exécuté dans le formulaire de préférences du streamer, pont window.KappappsPrefs (voir reference/prefs-panels.md).

preferences.config.js

Décrit l'arbre de préférences (groupes + items typés), compilé en config/preferences.config.json par le HUB (transpile). Réutilise des structures via modules/Model.js. Voir reference/preferences.md.

Exemple complet : WorkUnit dynamicTrigger → callback

js
// preferences.config.js
module.exports = {
  type: 'group',
  groupItems: {
    viewerAction: {
      type: 'dynamicTrigger',
      label: { en: 'Viewer action', fr: 'Action viewers' },
      required: false,
      wuOptions: { types: ['CHATCMD', 'PLDASHB'] },
      default: { type: 'CHATCMD', title: 'Action', keyword: 'action' },
    },
  },
};
js
// script.js — la valeur hydratée complète part directement vers /trigger/v2
const actionTrigger = await kapp.dynamicTriggers.createFromConfig(
  prefs.viewerAction,
  async (_trigger, request, resolve, reject) => {
    if (!gameIsRunning) {
      await reject('La partie est terminée');
      return 'rejected';
    }
    await kapp.chat.say(`${request.setter.display_name} joue !`);
    await resolve();
    return 'ok';
  },
);

if (actionTrigger) {
  actionTrigger.onTrigger(async () => 'second-listener'); // possible aussi après création
}

Manifeste : scope triggers:manage. Si TWREW est autorisé dans wuOptions.types, le channel doit aussi disposer de la feature Twitch Rewards. create() reste séparé et continue d'utiliser /trigger/v1 pour les applis existantes.

Top pièges (détails dans reference/ et common/PITFALLS.md)

  1. API V2 uniquement. kapp.medias (pluriel), kapp.announces, kapp.textToSpeech. Pas de formes V1 (kapp.tts, kapp.playMedia…).
  2. Tout est async : await chaque appel service ; getters User réseau aussi (Promise.all).
  3. finish() est explicite : une appli ne se termine pas toute seule. Oublier finish() = rewards jamais envoyées + appli qui reste ouverte.
  4. dataStorage (global) ≠ userDataStorage (par viewer) ≠ preferences (config streamer) : ne les confonds pas.
  5. dataStorage.set() écrase tout (pas de merge) : relis puis fusionne si besoin.
  6. Beaucoup d'images = beaucoup de requêtes navigateur. En production, les fichiers propres à l'app peuvent venir directement de Bunny ; utilise quand même Créer un atlas dans le DEV HUB pour réduire leur nombre. Vérifie l'analyse de poids ; les sources ne sont jamais supprimées automatiquement.
  7. Projet compilé = artifact dist/ autonome. Configure runtime.root, lance le build avant preview/push et préfère un bundle unique si les chunks au runtime n'apportent rien (voir PITFALLS.md).