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
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
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.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) :
| Compte | Requêtes par minute et par clé |
|---|---|
| Gratuit (facturation non activée) | 60 |
| Facturation active | 300 |
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.
?limit=50&cursor=… ; la réponse contient nextCursor (null sur la dernière page). Une page filtrée (rôle, catégorie…) peut contenir moins d'éléments que limit : continuez tant que nextCursor n'est pas null.id) ou, pour ceux que vous avez créés, par ext:<externalId> (encodé dans l'URL : /users/ext%3Alms-42). Chaque objet utilisateur renvoyé contient externalId (null pour un membre venu de LearnFloo).author : { "email": "formateur@client.fr" } pour un membre LearnFloo existant, ou { "externalId": "lms-42", "name": "Alice" } pour un utilisateur de votre plateforme (créé et ajouté comme membre à la première utilisation). Sans author, l'action est faite par le créateur de la clé.content) sont du HTML (celui de l'éditeur LearnFloo). Envoyez du texte brut par défaut : il est converti en paragraphes. Passez "format": "html" pour envoyer du HTML. En lecture, content est le HTML et text une version texte tronquée.GET lecture, POST création ou action, PATCH modification partielle (seuls les champs présents changent), DELETE suppression ou annulation.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
/live/entry est personnelle, à usage unique, valable 15 minutes. Demandez-la au moment du clic.externalId) : LearnFloo crée un compte invité la première fois et le retrouve ensuite.URL d'entrée personnelle pour un apprenant.
{
"sessionId": "p97…",
"user": { "externalId": "lms-user-42", "name": "Alice Martin" },
"role": "viewer"
}
| Champ | Description |
|---|---|
sessionId | Identifiant du live (voir GET /live/sessions) |
user.externalId | Identifiant stable de l'apprenant chez vous, 200 caractères max |
user.name | Nom affiché aux autres participants, mis à jour à chaque appel |
role | viewer (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".
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"
}
| Champ | Description |
|---|---|
title | Requis |
mode | broadcast (webinaire : seuls hôte et intervenants publient) ou conference (tout le monde publie). Défaut broadcast |
scheduledAt | Date ISO 8601 ou timestamp en millisecondes. Requis |
durationMin | Durée prévue, pour le calendrier. Défaut 60 |
maxParticipants | 2 à 1000. Défaut 100 |
recordingEnabled | Enregistrement automatique. Défaut true |
chat | Chat 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 |
hostEmail | Optionnel. 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é |
groups | Optionnel. 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 |
replay | Optionnel. 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é » |
replayGroups | Avec replay: "groups" : ids ou slugs des groupes, au moins un |
replayMinLevel | Avec 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.
Sessions de l'espace, plus récentes d'abord, 200 max. Filtres optionnels ?status=scheduled|live|ended|cancelled et ?group=<id ou slug>.
{ "sessions": [ { … }, … ] }
{
"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"
}
}
| Champ | Description |
|---|---|
status | scheduled, live, ended, cancelled. Utile pour afficher « Bientôt », « Rejoindre » ou « Replay » |
participantCount | Personnes connectées à l'instant T (0 hors live) |
recordingStatus | recording, processing, ready, failed ou null |
replayUrl | MP4 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 |
hlsStatus | Flux audience du webinaire : starting, live, ended, failed ou null |
chat | open, closed ou off, voir POST |
conversionCount, ctaClickCount | Inscriptions signalées pendant le live et clics sur les boutons du chat, voir /conversions |
groupIds | Groupes auxquels le live est réservé (vide = tout l'espace) |
replay, replayGroupIds, replayMinLevel | Qui 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 |
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 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.
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…" }
]
}
| Champ | Description |
|---|---|
externalId | Votre identifiant envoyé dans /live/entry. null pour les personnes venues directement de LearnFloo |
source | lms (entré par votre plateforme), invite (lien d'invitation), member (membre LearnFloo) |
role | host, speaker, viewer |
joinedAt, leftAt | Première entrée et dernière sortie. leftAt est null tant que la personne est dans la salle |
watchSec | Temps réel passé dans la salle, toutes connexions cumulées, borné à la durée du live |
watchPct | Part du live suivie, 0 à 100. Le champ à utiliser pour valider une présence (ex. >= 80) |
connections | Nombre 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.
É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.
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).
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"
}
| Champ | Description |
|---|---|
externalId | Identifiant de l'apprenant dans votre LMS (celui de /users). Retrouvé dans le lien du bouton, voir ci-dessous |
userId | Ou l'identifiant LearnFloo (lf_user du lien) |
email | Ou l'e-mail du compte LearnFloo |
name | Optionnel, prénom affiché dans l'annonce (sinon le nom du compte retrouvé, sinon annonce anonyme) |
label, amountCents, currency | Optionnels, 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.
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.
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).
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"
}
| Champ | Description |
|---|---|
id | Libre à 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 |
jingle | Jingle : 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, nextSceneId | Enchaî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) |
layout | solo (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[].kind | host (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, title | Cadrage cover ou contain ; vidéo en boucle ou sans son ; titre affiché dans la régie |
slots[].shape, filter, bgMode, bgBlur, bgImage, volume, muted | Camé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, volume | Vidé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 |
locked | Cadenas : 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 |
audios | Sons 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, transition | Couleur 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. |
framed | Encadré : 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.
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.
| Appel | Effet |
|---|---|
POST/live/sessions/:id/scenes/:sceneId/activate | Met 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).
| Appel | Effet |
|---|---|
POST…/scenes/:sceneId/overlays | Ajoute un sticker ou un bandeau (objet overlays[] ci-dessus). 201 { "overlay" } |
PATCH…/scenes/:sceneId/overlays/:overlayId | Modifie 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/:overlayId | Mê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/:overlayId | Retire l'élément, renvoie la scène |
POST…/scenes/:sceneId/slots | Ajoute un emplacement (objet slots[] ; position optionnelle, 0 = principal). 201 { "slot" } |
PATCH…/scenes/:sceneId/slots/:slotId | Modifie 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/:slotId | Retire 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" }
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.
{ "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": { … } }.
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.
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.
| Appel | Effet |
|---|---|
POST…/polls/:pollId/open | Ouvre 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/close | Ferme 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/band | Bandeau 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. |
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.
user.externalId (votre identifiant, stable) et, à la création, user.name. LearnFloo crée un compte invité la première fois, comme pour les lives, et le retrouve ensuite."format": "html" pour envoyer du HTML.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 }
}
| Champ | Description |
|---|---|
status | open (en attente de l'équipe), pending (en attente de l'utilisateur), resolved, closed |
priority | low, normal, high, urgent |
lastMessageBy | member ou staff : qui a écrit en dernier |
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 }).
{
"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 } ]
}
| Champ | Description |
|---|---|
user.externalId, user.name | Requis. 200 et 60 caractères max |
subject | Requis, 200 caractères max |
content | Texte brut (défaut) ou HTML avec "format": "html". Requis sauf si une pièce jointe est fournie |
priority | Défaut normal |
category | Libre, optionnel (affiché à l'équipe) |
attachments | Optionnel, 10 max, objets renvoyés par /attachments |
Réponse 201 : { "ticket": { … } }. L'équipe de l'espace est notifiée.
?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.
{ "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.
{ "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": { … } }.
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
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.
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).
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).
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à.
: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é.
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>).
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": "…" }
] }
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 }
| Appel | Ré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/badges | Catalogue : { "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/:id | Le défi avec ses participants ({ id, externalId, name, progress, completedAt, joinedAt }) |
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…" } }
| Appel | Corps / 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/:id | Le 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) |
| Appel | Corps / 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:… |
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" } ] }
| Appel | Ré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 jetons | Suivi 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.
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": { … } }
: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.
| Appel | Corps / 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 } |
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 } ] } }
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)
| Appel | Corps / 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/:id | La 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/:id | Supprime la publication, ses commentaires et réactions |
POST/posts/:id/comments | { "content", "format"?, "parentCommentId"?, "attachments"?, "author"? } → 201 { "comment": { … } } |
DELETE/comments/:id | Supprime 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…" } }
| Appel | Corps / 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/:id | Mê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 } |
Enregistrements du studio LearnFloo (écran, caméra, imports) et replays de lives importés, hébergés sur Bunny Stream.
| Appel | Ré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.
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.
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.
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.
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.
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.
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.
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é)
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.
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.
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.
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énement | data |
|---|---|
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) |
ping | Envoi 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.
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é.
| Appel | Corps / 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/test | Envoie un événement ping → 202 { "sent": true } |
GET/webhooks/:id/deliveries?limit=50 | Journal des envois (30 jours) : { "deliveries": [ { "id", "event", "attempt", "status": "pending|success|failed", "responseStatus", "error", "createdAt", "deliveredAt" } ] } |
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.
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 | |
|---|---|---|
| Principe | L'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 peut | Propriétaire ou administrateur de l'espace, avec son propre compte | Quiconque détient la clé |
| Attribution des actions | À l'utilisateur qui a autorisé | Au créateur de la clé |
| Risque | Aucun secret dans l'URL ni la configuration du client | Une clé dans l'URL peut se retrouver dans des journaux ; en cas de fuite, révoquez-la |
| Où ça se gère | Paramè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 | |
| Client | OAuth | Clé API |
|---|---|---|
| claude.ai, application Claude | Paramètres → Connecteurs → Ajouter un connecteur personnalisé, URL https://api.learnfloo.com/mcp, puis « Connecter » : la page d'autorisation LearnFloo s'ouvre | Même écran, URL https://api.learnfloo.com/mcp?key=lf_live_…, sans authentification |
| Claude Code | claude mcp add --transport http learnfloo https://api.learnfloo.com/mcp puis /mcp pour s'authentifier | claude 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.
?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.
159 outils, par famille. Colonne « Accès » : lecture (ouvert aux clés en lecture seule) ou écriture.
| Outil | Description | Accès | Route |
|---|---|---|---|
get_space | The space behind the API key: name, slug, visibility, price, member and course counts, group vocabulary, URL. Call it first to know where you are. | lecture | GET /space |
get_space_usage | Usage 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. | lecture | GET /space/usage |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_users | Members of the space, paginated (200 max per page), with role, status, XP and level. | lecture | GET /users |
get_user | One user with membership (role, status, XP, level) and stats (lessons completed, lives joined, posts, badges). | lecture | GET /users/:id |
create_user | Creates (or updates, idempotent on externalId) a user of your platform and adds them as member of the space. | écriture | POST /users |
update_user | Changes the name (users created by your platform only), the role or the status of a member. The owner cannot be changed. | écriture | PATCH /users/:id |
remove_user | Removes the member from the space (account and history kept). | écriture | DELETE /users/:id |
create_user_entry_url | Single-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. | écriture | POST /users/:id/entry |
get_user_progress | Progress of a user in every course of the space (lessons completed, percentage). | lecture | GET /users/:id/progress |
get_user_xp | Total XP, level and latest XP events of a user in the space. | lecture | GET /users/:id/xp |
award_xp | Gives (or takes, negative amount) XP to a user from your platform: quiz passed, homework handed in… Level recalculated. | écriture | POST /users/:id/xp |
get_user_badges | Badges awarded to a user. | lecture | GET /users/:id/badges |
get_leaderboard | Members ranked by XP (500 max), optionally within one group. | lecture | GET /leaderboard |
list_badges | Badges of the space with their criteria and XP reward. | lecture | GET /badges |
list_challenges | Challenges of the space (objective, dates, status, participants). | lecture | GET /challenges |
get_challenge | One challenge with its participants and their progress. | lecture | GET /challenges/:id |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_live_sessions | Live sessions of the space, newest first (200 max), with status, dates, replay and counters. | lecture | GET /live/sessions |
get_live_session | One live session: status, schedule, participants now, recording and replay, chat mode, conversions, active scene. | lecture | GET /live/sessions/:id |
create_live_session | Schedules a live (webinar or conference) in the space. Returns the session and hostUrl, the page where the host starts it. | écriture | POST /live/sessions |
update_live_session | Changes 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. | écriture | PATCH /live/sessions/:id |
cancel_live_session | Cancels a scheduled live (calendar event removed). A running or ended live cannot be cancelled. | écriture | DELETE /live/sessions/:id |
create_live_entry_url | Personal, single-use, 15-minute URL that signs a user of your platform into a live (viewer or speaker). Ask for it at click time. | écriture | POST /live/entry |
get_live_attendance | Attendance report: summary (attended, peak, average watch time, spectator and interactive hours) and per participant watch time and percentage. Costs 5 rate-limit tokens. | lecture | GET /live/sessions/:id/attendance |
get_live_participants | Instant state of the people registered on a live: connected, spectator, hand raised, role. | lecture | GET /live/sessions/:id/participants |
get_live_chat | Chat messages of a live, oldest first (2000 max), kept after the live. | lecture | GET /live/sessions/:id/chat |
get_live_replay | The replay of a live: MP4, HLS and embed URLs, thumbnail, duration, linked lesson, views. | lecture | GET /live/sessions/:id/replay |
report_live_conversion | Reports a sign-up or purchase made during a live: announced in the chat and on the scene, counted in the report, webhook live.conversion. | écriture | POST /live/sessions/:id/conversions |
list_live_invites | Shareable invitation links of a live (speaker, viewer, assistant, moderator). | lecture | GET /live/sessions/:id/invites |
create_live_invite | Creates a reusable invitation link for people without an account on your platform (external speaker, control room assistant, chat moderator). | écriture | POST /live/sessions/:id/invites |
revoke_live_invite | Revokes an invitation link. | écriture | DELETE /live/sessions/:id/invites/:inviteId |
set_participant_role | Gives or takes the floor, or names a control-room assistant or a moderator, for a participant already registered on the live. Applied immediately. | écriture | PUT /live/sessions/:id/participants/:userId/role |
set_extra_viewers | Adds viewers to the counter shown by the viewers widget of the scenes (never counted in attendance or billing). 0 removes it. | écriture | PUT /live/sessions/:id/viewers |
stage_chat_message | Shows a chat message on the scene (message slot, else bottom left); null removes it. | écriture | PUT /live/sessions/:id/chat/stage |
| Outil | Description | Accès | Route |
|---|---|---|---|
get_live_scenes | The whole control room of a live: scenes with slots and overlays, active scene, auto-advance state, media state (PDF page, video playing), temporary bands. | lecture | GET /live/sessions/:id/scenes |
get_live_scene | One scene of a live. | lecture | GET /live/sessions/:id/scenes/:sceneId |
create_scene | Creates 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. | écriture | POST /live/sessions/:id/scenes |
update_scene | Changes 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. | écriture | PATCH /live/sessions/:id/scenes/:sceneId |
delete_scene | Removes a scene (automatic layout if it was on air). Returns the control room. | écriture | DELETE /live/sessions/:id/scenes/:sceneId |
activate_scene | Puts a scene on air for everyone (room, audience stream, recording, external broadcasts). Its videos restart from the beginning. | écriture | POST /live/sessions/:id/scenes/:sceneId/activate |
set_active_scene | Sets the scene on air by id; null returns to the automatic layout (cameras in a grid, screen share large). | écriture | PUT /live/sessions/:id/scenes/active |
reorder_scenes | Sets the order of the scenes of a live. | écriture | PUT /live/sessions/:id/scenes/order |
set_auto_advance | Turns the automatic scene sequence on or off (each scene with durationSec gives way to nextSceneId or the next one), with or without loop. | écriture | PUT /live/sessions/:id/scenes/auto |
show_band | Shows a band at the top of every screen for a few seconds, whatever the scene (break, announcement). | écriture | POST /live/sessions/:id/bands |
control_scene_media | Turns the page of a PDF, or plays, pauses and seeks a video or the scene sound present in a scene, synchronised for everyone. | écriture | PUT /live/sessions/:id/media/:mediaId |
add_overlay | Adds a sticker, a band or a widget (poll, message, viewers) to a scene. Give it a stable id to update it later. | écriture | POST /live/sessions/:id/scenes/:sceneId/overlays |
update_overlay | Changes the given fields of one overlay of a scene ({ text }, { visible: false }, { x, y }…). Visible immediately if on air. | écriture | PATCH /live/sessions/:id/scenes/:sceneId/overlays/:overlayId |
update_overlay_everywhere | Changes the overlay carrying this id in every scene of the live: a common band (seats left, price, next step) updated in one call. | écriture | PATCH /live/sessions/:id/overlays/:overlayId |
delete_overlay | Removes an overlay from a scene. | écriture | DELETE /live/sessions/:id/scenes/:sceneId/overlays/:overlayId |
add_slot | Adds a slot (host, guest, screen, media, embed) to a scene; position 0 = main. | écriture | POST /live/sessions/:id/scenes/:sceneId/slots |
update_slot | Changes a slot of a scene (other media, guest, fit, geometry in free layout, position). | écriture | PATCH /live/sessions/:id/scenes/:sceneId/slots/:slotId |
delete_slot | Removes a slot from a scene. | écriture | DELETE /live/sessions/:id/scenes/:sceneId/slots/:slotId |
list_scene_templates | Scene templates of the space, reusable in every live. | lecture | GET /scene-templates |
create_scene_template | Saves a scene template from fields ({ name, scene }) or from an existing scene ({ name, sessionId, sceneId }). Same name = replaced. | écriture | POST /scene-templates |
delete_scene_template | Deletes a scene template (id or name). | écriture | DELETE /scene-templates/:id |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_live_polls | Polls and quizzes of a live with their counts, kept after the live. | lecture | GET /live/sessions/:id/polls |
get_live_poll | One poll or quiz with its results. | lecture | GET /live/sessions/:id/polls/:pollId |
create_poll | Creates 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. | écriture | POST /live/sessions/:id/polls |
update_poll | Changes a draft poll (same fields as creation). | écriture | PATCH /live/sessions/:id/polls/:pollId |
delete_poll | Deletes a poll and its votes, whatever its status. | écriture | DELETE /live/sessions/:id/polls/:pollId |
open_poll | Opens the vote to the participants (one poll open at a time, running live only). | écriture | POST /live/sessions/:id/polls/:pollId/open |
close_poll | Closes the vote; results shown unless showResults is false. | écriture | POST /live/sessions/:id/polls/:pollId/close |
show_poll_results | Shows or hides the counts to the participants (and the correct answer of a quiz). | écriture | PUT /live/sessions/:id/polls/:pollId/results |
stage_poll | Shows the poll in the scene (poll slot, else bottom right) in the room, the broadcast and the recording; false removes it. | écriture | PUT /live/sessions/:id/polls/:pollId/stage |
show_poll_band | Shows a 12-second band on the scene with the leading answers (or the right answer and the success rate of a quiz). | écriture | POST /live/sessions/:id/polls/:pollId/band |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_support_tickets | Tickets of one external user (externalId) or of the whole space (scope: "space", team dashboard), newest first. | lecture | GET /support/tickets |
get_support_ticket | A ticket with its messages (never the internal notes). Give externalId of the user, or scope: "space" for the team. | lecture | GET /support/tickets/:id |
create_support_ticket | Opens a support ticket on behalf of a user of your platform. The team of the space is notified. | écriture | POST /support/tickets |
reply_support_ticket | Adds 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. | écriture | POST /support/tickets/:id/messages |
set_support_ticket_status | Changes the status of a ticket: as the user (resolved or open), or as the team (any status, plus priority and assigneeEmail). | écriture | POST /support/tickets/:id/status |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_groups | Groups of the space (classes, teams, cohorts… see groupLabels of get_space) with member counts and automatic rules. | lecture | GET /groups |
get_group | One group (id or slug) with its members and their group role (leader or member). | lecture | GET /groups/:id |
create_group | Creates a group, manual or automatic (rule: level_min, streak_days, challenge_joined, challenge_completed, badge, course_completed). | écriture | POST /groups |
update_group | Changes name, slug, description, color or rule (null = manual group) of a group. | écriture | PATCH /groups/:id |
delete_group | Deletes a group; the content addressed only to it becomes visible to the whole space. | écriture | DELETE /groups/:id |
add_group_members | Adds members of the space to a group (500 per call, idempotent), as member or leader. | écriture | POST /groups/:id/members |
remove_group_member | Removes one member from a group. | écriture | DELETE /groups/:id/members/:userId |
get_user_groups | Groups of a user in the space, with their group role. | lecture | GET /users/:id/groups |
set_user_groups | Replaces the whole list of groups of a user (sync of classes from your platform). | écriture | PUT /users/:id/groups |
get_group_leaderboard | Members of a group ranked by XP. | lecture | GET /groups/:id/leaderboard |
get_group_stats | Follow-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. | lecture | GET /groups/:id/stats |
list_group_documents | Shared documents of a group, newest first. | lecture | GET /groups/:id/documents |
add_group_document | Adds a document (file stored via POST /api/v1/attachments, or any https link) to a group. | écriture | POST /groups/:id/documents |
delete_group_document | Deletes a document of a group. | écriture | DELETE /groups/:id/documents/:docId |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_courses | Published courses of the space (includeUnpublished for drafts too), with lesson counts and audience. | lecture | GET /courses |
get_course | A course (id or slug) with its modules and lessons; withContent adds the HTML of the lessons. | lecture | GET /courses/:id |
create_course | Creates a course (draft by default). The author, when given, must be owner, admin or teacher. | écriture | POST /courses |
update_course | Changes title, description, cover, published state or audience of a course. | écriture | PATCH /courses/:id |
delete_course | Deletes a course with its modules, lessons and progress. | écriture | DELETE /courses/:id |
create_module | Adds a module (section) to a course. | écriture | POST /courses/:id/modules |
create_lesson | Adds a lesson to a course, with rich content and an optional video (MP4, HLS, YouTube, Vimeo, or playbackUrl of a studio video). | écriture | POST /courses/:id/lessons |
get_lesson | One lesson with its content. | lecture | GET /lessons/:id |
update_lesson | Changes title, content, module, video or order of a lesson. | écriture | PATCH /lessons/:id |
delete_lesson | Deletes a lesson. | écriture | DELETE /lessons/:id |
get_course_progress | Every 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. | lecture | GET /courses/:id/progress |
complete_lesson | Marks a lesson as completed for a learner, as if validated in LearnFloo (XP, quests, badges, webhook). Idempotent. | écriture | POST /lessons/:id/complete |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_posts | Feed of the space, newest first, paginated; categories general, announcements, question, wins, resource. | lecture | GET /posts |
get_post | A post with its comments (oldest first, parentId for replies). | lecture | GET /posts/:id |
create_post | Creates a post in the feed (mentions, XP and notifications as in the app). pinned is for moderators. | écriture | POST /posts |
update_post | Changes title, category, content, pinned state or audience of a post. | écriture | PATCH /posts/:id |
delete_post | Deletes a post with its comments and reactions. | écriture | DELETE /posts/:id |
create_comment | Adds a comment (or a reply with parentCommentId) to a post. | écriture | POST /posts/:id/comments |
delete_comment | Deletes a comment and its replies. | écriture | DELETE /comments/:id |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_events | Calendar events by start date (scheduled lives included with liveSessionId). | lecture | GET /events |
get_event | One calendar event. | lecture | GET /events/:id |
create_event | Creates a calendar event (lives are created with create_live_session). | écriture | POST /events |
update_event | Changes an event (an event linked to a live is changed with update_live_session). | écriture | PATCH /events/:id |
delete_event | Deletes a calendar event. | écriture | DELETE /events/:id |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_videos | Studio recordings and imported live replays, newest first, paginated. | lecture | GET /videos |
get_video | One video with its playback URLs (ready videos only), duration, thumbnail, linked lesson or live. | lecture | GET /videos/:id |
update_video | Changes the title or description of a video. | écriture | PATCH /videos/:id |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_media | Images, PDF, videos, embeds, stickers and sounds of the space used in scenes and the studio. | lecture | GET /media |
list_media_folders | Folders of the media library. | lecture | GET /media/folders |
get_media | One item of the media library. | lecture | GET /media/:id |
add_media | Adds 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). | écriture | POST /media |
create_media_folder | Creates a folder in the media library. | écriture | POST /media/folders |
update_media | Changes the title or the folder (null for the root) of a media item. | écriture | PATCH /media/:id |
delete_media | Deletes a media item (and its file when hosted by LearnFloo). | écriture | DELETE /media/:id |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_webhooks | Outgoing webhooks of the space with their events and delivery state. | lecture | GET /webhooks |
get_webhook | One webhook. | lecture | GET /webhooks/:id |
create_webhook | Subscribes an https URL to events (["*"] for all). The signing secret is returned once. | écriture | POST /webhooks |
update_webhook | Changes url, events, description or active state (active: true resets the failure count). | écriture | PATCH /webhooks/:id |
delete_webhook | Deletes a webhook. | écriture | DELETE /webhooks/:id |
test_webhook | Sends a ping event to a webhook. | écriture | POST /webhooks/:id/test |
list_webhook_deliveries | Delivery log of a webhook over 30 days (attempts, status, errors). | lecture | GET /webhooks/:id/deliveries |
| Outil | Description | Accès | Route |
|---|---|---|---|
list_email_campaigns | Campaigns of the space (drafts, scheduled, sent, failed) with their statistics: sent, delivered, opened, clicked, bounced, spam, unsubscribed. kind "digest" lists the automatic digests sent. | lecture | GET /emails/campaigns |
get_email_campaign | One campaign with its content, audience and statistics. | lecture | GET /emails/campaigns/:id |
preview_email_campaign | The campaign as a reader will receive it (subject, HTML and text, frame and unsubscribe footer included), rendered with the key creator’s first name. | lecture | GET /emails/campaigns/:id/preview |
create_email_campaign | Creates 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. | écriture | POST /emails/campaigns |
update_email_campaign | Changes a draft (subject, preview text, body, audience). Scheduled or sent campaigns cannot be modified. | écriture | PATCH /emails/campaigns/:id |
send_email_campaign | Sends 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. | écriture | POST /emails/campaigns/:id/send |
cancel_email_campaign | A scheduled campaign goes back to draft. | écriture | POST /emails/campaigns/:id/cancel |
test_email_campaign | Sends the campaign to the creator of the API key only, subject prefixed with [Test]. | écriture | POST /emails/campaigns/:id/test |
delete_email_campaign | Deletes a campaign (a scheduled one is unscheduled; a campaign being sent cannot be deleted). | écriture | DELETE /emails/campaigns/:id |
list_email_segments | Saved audiences of the space (filters on roles, groups, level, seniority, activity, explicit members) with their last member count. | lecture | GET /emails/segments |
get_email_segment | A segment with its member count computed now and a sample of names. Costs 3 tokens. | lecture | GET /emails/segments/:id |
preview_email_segment | How many members a set of filters reaches now, without saving anything. Use it before creating a segment or sending. Costs 3 tokens. | lecture | POST /emails/segments/preview |
create_email_segment | Saves an audience for campaigns and the digest. | écriture | POST /emails/segments |
update_email_segment | Changes the name, description or filters of a segment. | écriture | PATCH /emails/segments/:id |
delete_email_segment | Deletes a segment (campaigns keep their history). | écriture | DELETE /emails/segments/:id |
list_email_templates | Reusable campaign texts of the space (subject, preview text, body). | lecture | GET /emails/templates |
create_email_template | Saves a campaign text to start future campaigns from (templateId of create_email_campaign). | écriture | POST /emails/templates |
update_email_template | Changes a reusable campaign text. | écriture | PATCH /emails/templates/:id |
delete_email_template | Deletes a reusable campaign text. | écriture | DELETE /emails/templates/:id |
list_email_notifications | The 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. | lecture | GET /emails/notifications |
preview_email_notification | Renders an automatic e-mail with sample values and the space branding: the saved text, or the subject and body given. | lecture | GET /emails/notifications/:kind/:lang/preview |
set_email_notification | Saves the space’s wording of an automatic e-mail for a kind and a language ({{variables}} listed by list_email_notifications). | écriture | PUT /emails/notifications/:kind/:lang |
reset_email_notification | Back to the platform’s default wording for that kind and language. | écriture | DELETE /emails/notifications/:kind/:lang |
get_email_digest | Settings of the automatic digest (cadence, day, hour, time zone, sections, audience) with the last and next send. | lecture | GET /emails/digest |
preview_email_digest | The digest as it would go out now (subject, HTML, and whether it would be empty). Costs 3 tokens. | lecture | GET /emails/digest/preview |
update_email_digest | Partial update of the digest settings: enable it, change the cadence (daily, weekly, monthly), day, hour, time zone, sections, number of items, subject, intro, audience. | écriture | PATCH /emails/digest |
send_email_digest | Sends the digest now: to the key creator only (test true), or to the whole audience. Costs 5 tokens. | écriture | POST /emails/digest/send |
get_email_sender | What the space’s e-mails leave from: the paid option, the domain, its DNS records (DKIM, Return-Path) and their verification, deliverability counters, suspension. | lecture | GET /emails/sender |
add_email_sender_domain | Registers the space’s sending domain (the paid option must be enabled by the owner in the app) and returns the DNS records to add. | écriture | POST /emails/sender |
update_email_sender | Changes the display name, sending address (on the domain) or reply address. | écriture | PATCH /emails/sender |
check_email_sender_domain | Asks the mail provider to look the DKIM and Return-Path records up and records the result. | écriture | POST /emails/sender/check |
test_email_sender | Sends the key creator a test e-mail with the space’s current sender. | écriture | POST /emails/sender/test |
remove_email_sender_domain | Removes the sending domain: e-mails go back to leaving via LearnFloo. | écriture | DELETE /emails/sender |
get_email_preferences | Whether a member still receives the space’s bulk e-mails (campaigns, digest). | lecture | GET /users/:id/email-preferences |
set_email_preferences | Subscribes or unsubscribes a member from the space’s bulk e-mails (for example to mirror an opt-out recorded in your platform). | écriture | PUT /users/:id/email-preferences |
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" } ] }
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.
{ "error": "message" }
| Code | Cas |
|---|---|
400 | Corps invalide, champ manquant, session terminée (Session is over), session inconnue, hôte non autorisé, règle métier non respectée |
401 | Clé absente, mal formée ou révoquée |
403 | Action interdite à l'auteur choisi (rôle insuffisant), propriétaire de l'espace |
404 | Chemin inconnu, ressource inconnue ou appartenant à un autre espace, ticket appartenant à un autre utilisateur |
405 | Méthode non prise en charge sur ce chemin |
413, 415 | Pièce jointe trop grosse ou d'un type non pris en charge |
429 | Limite 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.
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>
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.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.
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'] })
| Date | Changement |
|---|---|
| 2026-09-16 | Cours 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-15 | E-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-15 | Salle 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-15 | OpenAPI 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-15 | MCP : 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-13 | Scènes : champ jingle (joue sa durée puis la scène interrompue reprend là où elle en était). |
| 2026-09-13 | Scè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-13 | Scè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-13 | Scè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-13 | Scènes : champ framed (encadré : marge autour des emplacements, le fond reste visible), en création et en modification. |
| 2026-09-13 | Lecteur 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-13 | Lives : 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-12 | Tarif 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-12 | MCP : 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-12 | Serveur 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-12 | Scè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-11 | Scènes : widget viewers (compteur de spectateurs) et PUT /live/sessions/:id/viewers pour y ajouter des spectateurs ; extraViewers dans l'objet session. |
| 2026-09-11 | Scènes : style des emplacements poll et message (couleurs, opacité du fond, taille du texte, arrondi, bordure, ombre, police, éléments affichés). |
| 2026-09-11 | Scènes : options des vidéos autoplay, once, startSec, durationSec, volume ; startSec et durationSec aussi sur le son de scène (audio). |
| 2026-09-11 | Scènes : transitions fade, black, slide-*, zoom, wipe, blur et durée transitionMs. |
| 2026-09-11 | Scè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-11 | Sondages 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-11 | Lives : 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-11 | Scènes : champ locked (cadenas) ; une scène ou un modèle verrouillé refuse modifications et suppression jusqu'à { "locked": false }. |
| 2026-09-11 | Scè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-10 | Offre 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-10 | Devises : 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-10 | Inscriptions 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-10 | Chat 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-10 | Lives : 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-10 | Groupes (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-10 | Espaces 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-09 | Limite 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-07 | Webinaires : 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-07 | Support : /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-07 | GET /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-06 | Erreurs renvoyées en JSON propre { "error" }. Domaine personnalisé par espace pour les pages du live. API servie sur api.learnfloo.com. |
| 2026-09-05 | Première version : /entry, /sessions (POST, GET), /sessions/:id, /sessions/:id/attendance. |
Questions : équipe LearnFloo.