LearnFloo API

Français · English · Español

Lives, support, membres, groupes (classes), cours, fil, calendrier, vidéos et webhooks LearnFloo dans une plateforme externe (LMS, WordPress, CRM) · Référence v1 · mise à jour le 2026-09-16

Authentification

URL de base : https://api.learnfloo.com/api/v1. Les chemins ci-dessous sont relatifs à cette base : /live/… pour les lives, /support/… pour les tickets, /users, /courses, /posts, /events, /videos, /webhooks

Chaque appel porte une clé API fournie par l'administrateur de l'espace LearnFloo (Paramètres de l'espace → Intégration LMS) :

Authorization: Bearer lf_live_xxxxxxxx
La clé est un secret serveur à serveur. Ne jamais l'exposer dans un navigateur ni dans un dépôt. Une clé révoquée est refusée immédiatement (401). Une clé donne accès à tout l'espace, avec les droits d'un administrateur, selon son niveau d'accès choisi à la création : accès complet, ou lecture seule (seules les routes GET et les outils MCP de lecture ; une écriture renvoie 403 Read-only API key). Le niveau se change à tout moment dans les paramètres de l'espace.

Limite de débit

Chaque clé dispose d'un seau de jetons qui se remplit en continu selon le compte du propriétaire de l'espace et qui contient au plus une minute de requêtes (rafale possible jusqu'à ce plafond) :

CompteRequêtes par minute et par clé
Gratuit (facturation non activée)60
Facturation active300

Une requête consomme 1 jeton, sauf les rapports qui en consomment davantage : /live/sessions/:id/attendance, /courses/:id/progress et /space/usage coûtent 5 jetons, /users/:id/progress 3 jetons. Chaque réponse porte l'état du seau :

X-RateLimit-Limit: 120        # jetons par minute
X-RateLimit-Remaining: 117    # jetons disponibles à l'instant
X-RateLimit-Reset: 2          # secondes avant que le seau soit à nouveau plein

Au-delà, la réponse est 429 { "error": "Rate limit exceeded" } avec un en-tête Retry-After (secondes). Attendez ce délai avant de réessayer ; ne bouclez pas. Le compteur est réparti en plusieurs compartiments : X-RateLimit-Remaining est une estimation, et une rafale très proche du plafond peut déclencher un 429 un peu avant. Sous une forte concurrence (dizaines d'appels strictement simultanés avec la même clé), une réponse 503 Too many concurrent requests, retry avec Retry-After peut aussi survenir : même traitement, réessayez après le délai. Pour suivre l'activité de l'espace, préférez les webhooks à une interrogation répétée. Besoin de plus ? Créez plusieurs clés (une par usage) ou contactez l'équipe LearnFloo.

Conventions

Fonctionnement des lives

Apprenant                 Votre plateforme                    LearnFloo
   │ clique « Rejoindre »        │                                  │
   │────────────────────────────▶│  POST /live/entry                │
   │                             │─────────────────────────────────▶│ crée ou retrouve l'apprenant,
   │                             │        { url, expiresAt }        │ prépare un ticket unique
   │   ouvre url (iframe/onglet) │◀─────────────────────────────────│
   │◀────────────────────────────│                                  │
   │  GET url ────────────────────────────────────────────────────▶ │ connexion automatique,
   │ ◀──────────────────────────────────────────────────────────── │ salle du live

POST/live/entry

URL d'entrée personnelle pour un apprenant.

{
  "sessionId": "p97…",
  "user": { "externalId": "lms-user-42", "name": "Alice Martin" },
  "role": "viewer"
}
ChampDescription
sessionIdIdentifiant du live (voir GET /live/sessions)
user.externalIdIdentifiant stable de l'apprenant chez vous, 200 caractères max
user.nameNom affiché aux autres participants, mis à jour à chaque appel
roleviewer (défaut : regarde, chat, le formateur peut lui donner la parole) ou speaker (micro, caméra, partage d'écran dès l'entrée). Un apprenant promu par le formateur n'est pas rétrogradé par un appel viewer.

Réponse 200 :

{
  "url": "https://app.learnfloo.com/live/enter/4tcgzHpN…",
  "expiresAt": "2026-09-05T20:43:07.570Z",
  "role": "viewer",
  "sessionId": "p97…",
  "userId": "k97…"
}

Si le live n'a pas commencé, l'apprenant attend dans la salle d'attente et entre automatiquement au lancement. Si l'espace a un domaine personnalisé, url est sur ce domaine.

Erreurs : 400 Session is over (live terminé ou annulé), Session not found, user.externalId is required, role must be "viewer" or "speaker".

POST/live/sessions

Crée un live dans l'espace de la clé.

{
  "title": "Module 3 — Questions / réponses",
  "description": "optionnel",
  "mode": "broadcast",
  "scheduledAt": "2026-09-12T14:00:00Z",
  "durationMin": 60,
  "recordingEnabled": true,
  "chat": "open",
  "hostEmail": "formateur@client.fr",
  "groups": ["terminale-a"],
  "replay": "attendees"
}
ChampDescription
titleRequis
modebroadcast (webinaire : seuls hôte et intervenants publient) ou conference (tout le monde publie). Défaut broadcast
scheduledAtDate ISO 8601 ou timestamp en millisecondes. Requis
durationMinDurée prévue, pour le calendrier. Défaut 60
maxParticipants2 à 1000. Défaut 100
recordingEnabledEnregistrement automatique. Défaut true
chatChat du live : closed (disponible, panneau replié à l'arrivée, défaut), open (panneau ouvert à l'arrivée) ou off (désactivé : aucun message accepté). L'animateur peut le changer pendant le live
hostEmailOptionnel. Compte LearnFloo du formateur, membre de l'espace avec un rôle owner, admin, moderator ou teacher. Par défaut, le créateur de la clé
groupsOptionnel. Groupes (ids ou slugs) qui voient et peuvent rejoindre le live ; vide = tout l'espace. Les tickets /live/entry et les invitations ne sont pas concernés. En lecture : groupIds
replayOptionnel. Qui voit le replay (l'hôte et l'équipe de l'espace le voient toujours) : all (défaut : tous ceux qui ont accès au live), attendees (seulement les présents, en salle ou en spectateur), groups (membres des groupes de replayGroups), level (membres de niveau replayMinLevel et plus) ou none (pas de replay pour les membres). Pour un membre non autorisé, l'app n'envoie pas l'URL et affiche « Replay réservé »
replayGroupsAvec replay: "groups" : ids ou slugs des groupes, au moins un
replayMinLevelAvec replay: "level" : niveau minimum, de 2 à 10

Réponse 201 :

{ "session": { … voir GET /live/sessions/:id … }, "hostUrl": "https://app.learnfloo.com/<espace>/live/<id>" }

hostUrl est la page où le formateur lance le live, avec son compte LearnFloo.

GET/live/sessions

Sessions de l'espace, plus récentes d'abord, 200 max. Filtres optionnels ?status=scheduled|live|ended|cancelled et ?group=<id ou slug>.

{ "sessions": [ { … }, … ] }

GET/live/sessions/:id

{
  "session": {
    "id": "p97…",
    "title": "Module 3 — Questions / réponses",
    "description": null,
    "mode": "broadcast",
    "status": "ended",
    "scheduledAt": "2026-09-12T14:00:00.000Z",
    "startedAt": "2026-09-12T14:02:11.000Z",
    "endedAt": "2026-09-12T15:01:40.000Z",
    "participantCount": 0,
    "maxParticipants": 100,
    "recordingEnabled": true,
    "recordingStatus": "ready",
    "replayUrl": "https://…/play_1080p.mp4",
    "replayViews": 12,
    "hlsStatus": "ended",
    "hostId": "k12…",
    "chat": "closed",
    "conversionCount": 0,
    "ctaClickCount": 0,
    "groupIds": [],
    "replay": "all",
    "replayGroupIds": [],
    "replayMinLevel": null,
    "activeSceneId": null,
    "sceneCount": 3,
    "createdAt": "2026-09-01T09:00:00.000Z"
  }
}
ChampDescription
statusscheduled, live, ended, cancelled. Utile pour afficher « Bientôt », « Rejoindre » ou « Replay »
participantCountPersonnes connectées à l'instant T (0 hors live)
recordingStatusrecording, processing, ready, failed ou null
replayUrlMP4 du replay quand recordingStatus vaut ready, sinon null. Disponible dès la fin du live ; le fichier est ré-encodé en arrière-plan et l'URL peut changer quelques minutes après. Voir aussi /replay
hlsStatusFlux audience du webinaire : starting, live, ended, failed ou null
chatopen, closed ou off, voir POST
conversionCount, ctaClickCountInscriptions signalées pendant le live et clics sur les boutons du chat, voir /conversions
groupIdsGroupes auxquels le live est réservé (vide = tout l'espace)
replay, replayGroupIds, replayMinLevelQui voit le replay, voir POST. replayUrl est toujours renvoyé à la clé : si votre plateforme affiche le replay elle-même, c'est à elle d'appliquer la règle
Le MP4 du replay refuse les requêtes sans en-tête Referer : intégrez-le dans une page (balise <video>), ne l'ouvrez pas en lien direct. Pour être prévenu de la fin du live et du replay, abonnez un webhook aux événements live.session.ended et live.replay.ready.

PATCH/live/sessions/:id et DELETE/live/sessions/:id

PATCH modifie une session. Avant le live : title, description, mode, scheduledAt, durationMin, maxParticipants, recordingEnabled, chat, groups, replay, replayGroups, replayMinLevel ; l'événement du calendrier suit. Pendant le live : tout sauf la date, la durée et le format. Après le live : title, description, groups et les champs du replay (pour ouvrir ou restreindre un replay après coup). Un champ qui ne se modifie plus renvoie 400. Réponse 200 { "session": { … } }.

DELETE annule une session programmée (statut cancelled, événement retiré du calendrier, crédit Événement restitué). Réponse 200 { "session": { … } }. Un live en cours ou terminé renvoie 400 Only scheduled sessions can be cancelled.

GET/live/sessions/:id/attendance 5 jetons

Présences, temps de visionnage et audience. Pendant un live en cours, calculé jusqu'à l'instant de l'appel.

{
  "session": { … },
  "summary": {
    "liveDurationSec": 3540,
    "attended": 42,
    "peakConcurrent": 38,
    "peakAt": "2026-09-12T14:20:00.000Z",
    "averageConcurrent": 31,
    "averageWatchSec": 2610,
    "watchedHalfOrMore": 35,
    "replayViews": 12,
    "spectatorHours": 36.4,
    "interactiveHours": 2.1
  },
  "participants": [
    { "externalId": "lms-user-42", "name": "Alice Martin", "role": "viewer", "source": "lms",
      "joinedAt": "2026-09-12T14:03:10.000Z", "leftAt": "2026-09-12T15:01:40.000Z",
      "watchSec": 3120, "watchPct": 88, "connections": 2, "connected": false, "userId": "k97…" }
  ]
}
ChampDescription
externalIdVotre identifiant envoyé dans /live/entry. null pour les personnes venues directement de LearnFloo
sourcelms (entré par votre plateforme), invite (lien d'invitation), member (membre LearnFloo)
rolehost, speaker, viewer
joinedAt, leftAtPremière entrée et dernière sortie. leftAt est null tant que la personne est dans la salle
watchSecTemps réel passé dans la salle, toutes connexions cumulées, borné à la durée du live
watchPctPart du live suivie, 0 à 100. Le champ à utiliser pour valider une présence (ex. >= 80)
connectionsNombre d'entrées dans la salle (sortie puis retour = 2)

summary : durée du live, présents, pic d'audience et son heure, présence moyenne (spectateurs simultanés), visionnage moyen, personnes restées plus de la moitié du live, ouvertures du replay. spectatorHours cumule le temps de l'audience sur le flux HLS du webinaire, interactiveHours le temps des personnes connectées à la salle (hôte, intervenants, prises de parole) : ce sont les deux unités de facturation.

GET/live/sessions/:id/participants

État instantané des personnes inscrites sur la session (léger, pour un tableau de bord pendant le live ; les statistiques sont dans /attendance).

{ "session": { … }, "participants": [
  { "id": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "email": null, "image": null,
    "role": "viewer", "source": "lms", "connected": true, "spectator": true, "handRaisedAt": null,
    "joinedAt": "…", "lastSeenAt": "…" }
] }

spectator : regarde le flux HLS du webinaire (sans être dans la salle WebRTC). handRaisedAt : a demandé la parole.

GET/live/sessions/:id/chat

Messages du chat conservés après le live, du plus ancien au plus récent (?limit=500, 2000 max).

{ "session": { … }, "messages": [ { "id": "…", "userId": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "text": "Bonjour !", "imageUrl": null, "link": null, "sentAt": "…" } ] }

Les messages de l'animateur peuvent porter une image (imageUrl) et un bouton d'appel à l'action (link : { "url", "label" }), par exemple un lien d'inscription envoyé pendant le live. kind vaut message ou conversion (annonce automatique d'une inscription).

POST/live/sessions/:id/conversions

Votre LMS signale qu'un spectateur vient de s'inscrire ou d'acheter pendant le live. LearnFloo enregistre la conversion, met à jour le compteur affiché sous les boutons du chat, publie l'annonce dans le chat (« 🎉 Marie vient de rejoindre la formation », texte et anonymat réglés par l'animateur), affiche un bandeau sur la scène et émet le webhook live.conversion.

{
  "externalId": "lms-user-42",
  "name": "Marie Dupont",
  "label": "Formation SEO avancé",
  "amountCents": 49900,
  "currency": "eur"
}
ChampDescription
externalIdIdentifiant de l'apprenant dans votre LMS (celui de /users). Retrouvé dans le lien du bouton, voir ci-dessous
userIdOu l'identifiant LearnFloo (lf_user du lien)
emailOu l'e-mail du compte LearnFloo
nameOptionnel, prénom affiché dans l'annonce (sinon le nom du compte retrouvé, sinon annonce anonyme)
label, amountCents, currencyOptionnels, pour le bilan (produit acheté, montant dans la plus petite unité, devise eur par défaut ou usd)

Aucun identifiant n'est obligatoire : sans identifiant ni nom, l'annonce est anonyme (« 🎉 Nouvelle inscription »). Réponse 201 { "conversion": { "id", "userId", "externalId", "name", "label", "amountCents", "currency", "createdAt" }, "session": { … } }.

Retrouver le spectateur. Quand l'animateur envoie un bouton d'appel à l'action dans le chat, LearnFloo ajoute au lien trois paramètres : lf_live (id du live), lf_user (id LearnFloo du spectateur) et lf_ext (son identifiant dans votre LMS s'il a été créé via /users). Conservez-les sur votre page de vente (champ caché, cookie) et renvoyez-les au paiement : lf_live donne le :id de l'appel, lf_ext ou lf_user identifie l'acheteur. Les clics sur ces boutons sont comptés (ctaClickCount sur la session) et les conversions apparaissent dans le bilan du live.

GET/live/sessions/:id/replay

Le replay sous toutes ses formes.

{ "replay": {
  "sessionId": "p97…", "status": "ready",
  "replayUrl": "https://…/play_1080p.mp4",
  "hlsUrl": "https://…/playlist.m3u8", "embedUrl": "https://iframe.mediadelivery.net/embed/…",
  "thumbnailUrl": "https://…/thumbnail.jpg", "durationSec": 3540,
  "videoId": "v12…", "lessonId": null, "replayViews": 12
} }

replayUrl est disponible dès la fin du live (MP4 brut) ; hlsUrl, embedUrl, thumbnailUrl et durationSec arrivent quelques minutes plus tard, quand la copie adaptative est encodée (null avant). lessonId est renseigné si le formateur a publié le replay comme leçon d'un cours.

GETPOST/live/sessions/:id/invites et DELETE…/invites/:inviteId

Liens d'invitation pour des personnes qui n'ont pas de compte dans votre plateforme (intervenant externe, invité) : un lien partageable, réutilisable, sans ticket personnel.

POST { "role": "speaker", "label": "Intervenante", "expiresInHours": 48, "maxUses": 1 }
→ 201 { "invite": { "id": "…", "token": "…", "url": "https://app.learnfloo.com/invite/…", "role": "speaker", "label": "Intervenante",
                    "uses": 0, "maxUses": 1, "expiresAt": "…", "revoked": false, "active": true, "createdAt": "…" } }

role : viewer (défaut), speaker, assistant (régie : prépare et bascule les scènes, lance les sondages, sans caméra ni micro) ou moderator (modère le chat : suppression de messages, sourdine, réglage du chat, messages préparés, messages privés et sondages, sans caméra ni micro). expiresInHours et maxUses optionnels (sans limite par défaut). 20 liens actifs max par session. DELETE révoque le lien (réponse 200 avec l'invitation).

Scènes : principe

Un live LearnFloo se pilote comme un studio : des scènes nommées (disposition, caméras, partage d'écran, images, PDF, vidéos, embeds, stickers et bandeaux) que le présentateur ou la régie basculent en un clic. Tout ce que la régie fait dans l'application se fait aussi par l'API, en direct : créer une scène pendant le live, la mettre à l'antenne, changer le texte d'un bandeau, afficher une annonce, tourner la page d'un PDF, lancer une vidéo. Ce qui est à l'antenne est vu par tous les participants, le flux des spectateurs, l'enregistrement et les diffusions externes.

{
  "id": "intro", "name": "Introduction", "layout": "spotlight", "active": true,
  "slots": [
    { "id": "sl_a", "kind": "screen" },
    { "id": "sl_b", "kind": "host", "fit": "cover", "shape": "circle", "filter": "warm", "bgMode": "blur" },
    { "id": "sl_c", "kind": "guest", "guestIndex": 1 },
    { "id": "sl_d", "kind": "media", "mediaId": "m8…", "mediaKind": "pdf", "url": "https://…", "title": "Support" }
  ],
  "overlays": [
    { "id": "ticker", "type": "band", "text": "Inscriptions ouvertes jusqu'à 18 h", "visible": true,
      "x": 0, "y": 0.86, "w": 1, "h": 0.09, "opacity": 1, "bg": "#dc2626", "color": "#ffffff",
      "fontSize": 48, "bold": true, "scroll": true, "speed": 160, "align": "center" },
    { "id": "ov_live", "type": "sticker", "label": "LIVE", "bg": "#dc2626", "color": "#ffffff", "visible": true,
      "x": 0.82, "y": 0.06, "w": 0.16, "h": 0.09, "opacity": 1 }
  ],
  "background": "#0a0a0a", "backgroundImageUrl": null, "showLogo": true, "framed": false, "transition": "cut",
  "durationSec": 20, "nextSceneId": "offre"
}
ChampDescription
idLibre à la création (1 à 40 caractères : lettres, chiffres, -, _), sinon généré. Idem pour les slots et overlays : choisissez des ids stables (ticker, prix) pour les mettre à jour ensuite
jingleJingle : la scène joue sa durée (durationSec, ou à défaut la longueur de ses vidéos et sons), puis la scène qu'elle a interrompue reprend là où elle en était (vidéos et sons, temps restant de l'enchaînement), même sans enchaînement automatique. Atteint par l'enchaînement automatique, un jingle passe ensuite à la scène suivante. null pour retirer.
durationSec, nextSceneIdEnchaînement automatique : temps à l'écran en secondes (1 à 86400, compté après la transition d'arrivée ; absent = la scène reste jusqu'au prochain basculement ; prolongé jusqu'à la fin des vidéos et sons qui démarrent avec la scène, hors boucle, dont la longueur est connue : mediaSec, longueur du fichier en secondes mesurée par l'éditeur, aussi acceptée sur les emplacements vidéo et dans audios), puis scène suivante (absent = la suivante dans la liste, ou la première avec la boucle). Actif seulement quand l'enchaînement est allumé (PUT …/scenes/auto)
layoutsolo (premier élément plein écran), grid, spotlight (premier en grand, les autres en colonne), sidebyside, pip (premier plein écran, second en vignette), cinema (premier plein écran, bande en bas), free (chaque emplacement porte x, y, w, h)
slots[].kindhost (caméra de l'hôte), guest (guestIndex : n-ième intervenant par ordre d'arrivée, ou userId pour épingler une personne), screen (premier partage d'écran), media (mediaId de la médiathèque, ou url https directe avec mediaKind image, pdf ou video), embed (mediaId ou url YouTube, Vimeo, Loom)
slots[].fit, loop, muted, titleCadrage cover ou contain ; vidéo en boucle ou sans son ; titre affiché dans la régie
slots[].shape, filter, bgMode, bgBlur, bgImage, volume, mutedCaméras (host, guest) : forme rect (défaut), rounded, square, squircle (carré arrondi), circle, portrait ; filtre none, bw, noir, sepia, vintage, warm, cool, vivid, faded, bright, contrast ; fond appliqué sur l'appareil de la personne tant que la scène est à l'antenne : bgMode none, blur (bgBlur 1 à 20, défaut 10) ou image (bgImage : URL https ou preset:ocean, preset:forest, preset:sunset, preset:slate), absent = le choix de la personne ; son à l'antenne volume (0 à 1) et muted, pour la diffusion, l'enregistrement et les spectateurs (les intervenants s'entendent toujours). null pour retirer
slots[].autoplay, once, startSec, durationSec, volumeVidéo : démarre quand la scène passe à l'antenne (défaut true, sinon en attente des commandes de la régie), once seulement la première fois, début en secondes, durée du passage joué (en boucle ou arrêt à la fin du passage), volume 0 à 1 (null pour retirer). Même logique que le son de scène.
overlays[]type band (text, bg, color, fontSize en px à 1080p, bold, scroll, speed en px/s, align), sticker (emoji, ou imageUrl https, ou label badge avec bg et color), ou un widget de live poll / message / viewers (compteur de spectateurs, title = libellé), avec son style : bg (hex) et bgOpacity, color, accent (barres, badges), fontScale (0,5 à 2), radius (px à 1080p), borderColor, borderWidth, shadow, font (sans, rounded, serif, mono), align (message), showHeader, showVotes, showLetters (sondage), showName, showIcon (message) ; la taille se règle par w et h : là où la scène dessine le sondage ou le message du chat mis à l'antenne ; vide tant que rien n'est à l'antenne, et une scène sans emplacement utilise un coin par défaut. Géométrie en fractions du cadre 16:9 (x, y, w, h entre 0 et 1), visible, opacity
lockedCadenas : une scène verrouillée refuse toute modification et suppression (400), par l'application comme par l'API, tant qu'un appel n'envoie pas { "locked": false }. Elle peut toujours être mise à l'antenne et dupliquée
audiosSons de la scène, plusieurs possibles (6 au maximum), chacun { "mediaId": "…" } (fichier audio de la médiathèque) ou { "url": "https://…" }, avec autoplay (démarre quand la scène passe à l'antenne, défaut true), once (avec autoplay : seulement la première fois, défaut false), loop (défaut false), volume (0 à 1, défaut 1), startSec et durationSec (passage joué, en secondes ; null pour retirer). Joués chez tous les participants et captés dans la diffusion et l'enregistrement. Remplace la liste (un son déjà présent, même mediaId, garde ses autres réglages) ; [] les retire tous. L'ancien champ audio (un seul son, même forme) reste accepté en écriture, "audio": null retire tous les sons ; en lecture, les sons sont toujours dans audios. Lecture, pause et reprise depuis le début de chaque son avec PUT …/media/:mediaId.
background, backgroundImageUrl, showLogo, transitionCouleur CSS du fond, image de fond https (null pour la retirer), logo de l'espace dans le coin, transition à l'arrivée de la scène (la précédente reste dessous pendant l'animation) : cut, fade (fondu enchaîné), black (fondu au noir), slide-left, slide-right, slide-up, slide-down, zoom, wipe (balayage), blur (flou), avec transitionMs (100 à 3000, défaut 500). Jouée dans la salle, la diffusion et l'enregistrement.
framedEncadré : une marge tout autour des emplacements laisse voir le fond (couleur ou image) et votre marque. Pour toutes les dispositions sauf free ; défaut false, null pour le retirer

20 scènes par live, 9 emplacements et 30 stickers ou bandeaux par scène. Une scène se prépare avant le live (les intervenants sont désignés par rang d'arrivée) et se modifie pendant : la modification d'une scène à l'antenne est visible immédiatement.

GETPOST/live/sessions/:id/scenes et GETPATCHDELETE…/scenes/:sceneId

GET renvoie toute la régie :

{ "session": { … }, "activeSceneId": "intro",
  "autoAdvance": { "enabled": true, "loop": false, "nextSceneAt": "2026-09-12T14:05:20.000Z" },
  "scenes": [ { … } ],
  "media": [ { "mediaId": "m8…", "page": 3, "playing": false, "position": 0, "updatedAt": "…" } ],
  "bands": [ { "id": "bd_…", "text": "🎉 Marie vient de s'inscrire", "bg": "#059669", "color": "#ffffff", "until": "…" } ] }

POST crée une scène à partir de l'objet ci-dessus (tout est optionnel sauf ce qui donne du sens), ou d'un modèle de l'espace : { "template": "Écran partagé", "name": "Démo" }. "activate": true la met à l'antenne dans la foulée ; "afterId" la place après une scène donnée. Réponse 201 { "scene": { … } }.

POST /live/sessions/p97…/scenes
{ "id": "offre", "name": "Offre spéciale", "layout": "pip",
  "slots": [ { "kind": "media", "mediaId": "m12…" }, { "kind": "host" } ],
  "overlays": [ { "id": "prix", "type": "band", "text": "Formation SEO : 499 € jusqu'à ce soir", "y": 0.86 } ],
  "activate": true }

PATCH modifie les champs envoyés ; slots et overlays, quand ils sont présents, remplacent les listes entières (pour toucher un seul élément, voir ci-dessous). DELETE retire la scène (si elle était à l'antenne, retour à la disposition automatique) et renvoie la régie. PUT …/scenes/order { "sceneIds": [ … ] } réordonne.

À l'antenne, bandeaux, médias

AppelEffet
POST/live/sessions/:id/scenes/:sceneId/activateMet la scène à l'antenne (ses vidéos repartent du début). Réponse : la régie complète
PUT/live/sessions/:id/scenes/active { "sceneId": "intro" }Même chose ; { "sceneId": null } revient à la disposition automatique (caméras en grille, partage d'écran en grand)
PUT/live/sessions/:id/scenes/auto { "enabled": true, "loop": true }Enchaînement automatique : pendant le live, chaque scène qui porte durationSec laisse la place à nextSceneId (ou à la suivante) une fois son temps écoulé ; loop repart de la première à la fin de la liste. Un basculement manuel (application ou API) relance le chrono de la nouvelle scène. La régie renvoyée porte autoAdvance: { enabled, loop, nextSceneAt } (nextSceneAt : date ISO du prochain changement, ou null)
POST/live/sessions/:id/bands { "text": "Pause de 5 minutes", "bg": "#059669", "color": "#ffffff", "seconds": 8 }Bandeau affiché en haut de l'écran pendant seconds (2 à 60, 8 par défaut) sur tous les écrans, quelle que soit la scène. Réponse 201 { "band": { "id", "text", "bg", "color", "until" } }. Pour un texte permanent ou modifiable, préférez un bandeau de scène (overlays)
PUT/live/sessions/:id/media/:mediaId { "page": 4 }Page affichée d'un PDF présent dans une scène
PUT/live/sessions/:id/media/:mediaId { "playing": true, "position": 0 }Lecture, pause ou position (secondes) d'une vidéo présente dans une scène, ou d'un son d'une scène (audios[].mediaId), synchronisées chez tous

Chaque changement de scène à l'antenne émet le webhook live.scene.changed ({ session, scene }, scene à null pour la disposition automatique).

Stickers, bandeaux et emplacements d'une scène

AppelEffet
POST…/scenes/:sceneId/overlaysAjoute un sticker ou un bandeau (objet overlays[] ci-dessus). 201 { "overlay" }
PATCH…/scenes/:sceneId/overlays/:overlayIdModifie les champs envoyés : { "text": "Plus que 12 places" }, { "visible": false }, { "x": 0.1, "y": 0.1 }… Visible immédiatement si la scène est à l'antenne
PATCH/live/sessions/:id/overlays/:overlayIdMême chose dans toutes les scènes qui portent cet id : un bandeau commun (compteur, prix, prochaine étape) placé dans chaque scène avec le même id se met à jour d'un seul appel
DELETE…/scenes/:sceneId/overlays/:overlayIdRetire l'élément, renvoie la scène
POST…/scenes/:sceneId/slotsAjoute un emplacement (objet slots[] ; position optionnelle, 0 = principal). 201 { "slot" }
PATCH…/scenes/:sceneId/slots/:slotIdModifie l'emplacement (autre média via mediaId ou url, guestIndex, fit, géométrie en free, position pour le déplacer dans l'ordre)
DELETE…/scenes/:sceneId/slots/:slotIdRetire l'emplacement, renvoie la scène
# Le compteur de places, dans toutes les scènes, depuis votre CRM
PATCH /live/sessions/p97…/overlays/places
{ "text": "Plus que 7 places", "bg": "#f59e0b", "color": "#111111" }

GETPOST/scene-templates et DELETE/scene-templates/:id

Modèles de scènes de l'espace, réutilisables dans tous les lives (menu « Nouvelle » de la régie, ou "template" à la création d'une scène). POST : { "name": "Écran partagé", "scene": { … } } ou, pour enregistrer une scène existante, { "name": "…", "sessionId": "p97…", "sceneId": "intro" }. Même nom = remplacé. Réponse { "template": { "id", "name", "scene", "folderId", "createdAt", "updatedAt" } }. :id accepte l'identifiant ou le nom.

PUT/live/sessions/:id/participants/:userId/role

{ "role": "speaker" | "viewer" | "assistant" | "moderator" } pour un participant déjà inscrit sur la session (/participants) : donner ou reprendre la parole, ou nommer une régie. Appliqué immédiatement pendant le live. Réponse { "participant": { … } }.

Sondages et quiz : principe

Un live peut poser des sondages (avis) et des quiz (avec bonne réponse) aux participants, dans la salle comme dans l'audience du webinaire. Ils se préparent à l'avance (statut draft, invisibles des participants), se lancent pendant le live (open, un seul à la fois) puis se ferment (closed). Les résultats sont conservés après le live : GET …/polls les renvoie toujours, ils figurent aussi dans les statistiques et sur la page du replay. Dans l'application, l'hôte, le staff, les rôles moderator et assistant les gèrent depuis l'onglet « Sondages » du live et depuis « Préparer ».

{ "id": "k3…", "sessionId": "p97…", "kind": "quiz", "question": "Quelle est la capitale de l'Australie ?",
  "allowMultiple": false, "status": "closed", "resultsShown": true, "position": 2,
  "options": [ { "id": "o1_…", "text": "Sydney",   "votes": 31, "percent": 62, "correct": false },
               { "id": "o2_…", "text": "Canberra", "votes": 19, "percent": 38, "correct": true } ],
  "voters": 50, "correctVoters": 19,
  "openedAt": "…", "closedAt": "…", "createdAt": "…", "updatedAt": "…" }

kind : poll (défaut) ou quiz. Pour un quiz, correct à la création donne les index (à partir de 0) des bonnes réponses dans options ; la bonne réponse et correctVoters (votants qui ont exactement la bonne combinaison) sont révélés aux participants avec les résultats. percent est la part des votants ayant choisi l'option (avec allowMultiple, la somme dépasse 100). Un participant peut changer son vote tant que le sondage est ouvert ; un votant compte une fois.

GETPOST/live/sessions/:id/polls et GETPATCHDELETE…/polls/:pollId

GET …/polls renvoie { "polls": [ … ] } dans l'ordre de préparation, avec les décomptes ; ?status=draft|open|closed filtre. Réponse { "poll": { … } } pour les autres.

POST /live/sessions/p97…/polls
{ "question": "Quel sujet ensuite ?", "options": ["Tarifs", "Intégration LMS", "Replays"], "allowMultiple": true }
→ 201 { "poll": { "status": "draft", … } }

POST /live/sessions/p97…/polls
{ "kind": "quiz", "question": "Capitale de l'Australie ?", "options": ["Sydney", "Canberra"], "correct": [1], "open": true }
→ 201 { "poll": { "status": "open", … } }   // "open": true lance aussitôt (live en cours uniquement)

phase (à la création ou en PATCH) : live (défaut, lancé à la main pendant le live), waiting (sondage ou quiz d'avant-live : ouvert dans la salle d'attente dès sa création tant que le live est programmé, fermé automatiquement au démarrage, résultats réutilisables pendant le live via …/band et …/stage) ou closing (ouvert automatiquement à la fin du live, sur l'écran de clôture : avis, satisfaction). Un sondage waiting ouvert reste modifiable.

PATCH modifie un sondage draft (ou un sondage waiting ouvert) uniquement (mêmes champs ; si options change pour un quiz, redonnez correct). Un sondage lancé se duplique (nouveau POST) plutôt qu'il ne se modifie. DELETE supprime le sondage et ses votes, quel que soit son statut. 2 à 6 réponses, 100 sondages par live.

Lancer, résultats, fermer

AppelEffet
POST…/polls/:pollId/openOuvre le vote aux participants (carte sur la vidéo, message système dans le chat sauf { "announceInChat": false }). Refusé si un autre sondage est ouvert ou si le live n'est pas en cours. Webhook live.poll.opened.
PUT…/polls/:pollId/results { "shown": true }Montre (ou cache avec false) les décomptes aux participants, pendant le vote ou après la fermeture. Pour un quiz, révèle aussi la bonne réponse.
POST…/polls/:pollId/closeFerme le vote ; { "showResults": false } pour fermer sans afficher (défaut : affiché). Webhook live.poll.closed avec les résultats. Un sondage encore ouvert à la fin du live est fermé, résultats affichés.
PUT…/polls/:pollId/stage { "shown": true }Affiche le sondage dans la scène (emplacement poll de la scène à l'antenne, sinon coin en bas à droite) : question, réponses, nombre de votes, puis les barres et la bonne réponse quand les résultats sont affichés. Visible dans la salle, la diffusion et l'enregistrement. false le retire ; onStage dans l'objet sondage.
PUT/live/sessions/:id/viewers { "extra": 25 }Ajoute des spectateurs au compteur affiché par le widget viewers des scènes (nombre réel de personnes dans le live + extra). Jamais compté dans les présences, les statistiques ni la facturation. 0 retire l'ajout ; extraViewers dans l'objet session.
PUT/live/sessions/:id/chat/stage { "messageId": "…" }Met un message du chat à l'antenne (emplacement message de la scène, sinon bas gauche) : nom et texte, image éventuelle. { "messageId": null } le retire. Les messages privés sont refusés. Réponse { "message": { id, name, text, imageUrl, at } | null }.
POST…/polls/:pollId/bandBandeau de 12 secondes sur la scène (vidéo, diffusion, enregistrement) avec les réponses en tête, ou la bonne réponse et le taux de réussite pour un quiz. Live en cours, au moins un vote.

Support : principe

Vos utilisateurs ouvrent des tickets de support depuis votre plateforme (par exemple le plugin WordPress LearnFloo) ; votre équipe y répond dans l'espace LearnFloo, onglet Support, ou par l'API (côté équipe). Même clé API que pour les lives.

Objet ticket renvoyé par tous les appels :

{
  "id": "j57…", "number": 12, "subject": "Vidéo bloquée au module 3",
  "status": "open", "priority": "normal", "category": null,
  "messageCount": 3, "lastMessageAt": "2026-09-07T09:12:00.000Z", "lastMessageBy": "staff",
  "createdAt": "…", "updatedAt": "…", "resolvedAt": null, "closedAt": null,
  "assignee": { "name": "Julie", "image": null }
}
ChampDescription
statusopen (en attente de l'équipe), pending (en attente de l'utilisateur), resolved, closed
prioritylow, normal, high, urgent
lastMessageBymember ou staff : qui a écrit en dernier

GET/support/tickets

Tickets de l'utilisateur, plus récents d'abord. ?externalId=… requis, &status=active|open|pending|resolved|closed|all optionnel (all par défaut, active = open + pending).

{ "tickets": [ { … }, … ] }

Tous les tickets de l'espace (tableau de bord d'équipe) : ?scope=space&status=…&limit=100 (500 max). Chaque ticket porte alors son author ({ id, externalId, name, email, image }).

POST/support/tickets

{
  "user": { "externalId": "wp:42", "name": "Alice Martin" },
  "subject": "Vidéo bloquée au module 3",
  "content": "Bonjour,\n\nla vidéo s'arrête à 2 min.",
  "priority": "normal",
  "category": "wordpress",
  "attachments": [ { "kind": "image", "url": "https://…", "name": "capture.png", "mimeType": "image/png", "sizeBytes": 1234 } ]
}
ChampDescription
user.externalId, user.nameRequis. 200 et 60 caractères max
subjectRequis, 200 caractères max
contentTexte brut (défaut) ou HTML avec "format": "html". Requis sauf si une pièce jointe est fournie
priorityDéfaut normal
categoryLibre, optionnel (affiché à l'équipe)
attachmentsOptionnel, 10 max, objets renvoyés par /attachments

Réponse 201 : { "ticket": { … } }. L'équipe de l'espace est notifiée.

GET/support/tickets/:id

?externalId=… requis (ou ?scope=space pour l'équipe : n'importe quel ticket de l'espace, avec son author). Le ticket et ses messages visibles par l'utilisateur, du plus ancien au plus récent. Les notes internes de l'équipe ne sortent jamais par l'API.

{
  "ticket": {
    …,
    "messages": [
      { "id": "m3…", "content": "<p>Bonjour,</p>…", "attachments": [],
        "createdAt": "…", "fromStaff": false, "author": { "name": "Alice Martin", "image": null } },
      { "id": "m4…", "content": "<p>Pouvez-vous vider le cache ?</p>", "attachments": [],
        "createdAt": "…", "fromStaff": true, "author": { "name": "Julie", "image": "https://…" } }
    ]
  }
}
content est du HTML produit par l'éditeur LearnFloo : assainissez-le avant de l'afficher dans votre page (balises de texte, images, liens).

404 si le ticket n'existe pas ou n'appartient pas à cet utilisateur.

POST/support/tickets/:id/messages

{ "user": { "externalId": "wp:42", "name": "Alice Martin" }, "content": "Merci, c'est réglé.", "attachments": [] }

Réponse 201 : { "ticket": { … } } (sans les messages). Le ticket repasse en open et l'équipe (ou la personne assignée) est notifiée.

POST/support/tickets/:id/status

{ "user": { "externalId": "wp:42" }, "status": "resolved" }

L'utilisateur peut marquer son ticket resolved ou le rouvrir (open). Les autres statuts sont réservés à l'équipe. Réponse 200 : { "ticket": { … } }.

Support côté équipe (staff)

Pour synchroniser un help desk externe, les deux appels ci-dessus acceptent un objet staff à la place de user : l'action est faite par un membre de l'équipe de l'espace (owner, admin, moderator, teacher), par défaut le créateur de la clé, ou staff.email pour un autre compte LearnFloo.

POST /support/tickets/:id/messages
{ "staff": { "email": "julie@client.fr" }, "content": "Pouvez-vous vider le cache ?" }
→ le ticket passe en "pending", l'utilisateur est notifié

POST /support/tickets/:id/status
{ "staff": {}, "status": "closed", "priority": "high", "assigneeEmail": "julie@client.fr" }
→ n'importe quel statut ; priority et assigneeEmail (null pour désassigner) sont optionnels

POST/attachments

Corps brut du fichier, en-tête Content-Type = type du fichier (images, PDF, audio, texte, zip, Office), 25 Mo max. ?externalId=… optionnel (pièce jointe d'un utilisateur externe). À joindre ensuite à un ticket, une réponse, une publication ou un commentaire via attachments. /support/attachments est un alias.

{ "attachment": { "kind": "image", "url": "https://…/capture.png", "mimeType": "image/png", "sizeBytes": 1234 } }

Erreurs : 415 type non pris en charge, 413 fichier trop gros.

GET/space

L'espace derrière la clé, pour un test de connexion et l'affichage :

{ "space": { "id": "…", "slug": "mon-espace", "name": "Mon espace", "description": null, "logo": "https://…", "coverImage": null,
  "mode": "learning", "visibility": "private", "accessType": "paid", "priceCents": 2900, "priceCurrency": "eur", "priceInterval": "month", "category": "business",
  "aboutUrl": null, "rating": 4.8, "reviewCount": 12, "gamificationEnabled": true,
  "liveDomain": "live.client.fr", "memberCount": 128, "courseCount": 4, "groupCount": 6,
  "groupLabels": { "singular": "Classe", "plural": "Classes", "leader": "Professeur", "member": "Élève", "gender": "f" },
  "createdAt": "…", "url": "https://app.learnfloo.com/mon-espace" } }

visibility : public (listé sur www.learnfloo.com/communautes, page À propos aboutUrl ouverte à tous) ou private (accès par lien d'invitation uniquement). accessType : free ou paid ; un espace payant porte priceCents (montant dans la plus petite unité de la devise), priceCurrency (eur, prix TTC, ou usd, prix hors taxes) et priceInterval (month, year, once = accès à vie). rating est la moyenne des avis des membres (null sans avis).

GET/space/usage 5 jetons

Consommation de l'espace depuis la dernière facture, valorisée au tarif à l'usage de l'offre unique (heures de webinaire diffusées, heures de visio enregistrées, heures interactives, heures spectateur, lectures, minutes de vidéo conservées, heures de diffusion simultanée multipliées par le nombre de destinations, heures sous-titrées), l'état du compte du propriétaire et la limite de débit de la clé.

{ "usage": { "month": "2026-09", "currency": "eur", "billingActive": true, "liveAllowed": true,
  "units": { "liveHours": 6.5, "recordedVisioHours": 0, "interactiveHours": 12.5, "spectatorHours": 240.3, "simulcastHours": 3, "subtitleHours": 0, "plays": 1530, "storedMinutes": 412 },
  "allowance": { "storedMinutes": 60, "plays": 300 },
  "usageCents": 4812, "capCents": 10000, "capMode": "block" },
  "rateLimit": { "perMinute": 300 } }

usageCents est le coût de cet espace avant les franchises (allowance), le crédit de bienvenue et le plafond mensuel (capCents, capMode = block ou warn), qui s'appliquent au compte du propriétaire, tous espaces confondus. liveAllowed est faux quand le compte ne peut plus payer (crédit épuisé sans facturation active).

GETPOST/users

GET : membres de l'espace, paginés (?limit=50&cursor=…, 200 max), filtres ?role=member|contributor|teacher|moderator|admin|owner et ?status=active|suspended.

{ "users": [ { "id": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "email": null, "image": null,
               "member": { "role": "member", "status": "active", "totalXp": 320, "level": 3, "joinedAt": "…" } } ],
  "nextCursor": "…" }

POST : crée (ou met à jour) un utilisateur de votre plateforme et l'ajoute comme membre de l'espace, sans attendre qu'il rejoigne un live. Idempotent : rappeler avec le même externalId met le nom à jour.

{ "externalId": "lms-user-42", "name": "Alice Martin", "role": "member", "member": true }
→ 201 { "user": { …, "member": { … } }, "created": true }

role : member (défaut), contributor, teacher, moderator, admin. member: false crée le compte sans l'inscrire à l'espace. Réponse 200 si l'utilisateur existait déjà.

GETPATCHDELETE/users/:id

:id = identifiant LearnFloo ou ext:<externalId>.

GET → { "user": { "id": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "email": null, "image": null, "bio": null, "createdAt": "…",
  "member": { "role": "member", "status": "active", "totalXp": 320, "level": 3, "joinedAt": "…" },
  "stats": { "lessonsCompleted": 14, "liveSessionsJoined": 3, "posts": 2, "badges": 1 } } }

PATCH { "name": "Alice Martin-Dupont", "role": "contributor", "status": "suspended" }   → { "user": { … } }
DELETE → { "removed": true }   (retire le membre de l'espace ; son compte et son historique sont conservés)

Seuls les utilisateurs créés par votre plateforme peuvent être renommés. Le propriétaire de l'espace ne peut être ni modifié ni retiré.

POST/users/:id/entry

Connexion sans mot de passe (SSO) d'un utilisateur de votre plateforme dans l'espace LearnFloo : fil, cours, calendrier, classement… Même principe que /live/entry : URL personnelle, à usage unique, valable 15 minutes, à demander au clic. L'utilisateur est ajouté comme membre s'il ne l'est pas déjà. Réservé aux utilisateurs ext:… ; avec name, l'utilisateur est créé s'il n'existe pas.

POST /users/ext%3Alms-user-42/entry
{ "name": "Alice Martin", "redirect": "courses" }
→ { "url": "https://app.learnfloo.com/enter/…", "expiresAt": "…", "userId": "k97…", "redirect": "/mon-espace/courses" }

redirect : home (défaut), posts, courses, calendar, live, leaderboard, tickets, challenges, ou un chemin de l'espace (/mon-espace/courses/onboarding, /posts/<id>).

GET/users/:id/progress 3 jetons

Avancement d'un utilisateur dans chaque cours de l'espace.

{ "user": { … }, "courses": [
  { "id": "c1…", "slug": "onboarding", "title": "Onboarding", "published": true, "lessonCount": 8,
    "completedLessons": 5, "progressPct": 63, "completed": false, "lastCompletedAt": "…" }
] }

GETPOST/users/:id/xp

GET : total, niveau et derniers événements XP de l'utilisateur dans l'espace (?limit=100, 1000 max).

{ "user": { … }, "totalXp": 320, "level": 3, "events": [ { "id": "…", "amount": 25, "source": "lesson_complete", "refId": "l3…", "createdAt": "…" } ] }

POST : attribue des XP depuis votre plateforme (quiz réussi, devoir rendu…). amount de -1000 à 1000, reason (60 caractères, visible comme source api:<reason>) et refId optionnels. Le niveau est recalculé. 400 si la gamification est désactivée dans l'espace.

{ "amount": 50, "reason": "quiz-module-3", "refId": "quiz:874" }   → 201 { "user": { … }, "totalXp": 370, "level": 3, "awarded": 50 }

Leaderboard, badges, défis

AppelRéponse
GET/leaderboard?limit=50&group={ "leaderboard": [ { "rank": 1, "id", "externalId", "name", "image", "totalXp", "level", "role" } ] } (500 max) ; group restreint aux membres d'un groupe
GET/badgesCatalogue : { "badges": [ { "id", "key", "name", "description", "icon", "criteriaType", "criteriaTarget", "xpReward" } ] }
GET/users/:id/badges{ "user": { … }, "badges": [ { "id", "key", "name", "description", "icon", "awardedAt" } ] }
GET/challenges{ "challenges": [ { "id", "title", "description", "objectiveType", "objectiveTarget", "xpReward", "startsAt", "endsAt", "status": "upcoming|active|ended", "participantCount", "completedCount" } ] }
GET/challenges/:idLe défi avec ses participants ({ id, externalId, name, progress, completedAt, joinedAt })

Groupes : principe et audience

Un espace peut répartir ses membres en groupes : les classes d'une école, les promotions d'un organisme de formation, les équipes ou services d'une entreprise. Le vocabulaire se règle dans les paramètres de l'espace (groupLabels de GET /space) ; l'API parle toujours de groups. Chaque groupe a un slug utilisable à la place de son identifiant.

Un cours, une publication, un événement ou un live peut être adressé à certains groupes seulement : c'est le champ groups (tableau d'identifiants ou de slugs) à la création et à la modification, et groupIds en lecture. Vide = tout l'espace. Un membre ne voit que le contenu sans audience ou adressé à l'un de ses groupes ; les rôles owner, admin, moderator et teacher voient tout. Les listes acceptent ?group=<id ou slug> pour ne renvoyer que le contenu adressé à ce groupe.

POST /courses   { "title": "Maths — Terminale", "groups": ["terminale-a", "terminale-b"] }
POST /posts     { "title": "Sortie de vendredi", "content": "…", "groups": ["terminale-a"] }
POST /live/sessions { "title": "Révisions", "scheduledAt": "…", "groups": ["terminale-a"] }
GET  /courses?group=terminale-a

Dans un groupe, un membre est member (élève, apprenant…) ou leader (professeur, responsable…) : un leader peut gérer la liste des membres de son groupe depuis l'app, sans rôle particulier dans l'espace.

Groupes automatiques. Un groupe peut porter une rule : les membres qui la remplissent y sont ajoutés au fil de l'eau (et notifiés), les membres actuels dès la création de la règle. Les règles sont à sens unique, personne n'est retiré automatiquement ; on peut toujours ajouter ou retirer à la main. Types : level_min (level 2 à 10), streak_days (days), challenge_joined et challenge_completed (challenge = id, voir /challenges), badge (badge = id ou clé, voir /badges), course_completed (course = id ou slug). Utilisez ensuite le groupe comme audience : un cours réservé au « Niveau 5 et plus », un live pour ceux qui ont terminé un défi.

POST /groups  { "name": "Niveau 5 et plus", "rule": { "kind": "level_min", "level": 5 } }
POST /groups  { "name": "Module 3 terminé", "rule": { "kind": "course_completed", "course": "module-3" } }
PATCH /groups/module-3-termine  { "rule": null }        # redevient un groupe manuel
→ "group": { …, "rule": { "kind": "course_completed", "level": null, "days": null, "challengeId": null, "badgeId": null, "courseId": "c1…" } }

GETPOST/groups et GETPATCHDELETE/groups/:id

AppelCorps / réponse
GET/groups{ "groups": [ { "id", "slug", "name", "description", "color", "memberCount", "rule", "createdAt" } ] }, par nom
POST/groups{ "name", "slug"?, "description"?, "color"?, "rule"? }201 { "group": { … } }. Le slug est dérivé du nom s'il n'est pas donné ; color au format #rrggbb
GET/groups/:idLe groupe (id ou slug), ruleLabel lisible, et ses members : [ { "id", "externalId", "name", "email", "image", "groupRole": "leader|member", "auto", "addedAt" } ] (auto : ajouté par la règle)
PATCH/groups/:id{ "name"?, "slug"?, "description"?: string | null, "color"?: string | null, "rule"?: object | null }
DELETE/groups/:id{ "deleted": true }. Le groupe est retiré de l'audience des contenus qui le visaient (un contenu qui ne visait que lui redevient visible de tout l'espace)

Membres d'un groupe

AppelCorps / réponse
POST/groups/:id/members{ "users": ["k97…", "ext:lms-user-42"], "role"?: "member|leader" }{ "group", "added", "notFound": [] }. Les utilisateurs doivent déjà être membres de l'espace (voir POST /users) ; jusqu'à 500 par appel, idempotent
DELETE/groups/:id/members/:userId{ "removed": true }. :userId = identifiant LearnFloo ou ext:…

GETPUT/users/:id/groups

GET : groupes d'un utilisateur dans l'espace, avec son groupRole. PUT remplace la liste complète (pratique pour synchroniser les classes depuis votre LMS) :

PUT /users/ext%3Alms-user-42/groups
{ "groups": ["terminale-a", "club-robotique"], "role": "member" }
→ { "groups": [ { "id", "slug", "name", …, "groupRole": "member" } ] }

Classement, suivi et documents d'un groupe

AppelRéponse
GET/groups/:id/leaderboard?limit={ "group", "leaderboard": [ { "rank", "id", "externalId", "name", "image", "totalXp", "level", "role", "groupRole" } ] } : les membres du groupe classés par XP
GET/groups/:id/stats 5 jetonsSuivi de chaque membre : avancement dans les cours ouverts au groupe, leçons validées, publications, lives suivis, XP des 30 derniers jours, dernière activité. Voir ci-dessous
GET/groups/:id/documents{ "documents": [ { "id", "title", "url", "kind", "mimeType", "sizeBytes", "uploadedBy", "createdAt" } ] }, plus récents d'abord
POST/groups/:id/documents{ "title", "url", "kind"?, "mimeType"?, "sizeBytes"? }201 { "document" }. url : un fichier envoyé via POST /attachments, ou n'importe quel lien https (kind: "link", jamais supprimé du stockage)
DELETE/groups/:id/documents/:docId{ "deleted": true } ; un fichier envoyé par l'API est effacé du stockage
GET /groups/terminale-a/stats
{ "group": { "id", "slug": "terminale-a", "name": "Terminale A", … }, "generatedAt": "…",
  "summary": { "memberCount": 26, "learnerCount": 25, "activeLast7d": 19, "activeLast30d": 24, "inactive": 1,
               "avgProgressPct": 62, "avgVideoPct": 48, "videoLessonCount": 12, "lessonsCompleted": 410, "posts": 57, "livesAttended": 88, "xp30d": 5310 },
  "courses": [ { "id": "c1…", "title": "Maths — Terminale", "slug": "maths-terminale", "lessonCount": 12 } ],
  "members": [ { "id": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "email": null, "image": null, "groupRole": "member",
                 "totalXp": 320, "level": 3, "xp30d": 140, "lessonsCompleted": 9, "progressPct": 75,
                 "courses": [ { "id": "c1…", "slug": "maths-terminale", "completed": 9, "total": 12 } ],
                 "posts": 2, "livesAttended": 4, "videoPct": 55, "lastActivityAt": "…", "addedAt": "…" } ] }

summary porte sur les membres member (les leader sont exclus des moyennes) ; inactive = aucune activité depuis 30 jours. Les cours pris en compte sont les cours publiés adressés au groupe ou à tout l'espace.

GETPOST/courses

GET : cours publiés de l'espace (?includeUnpublished=1 pour tout voir, ?group= pour ceux adressés à un groupe).

{ "courses": [ { "id": "c1…", "slug": "onboarding", "title": "Onboarding", "description": null, "coverImage": null,
                 "published": true, "lessonCount": 8, "groupIds": [], "createdAt": "…", "url": "/mon-espace/courses/onboarding" } ] }

POST : crée un cours (brouillon par défaut). author optionnel doit être owner, admin ou teacher. groups : audience (ids ou slugs), vide = tout l'espace.

{ "title": "Onboarding", "slug": "onboarding", "description": "…", "coverImage": "https://…", "published": false, "groups": ["terminale-a"] }
→ 201 { "course": { … } }

GETPATCHDELETE/courses/:id

:id = identifiant ou slug du cours. GET renvoie la structure complète ; ajoutez ?withContent=1 pour le HTML des leçons.

{ "course": { …, "modules": [ { "id": "m1…", "title": "Semaine 1", "description": null, "order": 0 } ],
  "lessons": [ { "id": "l1…", "courseId": "c1…", "moduleId": "m1…", "title": "Bienvenue", "order": 0,
                 "videoUrl": "https://…", "videoDurationSec": 312, "createdAt": "…", "url": "/mon-espace/courses/onboarding/lessons/l1…" } ] } }

PATCH : title, description, coverImage, published, groups ([] ou null = tout l'espace). DELETE supprime le cours, ses modules, ses leçons et les progressions.

Modules et leçons

AppelCorps / réponse
POST/courses/:id/modules{ "title", "description"? }201 { "module": { id, courseId, title, description, order } }
POST/courses/:id/lessons{ "title", "content", "format"?, "moduleId"?, "videoUrl"?, "videoDurationSec"? }201 { "lesson": { … , "content" } }. videoUrl : URL d'une vidéo (MP4, HLS, YouTube, Vimeo) ou playbackUrl d'une vidéo du studio
GET/lessons/:id{ "lesson": { …, "content" } }
PATCH/lessons/:id{ "title"?, "content"?, "format"?, "moduleId"?: id | null, "videoUrl"?: url | null, "videoDurationSec"?: n | null, "order"? }
DELETE/lessons/:id{ "deleted": true }

GET/courses/:id/progress 5 jetons

Sans paramètre : tous les apprenants ayant commencé le cours, les plus avancés d'abord.

{ "course": { "id": "c1…", "title": "Onboarding", "lessonCount": 8 },
  "learners": [ { "id": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "email": null, "image": null,
                  "completedLessons": 8, "progressPct": 100, "completed": true, "lastCompletedAt": "…" } ] }

Avec ?externalId=lms-user-42 (ou ?userId=k97…) : le détail leçon par leçon de cet apprenant.

{ "course": { … }, "user": { …, "completedLessons": 5, "progressPct": 63,
  "lessons": [ { "id": "l1…", "title": "Bienvenue", "moduleId": "m1…", "completedAt": "…" }, { "id": "l2…", "title": "…", "moduleId": "m1…", "completedAt": null } ] } }

POST/lessons/:id/complete

Marque une leçon comme terminée pour un apprenant, comme s'il l'avait validée dans LearnFloo (XP, quêtes, badges, webhook lesson.completed). Idempotent.

{ "user": { "externalId": "lms-user-42", "name": "Alice Martin" } }     ou     { "userId": "k97…" }
→ 201 { "lessonId": "l1…", "user": { … }, "completedAt": "…", "created": true }    (200 si déjà terminée)

Publications et commentaires

AppelCorps / réponse
GET/posts?category=&group=&limit=&cursor=Fil de l'espace, plus récent d'abord : { "posts": [ … ], "nextCursor" }. Catégories : general, announcements, question, wins, resource. group : publications adressées à ce groupe
GET/posts/:idLa publication avec ses comments (du plus ancien au plus récent, parentId pour les réponses)
POST/posts{ "title"?, "category"?, "content", "format"?, "attachments"?, "pinned"?, "groups"?, "author"? }201 { "post": { … } }. Mentions, XP et notifications comme dans l'app. pinned réservé aux modérateurs ; un auteur sans rôle staff ne peut adresser (groups) que ses propres groupes
PATCH/posts/:id{ "title"?: string | null, "category"?, "content"?, "format"?, "pinned"?, "groups"? }
DELETE/posts/:idSupprime la publication, ses commentaires et réactions
POST/posts/:id/comments{ "content", "format"?, "parentCommentId"?, "attachments"?, "author"? }201 { "comment": { … } }
DELETE/comments/:idSupprime le commentaire et ses réponses
{ "post": { "id": "p1…", "title": "Bienvenue à la promo 12", "category": "announcements", "content": "<p>…</p>", "text": "…",
  "attachments": [], "pinned": true, "groupIds": ["g1…"], "commentCount": 3, "reactionCount": 12, "createdAt": "…",
  "author": { "id": "k12…", "externalId": null, "name": "Julie", "email": "julie@client.fr", "image": "https://…", "role": "admin", "level": 5 },
  "url": "/mon-espace/posts/p1…" } }

Événements du calendrier

AppelCorps / réponse
GET/events?from=&to=&type=&group=Événements par date de début croissante ; from / to en ISO ou millisecondes. Les lives programmés y figurent avec liveSessionId
GET/events/:id{ "event": { "id", "type", "title", "description", "startsAt", "endsAt", "location", "liveSessionId", "groupIds", "createdAt" } }
POST/events{ "type"?, "title", "description"?, "startsAt", "endsAt"?, "location"?, "groups"?, "author"? }. Types : event (défaut), workshop, masterclass, deadline, qa. Les lives se créent via /live/sessions
PATCH/events/:idMêmes champs ; endsAt: null pour l'effacer, groups: [] pour ouvrir à tout l'espace. Un événement lié à un live se modifie via PATCH /live/sessions/:id
DELETE/events/:id{ "deleted": true }

Vidéos du studio

Enregistrements du studio LearnFloo (écran, caméra, imports) et replays de lives importés, hébergés sur Bunny Stream.

AppelRéponse
GET/videos?status=ready&limit=&cursor={ "videos": [ … ], "nextCursor" }, plus récentes d'abord. Statuts : uploading, processing, ready, failed
GET/videos/:id{ "video": { … } }
PATCH/videos/:id{ "title"?, "description"? }
{ "video": { "id": "v1…", "title": "Démo module 3", "description": null, "mode": "screen_camera", "status": "ready",
  "durationSec": 612, "width": 1920, "height": 1080, "encodeProgress": null, "thumbnailUrl": "https://…",
  "playbackUrl": "https://…/play_1080p.mp4", "hlsUrl": "https://…/playlist.m3u8", "embedUrl": "https://iframe.mediadelivery.net/embed/…",
  "lessonId": null, "liveSessionId": null, "createdAt": "…", "updatedAt": "…", "author": { … } } }

Les URL de lecture ne sont renvoyées que pour les vidéos ready. Comme pour les replays, le MP4 exige un en-tête Referer : intégrez-le dans une page. L'envoi de vidéos se fait depuis l'app (studio) ; l'API ne prend pas de fichier vidéo.

GETPOST/media et GETPATCHDELETE/media/:id

Médiathèque de l'espace : les images, PDF, vidéos, embeds et stickers utilisés dans les scènes du live et le studio. GET : ?kind=image|pdf|video|embed|sticker&limit=&cursor=.

{ "media": [ { "id": "…", "kind": "pdf", "title": "Support module 3", "url": "https://…", "thumbnailUrl": null, "videoId": null, "sizeBytes": 812000,
              "folder": { "id": "…", "name": "Live du 12/09" }, "createdAt": "…" } ], "nextCursor": null }

POST ajoute un élément par URL (https, hébergé chez vous ou sur un CDN) :

{ "kind": "image", "url": "https://cdn.votre-site.fr/offre.png", "title": "Offre de rentrée", "folder": "Live du 12/09" }
→ 201 { "media": { "id": "m12…", "kind": "image", … } }

kind : image, pdf, video (fichier MP4 lisible en direct), embed (page YouTube, Vimeo ou Loom, convertie en lecteur), sticker (PNG transparent) ou audio (MP3, WAV, OGG, M4A : son d'une scène). folder : identifiant ou nom d'un dossier (créé au passage s'il n'existe pas ; liste sur GET /media/folders, création explicite avec POST /media/folders { "name" }). Un PDF externe doit autoriser les requêtes depuis app.learnfloo.com (en-tête CORS), les images et vidéos n'ont pas cette contrainte. Pour envoyer un fichier plutôt qu'une URL, passez par POST /attachments puis donnez l'URL obtenue.

PATCH /media/:id : title, folder (null pour la racine). DELETE /media/:id supprime l'élément (et le fichier s'il est hébergé par LearnFloo) ; les scènes qui l'utilisaient gardent l'URL.

E-mails : principe

Tout le socle e-mail de l'espace se pilote par l'API, sous /emails/… : les campagnes aux membres (rédaction, aperçu, test, envoi immédiat ou programmé, statistiques), les segments qui définissent qui les reçoit, les textes réutilisables, le libellé des e-mails automatiques (invitations de live), le récapitulatif automatique des nouveautés, le domaine d'envoi du client et les préférences de chaque membre. Les mêmes actions existent comme outils MCP (famille « E-mails »).

Règles communes : l'auteur de chaque écriture est le créateur de la clé API (c'est lui qui reçoit les envois de test) ; les corps sont en texte brut (ligne vide = paragraphe) ou en HTML avec "format": "html" ; {{prenom}} et {{espace}} sont remplacés pour chaque lecteur ; chaque e-mail groupé porte le lien de désabonnement de la personne, et une personne désabonnée ne reçoit plus ni campagne ni récapitulatif de cet espace. Une clé en lecture seule accède aux GET et à POST /emails/segments/preview.

GETPOST/emails/campaigns et GETPATCHDELETE/emails/campaigns/:id

POST { "subject": "Nouveautés de {{espace}}", "previewText": "Trois nouveautés cette semaine", "body": "Bonjour {{prenom}},

Voici…",
       "segmentId": "…", "templateId": "…", "send": "now" }
→ 201 { "campaign": { "id": "…", "kind": "campaign", "subject": "…", "previewText": "…", "body": "<p>…</p>", "status": "draft",
  "segmentId": null, "audienceLabel": null, "scheduledAt": null, "sentAt": null, "recipientCount": null,
  "stats": { "sent": 0, "delivered": 0, "opened": 0, "clicked": 0, "bounced": 0, "spam": 0, "unsubscribed": 0 },
  "error": null, "createdAt": "…", "updatedAt": "…" } }

GET /emails/campaigns?status=draft|scheduled|sending|sent|cancelled|failed&kind=campaign|digest liste les 200 dernières, les plus récentes d'abord (kind: "digest" : les récapitulatifs automatiques envoyés, avec leurs statistiques). POST crée un brouillon ; templateId reprend un texte réutilisable pour ce qui n'est pas fourni ; send vaut "now" ou une date ISO pour envoyer dans le même appel (3 jetons). PATCH modifie un brouillon seulement (subject, previewText, body + format, segmentId, null pour retirer). DELETE supprime (une campagne programmée est déprogrammée ; en cours d'envoi, refus 403).

Les stats viennent des accusés du fournisseur : delivered, opened, clicked, bounced, spam et unsubscribed se remplissent dans les heures qui suivent l'envoi.

Envoyer, programmer, annuler, tester, prévisualiser

POST /emails/campaigns/:id/send { "at": "2026-09-22T09:00:00+02:00" }   → { "campaign": { "status": "scheduled", … } }   (5 jetons ; sans "at" : envoi immédiat, status "sending")
POST /emails/campaigns/:id/cancel                                        → { "campaign": { "status": "draft", … } }
POST /emails/campaigns/:id/test                                          → { "sent": true, "to": "vous@…", "error": null }   (au créateur de la clé, objet préfixé [Test] ; 3 jetons)
GET  /emails/campaigns/:id/preview                                       → { "subject": "…", "html": "…", "text": "…" }   (rendu complet, cadre et pied de désabonnement compris)

Un envoi est irréversible dès que la campagne passe en sending : un assistant doit montrer l'aperçu et obtenir l'accord de l'utilisateur avant d'appeler /send. Les webhooks email.campaign.sent et email.campaign.failed signalent la fin de l'envoi.

GETPOST/emails/segments et GETPATCHDELETE/emails/segments/:id

Un segment est un filtre enregistré sur les membres actifs de l'espace, résolu au moment de l'envoi. Tous les filtres donnés doivent être vérifiés ; users s'ajoute au résultat.

POST { "name": "Nouveaux silencieux", "description": "…",
       "filters": { "roles": ["member"], "groups": ["promo-2026"], "minLevel": 1, "maxLevel": 2, "joinedWithinDays": 30, "activeWithinDays": 14, "users": ["ext:u-42"] } }
→ 201 { "segment": { "id": "…", "name": "…", "description": "…", "memberCount": 12,
                     "filters": { "roles": ["member"], "groups": [ { "id", "slug", "name" } ], "minLevel": 1, "maxLevel": 2, "joinedWithinDays": 30, "activeWithinDays": 14, "users": [ { "id", "name" } ] },
                     "createdAt": "…", "updatedAt": "…" } }
POST /emails/segments/preview { "filters": { … } }   → { "memberCount": 12, "sample": [ { "id", "name" } ], "capped": false, "filters": { … } }   (rien n'est enregistré ; 3 jetons)
GET  /emails/segments/:id                            → { "segment": { …, "memberCount": recalculé, "sample": […], "capped": false } }   (3 jetons)

roles : owner, admin, moderator, teacher, contributor, member ; groups accepte identifiants ou slugs ; activeWithinDays s'appuie sur la dernière ouverture de l'application. Cinquante segments par espace ; au-delà de 5 000 membres, capped passe à true.

GETPOST/emails/templates et PATCHDELETE/emails/templates/:id

Textes de campagne réutilisables (objet, texte d'aperçu, corps), à passer en templateId à la création d'une campagne. POST { "name", "subject", "previewText", "body", "format" }201 { "template": { "id", "name", "subject", "previewText", "body", "updatedAt" } }. Cinquante par espace.

GET/emails/notifications et PUTDELETE/emails/notifications/:kind/:lang

Le libellé des e-mails automatiques que l'espace envoie, par type et par langue (kind : live_invitation pour l'instant ; lang : fr, en, es). Le cadre (couleur, bouton, liens agenda, pied) reste celui de la plateforme.

GET → { "templates": [ { "kind": "live_invitation", "lang": "fr", "subject": "…", "body": "…", "custom": false, "updatedAt": null }, … ],
        "variables": { "live_invitation": [ { "key": "prenom", "label": { "fr", "en", "es" }, "sample": { … } }, … ] },
        "defaults": { "live_invitation": { "fr": { "subject", "body" }, … } } }
PUT    /emails/notifications/live_invitation/fr { "subject": "…", "body": "…", "format": "html" }   → { "template": { …, "custom": true } }
DELETE /emails/notifications/live_invitation/fr                                                     → texte par défaut rétabli
GET    /emails/notifications/live_invitation/fr/preview?subject=…&body=…                          → { "subject", "html" }   (valeurs d'exemple ; sans paramètres : le texte enregistré)

GETPATCH/emails/digest et GET…/digest/preview, POST…/digest/send

Récapitulatif automatique des nouveautés (publications, cours, leçons, lives et événements à venir, défis) envoyé aux membres à la cadence choisie.

GET → { "digest": { "enabled": false, "cadence": "weekly", "weekday": 1, "dayOfMonth": 1, "hour": 9, "timeZone": "Europe/Paris",
                    "sections": { "posts": true, "courses": true, "lessons": true, "lives": true, "events": true, "challenges": true },
                    "maxItems": 5, "subject": "", "intro": "", "onlyIfNews": true, "segmentId": null, "lastSentAt": null, "nextAt": null, "configured": false } }
PATCH { "enabled": true, "cadence": "weekly", "weekday": 2, "hour": 8, "sections": { "challenges": false }, "segmentId": null }   → { "digest": { …, "nextAt": "…" } }   (mise à jour partielle)
GET  /emails/digest/preview          → { "subject", "html", "empty": false }   (tel qu'il partirait maintenant ; 3 jetons)
POST /emails/digest/send { "test": true }   → { "sent": 1, "skipped": false, "error": null }   (test : au créateur de la clé ; sans "test" : à toute l'audience, 5 jetons)

cadence : daily, weekly (weekday 0 = dimanche … 6) ou monthly (dayOfMonth 1 à 28) ; hour en heure locale de timeZone ; subject et intro acceptent {{prenom}} et {{espace}}, null rétablit le texte de la plateforme.

GETPOSTPATCHDELETE/emails/sender et POST…/sender/check, …/sender/test

D'où partent les e-mails de l'espace. Par défaut « via LearnFloo » ; avec l'option payante « domaine d'envoi », depuis le domaine du client une fois ses enregistrements DNS vérifiés. L'activation de l'option reste dans l'application (elle est facturée au propriétaire) ; le reste se pilote ici.

GET → { "option": true, "active": false, "configured": true, "defaultFrom": "invitations@learnfloo.com", "priceCents": 1000, "currency": "eur",
        "sender": { "id": "…", "domain": "out.monecole.fr", "fromEmail": "contact@out.monecole.fr", "fromName": "Mon école", "replyTo": "contact@monecole.fr",
                    "dns": [ { "type": "TXT", "host": "…_domainkey.out.monecole.fr", "value": "k=rsa;…", "verified": false, "purpose": "dkim" },
                             { "type": "CNAME", "host": "pm-bounces.out.monecole.fr", "value": "pm.mtasv.net", "verified": false, "purpose": "return-path" } ],
                    "verifiedAt": null, "lastCheckedAt": null, "lastError": null, "testedAt": null, "sentCount": 0, "bounceCount": 0, "complaintCount": 0, "suspendedAt": null, "suspendReason": null } }
POST   /emails/sender { "domain": "out.monecole.fr", "fromEmail": "contact@out.monecole.fr", "fromName": "Mon école", "replyTo": "contact@monecole.fr" }   → 201, même réponse que GET
POST   /emails/sender/check   → { "dkimVerified": true, "returnPathVerified": true, …GET }   (demande la vérification des DNS)
PATCH  /emails/sender { "fromEmail", "fromName", "replyTo" }
POST   /emails/sender/test    → { "sent": true, "from": "…", "error": null }   (au créateur de la clé ; 3 jetons)
DELETE /emails/sender         → { "removed": true }   (retour « via LearnFloo »)

active vaut true quand les e-mails partent réellement du domaine (option activée, deux enregistrements vérifiés, pas de suspension). suspendedAt : envoi depuis le domaine coupé par la règle de rebonds et plaintes, à voir avec le support.

GETPUT/users/:id/email-preferences

GET → { "user": { "id", "externalId", "name", "email", "image" }, "hasEmail": true, "subscribed": true, "unsubscribedAt": null, "unsubscribedFromCampaignId": null }
PUT { "subscribed": false }   → même objet mis à jour

Abonnement d'un membre aux e-mails groupés de l'espace (campagnes et récapitulatif), à lire ou à synchroniser depuis votre plateforme : passer subscribed à false revient au clic sur « se désabonner » (webhook email.unsubscribed). Les e-mails individuels (invitations de live) ne sont pas concernés.

Webhooks : principe et événements

Plutôt que d'interroger l'API, faites-vous prévenir : LearnFloo envoie un POST JSON à l'URL de votre choix à chaque événement souscrit. Jusqu'à 10 webhooks par espace.

POST https://votre-backend.fr/learnfloo
Content-Type: application/json
X-LearnFloo-Event: live.session.ended
X-LearnFloo-Delivery: d7…
X-LearnFloo-Signature: t=1757404800,v1=5f1a…

{ "id": "d7…", "event": "live.session.ended", "createdAt": "2026-09-12T15:01:40.000Z",
  "space": { "id": "…", "slug": "mon-espace" },
  "data": { "session": { … objet session … } } }
Événementdata
live.session.created, live.session.updated, live.session.started, live.session.ended, live.session.cancelled{ session } (voir GET /live/sessions/:id). Après ended, appelez /attendance pour les présences
live.replay.ready{ session } avec replayUrl renseigné (MP4 brut, dès la fin du live)
live.conversion{ session, conversion } — inscription signalée via /conversions
live.poll.opened, live.poll.closed{ session, poll } — un sondage ou quiz lancé, puis fermé avec ses résultats
live.scene.changed{ session, scene } — une scène mise à l'antenne (scene à null : disposition automatique)
support.ticket.created{ ticket, author, message: { content, text, attachments } }
support.ticket.message{ ticket, message: { id, content, text, attachments, fromStaff, author, createdAt } } — réponse de l'utilisateur ou de l'équipe (jamais les notes internes)
support.ticket.status{ ticket, previousStatus, byStaff }
lesson.completed{ lesson: { id, title, courseId, moduleId }, user, completedAt }
video.lead{ lead: { id, email, name, userId, source, lessonId, interactionId, createdAt } } — email laissé dans une capture d'email d'une vidéo (source : lesson ou about pour la vidéo de présentation)
post.created{ post }
comment.created{ comment, post: { id, title } }
member.joined{ user, member } — nouveau membre (rejoint depuis LearnFloo ou créé par l'API)
email.campaign.sent, email.campaign.failed{ campaign } — fin d'envoi d'une campagne ou d'un récapitulatif (kind), avec recipientCount, stats.sent et error
email.unsubscribed{ user, campaignId, unsubscribedAt } — un membre s'est désabonné des e-mails groupés de l'espace (lien de désabonnement, page, ou API)
pingEnvoi de test (POST /webhooks/:id/test)

Les objets user / author contiennent { id, externalId, name, email, image } : externalId vous permet de retrouver la personne chez vous.

Signature et relances

Répondez 2xx en moins de 10 secondes (traitez ensuite). Vérifiez la signature : HMAC-SHA256 du texte <t>.<corps brut> avec le secret du webhook, comparé à v1 ; rejetez si t a plus de 5 minutes.

import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyLearnFloo(rawBody, signatureHeader, secret) {
  const { t, v1 } = Object.fromEntries(signatureHeader.split(',').map((kv) => kv.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}

En cas d'échec (réponse non 2xx, délai dépassé, erreur réseau), l'envoi est retenté après 1 min, 5 min, 30 min puis 2 h (5 tentatives). Un même événement peut donc arriver deux fois : utilisez id (ou X-LearnFloo-Delivery) pour dédoublonner. Après 20 échecs consécutifs, le webhook est mis en pause (active: false) ; réactivez-le avec PATCH une fois votre endpoint réparé.

Gestion des webhooks

AppelCorps / réponse
GET/webhooks{ "webhooks": [ { "id", "url", "events", "description", "active", "createdAt", "updatedAt", "lastDeliveryAt", "lastStatus", "failureCount" } ] }
POST/webhooks{ "url": "https://…", "events": ["live.session.ended", "live.replay.ready"], "secret"?, "description"? }201 { "webhook": { …, "secret" } }. events: ["*"] pour tout recevoir. Le secret (16 à 128 caractères, généré si absent) n'est renvoyé qu'à la création. URL en https obligatoire
GET/webhooks/:id{ "webhook": { … } }
PATCH/webhooks/:id{ "url"?, "events"?, "description"?, "active"? }. active: true remet le compteur d'échecs à zéro
DELETE/webhooks/:id{ "deleted": true }
POST/webhooks/:id/testEnvoie un événement ping202 { "sent": true }
GET/webhooks/:id/deliveries?limit=50Journal des envois (30 jours) : { "deliveries": [ { "id", "event", "attempt", "status": "pending|success|failed", "responseStatus", "error", "createdAt", "deliveredAt" } ] }

OpenAPI

La description OpenAPI 3.1 de l'API est générée depuis le catalogue des outils MCP (une opération par route, operationId = nom de l'outil MCP), pour les générateurs de clients, les outils d'API et les Actions des GPT de ChatGPT :

GET https://api.learnfloo.com/openapi.json                 toute l'API (159 opérations), sécurité clé API ou OAuth
GET https://api.learnfloo.com/openapi.json?preset=gpt      30 opérations du GPT LearnFloo (limite de ChatGPT par action), OAuth seul
GET https://api.learnfloo.com/openapi.json?tools=get_space,list_posts,create_post    un sous-ensemble d'outils
GET https://api.learnfloo.com/openapi.json?groups=live,courses                       des familles entières

Un GPT personnalisé se crée avec ce fichier et un client OAuth confidentiel (voir Serveur MCP) ; la marche à suivre et les instructions du GPT sont dans le LearnFloo Agent Kit.

Serveur MCP (assistants IA)

La même API est exposée comme serveur MCP (Model Context Protocol) pour les assistants et agents IA : Claude (application, claude.ai, Claude Code), ChatGPT, Cursor, ou tout client MCP. L'assistant voit l'espace à travers des outils nommés (list_live_sessions, create_post, get_group_stats…), chacun équivalent à une route de cette référence : mêmes champs, mêmes réponses, mêmes erreurs.

URL : https://api.learnfloo.com/mcp        (transport « Streamable HTTP », JSON-RPC 2.0, sans session)

Deux façons de se connecter, au choix de l'utilisateur selon le niveau de sécurité voulu :

OAuth (recommandé)Clé API
PrincipeL'assistant ouvre une page LearnFloo : l'utilisateur se connecte avec son compte, choisit l'espace, le niveau d'accès et les outils, et autorise. Jetons d'accès d'une heure renouvelés automatiquement, révocables.Une clé de l'espace (Authentification), en en-tête Authorization: Bearer lf_live_… ou, pour les clients sans en-têtes, dans l'URL https://api.learnfloo.com/mcp?key=lf_live_….
Qui peutPropriétaire ou administrateur de l'espace, avec son propre compteQuiconque détient la clé
Attribution des actionsÀ l'utilisateur qui a autoriséAu créateur de la clé
RisqueAucun secret dans l'URL ni la configuration du clientUne clé dans l'URL peut se retrouver dans des journaux ; en cas de fuite, révoquez-la
Où ça se gèreParamètres de l'espace → API et MCP : clés et autorisations OAuth côte à côte, chacune avec son niveau d'accès et ses outils ouverts

Configuration des clients

ClientOAuthClé API
claude.ai, application ClaudeParamètres → Connecteurs → Ajouter un connecteur personnalisé, URL https://api.learnfloo.com/mcp, puis « Connecter » : la page d'autorisation LearnFloo s'ouvreMême écran, URL https://api.learnfloo.com/mcp?key=lf_live_…, sans authentification
Claude Codeclaude mcp add --transport http learnfloo https://api.learnfloo.com/mcp puis /mcp pour s'authentifierclaude mcp add --transport http learnfloo https://api.learnfloo.com/mcp --header "Authorization: Bearer lf_live_…"
Cursor, Windsurf, autres{ "mcpServers": { "learnfloo": { "url": "https://api.learnfloo.com/mcp" } } } (le client lance l'autorisation){ "mcpServers": { "learnfloo": { "url": "https://api.learnfloo.com/mcp", "headers": { "Authorization": "Bearer lf_live_…" } } } }

Serveur OAuth 2.1 : découverte /.well-known/oauth-protected-resource et /.well-known/oauth-authorization-server, enregistrement dynamique des clients POST /oauth/register (clients publics), GET /oauth/authorize (PKCE S256 obligatoire), POST /oauth/token (authorization_code, refresh_token avec rotation), POST /oauth/revoke. Portées read et full ; le niveau réellement accordé est celui choisi sur la page d'autorisation, renvoyé dans scope. Une réponse 401 du serveur MCP porte WWW-Authenticate: Bearer resource_metadata=… pour que le client découvre le serveur OAuth.

Clients confidentiels. Pour un GPT de ChatGPT ou une intégration côté serveur, l'équipe LearnFloo enregistre un client avec client_secret (méthodes client_secret_post ou client_secret_basic, en plus de PKCE) et des URL de retour à joker (https://chatgpt.com/aip/*/oauth/callback). Les jetons d'accès obtenus (lfo_…) sont acceptés par le serveur MCP et par l'API REST (Authorization: Bearer lfo_…) : sur l'API REST, une autorisation n'ouvre que les routes des outils choisis sur la page d'autorisation (un outil = une route), sinon 403.

La clé dans l'URL (?key=) est visible dans l'historique et les journaux du client : réservez-la aux clients sans en-têtes quand OAuth n'est pas souhaité, créez une clé dédiée (nommée d'après l'assistant) et révoquez-la au moindre doute. Une clé MCP en lecture seule suffit pour analyser, résumer et répondre à des questions.

Outils ouverts. Pour chaque clé (dans les paramètres) et chaque autorisation OAuth (sur la page d'autorisation, modifiable ensuite dans les paramètres), un niveau d'accès (lecture seule ou complet) et la liste des outils MCP ouverts : tous les outils du niveau (défaut), ou une sélection (par exemple, un assistant de régie qui ne voit que les scènes et les sondages d'un live ; un assistant support qui ne voit que les tickets). tools/list ne renvoie que les outils ouverts ; un outil fermé est refusé comme inconnu.

Débit et coûts. Chaque message JSON-RPC consomme les jetons de la route sous-jacente (limite de débit) ; initialize, ping et tools/list en consomment un. Une erreur de l'API (400, 403, 404…) revient comme résultat isError avec son message ; une clé refusée ou une limite atteinte comme erreur JSON-RPC (HTTP 401 ou 429, Retry-After). Les objets renvoyés sont dans structuredContent et, en texte, dans content.

Ce que le MCP ne fait pas : l'envoi de fichiers (POST /attachments reste une route REST) et les webhooks entrants. Les appels d'écriture faits par un assistant sont attribués au créateur de la clé, sauf author ou user explicite, comme en REST.

Recettes (prompts) et kit pour assistants. Le serveur sert aussi des prompts MCP (prompts/list, prompts/get, argument facultatif request) : neuf recettes métier pour le propriétaire d'un espace (bilan de l'espace, préparation d'un live, régie, débrief, construction d'un cours, animation du fil, relance des membres, tickets de support, intégration), proposées par les clients qui affichent les prompts (Claude, Cursor…) sans rien installer. Les mêmes recettes existent comme skills (format ouvert Agent Skills) et comme plugin Claude Code dans le LearnFloo Agent Kit : claude plugin marketplace add learnfloo/agent-kit puis claude plugin install learnfloo@learnfloo ; script d'installation pour Codex, Cursor et Gemini CLI. Chaque recette montre le contenu avant toute écriture et se limite à la lecture avec une clé en lecture seule.

Outils

159 outils, par famille. Colonne « Accès » : lecture (ouvert aux clés en lecture seule) ou écriture.

Espace

OutilDescriptionAccèsRoute
get_spaceThe space behind the API key: name, slug, visibility, price, member and course counts, group vocabulary, URL. Call it first to know where you are.lectureGET /space
get_space_usageUsage of the space since the last invoice (webinar hours, interactive and spectator hours, plays, stored minutes), account state (billing active, live allowed, cap) and the rate limit of the key.lectureGET /space/usage

Membres et gamification

OutilDescriptionAccèsRoute
list_usersMembers of the space, paginated (200 max per page), with role, status, XP and level.lectureGET /users
get_userOne user with membership (role, status, XP, level) and stats (lessons completed, lives joined, posts, badges).lectureGET /users/:id
create_userCreates (or updates, idempotent on externalId) a user of your platform and adds them as member of the space.écriturePOST /users
update_userChanges the name (users created by your platform only), the role or the status of a member. The owner cannot be changed.écriturePATCH /users/:id
remove_userRemoves the member from the space (account and history kept).écritureDELETE /users/:id
create_user_entry_urlSingle-use, 15-minute sign-in URL that opens the space (feed, courses, calendar…) for a user of your platform (ext:… only; created when name is given). Ask for it at click time.écriturePOST /users/:id/entry
get_user_progressProgress of a user in every course of the space (lessons completed, percentage).lectureGET /users/:id/progress
get_user_xpTotal XP, level and latest XP events of a user in the space.lectureGET /users/:id/xp
award_xpGives (or takes, negative amount) XP to a user from your platform: quiz passed, homework handed in… Level recalculated.écriturePOST /users/:id/xp
get_user_badgesBadges awarded to a user.lectureGET /users/:id/badges
get_leaderboardMembers ranked by XP (500 max), optionally within one group.lectureGET /leaderboard
list_badgesBadges of the space with their criteria and XP reward.lectureGET /badges
list_challengesChallenges of the space (objective, dates, status, participants).lectureGET /challenges
get_challengeOne challenge with its participants and their progress.lectureGET /challenges/:id

Lives

OutilDescriptionAccèsRoute
list_live_sessionsLive sessions of the space, newest first (200 max), with status, dates, replay and counters.lectureGET /live/sessions
get_live_sessionOne live session: status, schedule, participants now, recording and replay, chat mode, conversions, active scene.lectureGET /live/sessions/:id
create_live_sessionSchedules a live (webinar or conference) in the space. Returns the session and hostUrl, the page where the host starts it.écriturePOST /live/sessions
update_live_sessionChanges a live. Before it starts: everything (date, duration, format, capacity, recording, chat, audience, replay rule). Once started: title, description, audience, capacity, chat, replay rule. Once ended: title, description, audience, replay rule.écriturePATCH /live/sessions/:id
cancel_live_sessionCancels a scheduled live (calendar event removed). A running or ended live cannot be cancelled.écritureDELETE /live/sessions/:id
create_live_entry_urlPersonal, single-use, 15-minute URL that signs a user of your platform into a live (viewer or speaker). Ask for it at click time.écriturePOST /live/entry
get_live_attendanceAttendance report: summary (attended, peak, average watch time, spectator and interactive hours) and per participant watch time and percentage. Costs 5 rate-limit tokens.lectureGET /live/sessions/:id/attendance
get_live_participantsInstant state of the people registered on a live: connected, spectator, hand raised, role.lectureGET /live/sessions/:id/participants
get_live_chatChat messages of a live, oldest first (2000 max), kept after the live.lectureGET /live/sessions/:id/chat
get_live_replayThe replay of a live: MP4, HLS and embed URLs, thumbnail, duration, linked lesson, views.lectureGET /live/sessions/:id/replay
report_live_conversionReports a sign-up or purchase made during a live: announced in the chat and on the scene, counted in the report, webhook live.conversion.écriturePOST /live/sessions/:id/conversions
list_live_invitesShareable invitation links of a live (speaker, viewer, assistant, moderator).lectureGET /live/sessions/:id/invites
create_live_inviteCreates a reusable invitation link for people without an account on your platform (external speaker, control room assistant, chat moderator).écriturePOST /live/sessions/:id/invites
revoke_live_inviteRevokes an invitation link.écritureDELETE /live/sessions/:id/invites/:inviteId
set_participant_roleGives or takes the floor, or names a control-room assistant or a moderator, for a participant already registered on the live. Applied immediately.écriturePUT /live/sessions/:id/participants/:userId/role
set_extra_viewersAdds viewers to the counter shown by the viewers widget of the scenes (never counted in attendance or billing). 0 removes it.écriturePUT /live/sessions/:id/viewers
stage_chat_messageShows a chat message on the scene (message slot, else bottom left); null removes it.écriturePUT /live/sessions/:id/chat/stage

Scènes et régie

OutilDescriptionAccèsRoute
get_live_scenesThe whole control room of a live: scenes with slots and overlays, active scene, auto-advance state, media state (PDF page, video playing), temporary bands.lectureGET /live/sessions/:id/scenes
get_live_sceneOne scene of a live.lectureGET /live/sessions/:id/scenes/:sceneId
create_sceneCreates a scene in a live, from its fields or from a template of the space ({ template: "name" }). activate: true puts it on air right away. Works before and during the live.écriturePOST /live/sessions/:id/scenes
update_sceneChanges the given fields of a scene; slots and overlays, when present, replace the whole lists (use the overlay and slot tools to touch one element). Visible immediately if on air.écriturePATCH /live/sessions/:id/scenes/:sceneId
delete_sceneRemoves a scene (automatic layout if it was on air). Returns the control room.écritureDELETE /live/sessions/:id/scenes/:sceneId
activate_scenePuts a scene on air for everyone (room, audience stream, recording, external broadcasts). Its videos restart from the beginning.écriturePOST /live/sessions/:id/scenes/:sceneId/activate
set_active_sceneSets the scene on air by id; null returns to the automatic layout (cameras in a grid, screen share large).écriturePUT /live/sessions/:id/scenes/active
reorder_scenesSets the order of the scenes of a live.écriturePUT /live/sessions/:id/scenes/order
set_auto_advanceTurns the automatic scene sequence on or off (each scene with durationSec gives way to nextSceneId or the next one), with or without loop.écriturePUT /live/sessions/:id/scenes/auto
show_bandShows a band at the top of every screen for a few seconds, whatever the scene (break, announcement).écriturePOST /live/sessions/:id/bands
control_scene_mediaTurns the page of a PDF, or plays, pauses and seeks a video or the scene sound present in a scene, synchronised for everyone.écriturePUT /live/sessions/:id/media/:mediaId
add_overlayAdds a sticker, a band or a widget (poll, message, viewers) to a scene. Give it a stable id to update it later.écriturePOST /live/sessions/:id/scenes/:sceneId/overlays
update_overlayChanges the given fields of one overlay of a scene ({ text }, { visible: false }, { x, y }…). Visible immediately if on air.écriturePATCH /live/sessions/:id/scenes/:sceneId/overlays/:overlayId
update_overlay_everywhereChanges the overlay carrying this id in every scene of the live: a common band (seats left, price, next step) updated in one call.écriturePATCH /live/sessions/:id/overlays/:overlayId
delete_overlayRemoves an overlay from a scene.écritureDELETE /live/sessions/:id/scenes/:sceneId/overlays/:overlayId
add_slotAdds a slot (host, guest, screen, media, embed) to a scene; position 0 = main.écriturePOST /live/sessions/:id/scenes/:sceneId/slots
update_slotChanges a slot of a scene (other media, guest, fit, geometry in free layout, position).écriturePATCH /live/sessions/:id/scenes/:sceneId/slots/:slotId
delete_slotRemoves a slot from a scene.écritureDELETE /live/sessions/:id/scenes/:sceneId/slots/:slotId
list_scene_templatesScene templates of the space, reusable in every live.lectureGET /scene-templates
create_scene_templateSaves a scene template from fields ({ name, scene }) or from an existing scene ({ name, sessionId, sceneId }). Same name = replaced.écriturePOST /scene-templates
delete_scene_templateDeletes a scene template (id or name).écritureDELETE /scene-templates/:id

Sondages et quiz

OutilDescriptionAccèsRoute
list_live_pollsPolls and quizzes of a live with their counts, kept after the live.lectureGET /live/sessions/:id/polls
get_live_pollOne poll or quiz with its results.lectureGET /live/sessions/:id/polls/:pollId
create_pollCreates a poll (opinion) or a quiz (with correct answers) in a live, as a draft, or opened right away with open: true during the live. 2 to 6 options.écriturePOST /live/sessions/:id/polls
update_pollChanges a draft poll (same fields as creation).écriturePATCH /live/sessions/:id/polls/:pollId
delete_pollDeletes a poll and its votes, whatever its status.écritureDELETE /live/sessions/:id/polls/:pollId
open_pollOpens the vote to the participants (one poll open at a time, running live only).écriturePOST /live/sessions/:id/polls/:pollId/open
close_pollCloses the vote; results shown unless showResults is false.écriturePOST /live/sessions/:id/polls/:pollId/close
show_poll_resultsShows or hides the counts to the participants (and the correct answer of a quiz).écriturePUT /live/sessions/:id/polls/:pollId/results
stage_pollShows the poll in the scene (poll slot, else bottom right) in the room, the broadcast and the recording; false removes it.écriturePUT /live/sessions/:id/polls/:pollId/stage
show_poll_bandShows a 12-second band on the scene with the leading answers (or the right answer and the success rate of a quiz).écriturePOST /live/sessions/:id/polls/:pollId/band

Support

OutilDescriptionAccèsRoute
list_support_ticketsTickets of one external user (externalId) or of the whole space (scope: "space", team dashboard), newest first.lectureGET /support/tickets
get_support_ticketA ticket with its messages (never the internal notes). Give externalId of the user, or scope: "space" for the team.lectureGET /support/tickets/:id
create_support_ticketOpens a support ticket on behalf of a user of your platform. The team of the space is notified.écriturePOST /support/tickets
reply_support_ticketAdds a message to a ticket, as the user ({ user }) or as the team ({ staff: {} } or { staff: { email } }): a staff reply puts the ticket in pending.écriturePOST /support/tickets/:id/messages
set_support_ticket_statusChanges the status of a ticket: as the user (resolved or open), or as the team (any status, plus priority and assigneeEmail).écriturePOST /support/tickets/:id/status

Groupes

OutilDescriptionAccèsRoute
list_groupsGroups of the space (classes, teams, cohorts… see groupLabels of get_space) with member counts and automatic rules.lectureGET /groups
get_groupOne group (id or slug) with its members and their group role (leader or member).lectureGET /groups/:id
create_groupCreates a group, manual or automatic (rule: level_min, streak_days, challenge_joined, challenge_completed, badge, course_completed).écriturePOST /groups
update_groupChanges name, slug, description, color or rule (null = manual group) of a group.écriturePATCH /groups/:id
delete_groupDeletes a group; the content addressed only to it becomes visible to the whole space.écritureDELETE /groups/:id
add_group_membersAdds members of the space to a group (500 per call, idempotent), as member or leader.écriturePOST /groups/:id/members
remove_group_memberRemoves one member from a group.écritureDELETE /groups/:id/members/:userId
get_user_groupsGroups of a user in the space, with their group role.lectureGET /users/:id/groups
set_user_groupsReplaces the whole list of groups of a user (sync of classes from your platform).écriturePUT /users/:id/groups
get_group_leaderboardMembers of a group ranked by XP.lectureGET /groups/:id/leaderboard
get_group_statsFollow-up of every member of a group: course progress, lessons completed, posts, lives attended, XP over 30 days, last activity, plus a summary. Costs 5 rate-limit tokens.lectureGET /groups/:id/stats
list_group_documentsShared documents of a group, newest first.lectureGET /groups/:id/documents
add_group_documentAdds a document (file stored via POST /api/v1/attachments, or any https link) to a group.écriturePOST /groups/:id/documents
delete_group_documentDeletes a document of a group.écritureDELETE /groups/:id/documents/:docId

Cours

OutilDescriptionAccèsRoute
list_coursesPublished courses of the space (includeUnpublished for drafts too), with lesson counts and audience.lectureGET /courses
get_courseA course (id or slug) with its modules and lessons; withContent adds the HTML of the lessons.lectureGET /courses/:id
create_courseCreates a course (draft by default). The author, when given, must be owner, admin or teacher.écriturePOST /courses
update_courseChanges title, description, cover, published state or audience of a course.écriturePATCH /courses/:id
delete_courseDeletes a course with its modules, lessons and progress.écritureDELETE /courses/:id
create_moduleAdds a module (section) to a course.écriturePOST /courses/:id/modules
create_lessonAdds a lesson to a course, with rich content and an optional video (MP4, HLS, YouTube, Vimeo, or playbackUrl of a studio video).écriturePOST /courses/:id/lessons
get_lessonOne lesson with its content.lectureGET /lessons/:id
update_lessonChanges title, content, module, video or order of a lesson.écriturePATCH /lessons/:id
delete_lessonDeletes a lesson.écritureDELETE /lessons/:id
get_course_progressEvery learner who started the course (most advanced first), or the lesson-by-lesson detail of one learner (externalId or userId). Costs 5 rate-limit tokens.lectureGET /courses/:id/progress
complete_lessonMarks a lesson as completed for a learner, as if validated in LearnFloo (XP, quests, badges, webhook). Idempotent.écriturePOST /lessons/:id/complete

Fil de la communauté

OutilDescriptionAccèsRoute
list_postsFeed of the space, newest first, paginated; categories general, announcements, question, wins, resource.lectureGET /posts
get_postA post with its comments (oldest first, parentId for replies).lectureGET /posts/:id
create_postCreates a post in the feed (mentions, XP and notifications as in the app). pinned is for moderators.écriturePOST /posts
update_postChanges title, category, content, pinned state or audience of a post.écriturePATCH /posts/:id
delete_postDeletes a post with its comments and reactions.écritureDELETE /posts/:id
create_commentAdds a comment (or a reply with parentCommentId) to a post.écriturePOST /posts/:id/comments
delete_commentDeletes a comment and its replies.écritureDELETE /comments/:id

Calendrier

OutilDescriptionAccèsRoute
list_eventsCalendar events by start date (scheduled lives included with liveSessionId).lectureGET /events
get_eventOne calendar event.lectureGET /events/:id
create_eventCreates a calendar event (lives are created with create_live_session).écriturePOST /events
update_eventChanges an event (an event linked to a live is changed with update_live_session).écriturePATCH /events/:id
delete_eventDeletes a calendar event.écritureDELETE /events/:id

Vidéos du studio

OutilDescriptionAccèsRoute
list_videosStudio recordings and imported live replays, newest first, paginated.lectureGET /videos
get_videoOne video with its playback URLs (ready videos only), duration, thumbnail, linked lesson or live.lectureGET /videos/:id
update_videoChanges the title or description of a video.écriturePATCH /videos/:id

Médiathèque

OutilDescriptionAccèsRoute
list_mediaImages, PDF, videos, embeds, stickers and sounds of the space used in scenes and the studio.lectureGET /media
list_media_foldersFolders of the media library.lectureGET /media/folders
get_mediaOne item of the media library.lectureGET /media/:id
add_mediaAdds an item to the media library from an https URL (image, pdf, video, embed of YouTube/Vimeo/Loom, sticker, audio), optionally in a folder (created when needed).écriturePOST /media
create_media_folderCreates a folder in the media library.écriturePOST /media/folders
update_mediaChanges the title or the folder (null for the root) of a media item.écriturePATCH /media/:id
delete_mediaDeletes a media item (and its file when hosted by LearnFloo).écritureDELETE /media/:id

Webhooks

OutilDescriptionAccèsRoute
list_webhooksOutgoing webhooks of the space with their events and delivery state.lectureGET /webhooks
get_webhookOne webhook.lectureGET /webhooks/:id
create_webhookSubscribes an https URL to events (["*"] for all). The signing secret is returned once.écriturePOST /webhooks
update_webhookChanges url, events, description or active state (active: true resets the failure count).écriturePATCH /webhooks/:id
delete_webhookDeletes a webhook.écritureDELETE /webhooks/:id
test_webhookSends a ping event to a webhook.écriturePOST /webhooks/:id/test
list_webhook_deliveriesDelivery log of a webhook over 30 days (attempts, status, errors).lectureGET /webhooks/:id/deliveries

E-mails

OutilDescriptionAccèsRoute
list_email_campaignsCampaigns of the space (drafts, scheduled, sent, failed) with their statistics: sent, delivered, opened, clicked, bounced, spam, unsubscribed. kind "digest" lists the automatic digests sent.lectureGET /emails/campaigns
get_email_campaignOne campaign with its content, audience and statistics.lectureGET /emails/campaigns/:id
preview_email_campaignThe campaign as a reader will receive it (subject, HTML and text, frame and unsubscribe footer included), rendered with the key creator’s first name.lectureGET /emails/campaigns/:id/preview
create_email_campaignCreates a campaign as a draft (or from a reusable text). Show the content to the user and get their agreement before sending: pass send "now" or an ISO date to send it in the same call, or call send_email_campaign later.écriturePOST /emails/campaigns
update_email_campaignChanges a draft (subject, preview text, body, audience). Scheduled or sent campaigns cannot be modified.écriturePATCH /emails/campaigns/:id
send_email_campaignSends a draft now, or schedules it (at: ISO date at least one minute ahead). Irreversible once sending: confirm with the user first. Costs 5 rate-limit tokens.écriturePOST /emails/campaigns/:id/send
cancel_email_campaignA scheduled campaign goes back to draft.écriturePOST /emails/campaigns/:id/cancel
test_email_campaignSends the campaign to the creator of the API key only, subject prefixed with [Test].écriturePOST /emails/campaigns/:id/test
delete_email_campaignDeletes a campaign (a scheduled one is unscheduled; a campaign being sent cannot be deleted).écritureDELETE /emails/campaigns/:id
list_email_segmentsSaved audiences of the space (filters on roles, groups, level, seniority, activity, explicit members) with their last member count.lectureGET /emails/segments
get_email_segmentA segment with its member count computed now and a sample of names. Costs 3 tokens.lectureGET /emails/segments/:id
preview_email_segmentHow many members a set of filters reaches now, without saving anything. Use it before creating a segment or sending. Costs 3 tokens.lecturePOST /emails/segments/preview
create_email_segmentSaves an audience for campaigns and the digest.écriturePOST /emails/segments
update_email_segmentChanges the name, description or filters of a segment.écriturePATCH /emails/segments/:id
delete_email_segmentDeletes a segment (campaigns keep their history).écritureDELETE /emails/segments/:id
list_email_templatesReusable campaign texts of the space (subject, preview text, body).lectureGET /emails/templates
create_email_templateSaves a campaign text to start future campaigns from (templateId of create_email_campaign).écriturePOST /emails/templates
update_email_templateChanges a reusable campaign text.écriturePATCH /emails/templates/:id
delete_email_templateDeletes a reusable campaign text.écritureDELETE /emails/templates/:id
list_email_notificationsThe wording of the automatic e-mails the space sends (kind × language, e.g. live_invitation × fr), the saved text or the platform default, with the variables each kind accepts.lectureGET /emails/notifications
preview_email_notificationRenders an automatic e-mail with sample values and the space branding: the saved text, or the subject and body given.lectureGET /emails/notifications/:kind/:lang/preview
set_email_notificationSaves the space’s wording of an automatic e-mail for a kind and a language ({{variables}} listed by list_email_notifications).écriturePUT /emails/notifications/:kind/:lang
reset_email_notificationBack to the platform’s default wording for that kind and language.écritureDELETE /emails/notifications/:kind/:lang
get_email_digestSettings of the automatic digest (cadence, day, hour, time zone, sections, audience) with the last and next send.lectureGET /emails/digest
preview_email_digestThe digest as it would go out now (subject, HTML, and whether it would be empty). Costs 3 tokens.lectureGET /emails/digest/preview
update_email_digestPartial update of the digest settings: enable it, change the cadence (daily, weekly, monthly), day, hour, time zone, sections, number of items, subject, intro, audience.écriturePATCH /emails/digest
send_email_digestSends the digest now: to the key creator only (test true), or to the whole audience. Costs 5 tokens.écriturePOST /emails/digest/send
get_email_senderWhat the space’s e-mails leave from: the paid option, the domain, its DNS records (DKIM, Return-Path) and their verification, deliverability counters, suspension.lectureGET /emails/sender
add_email_sender_domainRegisters the space’s sending domain (the paid option must be enabled by the owner in the app) and returns the DNS records to add.écriturePOST /emails/sender
update_email_senderChanges the display name, sending address (on the domain) or reply address.écriturePATCH /emails/sender
check_email_sender_domainAsks the mail provider to look the DKIM and Return-Path records up and records the result.écriturePOST /emails/sender/check
test_email_senderSends the key creator a test e-mail with the space’s current sender.écriturePOST /emails/sender/test
remove_email_sender_domainRemoves the sending domain: e-mails go back to leaving via LearnFloo.écritureDELETE /emails/sender
get_email_preferencesWhether a member still receives the space’s bulk e-mails (campaigns, digest).lectureGET /users/:id/email-preferences
set_email_preferencesSubscribes or unsubscribes a member from the space’s bulk e-mails (for example to mirror an opt-out recorded in your platform).écriturePUT /users/:id/email-preferences

GET/public/spaces

Annuaire des espaces publics, sans clé ni limite de débit, CORS ouvert (utilisable depuis un navigateur). URL complète : https://api.learnfloo.com/public/spaces. Réponse mise en cache 60 s.

{ "spaces": [ { "slug": "prince-ecom", "name": "Prince Ecom Academy", "description": "…", "logo": "https://…", "coverImage": "https://…",
    "category": "business", "mode": "learning", "accessType": "paid", "priceCents": 5900, "priceCurrency": "eur", "priceInterval": "month", "price": "59 €/mois",
    "memberCount": 163, "courseCount": 22, "rating": 5, "reviewCount": 15,
    "aboutUrl": "https://www.learnfloo.com/c/prince-ecom/", "appUrl": "https://app.learnfloo.com/c/prince-ecom" } ] }

GET/public/spaces/:slug

Page À propos d'un espace public : la fiche ci-dessus plus space.about (HTML), space.video (vidéo de présentation : embedUrl, playbackUrl, thumbnailUrl), owner (name, image), adminCount, les 30 derniers reviews (rating, text, createdAt, author) et courses, les cours en accès libre de l'espace (slug, title, description, coverImage, lessonCount, url de la page publique). 404 si l'espace n'existe pas ou est privé.

Cours en accès libre. Dans un espace public, un cours publié et ouvert à tous les membres peut être marqué « lisible sur le web » par son auteur (page du cours → Modifier), ou tous les cours de l'espace d'un coup (Paramètres → Accès et page À propos) : il est alors rendu, avec ses leçons et ses vidéos, sur www.learnfloo.com/c/<slug>/cours/<cours>/ (et /en/c/…/courses/…, /es/c/…/cursos/…), en lecture seule, avec un bouton pour rejoindre l'espace ; ces pages sont listées dans www.learnfloo.com/c/sitemap.xml.

Ces deux points d'entrée alimentent les pages www.learnfloo.com/communautes/ et www.learnfloo.com/c/<slug>/. Rejoindre un espace se fait toujours dans l'app (appUrl, paramètre ?join=1) ; pour un LMS, utilisez plutôt le SSO /users/:id/entry.

Erreurs

{ "error": "message" }
CodeCas
400Corps invalide, champ manquant, session terminée (Session is over), session inconnue, hôte non autorisé, règle métier non respectée
401Clé absente, mal formée ou révoquée
403Action interdite à l'auteur choisi (rôle insuffisant), propriétaire de l'espace
404Chemin inconnu, ressource inconnue ou appartenant à un autre espace, ticket appartenant à un autre utilisateur
405Méthode non prise en charge sur ce chemin
413, 415Pièce jointe trop grosse ou d'un type non pris en charge
429Limite de débit atteinte : respectez Retry-After

Côté apprenant, une URL d'entrée expirée ou déjà utilisée affiche une page LearnFloo explicite ; un nouveau clic dans votre plateforme (nouvel appel /live/entry ou /users/:id/entry) suffit.

Affichage (iframe, onglet)

Nouvel onglet (recommandé, notamment pour Safari) :

const { url } = await fetch('/mon-backend/live-entry', { method: 'POST' }).then((r) => r.json())
window.open(url, '_blank')

Iframe :

<iframe src="URL RENVOYÉE PAR /live/entry"
  allow="camera; microphone; display-capture; autoplay; fullscreen" allowfullscreen
  style="width:100%;height:80vh;border:0"></iframe>
L'attribut allow est indispensable : sans lui, le navigateur refuse micro, caméra et partage d'écran dans un iframe d'un autre domaine. Safari et Firefox en mode strict peuvent bloquer le stockage dans un iframe tiers ; basculez alors en nouvel onglet.

Domaine personnalisé

Sur demande, la salle du live est servie sous un sous-domaine à vous (ex. live.votre-domaine.com). Une seule action de votre côté : un enregistrement DNS CNAME du sous-domaine vers learnfloo-v2.b-cdn.net, puis prévenir LearnFloo. Ensuite, les URL de /live/entry sont sur votre domaine, sans changement dans votre code. Seules les pages du live y sont servies ; les URL de /users/:id/entry et des liens d'invitation restent sur app.learnfloo.com.

Exemple Node

const API = 'https://api.learnfloo.com/api/v1'
const headers = { Authorization: `Bearer ${process.env.LEARNFLOO_API_KEY}`, 'Content-Type': 'application/json' }

async function call(method, path, body) {
  const res = await fetch(API + path, { method, headers, body: body ? JSON.stringify(body) : undefined })
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, Number(res.headers.get('Retry-After') ?? 1) * 1000))
    return call(method, path, body)
  }
  const data = await res.json()
  if (!res.ok) throw new Error(data.error)
  return data
}

// Au clic « Rejoindre le live »
export const liveEntry = (sessionId, learner) =>
  call('POST', '/live/entry', { sessionId, user: { externalId: String(learner.id), name: learner.fullName }, role: 'viewer' }).then((d) => d.url)

// Au clic « Ouvrir la communauté »
export const spaceEntry = (learner) =>
  call('POST', `/users/ext%3A${encodeURIComponent(learner.id)}/entry`, { name: learner.fullName, redirect: 'posts' }).then((d) => d.url)

// Après le live : replay et présences validées
export async function liveReport(sessionId) {
  const [s, a] = await Promise.all([call('GET', `/live/sessions/${sessionId}`), call('GET', `/live/sessions/${sessionId}/attendance`)])
  return { replayUrl: s.session.replayUrl, validated: a.participants.filter((p) => p.externalId && p.watchPct >= 80) }
}

// Une fois : être prévenu de la fin des lives et des replays
export const subscribe = () =>
  call('POST', '/webhooks', { url: 'https://votre-backend.fr/learnfloo', events: ['live.session.ended', 'live.replay.ready', 'lesson.completed'] })

Historique

DateChangement
2026-09-16Cours en accès libre (SEO) : un cours d'un espace public marqué « lisible sur le web » est rendu sur www.learnfloo.com/c/<slug>/cours/<cours>/ avec ses leçons, en trois langues, et listé dans /c/sitemap.xml ; GET /public/spaces/:slug renvoie ces cours dans courses.
2026-09-15E-mails : tout le socle e-mail passe par l'API et le MCP (famille « E-mails », 35 outils) : campagnes (création, aperçu, test, envoi immédiat ou programmé, annulation, statistiques), segments et comptage d'audience, textes réutilisables, e-mails automatiques par type et langue, récapitulatif automatique (réglages, aperçu, envoi), domaine d'envoi (ajout, DNS, vérification, test, retrait), préférences e-mail d'un membre ; webhooks email.campaign.sent, email.campaign.failed, email.unsubscribed.
2026-09-15Salle d'attente et écran de clôture : dans l'application (page « Préparer », onglet « Attente & clôture »), compte à rebours, message d'accueil, image ou vidéo teaser, quiz d'avant-live ; offre avec bouton suivi (lien porteur de lf_live, lf_user, lf_ext comme les CTA du chat, inscriptions remontées par /conversions), affichée à l'audience pendant le live et sur l'écran de fin, compte à rebours de l'offre ; sondage de clôture. API : phase (live, waiting, closing) sur les sondages, en création, en modification et en lecture.
2026-09-15OpenAPI 3.1 générée depuis le catalogue MCP (GET /openapi.json, ?preset=gpt, ?tools=, ?groups=) ; clients OAuth confidentiels (secret + PKCE, URL de retour à joker) ; les jetons OAuth sont acceptés par l'API REST, limités aux outils choisis à l'autorisation ; GPT « LearnFloo » pour ChatGPT (instructions et marche à suivre dans le kit).
2026-09-15MCP : prompts (recettes métier, prompts/list et prompts/get, argument request) et LearnFloo Agent Kit (plugin Claude Code, skills pour Codex, Cursor et Gemini CLI). Version du serveur 1.1.0.
2026-09-13Scènes : champ jingle (joue sa durée puis la scène interrompue reprend là où elle en était).
2026-09-13Scènes : l'enchaînement automatique attend la fin des vidéos et sons démarrés avec la scène (hors boucle) ; champ mediaSec (longueur du fichier en secondes) sur les emplacements vidéo et dans audios.
2026-09-13Scènes : plusieurs sons par scène, champ audios (liste, 6 au maximum) ; l'ancien champ audio reste accepté en écriture, les scènes renvoient désormais audios.
2026-09-13Scènes : apparence des caméras, champs shape (forme), filter (filtre), bgMode, bgBlur, bgImage (fond appliqué sur l'appareil de la personne) ; volume et muted valent aussi pour les caméras (son à l'antenne).
2026-09-13Scènes : champ framed (encadré : marge autour des emplacements, le fond reste visible), en création et en modification.
2026-09-13Lecteur vidéo : webhook video.lead (email laissé dans une vidéo) ; GET /groups/:id/stats renvoie videoPct par membre (part des vidéos de leçon du groupe réellement regardée) et avgVideoPct, videoLessonCount dans summary.
2026-09-13Lives : règle du replay. Champs replay (all, attendees, groups, level, none), replayGroups et replayMinLevel sur POST / PATCH /live/sessions, renvoyés comme replay, replayGroupIds, replayMinLevel. PATCH accepte aussi mode et fonctionne pendant et après le live pour le titre, la description, les groupes et le replay.
2026-09-12Tarif unique à l'usage aligné sur la grille publique (www.learnfloo.com) : GET /space/usage renvoie aussi simulcastHours (heures de diffusion simultanée × destinations) et subtitleHours (heures sous-titrées, comptées quand la génération passera sur le nouveau moteur).
2026-09-12MCP : connexion OAuth 2.1 (/.well-known/oauth-authorization-server, /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke) : l'utilisateur autorise l'assistant sur une page LearnFloo en choisissant espace, niveau d'accès et outils ; l'autorisation se voit et se révoque dans les paramètres à côté des clés. Alternative à la clé dans l'URL, au choix de l'utilisateur.
2026-09-12Serveur MCP https://api.learnfloo.com/mcp pour les assistants IA (Claude, ChatGPT, Cursor…), avec les mêmes clés : un outil par route de l'API. Clés avec niveau d'accès (lecture seule ou complet : une écriture avec une clé en lecture seule renvoie 403) et liste d'outils MCP ouverts réglable par clé dans les paramètres de l'espace. Les clés existantes restent en accès complet, tous outils.
2026-09-12Scènes : enchaînement automatique. Champs durationSec et nextSceneId sur les scènes, PUT /live/sessions/:id/scenes/auto { enabled, loop }, objet autoAdvance dans la régie. Chaque changement automatique émet live.scene.changed comme un basculement manuel.
2026-09-11Scènes : widget viewers (compteur de spectateurs) et PUT /live/sessions/:id/viewers pour y ajouter des spectateurs ; extraViewers dans l'objet session.
2026-09-11Scènes : style des emplacements poll et message (couleurs, opacité du fond, taille du texte, arrondi, bordure, ombre, police, éléments affichés).
2026-09-11Scènes : options des vidéos autoplay, once, startSec, durationSec, volume ; startSec et durationSec aussi sur le son de scène (audio).
2026-09-11Scènes : transitions fade, black, slide-*, zoom, wipe, blur et durée transitionMs.
2026-09-11Scènes : champ audio (son de la scène : fichier audio de la médiathèque ou URL, autoplay, once, loop, volume), piloté par PUT …/media/:mediaId. Médiathèque : kind audio.
2026-09-11Sondages et quiz : /live/sessions/:id/polls (préparer, modifier, supprimer), …/open, …/close, PUT …/results, …/band ; kind: "quiz" avec correct. Résultats conservés après le live. Webhooks live.poll.opened et live.poll.closed. Dans l'application : onglet « Sondages » du live et de « Préparer », pour l'hôte, le staff, les rôles moderator et assistant. À l'antenne : emplacements poll et message dans les scènes (préparés à l'avance), PUT …/polls/:pollId/stage et PUT …/chat/stage pour afficher un sondage ou un message du chat dans la vidéo.
2026-09-11Lives : rôle moderator (modération du chat, messages préparés, messages privés) accepté par /invites et PUT …/participants/:userId/role. Les messages supprimés par un modérateur et les messages privés n'apparaissent pas dans GET …/chat.
2026-09-11Scènes : champ locked (cadenas) ; une scène ou un modèle verrouillé refuse modifications et suppression jusqu'à { "locked": false }.
2026-09-11Scènes et régie par API : un live se pilote comme un studio. /live/sessions/:id/scenes (liste, création, modification, suppression, ordre), mise à l'antenne (…/activate, PUT …/scenes/active), stickers, bandeaux et emplacements modifiables un par un (…/overlays/:overlayId, …/slots/:slotId, et PATCH /live/sessions/:id/overlays/:overlayId pour le même bandeau dans toutes les scènes), bandeaux éphémères POST …/bands, pilotage des PDF et vidéos PUT …/media/:mediaId, modèles /scene-templates, rôle d'un participant PUT …/participants/:userId/role (dont assistant, la régie, aussi accepté par /invites). Médiathèque : POST /media par URL, GET, PATCH, DELETE /media/:id, dossiers /media/folders. Webhook live.scene.changed ; session gagne activeSceneId et sceneCount.
2026-09-10Offre unique : la grille Gratuit / Starter / Growth / Scale / Événement disparaît. GET /space/usage renvoie désormais les six unités à l'usage (units), les franchises, usageCents, le plafond du compte et liveAllowed ; plan, planLabel, eventCredits et les quotas ne sont plus renvoyés, ni plan dans GET /space. La limite de débit dépend du compte du propriétaire (60 ou 300 requêtes par minute).
2026-09-10Devises : les espaces payants portent priceCurrency (eur ou usd) dans GET /space, GET /public/spaces et GET /public/spaces/:slug ; price est formaté dans la devise. POST /live/sessions/:id/conversions accepte currency à côté de amountCents (renvoyé sur la conversion et dans le webhook live.conversion).
2026-09-10Inscriptions pendant le live : POST /live/sessions/:id/conversions pour signaler un achat fait depuis le chat (annonce dans le chat, bandeau sur la scène, compteur), paramètres lf_live, lf_user, lf_ext ajoutés aux liens des boutons, webhook live.conversion, champs conversionCount et ctaClickCount sur la session, kind sur les messages du chat.
2026-09-10Chat des lives : les messages de l'animateur peuvent porter imageUrl et link (bouton d'appel à l'action) dans GET /live/sessions/:id/chat.
2026-09-10Lives : champ chat (open, closed, off) à la création, en modification et en lecture, pour ouvrir le chat à l'arrivée ou le désactiver. Modifiable pendant le live par l'animateur (webhook live.session.updated).
2026-09-10Groupes (classes, équipes, promotions… vocabulaire par espace) : /groups (liste, création, modification, suppression), /groups/:id/members, GET et PUT /users/:id/groups pour synchroniser les classes depuis un LMS. Par groupe : /groups/:id/leaderboard, suivi des membres /groups/:id/stats, documents partagés /groups/:id/documents ; /leaderboard?group=. Groupes automatiques : champ rule (niveau atteint, série de jours, défi rejoint ou terminé, badge, cours terminé). Audience : champ groups à la création et modification des cours, publications, événements et lives (groupIds en lecture), filtre ?group= sur leurs listes. GET /space gagne groupCount et groupLabels.
2026-09-10Espaces publics ou privés, gratuits ou payants : GET /space gagne priceCents, priceInterval, category, aboutUrl, rating, reviewCount ; accessType vaut désormais free ou paid. Nouveaux points d'entrée sans clé : GET /public/spaces (annuaire) et GET /public/spaces/:slug (page À propos, avis). Les membres d'un espace payant qui résilient passent en status: "expired" dans /users.
2026-09-09Limite de débit par clé selon l'offre (en-têtes X-RateLimit-*, 429 + Retry-After). Webhooks sortants signés (/webhooks, 13 événements, relances). Lives : PATCH et DELETE /live/sessions/:id, /participants, /chat, /replay, liens d'invitation /invites ; session gagne maxParticipants, replayViews, hlsStatus, hostId, createdAt. Support : scope=space (tous les tickets), réponses et statuts côté équipe (staff), /attachments générique. Espace : GET /space enrichi, /space/usage. Membres : /users (liste, création, modification, retrait), SSO /users/:id/entry, /users/:id/progress, XP (GET et POST /users/:id/xp), badges, /leaderboard, /challenges. Cours : /courses, modules, leçons, /courses/:id/progress, /lessons/:id/complete. Fil : /posts et commentaires. Calendrier : /events. Vidéos du studio : /videos. Médiathèque : /media. Pagination par curseur ; 405 sur méthode inconnue. Les chemins /entry, /sessions sont désormais documentés sous /live/… (inchangés).
2026-09-07Webinaires : l'audience regarde un flux HLS (latence 10 à 20 s) et ne se connecte plus à la salle ; un spectateur peut demander la parole et rejoint la salle quand l'hôte l'accepte. summary de /sessions/:id/attendance : ajout de spectatorHours et interactiveHours.
2026-09-07Support : /support/tickets (GET, POST), /support/tickets/:id, …/messages, …/status, /support/attachments, pour ouvrir et suivre des tickets au nom d'un utilisateur externe (plugin WordPress). GET /space pour tester une clé.
2026-09-07GET /sessions/:id/attendance : ajout de summary et, par participant, source, leftAt, watchSec, watchPct, connections (remplace lastSeenAt). Replay disponible dès la fin du live, ré-encodé ensuite. Mise en ligne de cette page.
2026-09-06Erreurs renvoyées en JSON propre { "error" }. Domaine personnalisé par espace pour les pages du live. API servie sur api.learnfloo.com.
2026-09-05Première version : /entry, /sessions (POST, GET), /sessions/:id, /sessions/:id/attendance.

Questions : équipe LearnFloo.