Directos, soporte, miembros, grupos (clases), cursos, muro, calendario, vídeos y webhooks de LearnFloo en una plataforma externa (LMS, WordPress, CRM) · Referencia v1 · actualizado el 2026-09-16
URL base: https://api.learnfloo.com/api/v1. Las rutas siguientes son relativas a esta base: /live/… para los directos, /support/… para los tickets, /users, /courses, /posts, /events, /videos, /webhooks…
Cada llamada lleva una clave API proporcionada por el administrador del espacio LearnFloo (Configuración del espacio → Integración LMS):
Authorization: Bearer lf_live_xxxxxxxx
401). Una clave da acceso a todo el espacio, con los permisos de un administrador, según el nivel de acceso elegido al crearla: acceso completo o solo lectura (solo las rutas GET y las herramientas MCP de lectura; una escritura devuelve 403 Read-only API key). El nivel se puede cambiar en cualquier momento en la configuración del espacio.Cada clave dispone de un cubo de tokens que se rellena de forma continua según la cuenta del propietario del espacio y que contiene como máximo un minuto de peticiones (ráfagas posibles hasta ese tope):
| Cuenta | Peticiones por minuto y por clave |
|---|---|
| Gratuita (facturación no activada) | 60 |
| Facturación activa | 300 |
Una petición consume 1 token, salvo los informes, que consumen más: /live/sessions/:id/attendance, /courses/:id/progress y /space/usage cuestan 5 tokens, /users/:id/progress 3 tokens. Cada respuesta indica el estado del cubo:
X-RateLimit-Limit: 120 # tokens por minuto
X-RateLimit-Remaining: 117 # tokens disponibles ahora mismo
X-RateLimit-Reset: 2 # segundos hasta que el cubo vuelva a estar lleno
Por encima, la respuesta es 429 { "error": "Rate limit exceeded" } con una cabecera Retry-After (segundos). Espera ese tiempo antes de reintentar; no hagas bucles. El contador se reparte en varios compartimentos: X-RateLimit-Remaining es una estimación, y una ráfaga muy cercana al tope puede provocar un 429 un poco antes. Con mucha concurrencia (decenas de llamadas estrictamente simultáneas con la misma clave), también puede llegar una respuesta 503 Too many concurrent requests, retry con Retry-After: mismo tratamiento, reintenta tras el plazo. Para seguir la actividad del espacio, usa los webhooks en lugar de consultas repetidas. ¿Necesitas más? Crea varias claves (una por uso) o contacta con el equipo de LearnFloo.
?limit=50&cursor=…; la respuesta contiene nextCursor (null en la última página). Una página filtrada (rol, categoría…) puede contener menos elementos que limit: continúa mientras nextCursor no sea null.id) o, para los que has creado tú, por ext:<externalId> (codificado en la URL: /users/ext%3Alms-42). Cada objeto de usuario devuelto contiene externalId (null para un miembro llegado desde LearnFloo).author: { "email": "formador@cliente.com" } para un miembro existente de LearnFloo, o { "externalId": "lms-42", "name": "Alice" } para un usuario de tu plataforma (creado y añadido como miembro en el primer uso). Sin author, la acción la realiza el creador de la clave.content) son HTML (el del editor de LearnFloo). Envía texto plano por defecto: se convierte en párrafos. Pasa "format": "html" para enviar HTML. En lectura, content es el HTML y text una versión de texto truncada.GET lectura, POST creación o acción, PATCH modificación parcial (solo cambian los campos presentes), DELETE eliminación o cancelación.Alumno Tu plataforma LearnFloo
│ hace clic en «Unirse» │ │
│────────────────────────────▶│ POST /live/entry │
│ │─────────────────────────────────▶│ crea o encuentra al alumno,
│ │ { url, expiresAt } │ prepara un ticket de un solo uso
│ abre url (iframe/pestaña) │◀─────────────────────────────────│
│◀────────────────────────────│ │
│ GET url ────────────────────────────────────────────────────▶ │ conexión automática,
│ ◀──────────────────────────────────────────────────────────── │ sala del directo
/live/entry es personal, de un solo uso y válida 15 minutos. Pídela en el momento del clic.externalId): LearnFloo crea una cuenta de invitado la primera vez y la reutiliza después.URL de entrada personal para un alumno.
{
"sessionId": "p97…",
"user": { "externalId": "lms-user-42", "name": "Alice Martin" },
"role": "viewer"
}
| Campo | Descripción |
|---|---|
sessionId | Identificador del directo (ver GET /live/sessions) |
user.externalId | Identificador estable del alumno en tu plataforma, 200 caracteres máx. |
user.name | Nombre mostrado a los demás participantes, actualizado en cada llamada |
role | viewer (por defecto: mira, chatea, el formador puede darle la palabra) o speaker (micro, cámara, compartir pantalla desde la entrada). Un alumno promovido por el formador no vuelve a viewer con una llamada viewer. |
Respuesta 200:
{
"url": "https://app.learnfloo.com/live/enter/4tcgzHpN…",
"expiresAt": "2026-09-05T20:43:07.570Z",
"role": "viewer",
"sessionId": "p97…",
"userId": "k97…"
}
Si el directo no ha empezado, el alumno espera en la sala de espera y entra automáticamente al inicio. Si el espacio tiene un dominio personalizado, url está en ese dominio.
Errores: 400 Session is over (directo terminado o cancelado), Session not found, user.externalId is required, role must be "viewer" or "speaker".
Crea un directo en el espacio de la clave.
{
"title": "Módulo 3 — Preguntas y respuestas",
"description": "opcional",
"mode": "broadcast",
"scheduledAt": "2026-09-12T14:00:00Z",
"durationMin": 60,
"recordingEnabled": true,
"chat": "open",
"hostEmail": "formador@cliente.com",
"groups": ["terminale-a"],
"replay": "attendees"
}
| Campo | Descripción |
|---|---|
title | Obligatorio |
mode | broadcast (webinar: solo publican el anfitrión y los ponentes) o conference (todos publican). Por defecto broadcast |
scheduledAt | Fecha ISO 8601 o marca de tiempo en milisegundos. Obligatorio |
durationMin | Duración prevista, para el calendario. Por defecto 60 |
maxParticipants | De 2 a 1000. Por defecto 100 |
recordingEnabled | Grabación automática. Por defecto true |
chat | Chat del directo: closed (disponible, panel plegado al entrar, por defecto), open (panel abierto al entrar) o off (desactivado: no se acepta ningún mensaje). El anfitrión puede cambiarlo durante el directo |
hostEmail | Opcional. Cuenta de LearnFloo del formador, miembro del espacio con el rol owner, admin, moderator o teacher. Por defecto, el creador de la clave |
groups | Opcional. Grupos (ids o slugs) que ven el directo y pueden unirse; vacío = todo el espacio. Los tickets /live/entry y las invitaciones no se ven afectados. En lectura: groupIds |
replay | Opcional. Quién ve la repetición (el anfitrión y el equipo del espacio siempre la ven): all (por defecto: todos los que tienen acceso al directo), attendees (solo los asistentes, en la sala o como espectadores), groups (miembros de los grupos de replayGroups), level (miembros de nivel replayMinLevel o superior) o none (sin repetición para los miembros). A un miembro sin acceso, la app no le envía la URL y muestra «Repetición reservada» |
replayGroups | Con replay: "groups": ids o slugs de los grupos, al menos uno |
replayMinLevel | Con replay: "level": nivel mínimo, de 2 a 10 |
Respuesta 201:
{ "session": { … ver GET /live/sessions/:id … }, "hostUrl": "https://app.learnfloo.com/<espacio>/live/<id>" }
hostUrl es la página donde el formador lanza el directo, con su cuenta de LearnFloo.
Sesiones del espacio, las más recientes primero, 200 máx. Filtros opcionales ?status=scheduled|live|ended|cancelled y ?group=<id o slug>.
{ "sessions": [ { … }, … ] }
{
"session": {
"id": "p97…",
"title": "Módulo 3 — Preguntas y respuestas",
"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"
}
}
| Campo | Descripción |
|---|---|
status | scheduled, live, ended, cancelled. Útil para mostrar «Pronto», «Unirse» o «Repetición» |
participantCount | Personas conectadas en este momento (0 fuera del directo) |
recordingStatus | recording, processing, ready, failed o null |
replayUrl | MP4 de la repetición cuando recordingStatus vale ready, si no null. Disponible en cuanto termina el directo; el archivo se recodifica en segundo plano y la URL puede cambiar unos minutos después. Ver también /replay |
hlsStatus | Flujo de audiencia del webinar: starting, live, ended, failed o null |
chat | open, closed o off, ver POST |
conversionCount, ctaClickCount | Inscripciones notificadas durante el directo y clics en los botones del chat, ver /conversions |
groupIds | Grupos a los que está reservado el directo (vacío = todo el espacio) |
replay, replayGroupIds, replayMinLevel | Quién ve la repetición, ver POST. replayUrl siempre se devuelve a la clave: si la plataforma externa muestra la repetición por su cuenta, es ella la que aplica la regla |
Referer: insértalo en una página (etiqueta <video>), no lo abras como enlace directo. Para enterarte del final del directo y de la repetición, suscribe un webhook a los eventos live.session.ended y live.replay.ready.PATCH modifica una sesión. Antes del directo: title, description, mode, scheduledAt, durationMin, maxParticipants, recordingEnabled, chat, groups, replay, replayGroups, replayMinLevel; el evento del calendario se actualiza. Durante el directo: todo salvo la fecha, la duración y el formato. Después: title, description, groups y los campos de la repetición (para abrir o restringir una repetición a posteriori). Un campo que ya no se puede modificar devuelve 400. Respuesta 200 { "session": { … } }.
DELETE cancela una sesión programada (estado cancelled, evento retirado del calendario, crédito de evento devuelto). Respuesta 200 { "session": { … } }. Un directo en curso o terminado devuelve 400 Only scheduled sessions can be cancelled.
Asistencia, tiempo de visionado y audiencia. Durante un directo en curso, se calcula hasta el momento de la llamada.
{
"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…" }
]
}
| Campo | Descripción |
|---|---|
externalId | Tu identificador enviado en /live/entry. null para las personas llegadas directamente desde LearnFloo |
source | lms (entró por tu plataforma), invite (enlace de invitación), member (miembro de LearnFloo) |
role | host, speaker, viewer |
joinedAt, leftAt | Primera entrada y última salida. leftAt es null mientras la persona está en la sala |
watchSec | Tiempo real pasado en la sala, sumando todas las conexiones, limitado a la duración del directo |
watchPct | Parte del directo seguida, de 0 a 100. El campo que hay que usar para validar una asistencia (ej.: >= 80) |
connections | Número de entradas en la sala (salir y volver = 2) |
summary: duración del directo, asistentes, pico de audiencia y su hora, asistencia media (espectadores simultáneos), visionado medio, personas que se quedaron más de la mitad del directo, aperturas de la repetición. spectatorHours suma el tiempo de la audiencia en el flujo HLS del webinar, interactiveHours el tiempo de las personas conectadas a la sala (anfitrión, ponentes, turnos de palabra): son las dos unidades de facturación.
Estado instantáneo de las personas inscritas en la sesión (ligero, para un panel durante el directo; las estadísticas están en /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: mira el flujo HLS del webinar (sin estar en la sala WebRTC). handRaisedAt: ha pedido la palabra.
Mensajes del chat conservados después del directo, del más antiguo al más reciente (?limit=500, 2000 máx.).
{ "session": { … }, "messages": [ { "id": "…", "userId": "k97…", "externalId": "lms-user-42", "name": "Alice Martin", "text": "¡Hola!", "imageUrl": null, "link": null, "sentAt": "…" } ] }
Los mensajes del anfitrión pueden llevar una imagen (imageUrl) y un botón de llamada a la acción (link: { "url", "label" }), por ejemplo un enlace de inscripción enviado durante el directo. kind vale message o conversion (anuncio automático de una inscripción).
Tu LMS notifica que un espectador acaba de inscribirse o de comprar durante el directo. LearnFloo registra la conversión, actualiza el contador mostrado bajo los botones del chat, publica el anuncio en el chat («🎉 Marie acaba de unirse a la formación», texto y anonimato definidos por el anfitrión), muestra un rótulo en la escena y emite el webhook live.conversion.
{
"externalId": "lms-user-42",
"name": "Marie Dupont",
"label": "Curso de SEO avanzado",
"amountCents": 49900,
"currency": "eur"
}
| Campo | Descripción |
|---|---|
externalId | Identificador del alumno en tu LMS (el de /users). Se encuentra en el enlace del botón, ver más abajo |
userId | O el identificador de LearnFloo (lf_user del enlace) |
email | O el email de la cuenta de LearnFloo |
name | Opcional, nombre mostrado en el anuncio (si no, el nombre de la cuenta encontrada; si no, anuncio anónimo) |
label, amountCents, currency | Opcionales, para el balance (producto comprado, importe en la unidad más pequeña, moneda eur por defecto o usd) |
Ningún identificador es obligatorio: sin identificador ni nombre, el anuncio es anónimo («🎉 Nueva inscripción»). Respuesta 201 { "conversion": { "id", "userId", "externalId", "name", "label", "amountCents", "currency", "createdAt" }, "session": { … } }.
Encontrar al espectador. Cuando el anfitrión envía un botón de llamada a la acción en el chat, LearnFloo añade tres parámetros al enlace: lf_live (id del directo), lf_user (id de LearnFloo del espectador) y lf_ext (su identificador en tu LMS si se creó mediante /users). Consérvalos en tu página de venta (campo oculto, cookie) y reenvíalos en el pago: lf_live da el :id de la llamada, lf_ext o lf_user identifica al comprador. Los clics en estos botones se cuentan (ctaClickCount en la sesión) y las conversiones aparecen en el balance del directo.
La repetición en todas sus formas.
{ "replay": {
"sessionId": "p97…", "status": "ready",
"replayUrl": "https://…/play_1080p.mp4",
"hlsUrl": "https://…/playlist.m3u8", "embedUrl": "https://iframe.mediadelivery.net/embed/…",
"thumbnailUrl": "https://…/thumbnail.jpg", "durationSec": 3540,
"videoId": "v12…", "lessonId": null, "replayViews": 12
} }
replayUrl está disponible en cuanto termina el directo (MP4 bruto); hlsUrl, embedUrl, thumbnailUrl y durationSec llegan unos minutos después, cuando se codifica la copia adaptativa (null antes). lessonId se rellena si el formador publicó la repetición como lección de un curso.
Enlaces de invitación para personas que no tienen cuenta en tu plataforma (ponente externo, invitado): un enlace compartible y reutilizable, sin ticket personal.
POST { "role": "speaker", "label": "Ponente invitada", "expiresInHours": 48, "maxUses": 1 }
→ 201 { "invite": { "id": "…", "token": "…", "url": "https://app.learnfloo.com/invite/…", "role": "speaker", "label": "Ponente invitada",
"uses": 0, "maxUses": 1, "expiresAt": "…", "revoked": false, "active": true, "createdAt": "…" } }
role: viewer (por defecto), speaker, assistant (realización: prepara y cambia las escenas, lanza las encuestas, sin cámara ni micro) o moderator (modera el chat: eliminación de mensajes, silencio, ajustes del chat, mensajes preparados, mensajes privados y encuestas, sin cámara ni micro). expiresInHours y maxUses son opcionales (sin límite por defecto). 20 enlaces activos máx. por sesión. DELETE revoca el enlace (respuesta 200 con la invitación).
Un directo de LearnFloo se dirige como un estudio: escenas con nombre (disposición, cámaras, pantalla compartida, imágenes, PDF, vídeos, insertados, stickers y rótulos) que el presentador o la realización cambian con un clic. Todo lo que la realización hace en la aplicación también se hace por la API, en directo: crear una escena durante el directo, ponerla en antena, cambiar el texto de un rótulo, mostrar un anuncio, pasar la página de un PDF, lanzar un vídeo. Lo que está en antena lo ven todos los participantes, el flujo de los espectadores, la grabación y las emisiones externas.
{
"id": "intro", "name": "Introducción", "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": "Material" }
],
"overlays": [
{ "id": "ticker", "type": "band", "text": "Inscripciones abiertas hasta las 18 h", "visible": true,
"x": 0, "y": 0.86, "w": 1, "h": 0.09, "opacity": 1, "bg": "#dc2626", "color": "#ffffff",
"fontSize": 48, "bold": true, "scroll": true, "speed": 160, "align": "center" },
{ "id": "ov_live", "type": "sticker", "label": "LIVE", "bg": "#dc2626", "color": "#ffffff", "visible": true,
"x": 0.82, "y": 0.06, "w": 0.16, "h": 0.09, "opacity": 1 }
],
"background": "#0a0a0a", "backgroundImageUrl": null, "showLogo": true, "framed": false, "transition": "cut",
"durationSec": 20, "nextSceneId": "oferta"
}
| Campo | Descripción |
|---|---|
id | Libre al crear (de 1 a 40 caracteres: letras, cifras, -, _), si no se genera. Igual para los slots y los overlays: elige ids estables (ticker, precio) para actualizarlos después |
jingle | Cortinilla: la escena se reproduce durante su duración (durationSec, o si no la duración de sus vídeos y sonidos), luego la escena que interrumpió se reanuda donde estaba (vídeos y sonidos, tiempo restante del encadenamiento), incluso sin encadenamiento automático. Alcanzada por el encadenamiento automático, una cortinilla pasa después a la escena siguiente. null para quitar. |
durationSec, nextSceneId | Encadenamiento automático: tiempo en pantalla en segundos (de 1 a 86400, contado después de la transición de entrada; ausente = la escena se queda hasta el siguiente cambio; se prolonga hasta el final de los vídeos y sonidos que empiezan con la escena, sin bucle, cuya duración se conoce: mediaSec, duración del archivo en segundos medida por el editor, también aceptada en los huecos de vídeo y en audios), luego la escena siguiente (ausente = la siguiente de la lista, o la primera con el bucle). Solo activo cuando el encadenamiento está encendido (PUT …/scenes/auto) |
layout | solo (primer elemento a pantalla completa), grid, spotlight (el primero en grande, los demás en columna), sidebyside, pip (el primero a pantalla completa, el segundo en miniatura), cinema (el primero a pantalla completa, franja abajo), free (cada hueco lleva x, y, w, h) |
slots[].kind | host (cámara del anfitrión), guest (guestIndex: n-ésimo ponente por orden de llegada, o userId para fijar a una persona), screen (primera pantalla compartida), media (mediaId de la mediateca, o url https directa con mediaKind image, pdf o video), embed (mediaId o url de YouTube, Vimeo, Loom) |
slots[].fit, loop, muted, title | Encuadre cover o contain; vídeo en bucle o sin sonido; título mostrado en la realización |
slots[].shape, filter, bgMode, bgBlur, bgImage, volume, muted | Cámaras (host, guest): forma rect (por defecto), rounded, square, squircle (cuadrado redondeado), circle, portrait; filtro none, bw, noir, sepia, vintage, warm, cool, vivid, faded, bright, contrast; fondo aplicado en el dispositivo de la persona mientras la escena está al aire: bgMode none, blur (bgBlur 1 a 20, por defecto 10) o image (bgImage: URL https o preset:ocean, preset:forest, preset:sunset, preset:slate), ausente = elección de la persona; sonido al aire volume (0 a 1) y muted, para la emisión, la grabación y los espectadores (los intervinientes siempre se oyen). null para quitar |
slots[].autoplay, once, startSec, durationSec, volume | Vídeo: empieza cuando la escena pasa a antena (por defecto true; si no, espera las órdenes de la realización), once solo la primera vez, inicio en segundos, duración del fragmento reproducido (en bucle o parada al final del fragmento), volumen de 0 a 1 (null para quitar). Misma lógica que el sonido de escena. |
overlays[] | type band (text, bg, color, fontSize en px a 1080p, bold, scroll, speed en px/s, align), sticker (emoji, o imageUrl https, o label insignia con bg y color), o un widget de directo poll / message / viewers (contador de espectadores, title = etiqueta), con su estilo: bg (hex) y bgOpacity, color, accent (barras, insignias), fontScale (0,5 a 2), radius (px a 1080p), borderColor, borderWidth, shadow, font (sans, rounded, serif, mono), align (mensaje), showHeader, showVotes, showLetters (encuesta), showName, showIcon (mensaje); el tamaño se ajusta con w y h: donde la escena dibuja la encuesta o el mensaje del chat puesto en antena; vacío mientras no hay nada en antena, y una escena sin ese hueco usa una esquina por defecto. Geometría en fracciones del cuadro 16:9 (x, y, w, h entre 0 y 1), visible, opacity |
locked | Candado: una escena bloqueada rechaza cualquier modificación y eliminación (400), tanto desde la aplicación como desde la API, hasta que una llamada envíe { "locked": false }. Se puede seguir poniendo en antena y duplicando |
audios | Sonidos de la escena, varios posibles (6 como máximo), cada uno { "mediaId": "…" } (archivo audio de la mediateca) o { "url": "https://…" }, con autoplay (empieza cuando la escena pasa a antena, por defecto true), once (con autoplay: solo la primera vez, por defecto false), loop (por defecto false), volume (de 0 a 1, por defecto 1), startSec y durationSec (fragmento reproducido, en segundos; null para quitar). Se reproducen en todos los participantes y se captan en la emisión y la grabación. Sustituye la lista (un sonido ya presente, mismo mediaId, conserva sus demás ajustes); [] los quita todos. El antiguo campo audio (un solo sonido, misma forma) sigue aceptado en escritura, "audio": null quita todos los sonidos; en lectura, los sonidos están siempre en audios. Reproducción, pausa y reinicio desde el principio de cada sonido con PUT …/media/:mediaId. |
background, backgroundImageUrl, showLogo, transition | Color CSS del fondo, imagen de fondo https (null para quitarla), logo del espacio en la esquina, transición a la llegada de la escena (la anterior se queda debajo durante la animación): cut, fade (fundido encadenado), black (fundido a negro), slide-left, slide-right, slide-up, slide-down, zoom, wipe (barrido), blur (desenfoque), con transitionMs (de 100 a 3000, por defecto 500). Se reproduce en la sala, la emisión y la grabación. |
framed | Enmarcado: un margen alrededor de los huecos deja ver el fondo (color o imagen) y tu marca. Para todas las disposiciones salvo free; por defecto false, null para quitarlo |
20 escenas por directo, 9 huecos y 30 stickers o rótulos por escena. Una escena se prepara antes del directo (los ponentes se designan por orden de llegada) y se modifica durante él: la modificación de una escena en antena se ve al instante.
GET devuelve toda la realización:
{ "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 acaba de inscribirse", "bg": "#059669", "color": "#ffffff", "until": "…" } ] }
POST crea una escena a partir del objeto anterior (todo es opcional salvo lo que le da sentido), o a partir de una plantilla del espacio: { "template": "Pantalla compartida", "name": "Demo" }. "activate": true la pone en antena en el acto; "afterId" la coloca después de una escena dada. Respuesta 201 { "scene": { … } }.
POST /live/sessions/p97…/scenes
{ "id": "oferta", "name": "Oferta especial", "layout": "pip",
"slots": [ { "kind": "media", "mediaId": "m12…" }, { "kind": "host" } ],
"overlays": [ { "id": "precio", "type": "band", "text": "Curso de SEO: 499 € hasta esta noche", "y": 0.86 } ],
"activate": true }
PATCH modifica los campos enviados; slots y overlays, cuando están presentes, sustituyen las listas completas (para tocar un solo elemento, ver más abajo). DELETE quita la escena (si estaba en antena, vuelta a la disposición automática) y devuelve la realización. PUT …/scenes/order { "sceneIds": [ … ] } reordena.
| Llamada | Efecto |
|---|---|
POST/live/sessions/:id/scenes/:sceneId/activate | Pone la escena en antena (sus vídeos vuelven a empezar). Respuesta: la realización completa |
PUT/live/sessions/:id/scenes/active { "sceneId": "intro" } | Lo mismo; { "sceneId": null } vuelve a la disposición automática (cámaras en cuadrícula, pantalla compartida en grande) |
PUT/live/sessions/:id/scenes/auto { "enabled": true, "loop": true } | Encadenamiento automático: durante el directo, cada escena que lleva durationSec deja paso a nextSceneId (o a la siguiente) cuando se agota su tiempo; loop vuelve a la primera al final de la lista. Un cambio manual (aplicación o API) reinicia el cronómetro de la nueva escena. La realización devuelta lleva autoAdvance: { enabled, loop, nextSceneAt } (nextSceneAt: fecha ISO del próximo cambio, o null) |
POST/live/sessions/:id/bands { "text": "Pausa de 5 minutos", "bg": "#059669", "color": "#ffffff", "seconds": 8 } | Rótulo mostrado en la parte superior de la pantalla durante seconds (de 2 a 60, 8 por defecto) en todas las pantallas, sea cual sea la escena. Respuesta 201 { "band": { "id", "text", "bg", "color", "until" } }. Para un texto permanente o modificable, usa mejor un rótulo de escena (overlays) |
PUT/live/sessions/:id/media/:mediaId { "page": 4 } | Página mostrada de un PDF presente en una escena |
PUT/live/sessions/:id/media/:mediaId { "playing": true, "position": 0 } | Reproducción, pausa o posición (segundos) de un vídeo presente en una escena, o de un sonido de una escena (audios[].mediaId), sincronizadas para todos |
Cada cambio de escena en antena emite el webhook live.scene.changed ({ session, scene }, scene a null para la disposición automática).
| Llamada | Efecto |
|---|---|
POST…/scenes/:sceneId/overlays | Añade un sticker o un rótulo (objeto overlays[] anterior). 201 { "overlay" } |
PATCH…/scenes/:sceneId/overlays/:overlayId | Modifica los campos enviados: { "text": "Solo quedan 12 plazas" }, { "visible": false }, { "x": 0.1, "y": 0.1 }… Visible al instante si la escena está en antena |
PATCH/live/sessions/:id/overlays/:overlayId | Lo mismo en todas las escenas que llevan ese id: un rótulo común (contador, precio, siguiente paso) colocado en cada escena con el mismo id se actualiza con una sola llamada |
DELETE…/scenes/:sceneId/overlays/:overlayId | Quita el elemento, devuelve la escena |
POST…/scenes/:sceneId/slots | Añade un hueco (objeto slots[]; position opcional, 0 = principal). 201 { "slot" } |
PATCH…/scenes/:sceneId/slots/:slotId | Modifica el hueco (otro medio mediante mediaId o url, guestIndex, fit, geometría en free, position para moverlo en el orden) |
DELETE…/scenes/:sceneId/slots/:slotId | Quita el hueco, devuelve la escena |
# El contador de plazas, en todas las escenas, desde tu CRM
PATCH /live/sessions/p97…/overlays/places
{ "text": "Solo quedan 7 plazas", "bg": "#f59e0b", "color": "#111111" }
Plantillas de escenas del espacio, reutilizables en todos los directos (menú «Nueva» de la realización, o "template" al crear una escena). POST: { "name": "Pantalla compartida", "scene": { … } } o, para guardar una escena existente, { "name": "…", "sessionId": "p97…", "sceneId": "intro" }. Mismo nombre = sustituida. Respuesta { "template": { "id", "name", "scene", "folderId", "createdAt", "updatedAt" } }. :id acepta el identificador o el nombre.
{ "role": "speaker" | "viewer" | "assistant" | "moderator" } para un participante ya inscrito en la sesión (/participants): dar o retirar la palabra, o nombrar a alguien de la realización. Se aplica al instante durante el directo. Respuesta { "participant": { … } }.
Un directo puede plantear encuestas (opinión) y cuestionarios (con respuesta correcta) a los participantes, en la sala y en la audiencia del webinar. Se preparan con antelación (estado draft, invisibles para los participantes), se lanzan durante el directo (open, uno cada vez) y luego se cierran (closed). Los resultados se conservan después del directo: GET …/polls los devuelve siempre, y también figuran en las estadísticas y en la página de la repetición. En la aplicación, el anfitrión, el staff y los roles moderator y assistant los gestionan desde la pestaña «Encuestas» del directo y desde «Preparar».
{ "id": "k3…", "sessionId": "p97…", "kind": "quiz", "question": "¿Cuál es la capital de 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 (por defecto) o quiz. Para un cuestionario, correct al crearlo da los índices (desde 0) de las respuestas correctas en options; la respuesta correcta y correctVoters (votantes que eligieron exactamente la combinación correcta) se revelan a los participantes con los resultados. percent es la parte de votantes que eligieron la opción (con allowMultiple, la suma supera 100). Un participante puede cambiar su voto mientras la encuesta está abierta; un votante cuenta una vez.
GET …/polls devuelve { "polls": [ … ] } en el orden de preparación, con los recuentos; ?status=draft|open|closed filtra. Respuesta { "poll": { … } } para las demás.
POST /live/sessions/p97…/polls
{ "question": "¿Qué tema después?", "options": ["Precios", "Integración LMS", "Repeticiones"], "allowMultiple": true }
→ 201 { "poll": { "status": "draft", … } }
POST /live/sessions/p97…/polls
{ "kind": "quiz", "question": "¿Capital de Australia?", "options": ["Sydney", "Canberra"], "correct": [1], "open": true }
→ 201 { "poll": { "status": "open", … } } // "open": true la lanza en el acto (solo con el directo en curso)
phase (al crear o en PATCH): live (por defecto, lanzado a mano durante el directo), waiting (encuesta o cuestionario previo: abierto en la sala de espera desde su creación mientras el directo está programado, cerrado automáticamente al empezar, resultados reutilizables durante el directo con …/band y …/stage) o closing (abierto automáticamente al terminar el directo, en la pantalla de cierre: opinión, satisfacción). Una encuesta waiting abierta sigue siendo modificable.
PATCH modifica solo una encuesta draft (o una encuesta waiting abierta) (mismos campos; si cambian las options de un cuestionario, vuelve a enviar correct). Una encuesta lanzada se duplica (nuevo POST) en lugar de modificarse. DELETE elimina la encuesta y sus votos, sea cual sea su estado. De 2 a 6 respuestas, 100 encuestas por directo.
| Llamada | Efecto |
|---|---|
POST…/polls/:pollId/open | Abre la votación a los participantes (tarjeta sobre el vídeo, mensaje del sistema en el chat salvo { "announceInChat": false }). Se rechaza si hay otra encuesta abierta o si el directo no está en curso. Webhook live.poll.opened. |
PUT…/polls/:pollId/results { "shown": true } | Muestra (u oculta con false) los recuentos a los participantes, durante la votación o después del cierre. En un cuestionario, también revela la respuesta correcta. |
POST…/polls/:pollId/close | Cierra la votación; { "showResults": false } para cerrar sin mostrarlos (por defecto: se muestran). Webhook live.poll.closed con los resultados. Una encuesta todavía abierta al final del directo se cierra con los resultados mostrados. |
PUT…/polls/:pollId/stage { "shown": true } | Muestra la encuesta en la escena (hueco poll de la escena en antena; si no, esquina inferior derecha): pregunta, respuestas, número de votos, y luego las barras y la respuesta correcta cuando se muestran los resultados. Visible en la sala, la emisión y la grabación. false la quita; onStage en el objeto encuesta. |
PUT/live/sessions/:id/viewers { "extra": 25 } | Añade espectadores al contador mostrado por el widget viewers de las escenas (número real de personas en el directo + extra). Nunca cuenta en la asistencia, las estadísticas ni la facturación. 0 quita lo añadido; extraViewers en el objeto sesión. |
PUT/live/sessions/:id/chat/stage { "messageId": "…" } | Pone un mensaje del chat en antena (hueco message de la escena; si no, abajo a la izquierda): nombre y texto, imagen si la hay. { "messageId": null } lo quita. Los mensajes privados se rechazan. Respuesta { "message": { id, name, text, imageUrl, at } | null }. |
POST…/polls/:pollId/band | Rótulo de 12 segundos en la escena (vídeo, emisión, grabación) con las respuestas en cabeza, o la respuesta correcta y el porcentaje de acierto en un cuestionario. Directo en curso, al menos un voto. |
Tus usuarios abren tickets de soporte desde tu plataforma (por ejemplo el plugin de WordPress de LearnFloo); tu equipo los responde en el espacio LearnFloo, pestaña Soporte, o por la API (lado del equipo). Misma clave API que para los directos.
user.externalId (tu identificador estable) y, al crear, user.name. LearnFloo crea una cuenta de invitado la primera vez, como para los directos, y la reutiliza después."format": "html" para enviar HTML.Objeto ticket devuelto por todas las llamadas:
{
"id": "j57…", "number": 12, "subject": "Vídeo bloqueado en el módulo 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 }
}
| Campo | Descripción |
|---|---|
status | open (esperando al equipo), pending (esperando al usuario), resolved, closed |
priority | low, normal, high, urgent |
lastMessageBy | member o staff: quién escribió el último |
Tickets del usuario, los más recientes primero. ?externalId=… obligatorio, &status=active|open|pending|resolved|closed|all opcional (all por defecto, active = open + pending).
{ "tickets": [ { … }, … ] }
Todos los tickets del espacio (panel del equipo): ?scope=space&status=…&limit=100 (500 máx.). Cada ticket lleva entonces su author ({ id, externalId, name, email, image }).
{
"user": { "externalId": "wp:42", "name": "Alice Martin" },
"subject": "Vídeo bloqueado en el módulo 3",
"content": "Hola,\n\nel vídeo se para a los 2 min.",
"priority": "normal",
"category": "wordpress",
"attachments": [ { "kind": "image", "url": "https://…", "name": "capture.png", "mimeType": "image/png", "sizeBytes": 1234 } ]
}
| Campo | Descripción |
|---|---|
user.externalId, user.name | Obligatorios. 200 y 60 caracteres máx. |
subject | Obligatorio, 200 caracteres máx. |
content | Texto plano (por defecto) o HTML con "format": "html". Obligatorio salvo si se aporta un archivo adjunto |
priority | Por defecto normal |
category | Libre, opcional (se muestra al equipo) |
attachments | Opcional, 10 máx., objetos devueltos por /attachments |
Respuesta 201: { "ticket": { … } }. Se notifica al equipo del espacio.
?externalId=… obligatorio (o ?scope=space para el equipo: cualquier ticket del espacio, con su author). El ticket y sus mensajes visibles para el usuario, del más antiguo al más reciente. Las notas internas del equipo nunca salen por la API.
{
"ticket": {
…,
"messages": [
{ "id": "m3…", "content": "<p>Hola,</p>…", "attachments": [],
"createdAt": "…", "fromStaff": false, "author": { "name": "Alice Martin", "image": null } },
{ "id": "m4…", "content": "<p>¿Puedes vaciar la caché?</p>", "attachments": [],
"createdAt": "…", "fromStaff": true, "author": { "name": "Julie", "image": "https://…" } }
]
}
}
content es HTML producido por el editor de LearnFloo: sanéalo antes de mostrarlo en tu página (etiquetas de texto, imágenes, enlaces).404 si el ticket no existe o no pertenece a este usuario.
{ "user": { "externalId": "wp:42", "name": "Alice Martin" }, "content": "Gracias, ya está resuelto.", "attachments": [] }
Respuesta 201: { "ticket": { … } } (sin los mensajes). El ticket vuelve a open y se notifica al equipo (o a la persona asignada).
{ "user": { "externalId": "wp:42" }, "status": "resolved" }
El usuario puede marcar su ticket como resolved o reabrirlo (open). Los demás estados están reservados al equipo. Respuesta 200: { "ticket": { … } }.
Para sincronizar un help desk externo, las dos llamadas anteriores aceptan un objeto staff en lugar de user: la acción la realiza un miembro del equipo del espacio (owner, admin, moderator, teacher), por defecto el creador de la clave, o staff.email para otra cuenta de LearnFloo.
POST /support/tickets/:id/messages
{ "staff": { "email": "julie@cliente.com" }, "content": "¿Puedes vaciar la caché?" }
→ el ticket pasa a "pending", se notifica al usuario
POST /support/tickets/:id/status
{ "staff": {}, "status": "closed", "priority": "high", "assigneeEmail": "julie@cliente.com" }
→ cualquier estado; priority y assigneeEmail (null para desasignar) son opcionales
Cuerpo bruto del archivo, cabecera Content-Type = tipo del archivo (imágenes, PDF, audio, texto, zip, Office), 25 MB máx. ?externalId=… opcional (adjunto de un usuario externo). Después se adjunta a un ticket, una respuesta, una publicación o un comentario mediante attachments. /support/attachments es un alias.
{ "attachment": { "kind": "image", "url": "https://…/capture.png", "mimeType": "image/png", "sizeBytes": 1234 } }
Errores: 415 tipo no admitido, 413 archivo demasiado grande.
El espacio detrás de la clave, para una prueba de conexión y la visualización:
{ "space": { "id": "…", "slug": "mi-espacio", "name": "Mi espacio", "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.cliente.com", "memberCount": 128, "courseCount": 4, "groupCount": 6,
"groupLabels": { "singular": "Clase", "plural": "Clases", "leader": "Profesor", "member": "Alumno", "gender": "f" },
"createdAt": "…", "url": "https://app.learnfloo.com/mi-espacio" } }
visibility: public (aparece en www.learnfloo.com/es/comunidades, página «Acerca de» aboutUrl abierta a todos) o private (acceso solo con enlace de invitación). accessType: free o paid; un espacio de pago lleva priceCents (importe en la unidad más pequeña de la moneda), priceCurrency (eur, precio con IVA, o usd, precio sin impuestos) y priceInterval (month, year, once = acceso de por vida). rating es la media de las opiniones de los miembros (null sin opiniones).
Consumo del espacio desde la última factura, valorado a la tarifa por uso de la oferta única (horas de webinar emitidas, horas de videollamada grabadas, horas interactivas, horas de espectador, reproducciones, minutos de vídeo almacenados, horas de emisión simultánea multiplicadas por el número de destinos, horas subtituladas), el estado de la cuenta del propietario y el límite de uso de la clave.
{ "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 es el coste de este espacio antes de lo incluido (allowance), el crédito de bienvenida y el tope mensual (capCents, capMode = block o warn), que se aplican a la cuenta del propietario, sumando todos sus espacios. liveAllowed es falso cuando la cuenta ya no puede pagar (crédito agotado sin facturación activa).
GET: miembros del espacio, paginados (?limit=50&cursor=…, 200 máx.), filtros ?role=member|contributor|teacher|moderator|admin|owner y ?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: crea (o actualiza) un usuario de tu plataforma y lo añade como miembro del espacio, sin esperar a que se una a un directo. Idempotente: volver a llamar con el mismo externalId actualiza el nombre.
{ "externalId": "lms-user-42", "name": "Alice Martin", "role": "member", "member": true }
→ 201 { "user": { …, "member": { … } }, "created": true }
role: member (por defecto), contributor, teacher, moderator, admin. member: false crea la cuenta sin inscribirla en el espacio. Respuesta 200 si el usuario ya existía.
:id = identificador de LearnFloo o 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 } (quita al miembro del espacio; su cuenta y su historial se conservan)
Solo se pueden renombrar los usuarios creados por tu plataforma. El propietario del espacio no se puede modificar ni quitar.
Acceso sin contraseña (SSO) de un usuario de tu plataforma al espacio LearnFloo: muro, cursos, calendario, clasificación… Mismo principio que /live/entry: URL personal, de un solo uso, válida 15 minutos, que se pide en el momento del clic. El usuario se añade como miembro si todavía no lo es. Reservado a los usuarios ext:…; con name, el usuario se crea si no existe.
POST /users/ext%3Alms-user-42/entry
{ "name": "Alice Martin", "redirect": "courses" }
→ { "url": "https://app.learnfloo.com/enter/…", "expiresAt": "…", "userId": "k97…", "redirect": "/mi-espacio/courses" }
redirect: home (por defecto), posts, courses, calendar, live, leaderboard, tickets, challenges, o una ruta del espacio (/mi-espacio/courses/onboarding, /posts/<id>).
Progreso de un usuario en cada curso del espacio.
{ "user": { … }, "courses": [
{ "id": "c1…", "slug": "onboarding", "title": "Onboarding", "published": true, "lessonCount": 8,
"completedLessons": 5, "progressPct": 63, "completed": false, "lastCompletedAt": "…" }
] }
GET: total, nivel y últimos eventos de XP del usuario en el espacio (?limit=100, 1000 máx.).
{ "user": { … }, "totalXp": 320, "level": 3, "events": [ { "id": "…", "amount": 25, "source": "lesson_complete", "refId": "l3…", "createdAt": "…" } ] }
POST: otorga XP desde tu plataforma (cuestionario aprobado, tarea entregada…). amount de -1000 a 1000, reason (60 caracteres, visible como fuente api:<reason>) y refId opcionales. El nivel se recalcula. 400 si la gamificación está desactivada en el espacio.
{ "amount": 50, "reason": "quiz-module-3", "refId": "quiz:874" } → 201 { "user": { … }, "totalXp": 370, "level": 3, "awarded": 50 }
| Llamada | Respuesta |
|---|---|
GET/leaderboard?limit=50&group= | { "leaderboard": [ { "rank": 1, "id", "externalId", "name", "image", "totalXp", "level", "role" } ] } (500 máx.); group limita a los miembros de un grupo |
GET/badges | Catálogo: { "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 | El reto con sus participants ({ id, externalId, name, progress, completedAt, joinedAt }) |
Un espacio puede repartir a sus miembros en grupos: las clases de una escuela, las promociones de un centro de formación, los equipos o departamentos de una empresa. El vocabulario se ajusta en la configuración del espacio (groupLabels de GET /space); la API siempre habla de groups. Cada grupo tiene un slug que se puede usar en lugar de su identificador.
Un curso, una publicación, un evento o un directo pueden dirigirse solo a ciertos grupos: es el campo groups (array de identificadores o slugs) al crear y modificar, y groupIds en lectura. Vacío = todo el espacio. Un miembro solo ve el contenido sin audiencia o dirigido a uno de sus grupos; los roles owner, admin, moderator y teacher lo ven todo. Las listas aceptan ?group=<id o slug> para devolver solo el contenido dirigido a ese grupo.
POST /courses { "title": "Matemáticas — 2.º Bachillerato", "groups": ["terminale-a", "terminale-b"] }
POST /posts { "title": "Salida del viernes", "content": "…", "groups": ["terminale-a"] }
POST /live/sessions { "title": "Repaso", "scheduledAt": "…", "groups": ["terminale-a"] }
GET /courses?group=terminale-a
En un grupo, un miembro es member (alumno, estudiante…) o leader (profesor, responsable…): un leader puede gestionar la lista de miembros de su grupo desde la app, sin un rol particular en el espacio.
Grupos automáticos. Un grupo puede llevar una rule: los miembros que la cumplen se añaden sobre la marcha (y reciben un aviso), los miembros actuales en cuanto se crea la regla. Las reglas son de un solo sentido, nadie se quita automáticamente; siempre se puede añadir o quitar a mano. Tipos: level_min (level de 2 a 10), streak_days (days), challenge_joined y challenge_completed (challenge = id, ver /challenges), badge (badge = id o clave, ver /badges), course_completed (course = id o slug). Después usa el grupo como audiencia: un curso reservado a «Nivel 5 o superior», un directo para quienes terminaron un reto.
POST /groups { "name": "Nivel 5 o superior", "rule": { "kind": "level_min", "level": 5 } }
POST /groups { "name": "Módulo 3 terminado", "rule": { "kind": "course_completed", "course": "module-3" } }
PATCH /groups/modulo-3-terminado { "rule": null } # vuelve a ser un grupo manual
→ "group": { …, "rule": { "kind": "course_completed", "level": null, "days": null, "challengeId": null, "badgeId": null, "courseId": "c1…" } }
| Llamada | Cuerpo / respuesta |
|---|---|
GET/groups | { "groups": [ { "id", "slug", "name", "description", "color", "memberCount", "rule", "createdAt" } ] }, por nombre |
POST/groups | { "name", "slug"?, "description"?, "color"?, "rule"? } → 201 { "group": { … } }. El slug se deriva del nombre si no se da; color en formato #rrggbb |
GET/groups/:id | El grupo (id o slug), ruleLabel legible (en francés), y sus members: [ { "id", "externalId", "name", "email", "image", "groupRole": "leader|member", "auto", "addedAt" } ] (auto: añadido por la regla) |
PATCH/groups/:id | { "name"?, "slug"?, "description"?: string | null, "color"?: string | null, "rule"?: object | null } |
DELETE/groups/:id | { "deleted": true }. El grupo se quita de la audiencia de los contenidos dirigidos a él (un contenido dirigido solo a este grupo vuelve a ser visible para todo el espacio) |
| Llamada | Cuerpo / respuesta |
|---|---|
POST/groups/:id/members | { "users": ["k97…", "ext:lms-user-42"], "role"?: "member|leader" } → { "group", "added", "notFound": [] }. Los usuarios ya deben ser miembros del espacio (ver POST /users); hasta 500 por llamada, idempotente |
DELETE/groups/:id/members/:userId | { "removed": true }. :userId = identificador de LearnFloo o ext:… |
GET: grupos de un usuario en el espacio, con su groupRole. PUT sustituye la lista completa (práctico para sincronizar las clases desde tu LMS):
PUT /users/ext%3Alms-user-42/groups
{ "groups": ["terminale-a", "club-robotique"], "role": "member" }
→ { "groups": [ { "id", "slug", "name", …, "groupRole": "member" } ] }
| Llamada | Respuesta |
|---|---|
GET/groups/:id/leaderboard?limit= | { "group", "leaderboard": [ { "rank", "id", "externalId", "name", "image", "totalXp", "level", "role", "groupRole" } ] }: los miembros del grupo clasificados por XP |
GET/groups/:id/stats 5 tokens | Seguimiento de cada miembro: progreso en los cursos abiertos al grupo, lecciones validadas, publicaciones, directos seguidos, XP de los últimos 30 días, última actividad. Ver más abajo |
GET/groups/:id/documents | { "documents": [ { "id", "title", "url", "kind", "mimeType", "sizeBytes", "uploadedBy", "createdAt" } ] }, los más recientes primero |
POST/groups/:id/documents | { "title", "url", "kind"?, "mimeType"?, "sizeBytes"? } → 201 { "document" }. url: un archivo enviado mediante POST /attachments, o cualquier enlace https (kind: "link", nunca se borra del almacenamiento) |
DELETE/groups/:id/documents/:docId | { "deleted": true }; un archivo enviado por la API se borra del almacenamiento |
GET /groups/terminale-a/stats
{ "group": { "id", "slug": "terminale-a", "name": "2.º Bachillerato 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": "Matemáticas — 2.º Bachillerato", "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 abarca a los miembros member (los leader quedan excluidos de las medias); inactive = ninguna actividad desde hace 30 días. Los cursos que se tienen en cuenta son los cursos publicados dirigidos al grupo o a todo el espacio.
GET: cursos publicados del espacio (?includeUnpublished=1 para verlo todo, ?group= para los dirigidos a un grupo).
{ "courses": [ { "id": "c1…", "slug": "onboarding", "title": "Onboarding", "description": null, "coverImage": null,
"published": true, "lessonCount": 8, "groupIds": [], "createdAt": "…", "url": "/mi-espacio/courses/onboarding" } ] }
POST: crea un curso (borrador por defecto). El author opcional debe ser owner, admin o teacher. groups: audiencia (ids o slugs), vacío = todo el espacio.
{ "title": "Onboarding", "slug": "onboarding", "description": "…", "coverImage": "https://…", "published": false, "groups": ["terminale-a"] }
→ 201 { "course": { … } }
:id = identificador o slug del curso. GET devuelve la estructura completa; añade ?withContent=1 para el HTML de las lecciones.
{ "course": { …, "modules": [ { "id": "m1…", "title": "Semana 1", "description": null, "order": 0 } ],
"lessons": [ { "id": "l1…", "courseId": "c1…", "moduleId": "m1…", "title": "Bienvenida", "order": 0,
"videoUrl": "https://…", "videoDurationSec": 312, "createdAt": "…", "url": "/mi-espacio/courses/onboarding/lessons/l1…" } ] } }
PATCH: title, description, coverImage, published, groups ([] o null = todo el espacio). DELETE elimina el curso, sus módulos, sus lecciones y los progresos.
| Llamada | Cuerpo / respuesta |
|---|---|
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 de un vídeo (MP4, HLS, YouTube, Vimeo) o playbackUrl de un vídeo del estudio |
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 } |
Sin parámetros: todos los alumnos que han empezado el curso, los más avanzados primero.
{ "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": "…" } ] }
Con ?externalId=lms-user-42 (o ?userId=k97…): el detalle lección por lección de ese alumno.
{ "course": { … }, "user": { …, "completedLessons": 5, "progressPct": 63,
"lessons": [ { "id": "l1…", "title": "Bienvenida", "moduleId": "m1…", "completedAt": "…" }, { "id": "l2…", "title": "…", "moduleId": "m1…", "completedAt": null } ] } }
Marca una lección como terminada para un alumno, como si la hubiera validado en LearnFloo (XP, misiones, insignias, webhook lesson.completed). Idempotente.
{ "user": { "externalId": "lms-user-42", "name": "Alice Martin" } } o { "userId": "k97…" }
→ 201 { "lessonId": "l1…", "user": { … }, "completedAt": "…", "created": true } (200 si ya estaba terminada)
| Llamada | Cuerpo / respuesta |
|---|---|
GET/posts?category=&group=&limit=&cursor= | Muro del espacio, lo más reciente primero: { "posts": [ … ], "nextCursor" }. Categorías: general, announcements, question, wins, resource. group: publicaciones dirigidas a ese grupo |
GET/posts/:id | La publicación con sus comments (del más antiguo al más reciente, parentId para las respuestas) |
POST/posts | { "title"?, "category"?, "content", "format"?, "attachments"?, "pinned"?, "groups"?, "author"? } → 201 { "post": { … } }. Menciones, XP y notificaciones como en la app. pinned reservado a los moderadores; un autor sin rol de staff solo puede dirigirse (groups) a sus propios grupos |
PATCH/posts/:id | { "title"?: string | null, "category"?, "content"?, "format"?, "pinned"?, "groups"? } |
DELETE/posts/:id | Elimina la publicación, sus comentarios y reacciones |
POST/posts/:id/comments | { "content", "format"?, "parentCommentId"?, "attachments"?, "author"? } → 201 { "comment": { … } } |
DELETE/comments/:id | Elimina el comentario y sus respuestas |
{ "post": { "id": "p1…", "title": "Bienvenida a la promoción 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@cliente.com", "image": "https://…", "role": "admin", "level": 5 },
"url": "/mi-espacio/posts/p1…" } }
| Llamada | Cuerpo / respuesta |
|---|---|
GET/events?from=&to=&type=&group= | Eventos por fecha de inicio ascendente; from / to en ISO o milisegundos. Los directos programados aparecen con 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"? }. Tipos: event (por defecto), workshop, masterclass, deadline, qa. Los directos se crean mediante /live/sessions |
PATCH/events/:id | Mismos campos; endsAt: null para borrarlo, groups: [] para abrirlo a todo el espacio. Un evento vinculado a un directo se modifica mediante PATCH /live/sessions/:id |
DELETE/events/:id | { "deleted": true } |
Grabaciones del estudio LearnFloo (pantalla, cámara, importaciones) y repeticiones de directos importadas, alojadas en Bunny Stream.
| Llamada | Respuesta |
|---|---|
GET/videos?status=ready&limit=&cursor= | { "videos": [ … ], "nextCursor" }, los más recientes primero. Estados: uploading, processing, ready, failed |
GET/videos/:id | { "video": { … } } |
PATCH/videos/:id | { "title"?, "description"? } |
{ "video": { "id": "v1…", "title": "Demo módulo 3", "description": null, "mode": "screen_camera", "status": "ready",
"durationSec": 612, "width": 1920, "height": 1080, "encodeProgress": null, "thumbnailUrl": "https://…",
"playbackUrl": "https://…/play_1080p.mp4", "hlsUrl": "https://…/playlist.m3u8", "embedUrl": "https://iframe.mediadelivery.net/embed/…",
"lessonId": null, "liveSessionId": null, "createdAt": "…", "updatedAt": "…", "author": { … } } }
Las URL de reproducción solo se devuelven para los vídeos ready. Como con las repeticiones, el MP4 exige una cabecera Referer: insértalo en una página. Los vídeos se envían desde la app (estudio); la API no acepta archivos de vídeo.
Mediateca del espacio: las imágenes, PDF, vídeos, insertados y stickers usados en las escenas del directo y en el estudio. GET: ?kind=image|pdf|video|embed|sticker&limit=&cursor=.
{ "media": [ { "id": "…", "kind": "pdf", "title": "Material módulo 3", "url": "https://…", "thumbnailUrl": null, "videoId": null, "sizeBytes": 812000,
"folder": { "id": "…", "name": "Directo del 12/09" }, "createdAt": "…" } ], "nextCursor": null }
POST añade un elemento por URL (https, alojado por ti o en un CDN):
{ "kind": "image", "url": "https://cdn.tu-sitio.com/oferta.png", "title": "Oferta de vuelta al cole", "folder": "Directo del 12/09" }
→ 201 { "media": { "id": "m12…", "kind": "image", … } }
kind: image, pdf, video (archivo MP4 reproducible directamente), embed (página de YouTube, Vimeo o Loom, convertida en reproductor), sticker (PNG transparente) o audio (MP3, WAV, OGG, M4A: sonido de una escena). folder: identificador o nombre de una carpeta (se crea sobre la marcha si no existe; lista con GET /media/folders, creación explícita con POST /media/folders { "name" }). Un PDF externo debe permitir las peticiones desde app.learnfloo.com (cabecera CORS); las imágenes y los vídeos no tienen esa restricción. Para enviar un archivo en lugar de una URL, pasa por POST /attachments y luego indica la URL obtenida.
PATCH /media/:id: title, folder (null para la raíz). DELETE /media/:id elimina el elemento (y el archivo si lo aloja LearnFloo); las escenas que lo usaban conservan la URL.
Toda la capa de correo de un espacio se controla por la API, bajo /emails/…: las campañas a los miembros (redacción, vista previa, prueba, envío inmediato o programado, estadísticas), los segmentos que definen quién las recibe, los textos reutilizables, el texto de los correos automáticos (invitaciones a directos), el resumen automático de novedades, el dominio de envío del cliente y las preferencias de cada miembro. Las mismas acciones existen como herramientas MCP (familia «Correos»).
Reglas comunes: el autor de cada escritura es el creador de la clave API (él recibe los envíos de prueba); los cuerpos van en texto plano (línea vacía = párrafo) o en HTML con "format": "html"; {{prenom}} (nombre) y {{espace}} (nombre del espacio) se sustituyen para cada lector; cada correo masivo lleva el enlace de baja de la persona, y una persona dada de baja no recibe ni campañas ni resúmenes de ese espacio. Una clave de solo lectura accede a los GET y a POST /emails/segments/preview.
POST { "subject": "Novedades de {{espace}}", "previewText": "Tres novedades esta semana", "body": "Hola {{prenom}},
Aquí…",
"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 lista las 200 últimas, las más recientes primero (kind: "digest": los resúmenes automáticos enviados, con sus estadísticas). POST crea un borrador; templateId toma un texto reutilizable para lo que no se indique; send vale "now" o una fecha ISO para enviar en la misma llamada (3 fichas). PATCH modifica solo un borrador (subject, previewText, body + format, segmentId, null para quitar). DELETE elimina (una campaña programada se desprograma; durante el envío, 403).
Las stats vienen de los acuses del proveedor: delivered, opened, clicked, bounced, spam y unsubscribed se rellenan en las horas siguientes al envío.
POST /emails/campaigns/:id/send { "at": "2026-09-22T09:00:00+02:00" } → { "campaign": { "status": "scheduled", … } } (5 fichas; sin "at": envío inmediato, status "sending")
POST /emails/campaigns/:id/cancel → { "campaign": { "status": "draft", … } }
POST /emails/campaigns/:id/test → { "sent": true, "to": "tu@…", "error": null } (al creador de la clave, asunto con prefijo [Test]; 3 fichas)
GET /emails/campaigns/:id/preview → { "subject": "…", "html": "…", "text": "…" } (renderizado completo, marco y pie de baja incluidos)
Un envío es irreversible en cuanto la campaña pasa a sending: un asistente debe mostrar la vista previa y obtener el acuerdo del usuario antes de llamar a /send. Los webhooks email.campaign.sent y email.campaign.failed avisan del final del envío.
Un segmento es un filtro guardado sobre los miembros activos del espacio, resuelto en el momento del envío. Todos los filtros indicados deben cumplirse; users se añade al resultado.
POST { "name": "Nuevos silenciosos", "description": "…",
"filters": { "roles": ["member"], "groups": ["promo-2026"], "minLevel": 1, "maxLevel": 2, "joinedWithinDays": 30, "activeWithinDays": 14, "users": ["ext:u-42"] } }
→ 201 { "segment": { "id": "…", "name": "…", "description": "…", "memberCount": 12,
"filters": { "roles": ["member"], "groups": [ { "id", "slug", "name" } ], "minLevel": 1, "maxLevel": 2, "joinedWithinDays": 30, "activeWithinDays": 14, "users": [ { "id", "name" } ] },
"createdAt": "…", "updatedAt": "…" } }
POST /emails/segments/preview { "filters": { … } } → { "memberCount": 12, "sample": [ { "id", "name" } ], "capped": false, "filters": { … } } (no se guarda nada; 3 fichas)
GET /emails/segments/:id → { "segment": { …, "memberCount": recalculado, "sample": […], "capped": false } } (3 fichas)
roles: owner, admin, moderator, teacher, contributor, member; groups acepta identificadores o slugs; activeWithinDays se basa en la última apertura de la aplicación. Cincuenta segmentos por espacio; más allá de 5 000 miembros, capped pasa a true.
Textos de campaña reutilizables (asunto, texto de vista previa, cuerpo), que se pasan como templateId al crear una campaña. POST { "name", "subject", "previewText", "body", "format" } → 201 { "template": { "id", "name", "subject", "previewText", "body", "updatedAt" } }. Cincuenta por espacio.
El texto de los correos automáticos que envía el espacio, por tipo e idioma (kind: live_invitation por ahora; lang: fr, en, es). El marco (color, botón, enlaces de agenda, pie) sigue siendo el de la plataforma.
GET → { "templates": [ { "kind": "live_invitation", "lang": "es", "subject": "…", "body": "…", "custom": false, "updatedAt": null }, … ],
"variables": { "live_invitation": [ { "key": "prenom", "label": { "fr", "en", "es" }, "sample": { … } }, … ] },
"defaults": { "live_invitation": { "es": { "subject", "body" }, … } } }
PUT /emails/notifications/live_invitation/es { "subject": "…", "body": "…", "format": "html" } → { "template": { …, "custom": true } }
DELETE /emails/notifications/live_invitation/es → texto por defecto restablecido
GET /emails/notifications/live_invitation/es/preview?subject=…&body=… → { "subject", "html" } (valores de ejemplo; sin parámetros: el texto guardado)
Resumen automático de novedades (publicaciones, cursos, lecciones, directos y eventos próximos, retos) enviado a los miembros con la cadencia elegida.
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": "…" } } (actualización parcial)
GET /emails/digest/preview → { "subject", "html", "empty": false } (tal como saldría ahora; 3 fichas)
POST /emails/digest/send { "test": true } → { "sent": 1, "skipped": false, "error": null } (prueba: al creador de la clave; sin "test": a toda la audiencia, 5 fichas)
cadence: daily, weekly (weekday 0 = domingo … 6) o monthly (dayOfMonth 1 a 28); hour en hora local de timeZone; subject e intro aceptan {{prenom}} y {{espace}}, null restablece el texto de la plataforma.
Desde dónde salen los correos del espacio. Por defecto «vía LearnFloo»; con la opción de pago «dominio de envío», desde el dominio del cliente una vez verificados sus registros DNS. Activar la opción sigue haciéndose en la aplicación (se factura al propietario); el resto se controla aquí.
GET → { "option": true, "active": false, "configured": true, "defaultFrom": "invitations@learnfloo.com", "priceCents": 1000, "currency": "eur",
"sender": { "id": "…", "domain": "out.miescuela.com", "fromEmail": "hola@out.miescuela.com", "fromName": "Mi escuela", "replyTo": "hola@miescuela.com",
"dns": [ { "type": "TXT", "host": "…_domainkey.out.miescuela.com", "value": "k=rsa;…", "verified": false, "purpose": "dkim" },
{ "type": "CNAME", "host": "pm-bounces.out.miescuela.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.miescuela.com", "fromEmail": "hola@out.miescuela.com", "fromName": "Mi escuela", "replyTo": "hola@miescuela.com" } → 201, misma respuesta que GET
POST /emails/sender/check → { "dkimVerified": true, "returnPathVerified": true, …GET } (pide la verificación de los DNS)
PATCH /emails/sender { "fromEmail", "fromName", "replyTo" }
POST /emails/sender/test → { "sent": true, "from": "…", "error": null } (al creador de la clave; 3 fichas)
DELETE /emails/sender → { "removed": true } (vuelta a «vía LearnFloo»)
active vale true cuando los correos salen realmente del dominio (opción activa, ambos registros verificados, sin suspensión). suspendedAt: envío desde el dominio cortado por la regla de rebotes y quejas, a tratar con el soporte.
GET → { "user": { "id", "externalId", "name", "email", "image" }, "hasEmail": true, "subscribed": true, "unsubscribedAt": null, "unsubscribedFromCampaignId": null }
PUT { "subscribed": false } → mismo objeto actualizado
Suscripción de un miembro a los correos masivos del espacio (campañas y resumen), para leer o sincronizar desde tu plataforma: poner subscribed en false equivale a pulsar «darse de baja» (webhook email.unsubscribed). Los correos individuales (invitaciones a directos) no se ven afectados.
En lugar de consultar la API, recibe avisos: LearnFloo envía un POST JSON a la URL que elijas en cada evento suscrito. Hasta 10 webhooks por espacio.
POST https://tu-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": "mi-espacio" },
"data": { "session": { … objeto session … } } }
| Evento | data |
|---|---|
live.session.created, live.session.updated, live.session.started, live.session.ended, live.session.cancelled | { session } (ver GET /live/sessions/:id). Después de ended, llama a /attendance para la asistencia |
live.replay.ready | { session } con replayUrl rellenado (MP4 bruto, en cuanto termina el directo) |
live.conversion | { session, conversion } — inscripción notificada mediante /conversions |
live.poll.opened, live.poll.closed | { session, poll } — una encuesta o cuestionario lanzado y luego cerrado con sus resultados |
live.scene.changed | { session, scene } — una escena puesta en antena (scene a null: disposición automática) |
support.ticket.created | { ticket, author, message: { content, text, attachments } } |
support.ticket.message | { ticket, message: { id, content, text, attachments, fromStaff, author, createdAt } } — respuesta del usuario o del equipo (nunca las notas internas) |
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 } } — correo dejado en una captura de correo de un vídeo (source: lesson, o about para el vídeo de presentación) |
post.created | { post } |
comment.created | { comment, post: { id, title } } |
member.joined | { user, member } — nuevo miembro (se unió desde LearnFloo o lo creó la API) |
email.campaign.sent, email.campaign.failed | { campaign } — fin del envío de una campaña o de un resumen (kind), con recipientCount, stats.sent y error |
email.unsubscribed | { user, campaignId, unsubscribedAt } — un miembro se dio de baja de los correos masivos del espacio (enlace de baja, página o API) |
ping | Envío de prueba (POST /webhooks/:id/test) |
Los objetos user / author contienen { id, externalId, name, email, image }: externalId te permite encontrar a la persona en tu plataforma.
Responde 2xx en menos de 10 segundos (procesa después). Verifica la firma: HMAC-SHA256 del texto <t>.<cuerpo bruto> con el secreto del webhook, comparado con v1; rechaza si t tiene más de 5 minutos.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyLearnFloo(rawBody, signatureHeader, secret) {
const { t, v1 } = Object.fromEntries(signatureHeader.split(',').map((kv) => kv.split('=')))
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}
En caso de fallo (respuesta distinta de 2xx, tiempo agotado, error de red), el envío se reintenta tras 1 min, 5 min, 30 min y luego 2 h (5 intentos). Por tanto, un mismo evento puede llegar dos veces: usa id (o X-LearnFloo-Delivery) para eliminar duplicados. Tras 20 fallos consecutivos, el webhook se pone en pausa (active: false); reactívalo con PATCH cuando tu endpoint esté reparado.
| Llamada | Cuerpo / respuesta |
|---|---|
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: ["*"] para recibirlo todo. El secreto (de 16 a 128 caracteres, generado si falta) solo se devuelve al crearlo. URL https obligatoria |
GET/webhooks/:id | { "webhook": { … } } |
PATCH/webhooks/:id | { "url"?, "events"?, "description"?, "active"? }. active: true pone a cero el contador de fallos |
DELETE/webhooks/:id | { "deleted": true } |
POST/webhooks/:id/test | Envía un evento ping → 202 { "sent": true } |
GET/webhooks/:id/deliveries?limit=50 | Registro de envíos (30 días): { "deliveries": [ { "id", "event", "attempt", "status": "pending|success|failed", "responseStatus", "error", "createdAt", "deliveredAt" } ] } |
La descripción OpenAPI 3.1 de la API se genera desde el catálogo de herramientas MCP (una operación por ruta, operationId = nombre de la herramienta MCP), para generadores de clientes, herramientas de API y las Actions de los GPT de ChatGPT:
GET https://api.learnfloo.com/openapi.json toda la API (159 operaciones), seguridad por clave API u OAuth
GET https://api.learnfloo.com/openapi.json?preset=gpt las 30 operaciones del GPT LearnFloo (límite de ChatGPT por action), solo OAuth
GET https://api.learnfloo.com/openapi.json?tools=get_space,list_posts,create_post un subconjunto de herramientas
GET https://api.learnfloo.com/openapi.json?groups=live,courses familias enteras
Un GPT personalizado se crea con este archivo y un cliente OAuth confidencial (ver Servidor MCP); los pasos y las instrucciones del GPT están en el LearnFloo Agent Kit.
La misma API se expone como servidor MCP (Model Context Protocol) para los asistentes y agentes de IA: Claude (aplicación, claude.ai, Claude Code), ChatGPT, Cursor o cualquier cliente MCP. El asistente ve el espacio a través de herramientas con nombre (list_live_sessions, create_post, get_group_stats…), cada una equivalente a una ruta de esta referencia: mismos campos, mismas respuestas, mismos errores.
URL: https://api.learnfloo.com/mcp (transporte «Streamable HTTP», JSON-RPC 2.0, sin sesión)
Dos formas de conectarse, a elección del usuario según el nivel de seguridad deseado:
| OAuth (recomendado) | Clave API | |
|---|---|---|
| Principio | El asistente abre una página de LearnFloo: el usuario inicia sesión con su cuenta, elige el espacio, el nivel de acceso y las herramientas, y autoriza. Tokens de acceso de una hora renovados automáticamente, revocables. | Una clave del espacio (Autenticación), en la cabecera Authorization: Bearer lf_live_… o, para los clientes sin cabeceras, en la URL https://api.learnfloo.com/mcp?key=lf_live_…. |
| Quién puede | Propietario o administrador del espacio, con su propia cuenta | Cualquiera que tenga la clave |
| Atribución de las acciones | Al usuario que autorizó | Al creador de la clave |
| Riesgo | Ningún secreto en la URL ni en la configuración del cliente | Una clave en la URL puede acabar en registros; si se filtra, revócala |
| Dónde se gestiona | Configuración del espacio → API y MCP: claves y autorizaciones OAuth una al lado de otra, cada una con su nivel de acceso y sus herramientas abiertas | |
| Cliente | OAuth | Clave API |
|---|---|---|
| claude.ai, aplicación Claude | Configuración → Conectores → Añadir un conector personalizado, URL https://api.learnfloo.com/mcp, luego «Conectar»: se abre la página de autorización de LearnFloo | La misma pantalla, URL https://api.learnfloo.com/mcp?key=lf_live_…, sin autenticación |
| Claude Code | claude mcp add --transport http learnfloo https://api.learnfloo.com/mcp y luego /mcp para autenticarse | claude mcp add --transport http learnfloo https://api.learnfloo.com/mcp --header "Authorization: Bearer lf_live_…" |
| Cursor, Windsurf, otros | { "mcpServers": { "learnfloo": { "url": "https://api.learnfloo.com/mcp" } } } (el cliente inicia la autorización) | { "mcpServers": { "learnfloo": { "url": "https://api.learnfloo.com/mcp", "headers": { "Authorization": "Bearer lf_live_…" } } } } |
Servidor OAuth 2.1: descubrimiento /.well-known/oauth-protected-resource y /.well-known/oauth-authorization-server, registro dinámico de clientes POST /oauth/register (clientes públicos), GET /oauth/authorize (PKCE S256 obligatorio), POST /oauth/token (authorization_code, refresh_token con rotación), POST /oauth/revoke. Ámbitos read y full; el nivel realmente concedido es el elegido en la página de autorización, devuelto en scope. Una respuesta 401 del servidor MCP lleva WWW-Authenticate: Bearer resource_metadata=… para que el cliente descubra el servidor OAuth.
Clientes confidenciales. Para un GPT de ChatGPT o una integración del lado del servidor, el equipo de LearnFloo registra un cliente con client_secret (client_secret_post o client_secret_basic, además de PKCE) y URL de retorno con comodín (https://chatgpt.com/aip/*/oauth/callback). Los tokens de acceso obtenidos (lfo_…) los aceptan el servidor MCP y la API REST (Authorization: Bearer lfo_…): en la API REST una autorización solo abre las rutas de las herramientas elegidas en la página de autorización (una herramienta = una ruta); si no, 403.
?key=) es visible en el historial y los registros del cliente: resérvala para los clientes sin cabeceras cuando no se quiera OAuth, crea una clave dedicada (con el nombre del asistente) y revócala ante la menor duda. Una clave MCP de solo lectura basta para analizar, resumir y responder preguntas.Herramientas abiertas. Para cada clave (en la configuración) y cada autorización OAuth (en la página de autorización, modificable después en la configuración), un nivel de acceso (solo lectura o completo) y la lista de herramientas MCP abiertas: todas las del nivel (por defecto) o una selección (por ejemplo, un asistente de realización que solo ve las escenas y las encuestas de un directo; un asistente de soporte que solo ve los tickets). tools/list solo devuelve las herramientas abiertas; una herramienta cerrada se rechaza como desconocida.
Uso y costes. Cada mensaje JSON-RPC consume los tokens de la ruta subyacente (límite de uso); initialize, ping y tools/list consumen uno. Un error de la API (400, 403, 404…) vuelve como resultado isError con su mensaje; una clave rechazada o un límite alcanzado, como error JSON-RPC (HTTP 401 o 429, Retry-After). Los objetos devueltos están en structuredContent y, como texto, en content.
Lo que no hace el MCP: el envío de archivos (POST /attachments sigue siendo una ruta REST) y los webhooks entrantes. Las llamadas de escritura hechas por un asistente se atribuyen al creador de la clave, salvo author o user explícito, como en REST.
Recetas (prompts) y kit para asistentes. El servidor también sirve prompts MCP (prompts/list, prompts/get, argumento opcional request): nueve recetas de trabajo para el propietario de un espacio (informe del espacio, preparación de un directo, control de escenas, debrief, construcción de un curso, animación del muro, seguimiento de miembros, tickets de soporte, integración), propuestas por los clientes que muestran los prompts (Claude, Cursor…) sin instalar nada. Las mismas recetas existen como skills (formato abierto Agent Skills) y como plugin de Claude Code en el LearnFloo Agent Kit: claude plugin marketplace add learnfloo/agent-kit y luego claude plugin install learnfloo@learnfloo; script de instalación para Codex, Cursor y Gemini CLI. Cada receta muestra el contenido antes de escribir y se limita a la lectura con una clave de solo lectura.
159 herramientas, por familia. Columna «Acceso»: lectura (abierta a las claves de solo lectura) o escritura.
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
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. | lectura | 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. | lectura | GET /space/usage |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_users | Members of the space, paginated (200 max per page), with role, status, XP and level. | lectura | GET /users |
get_user | One user with membership (role, status, XP, level) and stats (lessons completed, lives joined, posts, badges). | lectura | GET /users/:id |
create_user | Creates (or updates, idempotent on externalId) a user of your platform and adds them as member of the space. | escritura | 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. | escritura | PATCH /users/:id |
remove_user | Removes the member from the space (account and history kept). | escritura | 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. | escritura | POST /users/:id/entry |
get_user_progress | Progress of a user in every course of the space (lessons completed, percentage). | lectura | GET /users/:id/progress |
get_user_xp | Total XP, level and latest XP events of a user in the space. | lectura | 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. | escritura | POST /users/:id/xp |
get_user_badges | Badges awarded to a user. | lectura | GET /users/:id/badges |
get_leaderboard | Members ranked by XP (500 max), optionally within one group. | lectura | GET /leaderboard |
list_badges | Badges of the space with their criteria and XP reward. | lectura | GET /badges |
list_challenges | Challenges of the space (objective, dates, status, participants). | lectura | GET /challenges |
get_challenge | One challenge with its participants and their progress. | lectura | GET /challenges/:id |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_live_sessions | Live sessions of the space, newest first (200 max), with status, dates, replay and counters. | lectura | GET /live/sessions |
get_live_session | One live session: status, schedule, participants now, recording and replay, chat mode, conversions, active scene. | lectura | 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. | escritura | 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. | escritura | PATCH /live/sessions/:id |
cancel_live_session | Cancels a scheduled live (calendar event removed). A running or ended live cannot be cancelled. | escritura | 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. | escritura | 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. | lectura | GET /live/sessions/:id/attendance |
get_live_participants | Instant state of the people registered on a live: connected, spectator, hand raised, role. | lectura | GET /live/sessions/:id/participants |
get_live_chat | Chat messages of a live, oldest first (2000 max), kept after the live. | lectura | GET /live/sessions/:id/chat |
get_live_replay | The replay of a live: MP4, HLS and embed URLs, thumbnail, duration, linked lesson, views. | lectura | 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. | escritura | POST /live/sessions/:id/conversions |
list_live_invites | Shareable invitation links of a live (speaker, viewer, assistant, moderator). | lectura | 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). | escritura | POST /live/sessions/:id/invites |
revoke_live_invite | Revokes an invitation link. | escritura | 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. | escritura | 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. | escritura | PUT /live/sessions/:id/viewers |
stage_chat_message | Shows a chat message on the scene (message slot, else bottom left); null removes it. | escritura | PUT /live/sessions/:id/chat/stage |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
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. | lectura | GET /live/sessions/:id/scenes |
get_live_scene | One scene of a live. | lectura | 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. | escritura | 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. | escritura | PATCH /live/sessions/:id/scenes/:sceneId |
delete_scene | Removes a scene (automatic layout if it was on air). Returns the control room. | escritura | 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. | escritura | 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). | escritura | PUT /live/sessions/:id/scenes/active |
reorder_scenes | Sets the order of the scenes of a live. | escritura | 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. | escritura | 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). | escritura | 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. | escritura | 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. | escritura | 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. | escritura | 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. | escritura | PATCH /live/sessions/:id/overlays/:overlayId |
delete_overlay | Removes an overlay from a scene. | escritura | DELETE /live/sessions/:id/scenes/:sceneId/overlays/:overlayId |
add_slot | Adds a slot (host, guest, screen, media, embed) to a scene; position 0 = main. | escritura | POST /live/sessions/:id/scenes/:sceneId/slots |
update_slot | Changes a slot of a scene (other media, guest, fit, geometry in free layout, position). | escritura | PATCH /live/sessions/:id/scenes/:sceneId/slots/:slotId |
delete_slot | Removes a slot from a scene. | escritura | DELETE /live/sessions/:id/scenes/:sceneId/slots/:slotId |
list_scene_templates | Scene templates of the space, reusable in every live. | lectura | 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. | escritura | POST /scene-templates |
delete_scene_template | Deletes a scene template (id or name). | escritura | DELETE /scene-templates/:id |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_live_polls | Polls and quizzes of a live with their counts, kept after the live. | lectura | GET /live/sessions/:id/polls |
get_live_poll | One poll or quiz with its results. | lectura | 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. | escritura | POST /live/sessions/:id/polls |
update_poll | Changes a draft poll (same fields as creation). | escritura | PATCH /live/sessions/:id/polls/:pollId |
delete_poll | Deletes a poll and its votes, whatever its status. | escritura | DELETE /live/sessions/:id/polls/:pollId |
open_poll | Opens the vote to the participants (one poll open at a time, running live only). | escritura | POST /live/sessions/:id/polls/:pollId/open |
close_poll | Closes the vote; results shown unless showResults is false. | escritura | 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). | escritura | 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. | escritura | 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). | escritura | POST /live/sessions/:id/polls/:pollId/band |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_support_tickets | Tickets of one external user (externalId) or of the whole space (scope: "space", team dashboard), newest first. | lectura | 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. | lectura | 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. | escritura | 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. | escritura | 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). | escritura | POST /support/tickets/:id/status |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_groups | Groups of the space (classes, teams, cohorts… see groupLabels of get_space) with member counts and automatic rules. | lectura | GET /groups |
get_group | One group (id or slug) with its members and their group role (leader or member). | lectura | GET /groups/:id |
create_group | Creates a group, manual or automatic (rule: level_min, streak_days, challenge_joined, challenge_completed, badge, course_completed). | escritura | POST /groups |
update_group | Changes name, slug, description, color or rule (null = manual group) of a group. | escritura | PATCH /groups/:id |
delete_group | Deletes a group; the content addressed only to it becomes visible to the whole space. | escritura | DELETE /groups/:id |
add_group_members | Adds members of the space to a group (500 per call, idempotent), as member or leader. | escritura | POST /groups/:id/members |
remove_group_member | Removes one member from a group. | escritura | DELETE /groups/:id/members/:userId |
get_user_groups | Groups of a user in the space, with their group role. | lectura | GET /users/:id/groups |
set_user_groups | Replaces the whole list of groups of a user (sync of classes from your platform). | escritura | PUT /users/:id/groups |
get_group_leaderboard | Members of a group ranked by XP. | lectura | 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. | lectura | GET /groups/:id/stats |
list_group_documents | Shared documents of a group, newest first. | lectura | GET /groups/:id/documents |
add_group_document | Adds a document (file stored via POST /api/v1/attachments, or any https link) to a group. | escritura | POST /groups/:id/documents |
delete_group_document | Deletes a document of a group. | escritura | DELETE /groups/:id/documents/:docId |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_courses | Published courses of the space (includeUnpublished for drafts too), with lesson counts and audience. | lectura | GET /courses |
get_course | A course (id or slug) with its modules and lessons; withContent adds the HTML of the lessons. | lectura | GET /courses/:id |
create_course | Creates a course (draft by default). The author, when given, must be owner, admin or teacher. | escritura | POST /courses |
update_course | Changes title, description, cover, published state or audience of a course. | escritura | PATCH /courses/:id |
delete_course | Deletes a course with its modules, lessons and progress. | escritura | DELETE /courses/:id |
create_module | Adds a module (section) to a course. | escritura | 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). | escritura | POST /courses/:id/lessons |
get_lesson | One lesson with its content. | lectura | GET /lessons/:id |
update_lesson | Changes title, content, module, video or order of a lesson. | escritura | PATCH /lessons/:id |
delete_lesson | Deletes a lesson. | escritura | 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. | lectura | GET /courses/:id/progress |
complete_lesson | Marks a lesson as completed for a learner, as if validated in LearnFloo (XP, quests, badges, webhook). Idempotent. | escritura | POST /lessons/:id/complete |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_posts | Feed of the space, newest first, paginated; categories general, announcements, question, wins, resource. | lectura | GET /posts |
get_post | A post with its comments (oldest first, parentId for replies). | lectura | GET /posts/:id |
create_post | Creates a post in the feed (mentions, XP and notifications as in the app). pinned is for moderators. | escritura | POST /posts |
update_post | Changes title, category, content, pinned state or audience of a post. | escritura | PATCH /posts/:id |
delete_post | Deletes a post with its comments and reactions. | escritura | DELETE /posts/:id |
create_comment | Adds a comment (or a reply with parentCommentId) to a post. | escritura | POST /posts/:id/comments |
delete_comment | Deletes a comment and its replies. | escritura | DELETE /comments/:id |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_events | Calendar events by start date (scheduled lives included with liveSessionId). | lectura | GET /events |
get_event | One calendar event. | lectura | GET /events/:id |
create_event | Creates a calendar event (lives are created with create_live_session). | escritura | POST /events |
update_event | Changes an event (an event linked to a live is changed with update_live_session). | escritura | PATCH /events/:id |
delete_event | Deletes a calendar event. | escritura | DELETE /events/:id |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_videos | Studio recordings and imported live replays, newest first, paginated. | lectura | GET /videos |
get_video | One video with its playback URLs (ready videos only), duration, thumbnail, linked lesson or live. | lectura | GET /videos/:id |
update_video | Changes the title or description of a video. | escritura | PATCH /videos/:id |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_media | Images, PDF, videos, embeds, stickers and sounds of the space used in scenes and the studio. | lectura | GET /media |
list_media_folders | Folders of the media library. | lectura | GET /media/folders |
get_media | One item of the media library. | lectura | 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). | escritura | POST /media |
create_media_folder | Creates a folder in the media library. | escritura | POST /media/folders |
update_media | Changes the title or the folder (null for the root) of a media item. | escritura | PATCH /media/:id |
delete_media | Deletes a media item (and its file when hosted by LearnFloo). | escritura | DELETE /media/:id |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
list_webhooks | Outgoing webhooks of the space with their events and delivery state. | lectura | GET /webhooks |
get_webhook | One webhook. | lectura | GET /webhooks/:id |
create_webhook | Subscribes an https URL to events (["*"] for all). The signing secret is returned once. | escritura | POST /webhooks |
update_webhook | Changes url, events, description or active state (active: true resets the failure count). | escritura | PATCH /webhooks/:id |
delete_webhook | Deletes a webhook. | escritura | DELETE /webhooks/:id |
test_webhook | Sends a ping event to a webhook. | escritura | POST /webhooks/:id/test |
list_webhook_deliveries | Delivery log of a webhook over 30 days (attempts, status, errors). | lectura | GET /webhooks/:id/deliveries |
| Herramienta | Descripción | Acceso | Ruta |
|---|---|---|---|
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. | lectura | GET /emails/campaigns |
get_email_campaign | One campaign with its content, audience and statistics. | lectura | 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. | lectura | 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. | escritura | POST /emails/campaigns |
update_email_campaign | Changes a draft (subject, preview text, body, audience). Scheduled or sent campaigns cannot be modified. | escritura | 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. | escritura | POST /emails/campaigns/:id/send |
cancel_email_campaign | A scheduled campaign goes back to draft. | escritura | POST /emails/campaigns/:id/cancel |
test_email_campaign | Sends the campaign to the creator of the API key only, subject prefixed with [Test]. | escritura | POST /emails/campaigns/:id/test |
delete_email_campaign | Deletes a campaign (a scheduled one is unscheduled; a campaign being sent cannot be deleted). | escritura | 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. | lectura | GET /emails/segments |
get_email_segment | A segment with its member count computed now and a sample of names. Costs 3 tokens. | lectura | 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. | lectura | POST /emails/segments/preview |
create_email_segment | Saves an audience for campaigns and the digest. | escritura | POST /emails/segments |
update_email_segment | Changes the name, description or filters of a segment. | escritura | PATCH /emails/segments/:id |
delete_email_segment | Deletes a segment (campaigns keep their history). | escritura | DELETE /emails/segments/:id |
list_email_templates | Reusable campaign texts of the space (subject, preview text, body). | lectura | GET /emails/templates |
create_email_template | Saves a campaign text to start future campaigns from (templateId of create_email_campaign). | escritura | POST /emails/templates |
update_email_template | Changes a reusable campaign text. | escritura | PATCH /emails/templates/:id |
delete_email_template | Deletes a reusable campaign text. | escritura | 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. | lectura | 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. | lectura | 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). | escritura | PUT /emails/notifications/:kind/:lang |
reset_email_notification | Back to the platform’s default wording for that kind and language. | escritura | 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. | lectura | 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. | lectura | 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. | escritura | 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. | escritura | 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. | lectura | 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. | escritura | POST /emails/sender |
update_email_sender | Changes the display name, sending address (on the domain) or reply address. | escritura | PATCH /emails/sender |
check_email_sender_domain | Asks the mail provider to look the DKIM and Return-Path records up and records the result. | escritura | POST /emails/sender/check |
test_email_sender | Sends the key creator a test e-mail with the space’s current sender. | escritura | POST /emails/sender/test |
remove_email_sender_domain | Removes the sending domain: e-mails go back to leaving via LearnFloo. | escritura | DELETE /emails/sender |
get_email_preferences | Whether a member still receives the space’s bulk e-mails (campaigns, digest). | lectura | 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). | escritura | PUT /users/:id/email-preferences |
Directorio de los espacios públicos, sin clave ni límite de uso, CORS abierto (utilizable desde un navegador). URL completa: https://api.learnfloo.com/public/spaces. Respuesta en caché 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" } ] }
Página «Acerca de» de un espacio público: la ficha anterior más space.about (HTML), space.video (vídeo de presentación: embedUrl, playbackUrl, thumbnailUrl), owner (name, image), adminCount, las 30 últimas reviews (rating, text, createdAt, author) y courses, los cursos de acceso libre del espacio (slug, title, description, coverImage, lessonCount, url de la página pública). 404 si el espacio no existe o es privado.
Cursos de acceso libre. En un espacio público, un curso publicado y abierto a todos los miembros puede marcarse como «legible en la web» por su autor (página del curso → Editar), o todos los cursos del espacio de una vez (Ajustes → Acceso y página Acerca de): se muestra entonces, con sus lecciones y vídeos, en www.learnfloo.com/es/c/<slug>/cursos/<curso>/ (y /c/…/cours/…, /en/c/…/courses/…), en solo lectura, con un botón para unirse al espacio; estas páginas aparecen en www.learnfloo.com/c/sitemap.xml.
Estos dos puntos de entrada alimentan las páginas www.learnfloo.com/es/comunidades/ y www.learnfloo.com/es/c/<slug>/. Unirse a un espacio siempre se hace en la app (appUrl, parámetro ?join=1); para un LMS, usa mejor el SSO /users/:id/entry.
{ "error": "message" }
| Código | Caso |
|---|---|
400 | Cuerpo no válido, campo que falta, sesión terminada (Session is over), sesión desconocida, anfitrión no autorizado, regla de negocio no respetada |
401 | Clave ausente, mal formada o revocada |
403 | Acción prohibida al autor elegido (rol insuficiente), propietario del espacio |
404 | Ruta desconocida, recurso desconocido o perteneciente a otro espacio, ticket perteneciente a otro usuario |
405 | Método no admitido en esta ruta |
413, 415 | Adjunto demasiado grande o de un tipo no admitido |
429 | Límite de uso alcanzado: respeta Retry-After |
Del lado del alumno, una URL de entrada caducada o ya usada muestra una página explícita de LearnFloo; basta con un nuevo clic en tu plataforma (nueva llamada a /live/entry o /users/:id/entry).
Nueva pestaña (recomendado, sobre todo para Safari):
const { url } = await fetch('/mi-backend/live-entry', { method: 'POST' }).then((r) => r.json())
window.open(url, '_blank')
Iframe:
<iframe src="URL DEVUELTA POR /live/entry"
allow="camera; microphone; display-capture; autoplay; fullscreen" allowfullscreen
style="width:100%;height:80vh;border:0"></iframe>
allow es imprescindible: sin él, el navegador rechaza el micro, la cámara y la pantalla compartida en un iframe de otro dominio. Safari y Firefox en modo estricto pueden bloquear el almacenamiento en un iframe de terceros; en ese caso, pasa a una nueva pestaña.Bajo petición, la sala del directo se sirve en un subdominio tuyo (ej.: live.tu-dominio.com). Una sola acción por tu parte: un registro DNS CNAME del subdominio hacia learnfloo-v2.b-cdn.net, y avisar a LearnFloo. Después, las URL de /live/entry están en tu dominio, sin cambios en tu código. Solo se sirven allí las páginas del directo; las URL de /users/:id/entry y los enlaces de invitación siguen en 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
}
// Al hacer clic en «Unirse al directo»
export const liveEntry = (sessionId, learner) =>
call('POST', '/live/entry', { sessionId, user: { externalId: String(learner.id), name: learner.fullName }, role: 'viewer' }).then((d) => d.url)
// Al hacer clic en «Abrir la comunidad»
export const spaceEntry = (learner) =>
call('POST', `/users/ext%3A${encodeURIComponent(learner.id)}/entry`, { name: learner.fullName, redirect: 'posts' }).then((d) => d.url)
// Después del directo: repetición y asistencias validadas
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) }
}
// Una vez: recibir aviso del final de los directos y de las repeticiones
export const subscribe = () =>
call('POST', '/webhooks', { url: 'https://tu-backend.com/learnfloo', events: ['live.session.ended', 'live.replay.ready', 'lesson.completed'] })
| Fecha | Cambio |
|---|---|
| 2026-09-16 | Cursos de acceso libre (SEO): un curso de un espacio público marcado «legible en la web» se muestra en www.learnfloo.com/es/c/<slug>/cursos/<curso>/ con sus lecciones, en tres idiomas, y aparece en /c/sitemap.xml; GET /public/spaces/:slug devuelve estos cursos en courses. |
| 2026-09-15 | Correos: toda la capa de correo está disponible por la API y MCP (familia «Correos», 35 herramientas): campañas (creación, vista previa, prueba, envío inmediato o programado, cancelación, estadísticas), segmentos y recuento de audiencia, textos reutilizables, correos automáticos por tipo e idioma, resumen automático (ajustes, vista previa, envío), dominio de envío (alta, DNS, verificación, prueba, retirada), preferencias de correo de un miembro; webhooks email.campaign.sent, email.campaign.failed, email.unsubscribed. |
| 2026-09-15 | Sala de espera y pantalla de cierre: en la aplicación (página «Preparar», pestaña «Espera y cierre»), cuenta atrás, mensaje de bienvenida, imagen o vídeo teaser, cuestionario previo; oferta con botón rastreado (enlace con lf_live, lf_user, lf_ext como los CTA del chat, inscripciones notificadas por /conversions), mostrada a la audiencia durante el directo y en la pantalla de cierre, cuenta atrás de la oferta; encuesta de cierre. API: phase (live, waiting, closing) en las encuestas, al crear, modificar y leer. |
| 2026-09-15 | OpenAPI 3.1 generada desde el catálogo MCP (GET /openapi.json, ?preset=gpt, ?tools=, ?groups=); clientes OAuth confidenciales (secreto + PKCE, URL de retorno con comodín); tokens OAuth aceptados por la API REST, limitados a las herramientas elegidas en la autorización; GPT «LearnFloo» para ChatGPT (instrucciones y pasos en el kit). |
| 2026-09-15 | MCP: prompts (recetas de trabajo, prompts/list y prompts/get, argumento request) y LearnFloo Agent Kit (plugin de Claude Code, skills para Codex, Cursor y Gemini CLI). Versión del servidor 1.1.0. |
| 2026-09-13 | Escenas: campo jingle (cortinilla: se reproduce durante su duración y luego la escena interrumpida se reanuda donde estaba). |
| 2026-09-13 | Escenas: el encadenamiento automático espera al final de los vídeos y sonidos iniciados con la escena (sin bucle); campo mediaSec (duración del archivo en segundos) en los huecos de vídeo y en audios. |
| 2026-09-13 | Escenas: varios sonidos por escena, campo audios (lista, 6 como máximo); el antiguo campo audio sigue aceptado en escritura, las escenas devuelven ahora audios. |
| 2026-09-13 | Escenas: aspecto de las cámaras, campos shape (forma), filter (filtro), bgMode, bgBlur, bgImage (fondo aplicado en el dispositivo de la persona); volume y muted también valen para las cámaras (sonido al aire). |
| 2026-09-13 | Escenas: campo framed (enmarcado: margen alrededor de los huecos, el fondo sigue visible), al crear y al modificar. |
| 2026-09-13 | Reproductor de vídeo: webhook video.lead (correo dejado en un vídeo); GET /groups/:id/stats devuelve videoPct por miembro (parte de los vídeos de lección del grupo realmente vista) y avgVideoPct, videoLessonCount en summary. |
| 2026-09-13 | Directos: regla de la repetición. Campos replay (all, attendees, groups, level, none), replayGroups y replayMinLevel en POST / PATCH /live/sessions, devueltos como replay, replayGroupIds, replayMinLevel. PATCH también acepta mode y funciona durante y después del directo para el título, la descripción, los grupos y la repetición. |
| 2026-09-12 | Tarifa única por uso alineada con la tabla pública (www.learnfloo.com): GET /space/usage también devuelve simulcastHours (horas de emisión simultánea × destinos) y subtitleHours (horas subtituladas, contadas cuando la generación pase al nuevo motor). |
| 2026-09-12 | MCP: conexión OAuth 2.1 (/.well-known/oauth-authorization-server, /oauth/register, /oauth/authorize, /oauth/token, /oauth/revoke): el usuario autoriza al asistente en una página de LearnFloo eligiendo espacio, nivel de acceso y herramientas; la autorización se ve y se revoca en la configuración, junto a las claves. Alternativa a la clave en la URL, a elección del usuario. |
| 2026-09-12 | Servidor MCP https://api.learnfloo.com/mcp para los asistentes de IA (Claude, ChatGPT, Cursor…), con las mismas claves: una herramienta por ruta de la API. Claves con nivel de acceso (solo lectura o completo: una escritura con una clave de solo lectura devuelve 403) y lista de herramientas MCP abiertas ajustable por clave en la configuración del espacio. Las claves existentes siguen con acceso completo y todas las herramientas. |
| 2026-09-12 | Escenas: encadenamiento automático. Campos durationSec y nextSceneId en las escenas, PUT /live/sessions/:id/scenes/auto { enabled, loop }, objeto autoAdvance en la realización. Cada cambio automático emite live.scene.changed como un cambio manual. |
| 2026-09-11 | Escenas: widget viewers (contador de espectadores) y PUT /live/sessions/:id/viewers para añadirle espectadores; extraViewers en el objeto sesión. |
| 2026-09-11 | Escenas: estilo de los huecos poll y message (colores, opacidad del fondo, tamaño del texto, redondeo, borde, sombra, fuente, elementos mostrados). |
| 2026-09-11 | Escenas: opciones de los vídeos autoplay, once, startSec, durationSec, volume; startSec y durationSec también en el sonido de escena (audio). |
| 2026-09-11 | Escenas: transiciones fade, black, slide-*, zoom, wipe, blur y duración transitionMs. |
| 2026-09-11 | Escenas: campo audio (sonido de la escena: archivo audio de la mediateca o URL, autoplay, once, loop, volume), controlado con PUT …/media/:mediaId. Mediateca: kind audio. |
| 2026-09-11 | Encuestas y cuestionarios: /live/sessions/:id/polls (preparar, modificar, eliminar), …/open, …/close, PUT …/results, …/band; kind: "quiz" con correct. Resultados conservados después del directo. Webhooks live.poll.opened y live.poll.closed. En la aplicación: pestaña «Encuestas» del directo y de «Preparar», para el anfitrión, el staff y los roles moderator y assistant. En antena: huecos poll y message en las escenas (preparados con antelación), PUT …/polls/:pollId/stage y PUT …/chat/stage para mostrar una encuesta o un mensaje del chat en el vídeo. |
| 2026-09-11 | Directos: rol moderator (moderación del chat, mensajes preparados, mensajes privados) aceptado por /invites y PUT …/participants/:userId/role. Los mensajes eliminados por un moderador y los mensajes privados no aparecen en GET …/chat. |
| 2026-09-11 | Escenas: campo locked (candado); una escena o plantilla bloqueada rechaza modificaciones y eliminación hasta { "locked": false }. |
| 2026-09-11 | Escenas y realización por API: un directo se dirige como un estudio. /live/sessions/:id/scenes (lista, creación, modificación, eliminación, orden), puesta en antena (…/activate, PUT …/scenes/active), stickers, rótulos y huecos modificables uno a uno (…/overlays/:overlayId, …/slots/:slotId, y PATCH /live/sessions/:id/overlays/:overlayId para el mismo rótulo en todas las escenas), rótulos efímeros POST …/bands, control de los PDF y vídeos PUT …/media/:mediaId, plantillas /scene-templates, rol de un participante PUT …/participants/:userId/role (incluido assistant, la realización, también aceptado por /invites). Mediateca: POST /media por URL, GET, PATCH, DELETE /media/:id, carpetas /media/folders. Webhook live.scene.changed; session gana activeSceneId y sceneCount. |
| 2026-09-10 | Oferta única: desaparece la tabla Gratis / Starter / Growth / Scale / Evento. GET /space/usage devuelve ahora las seis unidades por uso (units), lo incluido, usageCents, el tope de la cuenta y liveAllowed; plan, planLabel, eventCredits y las cuotas ya no se devuelven, ni plan en GET /space. El límite de uso depende de la cuenta del propietario (60 o 300 peticiones por minuto). |
| 2026-09-10 | Monedas: los espacios de pago llevan priceCurrency (eur o usd) en GET /space, GET /public/spaces y GET /public/spaces/:slug; price se formatea en la moneda. POST /live/sessions/:id/conversions acepta currency junto a amountCents (devuelto en la conversión y en el webhook live.conversion). |
| 2026-09-10 | Inscripciones durante el directo: POST /live/sessions/:id/conversions para notificar una compra hecha desde el chat (anuncio en el chat, rótulo en la escena, contador), parámetros lf_live, lf_user, lf_ext añadidos a los enlaces de los botones, webhook live.conversion, campos conversionCount y ctaClickCount en la sesión, kind en los mensajes del chat. |
| 2026-09-10 | Chat de los directos: los mensajes del anfitrión pueden llevar imageUrl y link (botón de llamada a la acción) en GET /live/sessions/:id/chat. |
| 2026-09-10 | Directos: campo chat (open, closed, off) al crear, al modificar y en lectura, para abrir el chat al entrar o desactivarlo. El anfitrión puede cambiarlo durante el directo (webhook live.session.updated). |
| 2026-09-10 | Grupos (clases, equipos, promociones… vocabulario por espacio): /groups (lista, creación, modificación, eliminación), /groups/:id/members, GET y PUT /users/:id/groups para sincronizar las clases desde un LMS. Por grupo: /groups/:id/leaderboard, seguimiento de los miembros /groups/:id/stats, documentos compartidos /groups/:id/documents; /leaderboard?group=. Grupos automáticos: campo rule (nivel alcanzado, racha de días, reto unido o terminado, insignia, curso terminado). Audiencia: campo groups al crear y modificar cursos, publicaciones, eventos y directos (groupIds en lectura), filtro ?group= en sus listas. GET /space gana groupCount y groupLabels. |
| 2026-09-10 | Espacios públicos o privados, gratuitos o de pago: GET /space gana priceCents, priceInterval, category, aboutUrl, rating, reviewCount; accessType vale ahora free o paid. Nuevos puntos de entrada sin clave: GET /public/spaces (directorio) y GET /public/spaces/:slug (página «Acerca de», opiniones). Los miembros de un espacio de pago que cancelan pasan a status: "expired" en /users. |
| 2026-09-09 | Límite de uso por clave según la oferta (cabeceras X-RateLimit-*, 429 + Retry-After). Webhooks salientes firmados (/webhooks, 13 eventos, reintentos). Directos: PATCH y DELETE /live/sessions/:id, /participants, /chat, /replay, enlaces de invitación /invites; session gana maxParticipants, replayViews, hlsStatus, hostId, createdAt. Soporte: scope=space (todos los tickets), respuestas y estados del lado del equipo (staff), /attachments genérico. Espacio: GET /space ampliado, /space/usage. Miembros: /users (lista, creación, modificación, retirada), SSO /users/:id/entry, /users/:id/progress, XP (GET y POST /users/:id/xp), insignias, /leaderboard, /challenges. Cursos: /courses, módulos, lecciones, /courses/:id/progress, /lessons/:id/complete. Muro: /posts y comentarios. Calendario: /events. Vídeos del estudio: /videos. Mediateca: /media. Paginación por cursor; 405 en método desconocido. Las rutas /entry y /sessions se documentan ahora bajo /live/… (sin cambios). |
| 2026-09-07 | Webinars: la audiencia mira un flujo HLS (latencia de 10 a 20 s) y ya no se conecta a la sala; un espectador puede pedir la palabra y entra en la sala cuando el anfitrión lo acepta. summary de /sessions/:id/attendance: se añaden spectatorHours e interactiveHours. |
| 2026-09-07 | Soporte: /support/tickets (GET, POST), /support/tickets/:id, …/messages, …/status, /support/attachments, para abrir y seguir tickets en nombre de un usuario externo (plugin de WordPress). GET /space para probar una clave. |
| 2026-09-07 | GET /sessions/:id/attendance: se añade summary y, por participante, source, leftAt, watchSec, watchPct, connections (sustituye a lastSeenAt). Repetición disponible en cuanto termina el directo, recodificada después. Publicación de esta página. |
| 2026-09-06 | Errores devueltos en JSON limpio { "error" }. Dominio personalizado por espacio para las páginas del directo. API servida en api.learnfloo.com. |
| 2026-09-05 | Primera versión: /entry, /sessions (POST, GET), /sessions/:id, /sessions/:id/attendance. |
Preguntas: equipo de LearnFloo.