LearnFloo live sessions, support, members, groups (classes), courses, feed, calendar, videos and webhooks in an external platform (LMS, WordPress, CRM) · v1 reference · updated 2026-09-16
Base URL: https://api.learnfloo.com/api/v1. The paths below are relative to this base: /live/… for live sessions, /support/… for tickets, /users, /courses, /posts, /events, /videos, /webhooks…
Every call carries an API key provided by the administrator of the LearnFloo space (Space settings → LMS integration):
Authorization: Bearer lf_live_xxxxxxxx
401). A key gives access to the whole space, with an administrator's rights, according to the access level chosen when it was created: full access or read only (only the GET routes and the read MCP tools; a write returns 403 Read-only API key). The level can be changed at any time in the space settings.Each key has a token bucket that refills continuously according to the space owner's account and holds at most one minute of requests (bursts possible up to that ceiling):
| Account | Requests per minute per key |
|---|---|
| Free (billing not activated) | 60 |
| Billing active | 300 |
A request uses 1 token, except reports, which use more: /live/sessions/:id/attendance, /courses/:id/progress and /space/usage cost 5 tokens, /users/:id/progress 3 tokens. Every response carries the state of the bucket:
X-RateLimit-Limit: 120 # tokens per minute
X-RateLimit-Remaining: 117 # tokens available right now
X-RateLimit-Reset: 2 # seconds until the bucket is full again
Beyond that, the response is 429 { "error": "Rate limit exceeded" } with a Retry-After header (seconds). Wait for that delay before retrying; don't loop. The counter is spread over several buckets: X-RateLimit-Remaining is an estimate, and a burst very close to the ceiling may trigger a 429 slightly earlier. Under heavy concurrency (dozens of strictly simultaneous calls with the same key), a 503 Too many concurrent requests, retry response with Retry-After may also occur: same handling, retry after the delay. To follow the space's activity, prefer webhooks to repeated polling. Need more? Create several keys (one per use) or contact the LearnFloo team.
?limit=50&cursor=…; the response contains nextCursor (null on the last page). A filtered page (role, category…) may contain fewer items than limit: keep going while nextCursor isn't null.id) or, for those you created, by ext:<externalId> (URL-encoded: /users/ext%3Alms-42). Every user object returned contains externalId (null for a member who came from LearnFloo).author object: { "email": "trainer@client.com" } for an existing LearnFloo member, or { "externalId": "lms-42", "name": "Alice" } for a user of your platform (created and added as a member on first use). Without author, the action is done by the key's creator.content) is HTML (from the LearnFloo editor). Send plain text by default: it is converted into paragraphs. Pass "format": "html" to send HTML. When reading, content is the HTML and text a truncated plain-text version.GET read, POST create or action, PATCH partial update (only the fields present change), DELETE delete or cancel.Learner Your platform LearnFloo
│ clicks “Join” │ │
│────────────────────────────▶│ POST /live/entry │
│ │─────────────────────────────────▶│ creates or finds the learner,
│ │ { url, expiresAt } │ prepares a single-use ticket
│ opens url (iframe/tab) │◀─────────────────────────────────│
│◀────────────────────────────│ │
│ GET url ────────────────────────────────────────────────────▶ │ automatic sign-in,
│ ◀──────────────────────────────────────────────────────────── │ live room
/live/entry is personal, single-use and valid for 15 minutes. Request it at the moment of the click.externalId): LearnFloo creates a guest account the first time and finds it again afterwards.Personal entry URL for a learner.
{
"sessionId": "p97…",
"user": { "externalId": "lms-user-42", "name": "Alice Martin" },
"role": "viewer"
}
| Field | Description |
|---|---|
sessionId | Live session identifier (see GET /live/sessions) |
user.externalId | The learner's stable identifier on your side, 200 characters max |
user.name | Name shown to the other participants, updated on every call |
role | viewer (default: watches, chats, the trainer can give them the floor) or speaker (mic, camera, screen sharing from the start). A learner promoted by the trainer isn't demoted by a viewer call. |
Response 200:
{
"url": "https://app.learnfloo.com/live/enter/4tcgzHpN…",
"expiresAt": "2026-09-05T20:43:07.570Z",
"role": "viewer",
"sessionId": "p97…",
"userId": "k97…"
}
If the live session hasn't started, the learner waits in the waiting room and enters automatically when it starts. If the space has a custom domain, url is on that domain.
Errors: 400 Session is over (live session ended or cancelled), Session not found, user.externalId is required, role must be "viewer" or "speaker".
Creates a live session in the key's space.
{
"title": "Module 3 — Questions and answers",
"description": "optional",
"mode": "broadcast",
"scheduledAt": "2026-09-12T14:00:00Z",
"durationMin": 60,
"recordingEnabled": true,
"chat": "open",
"hostEmail": "trainer@client.com",
"groups": ["terminale-a"],
"replay": "attendees"
}
| Field | Description |
|---|---|
title | Required |
mode | broadcast (webinar: only the host and speakers publish) or conference (everyone publishes). Default broadcast |
scheduledAt | ISO 8601 date or timestamp in milliseconds. Required |
durationMin | Planned duration, for the calendar. Default 60 |
maxParticipants | 2 to 1000. Default 100 |
recordingEnabled | Automatic recording. Default true |
chat | Live chat: closed (available, panel collapsed on arrival, default), open (panel open on arrival) or off (disabled: no message accepted). The host can change it during the live session |
hostEmail | Optional. The trainer's LearnFloo account, a member of the space with the owner, admin, moderator or teacher role. Defaults to the key's creator |
groups | Optional. Groups (ids or slugs) that see and can join the live session; empty = the whole space. /live/entry tickets and invitations aren't affected. When reading: groupIds |
replay | Optional. Who watches the replay (the host and the space team always can): all (default: everyone who can access the live session), attendees (only the people who attended, in the room or as viewers), groups (members of the replayGroups groups), level (members at level replayMinLevel and above) or none (no replay for the members). For a member who isn't allowed, the app sends no URL and shows “Restricted replay” |
replayGroups | With replay: "groups": ids or slugs of the groups, at least one |
replayMinLevel | With replay: "level": minimum level, 2 to 10 |
Response 201:
{ "session": { … see GET /live/sessions/:id … }, "hostUrl": "https://app.learnfloo.com/<space>/live/<id>" }
hostUrl is the page where the trainer starts the live session, with their LearnFloo account.
Sessions of the space, most recent first, 200 max. Optional filters ?status=scheduled|live|ended|cancelled and ?group=<id or slug>.
{ "sessions": [ { … }, … ] }
{
"session": {
"id": "p97…",
"title": "Module 3 — Questions and answers",
"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"
}
}
| Field | Description |
|---|---|
status | scheduled, live, ended, cancelled. Useful to show “Soon”, “Join” or “Replay” |
participantCount | People connected right now (0 outside a live session) |
recordingStatus | recording, processing, ready, failed or null |
replayUrl | Replay MP4 when recordingStatus is ready, otherwise null. Available as soon as the live session ends; the file is re-encoded in the background and the URL may change a few minutes later. See also /replay |
hlsStatus | Webinar audience stream: starting, live, ended, failed or null |
chat | open, closed or off, see POST |
conversionCount, ctaClickCount | Sign-ups reported during the live session and clicks on chat buttons, see /conversions |
groupIds | Groups the live session is restricted to (empty = the whole space) |
replay, replayGroupIds, replayMinLevel | Who watches the replay, see POST. replayUrl is always returned to the key: if your platform shows the replay itself, it applies the rule |
Referer header: embed it in a page (<video> tag), don't open it as a direct link. To be told when the live session ends and when the replay is ready, subscribe a webhook to the live.session.ended and live.replay.ready events.PATCH edits a session. Before the live session: title, description, mode, scheduledAt, durationMin, maxParticipants, recordingEnabled, chat, groups, replay, replayGroups, replayMinLevel; the calendar event follows. During the live session: everything but the date, duration and format. After it: title, description, groups and the replay fields (to open up or restrict a replay afterwards). A field that can no longer change returns 400. Response 200 { "session": { … } }.
DELETE cancels a scheduled session (status cancelled, event removed from the calendar, event credit returned). Response 200 { "session": { … } }. A live session in progress or ended returns 400 Only scheduled sessions can be cancelled.
Attendance, watch time and audience. During a live session in progress, computed up to the time of the call.
{
"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…" }
]
}
| Field | Description |
|---|---|
externalId | Your identifier sent in /live/entry. null for people who came directly from LearnFloo |
source | lms (entered through your platform), invite (invitation link), member (LearnFloo member) |
role | host, speaker, viewer |
joinedAt, leftAt | First entry and last exit. leftAt is null while the person is in the room |
watchSec | Actual time spent in the room, all connections combined, capped at the live session's duration |
watchPct | Share of the live session followed, 0 to 100. The field to use to validate attendance (e.g. >= 80) |
connections | Number of entries into the room (leaving then coming back = 2) |
summary: live session duration, attendees, audience peak and its time, average attendance (simultaneous viewers), average watch time, people who stayed more than half of the live session, replay views. spectatorHours sums the audience's time on the webinar's HLS stream, interactiveHours the time of the people connected to the room (host, speakers, people given the floor): these are the two billing units.
Instant state of the people registered on the session (lightweight, for a dashboard during the live session; statistics are in /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: watches the webinar's HLS stream (without being in the WebRTC room). handRaisedAt: has asked to speak.
Chat messages kept after the live session, oldest first (?limit=500, 2000 max).
{ "session": { … }, "messages": [ { "id": "…", "userId": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "text": "Hello!", "imageUrl": null, "link": null, "sentAt": "…" } ] }
The host's messages can carry an image (imageUrl) and a call-to-action button (link: { "url", "label" }), for example a sign-up link sent during the live session. kind is message or conversion (automatic announcement of a sign-up).
Your LMS reports that a viewer has just signed up or bought during the live session. LearnFloo records the conversion, updates the counter shown under the chat buttons, posts the announcement in the chat (“🎉 Marie just joined the course”, text and anonymity set by the host), shows a banner on the scene and sends the live.conversion webhook.
{
"externalId": "lms-user-42",
"name": "Marie Dupont",
"label": "Advanced SEO course",
"amountCents": 49900,
"currency": "eur"
}
| Field | Description |
|---|---|
externalId | The learner's identifier in your LMS (the one from /users). Found in the button link, see below |
userId | Or the LearnFloo identifier (lf_user in the link) |
email | Or the email of the LearnFloo account |
name | Optional, first name shown in the announcement (otherwise the name of the account found, otherwise an anonymous announcement) |
label, amountCents, currency | Optional, for the report (product bought, amount in the smallest unit, currency eur by default or usd) |
No identifier is required: with neither identifier nor name, the announcement is anonymous (“🎉 New sign-up”). Response 201 { "conversion": { "id", "userId", "externalId", "name", "label", "amountCents", "currency", "createdAt" }, "session": { … } }.
Finding the viewer. When the host sends a call-to-action button in the chat, LearnFloo adds three parameters to the link: lf_live (live session id), lf_user (the viewer's LearnFloo id) and lf_ext (their identifier in your LMS if they were created via /users). Keep them on your sales page (hidden field, cookie) and send them back at payment: lf_live gives the call's :id, lf_ext or lf_user identifies the buyer. Clicks on these buttons are counted (ctaClickCount on the session) and conversions appear in the live session's report.
The replay in all its forms.
{ "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 is available as soon as the live session ends (raw MP4); hlsUrl, embedUrl, thumbnailUrl and durationSec arrive a few minutes later, once the adaptive copy is encoded (null before). lessonId is set if the trainer published the replay as a course lesson.
Invitation links for people who don't have an account on your platform (external speaker, guest): a shareable, reusable link, without a personal ticket.
POST { "role": "speaker", "label": "Guest speaker", "expiresInHours": 48, "maxUses": 1 }
→ 201 { "invite": { "id": "…", "token": "…", "url": "https://app.learnfloo.com/invite/…", "role": "speaker", "label": "Guest speaker",
"uses": 0, "maxUses": 1, "expiresAt": "…", "revoked": false, "active": true, "createdAt": "…" } }
role: viewer (default), speaker, assistant (production: prepares and switches scenes, launches polls, without camera or mic) or moderator (moderates the chat: deleting messages, muting, chat settings, prepared messages, private messages and polls, without camera or mic). expiresInHours and maxUses are optional (no limit by default). 20 active links max per session. DELETE revokes the link (response 200 with the invitation).
A LearnFloo live session is run like a studio: named scenes (layout, cameras, screen sharing, images, PDFs, videos, embeds, stickers and banners) that the presenter or the production crew switch in one click. Everything the production crew does in the app can also be done through the API, live: create a scene during the live session, put it on air, change a banner's text, show an announcement, turn a PDF page, start a video. What is on air is seen by all participants, the viewers' stream, the recording and external broadcasts.
{
"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": "Slides" }
],
"overlays": [
{ "id": "ticker", "type": "band", "text": "Registration open until 6 pm", "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": "offer"
}
| Field | Description |
|---|---|
id | Free at creation (1 to 40 characters: letters, digits, -, _), otherwise generated. Same for slots and overlays: choose stable ids (ticker, price) to update them later |
jingle | Jingle: the scene plays its duration (durationSec, or else the length of its videos and sounds), then the scene it interrupted resumes where it was (videos and sounds, time left in the auto-advance), even without auto-advance. Reached by auto-advance, a jingle then goes on to the next scene. null to remove. |
durationSec, nextSceneId | Auto-advance: time on screen in seconds (1 to 86400, counted after the arrival transition; absent = the scene stays until the next switch; stretched to the end of the videos and sounds that start with the scene, not looped, whose length is known: mediaSec, file length in seconds measured by the editor, also accepted on video slots and in audios), then the next scene (absent = the next one in the list, or the first one when looping). Only active when auto-advance is on (PUT …/scenes/auto) |
layout | solo (first item full screen), grid, spotlight (first one large, the others in a column), sidebyside, pip (first full screen, second as a thumbnail), cinema (first full screen, strip at the bottom), free (each slot carries x, y, w, h) |
slots[].kind | host (the host's camera), guest (guestIndex: n-th speaker in order of arrival, or userId to pin a person), screen (first screen share), media (mediaId from the media library, or a direct https url with mediaKind image, pdf or video), embed (mediaId or a YouTube, Vimeo, Loom url) |
slots[].fit, loop, muted, title | Framing cover or contain; looping or muted video; title shown in the production desk |
slots[].shape, filter, bgMode, bgBlur, bgImage, volume, muted | Cameras (host, guest): shape rect (default), rounded, square, squircle (rounded square), circle, portrait; filter none, bw, noir, sepia, vintage, warm, cool, vivid, faded, bright, contrast; background applied on the person's device while the scene is on the air: bgMode none, blur (bgBlur 1 to 20, default 10) or image (bgImage: https URL or preset:ocean, preset:forest, preset:sunset, preset:slate), absent = the person's own choice; sound on the air volume (0 to 1) and muted, for the broadcast, the recording and the audience (speakers always hear each other). null to remove |
slots[].autoplay, once, startSec, durationSec, volume | Video: starts when the scene goes on air (default true, otherwise waits for the production desk's commands), once only the first time, start in seconds, duration of the part played (looped or stopped at the end of the part), volume 0 to 1 (null to remove). Same logic as the scene sound. |
overlays[] | type band (text, bg, color, fontSize in px at 1080p, bold, scroll, speed in px/s, align), sticker (emoji, or an https imageUrl, or a badge label with bg and color), or a live widget poll / message / viewers (viewer counter, title = label), with its style: bg (hex) and bgOpacity, color, accent (bars, badges), fontScale (0.5 to 2), radius (px at 1080p), borderColor, borderWidth, shadow, font (sans, rounded, serif, mono), align (message), showHeader, showVotes, showLetters (poll), showName, showIcon (message); the size is set with w and h: where the scene draws the poll or the chat message put on air; empty while nothing is on air, and a scene without such a slot uses a default corner. Geometry in fractions of the 16:9 frame (x, y, w, h between 0 and 1), visible, opacity |
locked | Padlock: a locked scene refuses any edit and deletion (400), from the app as from the API, until a call sends { "locked": false }. It can still be put on air and duplicated |
audios | Sounds of the scene, several allowed (6 max), each { "mediaId": "…" } (audio file from the media library) or { "url": "https://…" }, with autoplay (starts when the scene goes on air, default true), once (with autoplay: only the first time, default false), loop (default false), volume (0 to 1, default 1), startSec and durationSec (part played, in seconds; null to remove). Played for all participants and captured in the broadcast and the recording. Replaces the list (a sound already there, same mediaId, keeps its other settings); [] removes them all. The older audio field (a single sound, same shape) is still accepted on write, "audio": null removes every sound; on read, sounds are always in audios. Play, pause and restart each sound from the beginning with PUT …/media/:mediaId. |
background, backgroundImageUrl, showLogo, transition | CSS background colour, https background image (null to remove it), the space's logo in the corner, transition when the scene arrives (the previous one stays underneath during the animation): cut, fade (crossfade), black (fade to black), slide-left, slide-right, slide-up, slide-down, zoom, wipe, blur, with transitionMs (100 to 3000, default 500). Played in the room, the broadcast and the recording. |
framed | Framed: a margin all around the slots lets the background (colour or image) and your branding show. For every layout but free; default false, null to remove it |
20 scenes per live session, 9 slots and 30 stickers or banners per scene. A scene is prepared before the live session (speakers are designated by order of arrival) and edited during it: editing a scene on air is visible immediately.
GET returns the whole production desk:
{ "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 just signed up", "bg": "#059669", "color": "#ffffff", "until": "…" } ] }
POST creates a scene from the object above (everything is optional except what gives it meaning), or from a template of the space: { "template": "Shared screen", "name": "Demo" }. "activate": true puts it on air right away; "afterId" places it after a given scene. Response 201 { "scene": { … } }.
POST /live/sessions/p97…/scenes
{ "id": "offer", "name": "Special offer", "layout": "pip",
"slots": [ { "kind": "media", "mediaId": "m12…" }, { "kind": "host" } ],
"overlays": [ { "id": "price", "type": "band", "text": "SEO course: €499 until tonight", "y": 0.86 } ],
"activate": true }
PATCH edits the fields sent; slots and overlays, when present, replace the whole lists (to touch a single item, see below). DELETE removes the scene (if it was on air, back to the automatic layout) and returns the production desk. PUT …/scenes/order { "sceneIds": [ … ] } reorders.
| Call | Effect |
|---|---|
POST/live/sessions/:id/scenes/:sceneId/activate | Puts the scene on air (its videos restart from the beginning). Response: the full production desk |
PUT/live/sessions/:id/scenes/active { "sceneId": "intro" } | Same thing; { "sceneId": null } goes back to the automatic layout (cameras in a grid, screen share large) |
PUT/live/sessions/:id/scenes/auto { "enabled": true, "loop": true } | Auto-advance: during the live session, each scene with durationSec gives way to nextSceneId (or the next one) once its time is up; loop goes back to the first one at the end of the list. A manual switch (app or API) restarts the timer of the new scene. The returned production desk carries autoAdvance: { enabled, loop, nextSceneAt } (nextSceneAt: ISO date of the next change, or null) |
POST/live/sessions/:id/bands { "text": "5-minute break", "bg": "#059669", "color": "#ffffff", "seconds": 8 } | Banner shown at the top of the screen for seconds (2 to 60, 8 by default) on every screen, whatever the scene. Response 201 { "band": { "id", "text", "bg", "color", "until" } }. For a permanent or editable text, prefer a scene banner (overlays) |
PUT/live/sessions/:id/media/:mediaId { "page": 4 } | Page shown of a PDF present in a scene |
PUT/live/sessions/:id/media/:mediaId { "playing": true, "position": 0 } | Play, pause or position (seconds) of a video present in a scene, or of one of a scene's sounds (audios[].mediaId), synchronised for everyone |
Each change of the scene on air sends the live.scene.changed webhook ({ session, scene }, scene set to null for the automatic layout).
| Call | Effect |
|---|---|
POST…/scenes/:sceneId/overlays | Adds a sticker or a banner (overlays[] object above). 201 { "overlay" } |
PATCH…/scenes/:sceneId/overlays/:overlayId | Edits the fields sent: { "text": "Only 12 seats left" }, { "visible": false }, { "x": 0.1, "y": 0.1 }… Visible immediately if the scene is on air |
PATCH/live/sessions/:id/overlays/:overlayId | Same thing in all the scenes that carry this id: a shared banner (counter, price, next step) placed in each scene with the same id is updated in a single call |
DELETE…/scenes/:sceneId/overlays/:overlayId | Removes the item, returns the scene |
POST…/scenes/:sceneId/slots | Adds a slot (slots[] object; optional position, 0 = main). 201 { "slot" } |
PATCH…/scenes/:sceneId/slots/:slotId | Edits the slot (other media via mediaId or url, guestIndex, fit, geometry in free, position to move it in the order) |
DELETE…/scenes/:sceneId/slots/:slotId | Removes the slot, returns the scene |
# The seat counter, in every scene, from your CRM
PATCH /live/sessions/p97…/overlays/places
{ "text": "Only 7 seats left", "bg": "#f59e0b", "color": "#111111" }
Scene templates of the space, reusable in every live session (“New” menu of the production desk, or "template" when creating a scene). POST: { "name": "Shared screen", "scene": { … } } or, to save an existing scene, { "name": "…", "sessionId": "p97…", "sceneId": "intro" }. Same name = replaced. Response { "template": { "id", "name", "scene", "folderId", "createdAt", "updatedAt" } }. :id accepts the identifier or the name.
{ "role": "speaker" | "viewer" | "assistant" | "moderator" } for a participant already registered on the session (/participants): give or take back the floor, or appoint a production crew member. Applied immediately during the live session. Response { "participant": { … } }.
A live session can ask participants polls (opinion) and quizzes (with a correct answer), in the room as well as in the webinar audience. They are prepared in advance (status draft, invisible to participants), launched during the live session (open, one at a time) then closed (closed). Results are kept after the live session: GET …/polls always returns them, and they also appear in the statistics and on the replay page. In the app, the host, the staff and the moderator and assistant roles manage them from the live session's “Polls” tab and from “Prepare”.
{ "id": "k3…", "sessionId": "p97…", "kind": "quiz", "question": "What is the capital of Australia?",
"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 (default) or quiz. For a quiz, correct at creation gives the indexes (from 0) of the correct answers in options; the correct answer and correctVoters (voters who picked exactly the right combination) are revealed to participants with the results. percent is the share of voters who chose the option (with allowMultiple, the total exceeds 100). A participant can change their vote while the poll is open; a voter counts once.
GET …/polls returns { "polls": [ … ] } in preparation order, with the counts; ?status=draft|open|closed filters. Response { "poll": { … } } for the others.
POST /live/sessions/p97…/polls
{ "question": "Which topic next?", "options": ["Pricing", "LMS integration", "Replays"], "allowMultiple": true }
→ 201 { "poll": { "status": "draft", … } }
POST /live/sessions/p97…/polls
{ "kind": "quiz", "question": "Capital of Australia?", "options": ["Sydney", "Canberra"], "correct": [1], "open": true }
→ 201 { "poll": { "status": "open", … } } // "open": true launches it right away (live session in progress only)
phase (at creation or in PATCH): live (default, launched by hand during the live), waiting (pre-live poll or quiz: open in the waiting room as soon as it exists while the live is scheduled, closed automatically at launch, results reusable during the live through …/band and …/stage) or closing (opened automatically when the live ends, on the closing screen: feedback, satisfaction). An open waiting poll stays editable.
PATCH edits a draft poll (or an open waiting poll) only (same fields; if options changes for a quiz, send correct again). A launched poll is duplicated (new POST) rather than edited. DELETE deletes the poll and its votes, whatever its status. 2 to 6 answers, 100 polls per live session.
| Call | Effect |
|---|---|
POST…/polls/:pollId/open | Opens voting to participants (card on the video, system message in the chat unless { "announceInChat": false }). Refused if another poll is open or if the live session isn't in progress. Webhook live.poll.opened. |
PUT…/polls/:pollId/results { "shown": true } | Shows (or hides with false) the counts to participants, during the vote or after closing. For a quiz, also reveals the correct answer. |
POST…/polls/:pollId/close | Closes voting; { "showResults": false } to close without showing them (default: shown). Webhook live.poll.closed with the results. A poll still open at the end of the live session is closed, results shown. |
PUT…/polls/:pollId/stage { "shown": true } | Shows the poll in the scene (the poll slot of the scene on air, otherwise the bottom-right corner): question, answers, number of votes, then the bars and the correct answer when results are shown. Visible in the room, the broadcast and the recording. false removes it; onStage in the poll object. |
PUT/live/sessions/:id/viewers { "extra": 25 } | Adds viewers to the counter shown by the scenes' viewers widget (actual number of people in the live session + extra). Never counted in attendance, statistics or billing. 0 removes the addition; extraViewers in the session object. |
PUT/live/sessions/:id/chat/stage { "messageId": "…" } | Puts a chat message on air (the scene's message slot, otherwise bottom left): name and text, image if any. { "messageId": null } removes it. Private messages are refused. Response { "message": { id, name, text, imageUrl, at } | null }. |
POST…/polls/:pollId/band | 12-second banner on the scene (video, broadcast, recording) with the leading answers, or the correct answer and the success rate for a quiz. Live session in progress, at least one vote. |
Your users open support tickets from your platform (for example the LearnFloo WordPress plugin); your team answers them in the LearnFloo space, Support tab, or through the API (team side). Same API key as for live sessions.
user.externalId (your stable identifier) and, at creation, user.name. LearnFloo creates a guest account the first time, as for live sessions, and finds it again afterwards."format": "html" to send HTML.ticket object returned by every call:
{
"id": "j57…", "number": 12, "subject": "Video stuck in 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 }
}
| Field | Description |
|---|---|
status | open (waiting for the team), pending (waiting for the user), resolved, closed |
priority | low, normal, high, urgent |
lastMessageBy | member or staff: who wrote last |
The user's tickets, most recent first. ?externalId=… required, &status=active|open|pending|resolved|closed|all optional (all by default, active = open + pending).
{ "tickets": [ { … }, … ] }
All the space's tickets (team dashboard): ?scope=space&status=…&limit=100 (500 max). Each ticket then carries its author ({ id, externalId, name, email, image }).
{
"user": { "externalId": "wp:42", "name": "Alice Martin" },
"subject": "Video stuck in module 3",
"content": "Hello,\n\nthe video stops at 2 min.",
"priority": "normal",
"category": "wordpress",
"attachments": [ { "kind": "image", "url": "https://…", "name": "capture.png", "mimeType": "image/png", "sizeBytes": 1234 } ]
}
| Field | Description |
|---|---|
user.externalId, user.name | Required. 200 and 60 characters max |
subject | Required, 200 characters max |
content | Plain text (default) or HTML with "format": "html". Required unless an attachment is provided |
priority | Default normal |
category | Free text, optional (shown to the team) |
attachments | Optional, 10 max, objects returned by /attachments |
Response 201: { "ticket": { … } }. The space team is notified.
?externalId=… required (or ?scope=space for the team: any ticket of the space, with its author). The ticket and its messages visible to the user, oldest first. The team's internal notes never leave through the API.
{
"ticket": {
…,
"messages": [
{ "id": "m3…", "content": "<p>Hello,</p>…", "attachments": [],
"createdAt": "…", "fromStaff": false, "author": { "name": "Alice Martin", "image": null } },
{ "id": "m4…", "content": "<p>Could you clear the cache?</p>", "attachments": [],
"createdAt": "…", "fromStaff": true, "author": { "name": "Julie", "image": "https://…" } }
]
}
}
content is HTML produced by the LearnFloo editor: sanitise it before showing it in your page (text tags, images, links).404 if the ticket doesn't exist or doesn't belong to this user.
{ "user": { "externalId": "wp:42", "name": "Alice Martin" }, "content": "Thanks, it's fixed.", "attachments": [] }
Response 201: { "ticket": { … } } (without the messages). The ticket goes back to open and the team (or the assigned person) is notified.
{ "user": { "externalId": "wp:42" }, "status": "resolved" }
The user can mark their ticket resolved or reopen it (open). The other statuses are reserved for the team. Response 200: { "ticket": { … } }.
To sync an external help desk, the two calls above accept a staff object instead of user: the action is done by a member of the space team (owner, admin, moderator, teacher), by default the key's creator, or staff.email for another LearnFloo account.
POST /support/tickets/:id/messages
{ "staff": { "email": "julie@client.com" }, "content": "Could you clear the cache?" }
→ the ticket goes to "pending", the user is notified
POST /support/tickets/:id/status
{ "staff": {}, "status": "closed", "priority": "high", "assigneeEmail": "julie@client.com" }
→ any status; priority and assigneeEmail (null to unassign) are optional
Raw file body, Content-Type header = the file's type (images, PDF, audio, text, zip, Office), 25 MB max. ?externalId=… optional (attachment of an external user). Then attach it to a ticket, a reply, a post or a comment via attachments. /support/attachments is an alias.
{ "attachment": { "kind": "image", "url": "https://…/capture.png", "mimeType": "image/png", "sizeBytes": 1234 } }
Errors: 415 unsupported type, 413 file too large.
The space behind the key, for a connection test and display:
{ "space": { "id": "…", "slug": "my-space", "name": "My space", "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.com", "memberCount": 128, "courseCount": 4, "groupCount": 6,
"groupLabels": { "singular": "Class", "plural": "Classes", "leader": "Teacher", "member": "Pupil", "gender": "f" },
"createdAt": "…", "url": "https://app.learnfloo.com/my-space" } }
visibility: public (listed on www.learnfloo.com/en/communities, About page aboutUrl open to everyone) or private (access by invitation link only). accessType: free or paid; a paid space carries priceCents (amount in the currency's smallest unit), priceCurrency (eur, price including VAT, or usd, price excluding tax) and priceInterval (month, year, once = lifetime access). rating is the average of the members' reviews (null without reviews).
Usage of the space since the last invoice, valued at the pay-as-you-go rates of the single offer (webinar hours broadcast, recorded video call hours, interactive hours, viewer hours, plays, stored video minutes, simulcast hours multiplied by the number of destinations, subtitled hours), the state of the owner's account and the key's rate limit.
{ "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 is this space's cost before the included allowances (allowance), the welcome credit and the monthly cap (capCents, capMode = block or warn), which apply to the owner's account, all spaces combined. liveAllowed is false when the account can no longer pay (credit used up without active billing).
GET: members of the space, paginated (?limit=50&cursor=…, 200 max), filters ?role=member|contributor|teacher|moderator|admin|owner and ?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: creates (or updates) a user of your platform and adds them as a member of the space, without waiting for them to join a live session. Idempotent: calling again with the same externalId updates the name.
{ "externalId": "lms-user-42", "name": "Alice Martin", "role": "member", "member": true }
→ 201 { "user": { …, "member": { … } }, "created": true }
role: member (default), contributor, teacher, moderator, admin. member: false creates the account without enrolling it in the space. Response 200 if the user already existed.
:id = LearnFloo identifier or 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 } (removes the member from the space; their account and history are kept)
Only users created by your platform can be renamed. The space owner can be neither edited nor removed.
Password-less sign-in (SSO) of a user of your platform into the LearnFloo space: feed, courses, calendar, leaderboard… Same principle as /live/entry: personal, single-use URL, valid for 15 minutes, to request at the click. The user is added as a member if they aren't one already. Reserved for ext:… users; with name, the user is created if they don't exist.
POST /users/ext%3Alms-user-42/entry
{ "name": "Alice Martin", "redirect": "courses" }
→ { "url": "https://app.learnfloo.com/enter/…", "expiresAt": "…", "userId": "k97…", "redirect": "/my-space/courses" }
redirect: home (default), posts, courses, calendar, live, leaderboard, tickets, challenges, or a path of the space (/my-space/courses/onboarding, /posts/<id>).
A user's progress in each course of the space.
{ "user": { … }, "courses": [
{ "id": "c1…", "slug": "onboarding", "title": "Onboarding", "published": true, "lessonCount": 8,
"completedLessons": 5, "progressPct": 63, "completed": false, "lastCompletedAt": "…" }
] }
GET: total, level and latest XP events of the user in the space (?limit=100, 1000 max).
{ "user": { … }, "totalXp": 320, "level": 3, "events": [ { "id": "…", "amount": 25, "source": "lesson_complete", "refId": "l3…", "createdAt": "…" } ] }
POST: awards XP from your platform (quiz passed, assignment handed in…). amount from -1000 to 1000, reason (60 characters, visible as the source api:<reason>) and refId optional. The level is recalculated. 400 if gamification is disabled in the space.
{ "amount": 50, "reason": "quiz-module-3", "refId": "quiz:874" } → 201 { "user": { … }, "totalXp": 370, "level": 3, "awarded": 50 }
| Call | Response |
|---|---|
GET/leaderboard?limit=50&group= | { "leaderboard": [ { "rank": 1, "id", "externalId", "name", "image", "totalXp", "level", "role" } ] } (500 max); group restricts to the members of a group |
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 | The challenge with its participants ({ id, externalId, name, progress, completedAt, joinedAt }) |
A space can split its members into groups: a school's classes, a training organisation's cohorts, a company's teams or departments. The wording is set in the space settings (groupLabels of GET /space); the API always talks about groups. Each group has a slug usable instead of its identifier.
A course, a post, an event or a live session can be addressed to certain groups only: that's the groups field (array of identifiers or slugs) at creation and update, and groupIds when reading. Empty = the whole space. A member only sees content without an audience or addressed to one of their groups; the owner, admin, moderator and teacher roles see everything. Lists accept ?group=<id or slug> to return only the content addressed to that group.
POST /courses { "title": "Maths — Year 12", "groups": ["terminale-a", "terminale-b"] }
POST /posts { "title": "Friday's outing", "content": "…", "groups": ["terminale-a"] }
POST /live/sessions { "title": "Revision", "scheduledAt": "…", "groups": ["terminale-a"] }
GET /courses?group=terminale-a
In a group, a member is member (pupil, learner…) or leader (teacher, manager…): a leader can manage the list of members of their group from the app, without any particular role in the space.
Automatic groups. A group can carry a rule: members who meet it are added as they go (and notified), current members as soon as the rule is created. Rules are one-way, nobody is removed automatically; you can always add or remove people by hand. Types: level_min (level 2 to 10), streak_days (days), challenge_joined and challenge_completed (challenge = id, see /challenges), badge (badge = id or key, see /badges), course_completed (course = id or slug). Then use the group as an audience: a course reserved for “Level 5 and above”, a live session for those who finished a challenge.
POST /groups { "name": "Level 5 and above", "rule": { "kind": "level_min", "level": 5 } }
POST /groups { "name": "Module 3 finished", "rule": { "kind": "course_completed", "course": "module-3" } }
PATCH /groups/module-3-finished { "rule": null } # becomes a manual group again
→ "group": { …, "rule": { "kind": "course_completed", "level": null, "days": null, "challengeId": null, "badgeId": null, "courseId": "c1…" } }
| Call | Body / response |
|---|---|
GET/groups | { "groups": [ { "id", "slug", "name", "description", "color", "memberCount", "rule", "createdAt" } ] }, by name |
POST/groups | { "name", "slug"?, "description"?, "color"?, "rule"? } → 201 { "group": { … } }. The slug is derived from the name if not given; color in the #rrggbb format |
GET/groups/:id | The group (id or slug), a readable ruleLabel (in French), and its members: [ { "id", "externalId", "name", "email", "image", "groupRole": "leader|member", "auto", "addedAt" } ] (auto: added by the rule) |
PATCH/groups/:id | { "name"?, "slug"?, "description"?: string | null, "color"?: string | null, "rule"?: object | null } |
DELETE/groups/:id | { "deleted": true }. The group is removed from the audience of the content that targeted it (content that targeted only this group becomes visible to the whole space again) |
| Call | Body / response |
|---|---|
POST/groups/:id/members | { "users": ["k97…", "ext:lms-user-42"], "role"?: "member|leader" } → { "group", "added", "notFound": [] }. Users must already be members of the space (see POST /users); up to 500 per call, idempotent |
DELETE/groups/:id/members/:userId | { "removed": true }. :userId = LearnFloo identifier or ext:… |
GET: a user's groups in the space, with their groupRole. PUT replaces the whole list (handy to sync classes from your LMS):
PUT /users/ext%3Alms-user-42/groups
{ "groups": ["terminale-a", "club-robotique"], "role": "member" }
→ { "groups": [ { "id", "slug", "name", …, "groupRole": "member" } ] }
| Call | Response |
|---|---|
GET/groups/:id/leaderboard?limit= | { "group", "leaderboard": [ { "rank", "id", "externalId", "name", "image", "totalXp", "level", "role", "groupRole" } ] }: the group's members ranked by XP |
GET/groups/:id/stats 5 tokens | Follow-up of each member: progress in the courses open to the group, lessons completed, posts, live sessions attended, XP over the last 30 days, last activity. See below |
GET/groups/:id/documents | { "documents": [ { "id", "title", "url", "kind", "mimeType", "sizeBytes", "uploadedBy", "createdAt" } ] }, most recent first |
POST/groups/:id/documents | { "title", "url", "kind"?, "mimeType"?, "sizeBytes"? } → 201 { "document" }. url: a file uploaded via POST /attachments, or any https link (kind: "link", never deleted from storage) |
DELETE/groups/:id/documents/:docId | { "deleted": true }; a file uploaded through the API is erased from storage |
GET /groups/terminale-a/stats
{ "group": { "id", "slug": "terminale-a", "name": "Year 12 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 — Year 12", "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 covers the members with the member role (leaders are excluded from the averages); inactive = no activity for 30 days. The courses taken into account are the published courses addressed to the group or to the whole space.
GET: published courses of the space (?includeUnpublished=1 to see everything, ?group= for those addressed to a group).
{ "courses": [ { "id": "c1…", "slug": "onboarding", "title": "Onboarding", "description": null, "coverImage": null,
"published": true, "lessonCount": 8, "groupIds": [], "createdAt": "…", "url": "/my-space/courses/onboarding" } ] }
POST: creates a course (draft by default). An optional author must be owner, admin or teacher. groups: audience (ids or slugs), empty = the whole space.
{ "title": "Onboarding", "slug": "onboarding", "description": "…", "coverImage": "https://…", "published": false, "groups": ["terminale-a"] }
→ 201 { "course": { … } }
:id = course identifier or slug. GET returns the full structure; add ?withContent=1 for the lessons' HTML.
{ "course": { …, "modules": [ { "id": "m1…", "title": "Week 1", "description": null, "order": 0 } ],
"lessons": [ { "id": "l1…", "courseId": "c1…", "moduleId": "m1…", "title": "Welcome", "order": 0,
"videoUrl": "https://…", "videoDurationSec": 312, "createdAt": "…", "url": "/my-space/courses/onboarding/lessons/l1…" } ] } }
PATCH: title, description, coverImage, published, groups ([] or null = the whole space). DELETE deletes the course, its modules, its lessons and the progress records.
| Call | Body / response |
|---|---|
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 of a video (MP4, HLS, YouTube, Vimeo) or the playbackUrl of a studio video |
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 } |
Without parameters: all the learners who started the course, the most advanced first.
{ "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": "…" } ] }
With ?externalId=lms-user-42 (or ?userId=k97…): the lesson-by-lesson detail for this learner.
{ "course": { … }, "user": { …, "completedLessons": 5, "progressPct": 63,
"lessons": [ { "id": "l1…", "title": "Welcome", "moduleId": "m1…", "completedAt": "…" }, { "id": "l2…", "title": "…", "moduleId": "m1…", "completedAt": null } ] } }
Marks a lesson as finished for a learner, as if they had completed it in LearnFloo (XP, quests, badges, lesson.completed webhook). Idempotent.
{ "user": { "externalId": "lms-user-42", "name": "Alice Martin" } } or { "userId": "k97…" }
→ 201 { "lessonId": "l1…", "user": { … }, "completedAt": "…", "created": true } (200 if already finished)
| Call | Body / response |
|---|---|
GET/posts?category=&group=&limit=&cursor= | The space's feed, most recent first: { "posts": [ … ], "nextCursor" }. Categories: general, announcements, question, wins, resource. group: posts addressed to that group |
GET/posts/:id | The post with its comments (oldest first, parentId for replies) |
POST/posts | { "title"?, "category"?, "content", "format"?, "attachments"?, "pinned"?, "groups"?, "author"? } → 201 { "post": { … } }. Mentions, XP and notifications as in the app. pinned reserved for moderators; an author without a staff role can only address (groups) their own groups |
PATCH/posts/:id | { "title"?: string | null, "category"?, "content"?, "format"?, "pinned"?, "groups"? } |
DELETE/posts/:id | Deletes the post, its comments and reactions |
POST/posts/:id/comments | { "content", "format"?, "parentCommentId"?, "attachments"?, "author"? } → 201 { "comment": { … } } |
DELETE/comments/:id | Deletes the comment and its replies |
{ "post": { "id": "p1…", "title": "Welcome, cohort 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.com", "image": "https://…", "role": "admin", "level": 5 },
"url": "/my-space/posts/p1…" } }
| Call | Body / response |
|---|---|
GET/events?from=&to=&type=&group= | Events by ascending start date; from / to in ISO or milliseconds. Scheduled live sessions appear with 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 (default), workshop, masterclass, deadline, qa. Live sessions are created via /live/sessions |
PATCH/events/:id | Same fields; endsAt: null to clear it, groups: [] to open it to the whole space. An event linked to a live session is edited via PATCH /live/sessions/:id |
DELETE/events/:id | { "deleted": true } |
Recordings from the LearnFloo studio (screen, camera, imports) and imported live replays, hosted on Bunny Stream.
| Call | Response |
|---|---|
GET/videos?status=ready&limit=&cursor= | { "videos": [ … ], "nextCursor" }, most recent first. Statuses: uploading, processing, ready, failed |
GET/videos/:id | { "video": { … } } |
PATCH/videos/:id | { "title"?, "description"? } |
{ "video": { "id": "v1…", "title": "Module 3 demo", "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": { … } } }
Playback URLs are only returned for ready videos. As for replays, the MP4 requires a Referer header: embed it in a page. Videos are uploaded from the app (studio); the API doesn't take video files.
The space's media library: the images, PDFs, videos, embeds and stickers used in live scenes and in the studio. GET: ?kind=image|pdf|video|embed|sticker&limit=&cursor=.
{ "media": [ { "id": "…", "kind": "pdf", "title": "Module 3 slides", "url": "https://…", "thumbnailUrl": null, "videoId": null, "sizeBytes": 812000,
"folder": { "id": "…", "name": "Live 12/09" }, "createdAt": "…" } ], "nextCursor": null }
POST adds an item by URL (https, hosted by you or on a CDN):
{ "kind": "image", "url": "https://cdn.your-site.com/offer.png", "title": "Back-to-school offer", "folder": "Live 12/09" }
→ 201 { "media": { "id": "m12…", "kind": "image", … } }
kind: image, pdf, video (MP4 file playable directly), embed (YouTube, Vimeo or Loom page, converted into a player), sticker (transparent PNG) or audio (MP3, WAV, OGG, M4A: a scene's sound). folder: identifier or name of a folder (created on the fly if it doesn't exist; list with GET /media/folders, explicit creation with POST /media/folders { "name" }). An external PDF must allow requests from app.learnfloo.com (CORS header); images and videos don't have this constraint. To upload a file rather than a URL, go through POST /attachments then give the URL obtained.
PATCH /media/:id: title, folder (null for the root). DELETE /media/:id deletes the item (and the file if LearnFloo hosts it); scenes that used it keep the URL.
The whole e-mail layer of a space is driven by the API, under /emails/…: the campaigns to the members (writing, preview, test, immediate or scheduled send, statistics), the segments that define who receives them, the reusable texts, the wording of the automatic e-mails (live invitations), the automatic digest of what is new, the client's sending domain and each member's preferences. The same actions exist as MCP tools ("E-mails" family).
Common rules: the author of every write is the creator of the API key (they receive the test sends); bodies are plain text (blank line = paragraph) or HTML with "format": "html"; {{prenom}} (first name) and {{espace}} (space name) are replaced for each reader; every bulk e-mail carries the person's unsubscribe link, and an unsubscribed person receives neither campaign nor digest from that space any more. A read-only key can use the GET routes and POST /emails/segments/preview.
POST { "subject": "What's new at {{espace}}", "previewText": "Three novelties this week", "body": "Hello {{prenom}},
Here is…",
"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 lists the last 200, newest first (kind: "digest": the automatic digests sent, with their statistics). POST creates a draft; templateId takes a reusable text for what is not given; send is "now" or an ISO date to send in the same call (3 tokens). PATCH changes a draft only (subject, previewText, body + format, segmentId, null to remove). DELETE deletes (a scheduled campaign is unscheduled; while sending, 403).
stats come from the provider's receipts: delivered, opened, clicked, bounced, spam and unsubscribed fill in during the hours after the send.
POST /emails/campaigns/:id/send { "at": "2026-09-22T09:00:00+02:00" } → { "campaign": { "status": "scheduled", … } } (5 tokens; without "at": immediate send, status "sending")
POST /emails/campaigns/:id/cancel → { "campaign": { "status": "draft", … } }
POST /emails/campaigns/:id/test → { "sent": true, "to": "you@…", "error": null } (to the key's creator, subject prefixed [Test]; 3 tokens)
GET /emails/campaigns/:id/preview → { "subject": "…", "html": "…", "text": "…" } (full rendering, frame and unsubscribe footer included)
A send is irreversible as soon as the campaign turns sending: an assistant must show the preview and get the user's agreement before calling /send. The email.campaign.sent and email.campaign.failed webhooks report the end of the send.
A segment is a saved filter on the active members of the space, resolved at send time. Every filter given must match; users are added to the result.
POST { "name": "Silent newcomers", "description": "…",
"filters": { "roles": ["member"], "groups": ["cohort-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": { … } } (nothing saved; 3 tokens)
GET /emails/segments/:id → { "segment": { …, "memberCount": recomputed, "sample": […], "capped": false } } (3 tokens)
roles: owner, admin, moderator, teacher, contributor, member; groups accepts ids or slugs; activeWithinDays relies on the last time the app was opened. Fifty segments per space; beyond 5,000 members, capped turns true.
Reusable campaign texts (subject, preview text, body), passed as templateId when creating a campaign. POST { "name", "subject", "previewText", "body", "format" } → 201 { "template": { "id", "name", "subject", "previewText", "body", "updatedAt" } }. Fifty per space.
The wording of the automatic e-mails the space sends, per kind and language (kind: live_invitation for now; lang: fr, en, es). The frame (colour, button, calendar links, footer) stays the platform's.
GET → { "templates": [ { "kind": "live_invitation", "lang": "en", "subject": "…", "body": "…", "custom": false, "updatedAt": null }, … ],
"variables": { "live_invitation": [ { "key": "prenom", "label": { "fr", "en", "es" }, "sample": { … } }, … ] },
"defaults": { "live_invitation": { "en": { "subject", "body" }, … } } }
PUT /emails/notifications/live_invitation/en { "subject": "…", "body": "…", "format": "html" } → { "template": { …, "custom": true } }
DELETE /emails/notifications/live_invitation/en → default text restored
GET /emails/notifications/live_invitation/en/preview?subject=…&body=… → { "subject", "html" } (sample values; without parameters: the saved text)
Automatic digest of what is new (posts, courses, lessons, upcoming live sessions and events, challenges) sent to the members at the chosen cadence.
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": "…" } } (partial update)
GET /emails/digest/preview → { "subject", "html", "empty": false } (as it would go out now; 3 tokens)
POST /emails/digest/send { "test": true } → { "sent": 1, "skipped": false, "error": null } (test: to the key's creator; without "test": to the whole audience, 5 tokens)
cadence: daily, weekly (weekday 0 = Sunday … 6) or monthly (dayOfMonth 1 to 28); hour in the local time of timeZone; subject and intro accept {{prenom}} and {{espace}}, null restores the platform text.
Where the space's e-mails leave from. By default "via LearnFloo"; with the paid "sending domain" option, from the client's domain once its DNS records are verified. Enabling the option stays in the app (it is billed to the owner); everything else is driven here.
GET → { "option": true, "active": false, "configured": true, "defaultFrom": "invitations@learnfloo.com", "priceCents": 1000, "currency": "eur",
"sender": { "id": "…", "domain": "out.myschool.com", "fromEmail": "hello@out.myschool.com", "fromName": "My school", "replyTo": "hello@myschool.com",
"dns": [ { "type": "TXT", "host": "…_domainkey.out.myschool.com", "value": "k=rsa;…", "verified": false, "purpose": "dkim" },
{ "type": "CNAME", "host": "pm-bounces.out.myschool.com", "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.myschool.com", "fromEmail": "hello@out.myschool.com", "fromName": "My school", "replyTo": "hello@myschool.com" } → 201, same answer as GET
POST /emails/sender/check → { "dkimVerified": true, "returnPathVerified": true, …GET } (asks for the DNS check)
PATCH /emails/sender { "fromEmail", "fromName", "replyTo" }
POST /emails/sender/test → { "sent": true, "from": "…", "error": null } (to the key's creator; 3 tokens)
DELETE /emails/sender → { "removed": true } (back to "via LearnFloo")
active is true when e-mails really leave from the domain (option on, both records verified, no suspension). suspendedAt: sending from the domain stopped by the bounce and complaint rule, to be seen with support.
GET → { "user": { "id", "externalId", "name", "email", "image" }, "hasEmail": true, "subscribed": true, "unsubscribedAt": null, "unsubscribedFromCampaignId": null }
PUT { "subscribed": false } → same object, updated
A member's subscription to the space's bulk e-mails (campaigns and digest), to read or to mirror from your platform: setting subscribed to false equals a click on "unsubscribe" (email.unsubscribed webhook). Individual e-mails (live invitations) are not affected.
Rather than polling the API, get notified: LearnFloo sends a JSON POST to the URL of your choice for each subscribed event. Up to 10 webhooks per space.
POST https://your-backend.com/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": "my-space" },
"data": { "session": { … session object … } } }
| Event | data |
|---|---|
live.session.created, live.session.updated, live.session.started, live.session.ended, live.session.cancelled | { session } (see GET /live/sessions/:id). After ended, call /attendance for attendance |
live.replay.ready | { session } with replayUrl set (raw MP4, as soon as the live session ends) |
live.conversion | { session, conversion } — sign-up reported via /conversions |
live.poll.opened, live.poll.closed | { session, poll } — a poll or quiz launched, then closed with its results |
live.scene.changed | { session, scene } — a scene put on air (scene set to null: automatic layout) |
support.ticket.created | { ticket, author, message: { content, text, attachments } } |
support.ticket.message | { ticket, message: { id, content, text, attachments, fromStaff, author, createdAt } } — a reply from the user or the team (never internal notes) |
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 left in an email capture of a video (source: lesson, or about for the presentation video) |
post.created | { post } |
comment.created | { comment, post: { id, title } } |
member.joined | { user, member } — new member (joined from LearnFloo or created by the API) |
email.campaign.sent, email.campaign.failed | { campaign } — end of send of a campaign or a digest (kind), with recipientCount, stats.sent and error |
email.unsubscribed | { user, campaignId, unsubscribedAt } — a member unsubscribed from the space's bulk e-mails (unsubscribe link, page, or API) |
ping | Test delivery (POST /webhooks/:id/test) |
The user / author objects contain { id, externalId, name, email, image }: externalId lets you find the person on your side.
Reply 2xx within 10 seconds (process afterwards). Check the signature: HMAC-SHA256 of the text <t>.<raw body> with the webhook's secret, compared with v1; reject if t is more than 5 minutes old.
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))
}
On failure (non-2xx response, timeout, network error), delivery is retried after 1 min, 5 min, 30 min then 2 h (5 attempts). The same event may therefore arrive twice: use id (or X-LearnFloo-Delivery) to deduplicate. After 20 consecutive failures, the webhook is paused (active: false); reactivate it with PATCH once your endpoint is fixed.
| Call | Body / response |
|---|---|
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: ["*"] to receive everything. The secret (16 to 128 characters, generated if absent) is only returned at creation. https URL required |
GET/webhooks/:id | { "webhook": { … } } |
PATCH/webhooks/:id | { "url"?, "events"?, "description"?, "active"? }. active: true resets the failure counter |
DELETE/webhooks/:id | { "deleted": true } |
POST/webhooks/:id/test | Sends a ping event → 202 { "sent": true } |
GET/webhooks/:id/deliveries?limit=50 | Delivery log (30 days): { "deliveries": [ { "id", "event", "attempt", "status": "pending|success|failed", "responseStatus", "error", "createdAt", "deliveredAt" } ] } |
The OpenAPI 3.1 description of the API is generated from the MCP tool catalogue (one operation per route, operationId = MCP tool name), for client generators, API tools and the GPT Actions of ChatGPT:
GET https://api.learnfloo.com/openapi.json the whole API (159 operations), API key or OAuth security
GET https://api.learnfloo.com/openapi.json?preset=gpt the 30 operations of the LearnFloo GPT (ChatGPT's limit per action), OAuth only
GET https://api.learnfloo.com/openapi.json?tools=get_space,list_posts,create_post a subset of tools
GET https://api.learnfloo.com/openapi.json?groups=live,courses whole families
A custom GPT is built from this file and a confidential OAuth client (see MCP server); the steps and the instructions of the GPT are in the LearnFloo Agent Kit.
The same API is exposed as an MCP server (Model Context Protocol) for AI assistants and agents: Claude (app, claude.ai, Claude Code), ChatGPT, Cursor, or any MCP client. The assistant sees the space through named tools (list_live_sessions, create_post, get_group_stats…), each equivalent to a route of this reference: same fields, same responses, same errors.
URL: https://api.learnfloo.com/mcp (“Streamable HTTP” transport, JSON-RPC 2.0, stateless)
Two ways to connect, chosen by the user according to the level of security they want:
| OAuth (recommended) | API key | |
|---|---|---|
| Principle | The assistant opens a LearnFloo page: the user signs in with their account, chooses the space, the access level and the tools, and authorises. One-hour access tokens renewed automatically, revocable. | A key of the space (Authentication), in the Authorization: Bearer lf_live_… header or, for clients without headers, in the URL https://api.learnfloo.com/mcp?key=lf_live_…. |
| Who can | Owner or administrator of the space, with their own account | Anyone who holds the key |
| Actions attributed to | The user who authorised | The key's creator |
| Risk | No secret in the URL or in the client configuration | A key in the URL can end up in logs; if it leaks, revoke it |
| Where it's managed | Space settings → API and MCP: keys and OAuth authorisations side by side, each with its access level and its enabled tools | |
| Client | OAuth | API key |
|---|---|---|
| claude.ai, Claude app | Settings → Connectors → Add a custom connector, URL https://api.learnfloo.com/mcp, then “Connect”: the LearnFloo authorisation page opens | Same screen, URL https://api.learnfloo.com/mcp?key=lf_live_…, without authentication |
| Claude Code | claude mcp add --transport http learnfloo https://api.learnfloo.com/mcp then /mcp to authenticate | claude mcp add --transport http learnfloo https://api.learnfloo.com/mcp --header "Authorization: Bearer lf_live_…" |
| Cursor, Windsurf, others | { "mcpServers": { "learnfloo": { "url": "https://api.learnfloo.com/mcp" } } } (the client starts the authorisation) | { "mcpServers": { "learnfloo": { "url": "https://api.learnfloo.com/mcp", "headers": { "Authorization": "Bearer lf_live_…" } } } } |
OAuth 2.1 server: discovery /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, dynamic client registration POST /oauth/register (public clients), GET /oauth/authorize (PKCE S256 required), POST /oauth/token (authorization_code, refresh_token with rotation), POST /oauth/revoke. Scopes read and full; the level actually granted is the one chosen on the authorisation page, returned in scope. A 401 response from the MCP server carries WWW-Authenticate: Bearer resource_metadata=… so that the client discovers the OAuth server.
Confidential clients. For a ChatGPT GPT or a server-side integration, the LearnFloo team registers a client with a client_secret (client_secret_post or client_secret_basic, on top of PKCE) and wildcard redirect URLs (https://chatgpt.com/aip/*/oauth/callback). The access tokens obtained (lfo_…) are accepted by the MCP server and by the REST API (Authorization: Bearer lfo_…): on the REST API a grant opens only the routes of the tools chosen on the consent page (one tool = one route), 403 otherwise.
?key=) is visible in the client's history and logs: keep it for clients without headers when OAuth isn't wanted, create a dedicated key (named after the assistant) and revoke it at the slightest doubt. A read-only MCP key is enough to analyse, summarise and answer questions.Enabled tools. For each key (in the settings) and each OAuth authorisation (on the authorisation page, editable later in the settings), an access level (read only or full) and the list of enabled MCP tools: all the tools of the level (default), or a selection (for example, a production assistant that only sees a live session's scenes and polls; a support assistant that only sees tickets). tools/list only returns the enabled tools; a disabled tool is refused as unknown.
Rate and costs. Each JSON-RPC message uses the tokens of the underlying route (rate limit); initialize, ping and tools/list use one. An API error (400, 403, 404…) comes back as an isError result with its message; a refused key or a reached limit as a JSON-RPC error (HTTP 401 or 429, Retry-After). Returned objects are in structuredContent and, as text, in content.
What MCP doesn't do: file uploads (POST /attachments remains a REST route) and incoming webhooks. Write calls made by an assistant are attributed to the key's creator, unless an explicit author or user is given, as in REST.
Recipes (prompts) and assistant kit. The server also serves MCP prompts (prompts/list, prompts/get, optional request argument): nine job recipes for the owner of a space (space report, live preparation, control room, debrief, course building, feed animation, member follow-up, support tickets, integration), offered by the clients that list prompts (Claude, Cursor…) with nothing to install. The same recipes exist as skills (open Agent Skills format) and as a Claude Code plugin in the LearnFloo Agent Kit: claude plugin marketplace add learnfloo/agent-kit then claude plugin install learnfloo@learnfloo; install script for Codex, Cursor and Gemini CLI. Every recipe shows the content before any write and sticks to reading with a read-only key.
159 tools, by family. “Access” column: read (open to read-only keys) or write.
| Tool | Description | Access | 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. | read | 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. | read | GET /space/usage |
| Tool | Description | Access | Route |
|---|---|---|---|
list_users | Members of the space, paginated (200 max per page), with role, status, XP and level. | read | GET /users |
get_user | One user with membership (role, status, XP, level) and stats (lessons completed, lives joined, posts, badges). | read | GET /users/:id |
create_user | Creates (or updates, idempotent on externalId) a user of your platform and adds them as member of the space. | write | 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. | write | PATCH /users/:id |
remove_user | Removes the member from the space (account and history kept). | write | 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. | write | POST /users/:id/entry |
get_user_progress | Progress of a user in every course of the space (lessons completed, percentage). | read | GET /users/:id/progress |
get_user_xp | Total XP, level and latest XP events of a user in the space. | read | 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. | write | POST /users/:id/xp |
get_user_badges | Badges awarded to a user. | read | GET /users/:id/badges |
get_leaderboard | Members ranked by XP (500 max), optionally within one group. | read | GET /leaderboard |
list_badges | Badges of the space with their criteria and XP reward. | read | GET /badges |
list_challenges | Challenges of the space (objective, dates, status, participants). | read | GET /challenges |
get_challenge | One challenge with its participants and their progress. | read | GET /challenges/:id |
| Tool | Description | Access | Route |
|---|---|---|---|
list_live_sessions | Live sessions of the space, newest first (200 max), with status, dates, replay and counters. | read | GET /live/sessions |
get_live_session | One live session: status, schedule, participants now, recording and replay, chat mode, conversions, active scene. | read | 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. | write | 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. | write | PATCH /live/sessions/:id |
cancel_live_session | Cancels a scheduled live (calendar event removed). A running or ended live cannot be cancelled. | write | 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. | write | 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. | read | GET /live/sessions/:id/attendance |
get_live_participants | Instant state of the people registered on a live: connected, spectator, hand raised, role. | read | GET /live/sessions/:id/participants |
get_live_chat | Chat messages of a live, oldest first (2000 max), kept after the live. | read | GET /live/sessions/:id/chat |
get_live_replay | The replay of a live: MP4, HLS and embed URLs, thumbnail, duration, linked lesson, views. | read | 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. | write | POST /live/sessions/:id/conversions |
list_live_invites | Shareable invitation links of a live (speaker, viewer, assistant, moderator). | read | 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). | write | POST /live/sessions/:id/invites |
revoke_live_invite | Revokes an invitation link. | write | 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. | write | 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. | write | PUT /live/sessions/:id/viewers |
stage_chat_message | Shows a chat message on the scene (message slot, else bottom left); null removes it. | write | PUT /live/sessions/:id/chat/stage |
| Tool | Description | Access | 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. | read | GET /live/sessions/:id/scenes |
get_live_scene | One scene of a live. | read | 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. | write | 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. | write | PATCH /live/sessions/:id/scenes/:sceneId |
delete_scene | Removes a scene (automatic layout if it was on air). Returns the control room. | write | 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. | write | 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). | write | PUT /live/sessions/:id/scenes/active |
reorder_scenes | Sets the order of the scenes of a live. | write | 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. | write | 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). | write | 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. | write | 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. | write | 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. | write | 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. | write | PATCH /live/sessions/:id/overlays/:overlayId |
delete_overlay | Removes an overlay from a scene. | write | DELETE /live/sessions/:id/scenes/:sceneId/overlays/:overlayId |
add_slot | Adds a slot (host, guest, screen, media, embed) to a scene; position 0 = main. | write | POST /live/sessions/:id/scenes/:sceneId/slots |
update_slot | Changes a slot of a scene (other media, guest, fit, geometry in free layout, position). | write | PATCH /live/sessions/:id/scenes/:sceneId/slots/:slotId |
delete_slot | Removes a slot from a scene. | write | DELETE /live/sessions/:id/scenes/:sceneId/slots/:slotId |
list_scene_templates | Scene templates of the space, reusable in every live. | read | 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. | write | POST /scene-templates |
delete_scene_template | Deletes a scene template (id or name). | write | DELETE /scene-templates/:id |
| Tool | Description | Access | Route |
|---|---|---|---|
list_live_polls | Polls and quizzes of a live with their counts, kept after the live. | read | GET /live/sessions/:id/polls |
get_live_poll | One poll or quiz with its results. | read | 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. | write | POST /live/sessions/:id/polls |
update_poll | Changes a draft poll (same fields as creation). | write | PATCH /live/sessions/:id/polls/:pollId |
delete_poll | Deletes a poll and its votes, whatever its status. | write | DELETE /live/sessions/:id/polls/:pollId |
open_poll | Opens the vote to the participants (one poll open at a time, running live only). | write | POST /live/sessions/:id/polls/:pollId/open |
close_poll | Closes the vote; results shown unless showResults is false. | write | 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). | write | 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. | write | 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). | write | POST /live/sessions/:id/polls/:pollId/band |
| Tool | Description | Access | Route |
|---|---|---|---|
list_support_tickets | Tickets of one external user (externalId) or of the whole space (scope: "space", team dashboard), newest first. | read | 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. | read | 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. | write | 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. | write | 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). | write | POST /support/tickets/:id/status |
| Tool | Description | Access | Route |
|---|---|---|---|
list_groups | Groups of the space (classes, teams, cohorts… see groupLabels of get_space) with member counts and automatic rules. | read | GET /groups |
get_group | One group (id or slug) with its members and their group role (leader or member). | read | GET /groups/:id |
create_group | Creates a group, manual or automatic (rule: level_min, streak_days, challenge_joined, challenge_completed, badge, course_completed). | write | POST /groups |
update_group | Changes name, slug, description, color or rule (null = manual group) of a group. | write | PATCH /groups/:id |
delete_group | Deletes a group; the content addressed only to it becomes visible to the whole space. | write | DELETE /groups/:id |
add_group_members | Adds members of the space to a group (500 per call, idempotent), as member or leader. | write | POST /groups/:id/members |
remove_group_member | Removes one member from a group. | write | DELETE /groups/:id/members/:userId |
get_user_groups | Groups of a user in the space, with their group role. | read | GET /users/:id/groups |
set_user_groups | Replaces the whole list of groups of a user (sync of classes from your platform). | write | PUT /users/:id/groups |
get_group_leaderboard | Members of a group ranked by XP. | read | 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. | read | GET /groups/:id/stats |
list_group_documents | Shared documents of a group, newest first. | read | GET /groups/:id/documents |
add_group_document | Adds a document (file stored via POST /api/v1/attachments, or any https link) to a group. | write | POST /groups/:id/documents |
delete_group_document | Deletes a document of a group. | write | DELETE /groups/:id/documents/:docId |
| Tool | Description | Access | Route |
|---|---|---|---|
list_courses | Published courses of the space (includeUnpublished for drafts too), with lesson counts and audience. | read | GET /courses |
get_course | A course (id or slug) with its modules and lessons; withContent adds the HTML of the lessons. | read | GET /courses/:id |
create_course | Creates a course (draft by default). The author, when given, must be owner, admin or teacher. | write | POST /courses |
update_course | Changes title, description, cover, published state or audience of a course. | write | PATCH /courses/:id |
delete_course | Deletes a course with its modules, lessons and progress. | write | DELETE /courses/:id |
create_module | Adds a module (section) to a course. | write | 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). | write | POST /courses/:id/lessons |
get_lesson | One lesson with its content. | read | GET /lessons/:id |
update_lesson | Changes title, content, module, video or order of a lesson. | write | PATCH /lessons/:id |
delete_lesson | Deletes a lesson. | write | 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. | read | GET /courses/:id/progress |
complete_lesson | Marks a lesson as completed for a learner, as if validated in LearnFloo (XP, quests, badges, webhook). Idempotent. | write | POST /lessons/:id/complete |
| Tool | Description | Access | Route |
|---|---|---|---|
list_posts | Feed of the space, newest first, paginated; categories general, announcements, question, wins, resource. | read | GET /posts |
get_post | A post with its comments (oldest first, parentId for replies). | read | GET /posts/:id |
create_post | Creates a post in the feed (mentions, XP and notifications as in the app). pinned is for moderators. | write | POST /posts |
update_post | Changes title, category, content, pinned state or audience of a post. | write | PATCH /posts/:id |
delete_post | Deletes a post with its comments and reactions. | write | DELETE /posts/:id |
create_comment | Adds a comment (or a reply with parentCommentId) to a post. | write | POST /posts/:id/comments |
delete_comment | Deletes a comment and its replies. | write | DELETE /comments/:id |
| Tool | Description | Access | Route |
|---|---|---|---|
list_events | Calendar events by start date (scheduled lives included with liveSessionId). | read | GET /events |
get_event | One calendar event. | read | GET /events/:id |
create_event | Creates a calendar event (lives are created with create_live_session). | write | POST /events |
update_event | Changes an event (an event linked to a live is changed with update_live_session). | write | PATCH /events/:id |
delete_event | Deletes a calendar event. | write | DELETE /events/:id |
| Tool | Description | Access | Route |
|---|---|---|---|
list_videos | Studio recordings and imported live replays, newest first, paginated. | read | GET /videos |
get_video | One video with its playback URLs (ready videos only), duration, thumbnail, linked lesson or live. | read | GET /videos/:id |
update_video | Changes the title or description of a video. | write | PATCH /videos/:id |
| Tool | Description | Access | Route |
|---|---|---|---|
list_media | Images, PDF, videos, embeds, stickers and sounds of the space used in scenes and the studio. | read | GET /media |
list_media_folders | Folders of the media library. | read | GET /media/folders |
get_media | One item of the media library. | read | 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). | write | POST /media |
create_media_folder | Creates a folder in the media library. | write | POST /media/folders |
update_media | Changes the title or the folder (null for the root) of a media item. | write | PATCH /media/:id |
delete_media | Deletes a media item (and its file when hosted by LearnFloo). | write | DELETE /media/:id |
| Tool | Description | Access | Route |
|---|---|---|---|
list_webhooks | Outgoing webhooks of the space with their events and delivery state. | read | GET /webhooks |
get_webhook | One webhook. | read | GET /webhooks/:id |
create_webhook | Subscribes an https URL to events (["*"] for all). The signing secret is returned once. | write | POST /webhooks |
update_webhook | Changes url, events, description or active state (active: true resets the failure count). | write | PATCH /webhooks/:id |
delete_webhook | Deletes a webhook. | write | DELETE /webhooks/:id |
test_webhook | Sends a ping event to a webhook. | write | POST /webhooks/:id/test |
list_webhook_deliveries | Delivery log of a webhook over 30 days (attempts, status, errors). | read | GET /webhooks/:id/deliveries |
| Tool | Description | Access | 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. | read | GET /emails/campaigns |
get_email_campaign | One campaign with its content, audience and statistics. | read | 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. | read | 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. | write | POST /emails/campaigns |
update_email_campaign | Changes a draft (subject, preview text, body, audience). Scheduled or sent campaigns cannot be modified. | write | 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. | write | POST /emails/campaigns/:id/send |
cancel_email_campaign | A scheduled campaign goes back to draft. | write | POST /emails/campaigns/:id/cancel |
test_email_campaign | Sends the campaign to the creator of the API key only, subject prefixed with [Test]. | write | POST /emails/campaigns/:id/test |
delete_email_campaign | Deletes a campaign (a scheduled one is unscheduled; a campaign being sent cannot be deleted). | write | 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. | read | GET /emails/segments |
get_email_segment | A segment with its member count computed now and a sample of names. Costs 3 tokens. | read | 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. | read | POST /emails/segments/preview |
create_email_segment | Saves an audience for campaigns and the digest. | write | POST /emails/segments |
update_email_segment | Changes the name, description or filters of a segment. | write | PATCH /emails/segments/:id |
delete_email_segment | Deletes a segment (campaigns keep their history). | write | DELETE /emails/segments/:id |
list_email_templates | Reusable campaign texts of the space (subject, preview text, body). | read | GET /emails/templates |
create_email_template | Saves a campaign text to start future campaigns from (templateId of create_email_campaign). | write | POST /emails/templates |
update_email_template | Changes a reusable campaign text. | write | PATCH /emails/templates/:id |
delete_email_template | Deletes a reusable campaign text. | write | 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. | read | 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. | read | 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). | write | PUT /emails/notifications/:kind/:lang |
reset_email_notification | Back to the platform’s default wording for that kind and language. | write | 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. | read | 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. | read | 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. | write | 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. | write | 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. | read | 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. | write | POST /emails/sender |
update_email_sender | Changes the display name, sending address (on the domain) or reply address. | write | PATCH /emails/sender |
check_email_sender_domain | Asks the mail provider to look the DKIM and Return-Path records up and records the result. | write | POST /emails/sender/check |
test_email_sender | Sends the key creator a test e-mail with the space’s current sender. | write | POST /emails/sender/test |
remove_email_sender_domain | Removes the sending domain: e-mails go back to leaving via LearnFloo. | write | DELETE /emails/sender |
get_email_preferences | Whether a member still receives the space’s bulk e-mails (campaigns, digest). | read | 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). | write | PUT /users/:id/email-preferences |
Directory of public spaces, without a key or rate limit, CORS open (usable from a browser). Full URL: https://api.learnfloo.com/public/spaces. Response cached for 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" } ] }
About page of a public space: the card above plus space.about (HTML), space.video (introduction video: embedUrl, playbackUrl, thumbnailUrl), owner (name, image), adminCount, the last 30 reviews (rating, text, createdAt, author) and courses, the space's open-access courses (slug, title, description, coverImage, lessonCount, url of the public page). 404 if the space doesn't exist or is private.
Open-access courses. In a public space, a published course open to every member can be marked “readable on the web” by its author (course page → Edit), or every course of the space at once (Settings → Access and About page): it is then rendered, with its lessons and videos, on www.learnfloo.com/en/c/<slug>/courses/<course>/ (and /c/…/cours/…, /es/c/…/cursos/…), read only, with a button to join the space; these pages are listed in www.learnfloo.com/c/sitemap.xml.
These two endpoints feed the pages www.learnfloo.com/en/communities/ and www.learnfloo.com/en/c/<slug>/. Joining a space always happens in the app (appUrl, ?join=1 parameter); for an LMS, use the /users/:id/entry SSO instead.
{ "error": "message" }
| Code | Case |
|---|---|
400 | Invalid body, missing field, session over (Session is over), unknown session, host not allowed, business rule not met |
401 | Key missing, malformed or revoked |
403 | Action forbidden to the chosen author (insufficient role), space owner |
404 | Unknown path, unknown resource or one belonging to another space, ticket belonging to another user |
405 | Method not supported on this path |
413, 415 | Attachment too large or of an unsupported type |
429 | Rate limit reached: respect Retry-After |
On the learner's side, an expired or already used entry URL shows an explicit LearnFloo page; a new click in your platform (new /live/entry or /users/:id/entry call) is enough.
New tab (recommended, especially for Safari):
const { url } = await fetch('/my-backend/live-entry', { method: 'POST' }).then((r) => r.json())
window.open(url, '_blank')
Iframe:
<iframe src="URL RETURNED BY /live/entry"
allow="camera; microphone; display-capture; autoplay; fullscreen" allowfullscreen
style="width:100%;height:80vh;border:0"></iframe>
allow attribute is essential: without it, the browser refuses mic, camera and screen sharing in an iframe from another domain. Safari and Firefox in strict mode may block storage in a third-party iframe; switch to a new tab in that case.On request, the live room is served under a subdomain of yours (e.g. live.your-domain.com). Only one action on your side: a DNS CNAME record from the subdomain to learnfloo-v2.b-cdn.net, then let LearnFloo know. After that, the /live/entry URLs are on your domain, with no change in your code. Only the live pages are served there; the /users/:id/entry URLs and invitation links stay on 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
}
// On the “Join the live session” click
export const liveEntry = (sessionId, learner) =>
call('POST', '/live/entry', { sessionId, user: { externalId: String(learner.id), name: learner.fullName }, role: 'viewer' }).then((d) => d.url)
// On the “Open the community” click
export const spaceEntry = (learner) =>
call('POST', `/users/ext%3A${encodeURIComponent(learner.id)}/entry`, { name: learner.fullName, redirect: 'posts' }).then((d) => d.url)
// After the live session: replay and validated attendance
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) }
}
// Once: get notified when live sessions end and replays are ready
export const subscribe = () =>
call('POST', '/webhooks', { url: 'https://your-backend.com/learnfloo', events: ['live.session.ended', 'live.replay.ready', 'lesson.completed'] })
| Date | Change |
|---|---|
| 2026-09-16 | Open-access courses (SEO): a course of a public space marked “readable on the web” is rendered on www.learnfloo.com/en/c/<slug>/courses/<course>/ with its lessons, in three languages, and listed in /c/sitemap.xml; GET /public/spaces/:slug returns these courses in courses. |
| 2026-09-15 | E-mails: the whole e-mail layer is available through the API and MCP ("E-mails" family, 35 tools): campaigns (creation, preview, test, immediate or scheduled send, cancellation, statistics), segments and audience count, reusable texts, automatic e-mails per kind and language, automatic digest (settings, preview, send), sending domain (add, DNS, check, test, remove), a member's e-mail preferences; email.campaign.sent, email.campaign.failed, email.unsubscribed webhooks. |
| 2026-09-15 | Waiting room and closing screen: in the app (“Prepare” page, “Waiting & closing” tab), countdown, welcome message, image or teaser video, pre-live quiz; offer with a tracked button (link carrying lf_live, lf_user, lf_ext like the chat CTAs, sign-ups reported through /conversions), shown to the audience during the live and on the closing screen, offer countdown; closing poll. API: phase (live, waiting, closing) on polls, at creation, update and read. |
| 2026-09-15 | OpenAPI 3.1 generated from the MCP catalogue (GET /openapi.json, ?preset=gpt, ?tools=, ?groups=); confidential OAuth clients (secret + PKCE, wildcard redirect URLs); OAuth tokens accepted by the REST API, limited to the tools chosen at consent; “LearnFloo” GPT for ChatGPT (instructions and steps in the kit). |
| 2026-09-15 | MCP: prompts (job recipes, prompts/list and prompts/get, request argument) and LearnFloo Agent Kit (Claude Code plugin, skills for Codex, Cursor and Gemini CLI). Server version 1.1.0. |
| 2026-09-13 | Scenes: jingle field (plays its duration, then the interrupted scene resumes where it was). |
| 2026-09-13 | Scenes: auto-advance waits for the end of the videos and sounds started with the scene (not looped); mediaSec field (file length in seconds) on video slots and in audios. |
| 2026-09-13 | Scenes: several sounds per scene, audios field (list, 6 max); the older audio field is still accepted on write, scenes now return audios. |
| 2026-09-13 | Scenes: camera look, shape, filter, bgMode, bgBlur, bgImage fields (background applied on the person's device); volume and muted also apply to cameras (sound on the air). |
| 2026-09-13 | Scenes: framed field (margin around the slots, the background stays visible), on creation and update. |
| 2026-09-13 | Video player: video.lead webhook (email left in a video); GET /groups/:id/stats returns videoPct per member (share of the group's lesson videos actually watched) and avgVideoPct, videoLessonCount in summary. |
| 2026-09-13 | Live sessions: replay rule. replay (all, attendees, groups, level, none), replayGroups and replayMinLevel fields on POST / PATCH /live/sessions, returned as replay, replayGroupIds, replayMinLevel. PATCH also takes mode and works during and after the live session for the title, description, groups and replay. |
| 2026-09-12 | Single pay-as-you-go rate aligned with the public price list (www.learnfloo.com): GET /space/usage also returns simulcastHours (simulcast hours × destinations) and subtitleHours (subtitled hours, counted once generation moves to the new engine). |
| 2026-09-12 | MCP: OAuth 2.1 connection (/.well-known/oauth-authorization-server, /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke): the user authorises the assistant on a LearnFloo page, choosing the space, the access level and the tools; the authorisation is shown and revoked in the settings next to the keys. An alternative to the key in the URL, at the user's choice. |
| 2026-09-12 | MCP server https://api.learnfloo.com/mcp for AI assistants (Claude, ChatGPT, Cursor…), with the same keys: one tool per API route. Keys with an access level (read only or full: a write with a read-only key returns 403) and a list of enabled MCP tools set per key in the space settings. Existing keys stay on full access, all tools. |
| 2026-09-12 | Scenes: auto-advance. durationSec and nextSceneId fields on scenes, PUT /live/sessions/:id/scenes/auto { enabled, loop }, autoAdvance object in the production desk. Each automatic change sends live.scene.changed like a manual switch. |
| 2026-09-11 | Scenes: viewers widget (viewer counter) and PUT /live/sessions/:id/viewers to add viewers to it; extraViewers in the session object. |
| 2026-09-11 | Scenes: style of the poll and message slots (colours, background opacity, text size, rounding, border, shadow, font, items shown). |
| 2026-09-11 | Scenes: video options autoplay, once, startSec, durationSec, volume; startSec and durationSec also on the scene sound (audio). |
| 2026-09-11 | Scenes: transitions fade, black, slide-*, zoom, wipe, blur and duration transitionMs. |
| 2026-09-11 | Scenes: audio field (scene sound: audio file from the media library or URL, autoplay, once, loop, volume), controlled with PUT …/media/:mediaId. Media library: kind audio. |
| 2026-09-11 | Polls and quizzes: /live/sessions/:id/polls (prepare, edit, delete), …/open, …/close, PUT …/results, …/band; kind: "quiz" with correct. Results kept after the live session. Webhooks live.poll.opened and live.poll.closed. In the app: “Polls” tab of the live session and of “Prepare”, for the host, the staff and the moderator and assistant roles. On air: poll and message slots in scenes (prepared in advance), PUT …/polls/:pollId/stage and PUT …/chat/stage to show a poll or a chat message in the video. |
| 2026-09-11 | Live sessions: moderator role (chat moderation, prepared messages, private messages) accepted by /invites and PUT …/participants/:userId/role. Messages deleted by a moderator and private messages don't appear in GET …/chat. |
| 2026-09-11 | Scenes: locked field (padlock); a locked scene or template refuses edits and deletion until { "locked": false }. |
| 2026-09-11 | Scenes and production via the API: a live session is run like a studio. /live/sessions/:id/scenes (list, create, edit, delete, order), putting on air (…/activate, PUT …/scenes/active), stickers, banners and slots editable one by one (…/overlays/:overlayId, …/slots/:slotId, and PATCH /live/sessions/:id/overlays/:overlayId for the same banner in every scene), temporary banners POST …/bands, control of PDFs and videos PUT …/media/:mediaId, templates /scene-templates, a participant's role PUT …/participants/:userId/role (including assistant, the production crew, also accepted by /invites). Media library: POST /media by URL, GET, PATCH, DELETE /media/:id, folders /media/folders. Webhook live.scene.changed; session gains activeSceneId and sceneCount. |
| 2026-09-10 | Single offer: the Free / Starter / Growth / Scale / Event grid goes away. GET /space/usage now returns the six pay-as-you-go units (units), the allowances, usageCents, the account cap and liveAllowed; plan, planLabel, eventCredits and the quotas are no longer returned, nor is plan in GET /space. The rate limit depends on the owner's account (60 or 300 requests per minute). |
| 2026-09-10 | Currencies: paid spaces carry priceCurrency (eur or usd) in GET /space, GET /public/spaces and GET /public/spaces/:slug; price is formatted in the currency. POST /live/sessions/:id/conversions accepts currency next to amountCents (returned on the conversion and in the live.conversion webhook). |
| 2026-09-10 | Sign-ups during the live session: POST /live/sessions/:id/conversions to report a purchase made from the chat (announcement in the chat, banner on the scene, counter), lf_live, lf_user, lf_ext parameters added to button links, live.conversion webhook, conversionCount and ctaClickCount fields on the session, kind on chat messages. |
| 2026-09-10 | Live chat: the host's messages can carry imageUrl and link (call-to-action button) in GET /live/sessions/:id/chat. |
| 2026-09-10 | Live sessions: chat field (open, closed, off) at creation, update and read, to open the chat on arrival or disable it. Can be changed during the live session by the host (live.session.updated webhook). |
| 2026-09-10 | Groups (classes, teams, cohorts… wording per space): /groups (list, create, edit, delete), /groups/:id/members, GET and PUT /users/:id/groups to sync classes from an LMS. Per group: /groups/:id/leaderboard, member follow-up /groups/:id/stats, shared documents /groups/:id/documents; /leaderboard?group=. Automatic groups: rule field (level reached, day streak, challenge joined or finished, badge, course finished). Audience: groups field when creating and editing courses, posts, events and live sessions (groupIds when reading), ?group= filter on their lists. GET /space gains groupCount and groupLabels. |
| 2026-09-10 | Public or private, free or paid spaces: GET /space gains priceCents, priceInterval, category, aboutUrl, rating, reviewCount; accessType is now free or paid. New endpoints without a key: GET /public/spaces (directory) and GET /public/spaces/:slug (About page, reviews). Members of a paid space who cancel move to status: "expired" in /users. |
| 2026-09-09 | Rate limit per key according to the plan (X-RateLimit-* headers, 429 + Retry-After). Signed outgoing webhooks (/webhooks, 13 events, retries). Live sessions: PATCH and DELETE /live/sessions/:id, /participants, /chat, /replay, invitation links /invites; session gains maxParticipants, replayViews, hlsStatus, hostId, createdAt. Support: scope=space (all tickets), replies and statuses on the team side (staff), generic /attachments. Space: richer GET /space, /space/usage. Members: /users (list, create, edit, remove), SSO /users/:id/entry, /users/:id/progress, XP (GET and POST /users/:id/xp), badges, /leaderboard, /challenges. Courses: /courses, modules, lessons, /courses/:id/progress, /lessons/:id/complete. Feed: /posts and comments. Calendar: /events. Studio videos: /videos. Media library: /media. Cursor pagination; 405 on unknown method. The /entry and /sessions paths are now documented under /live/… (unchanged). |
| 2026-09-07 | Webinars: the audience watches an HLS stream (10 to 20 s latency) and no longer connects to the room; a viewer can ask to speak and joins the room when the host accepts. summary of /sessions/:id/attendance: spectatorHours and interactiveHours added. |
| 2026-09-07 | Support: /support/tickets (GET, POST), /support/tickets/:id, …/messages, …/status, /support/attachments, to open and follow tickets on behalf of an external user (WordPress plugin). GET /space to test a key. |
| 2026-09-07 | GET /sessions/:id/attendance: summary added and, per participant, source, leftAt, watchSec, watchPct, connections (replaces lastSeenAt). Replay available as soon as the live session ends, re-encoded afterwards. This page goes online. |
| 2026-09-06 | Errors returned as clean JSON { "error" }. Custom domain per space for the live pages. API served on api.learnfloo.com. |
| 2026-09-05 | First version: /entry, /sessions (POST, GET), /sessions/:id, /sessions/:id/attendance. |
Questions: the LearnFloo team.