LearnFloo API

Français · English · Español

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

Autenticación

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
La clave es un secreto de servidor a servidor. No la expongas nunca en un navegador ni en un repositorio. Una clave revocada se rechaza de inmediato (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.

Límite de uso

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):

CuentaPeticiones por minuto y por clave
Gratuita (facturación no activada)60
Facturación activa300

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.

Convenciones

Cómo funcionan los directos

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

POST/live/entry

URL de entrada personal para un alumno.

{
  "sessionId": "p97…",
  "user": { "externalId": "lms-user-42", "name": "Alice Martin" },
  "role": "viewer"
}
CampoDescripción
sessionIdIdentificador del directo (ver GET /live/sessions)
user.externalIdIdentificador estable del alumno en tu plataforma, 200 caracteres máx.
user.nameNombre mostrado a los demás participantes, actualizado en cada llamada
roleviewer (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".

POST/live/sessions

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"
}
CampoDescripción
titleObligatorio
modebroadcast (webinar: solo publican el anfitrión y los ponentes) o conference (todos publican). Por defecto broadcast
scheduledAtFecha ISO 8601 o marca de tiempo en milisegundos. Obligatorio
durationMinDuración prevista, para el calendario. Por defecto 60
maxParticipantsDe 2 a 1000. Por defecto 100
recordingEnabledGrabación automática. Por defecto true
chatChat 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
hostEmailOpcional. Cuenta de LearnFloo del formador, miembro del espacio con el rol owner, admin, moderator o teacher. Por defecto, el creador de la clave
groupsOpcional. 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
replayOpcional. 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»
replayGroupsCon replay: "groups": ids o slugs de los grupos, al menos uno
replayMinLevelCon 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.

GET/live/sessions

Sesiones del espacio, las más recientes primero, 200 máx. Filtros opcionales ?status=scheduled|live|ended|cancelled y ?group=<id o slug>.

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

GET/live/sessions/:id

{
  "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"
  }
}
CampoDescripción
statusscheduled, live, ended, cancelled. Útil para mostrar «Pronto», «Unirse» o «Repetición»
participantCountPersonas conectadas en este momento (0 fuera del directo)
recordingStatusrecording, processing, ready, failed o null
replayUrlMP4 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
hlsStatusFlujo de audiencia del webinar: starting, live, ended, failed o null
chatopen, closed o off, ver POST
conversionCount, ctaClickCountInscripciones notificadas durante el directo y clics en los botones del chat, ver /conversions
groupIdsGrupos a los que está reservado el directo (vacío = todo el espacio)
replay, replayGroupIds, replayMinLevelQuié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
El MP4 de la repetición rechaza las peticiones sin cabecera 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/live/sessions/:id y DELETE/live/sessions/:id

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.

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

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…" }
  ]
}
CampoDescripción
externalIdTu identificador enviado en /live/entry. null para las personas llegadas directamente desde LearnFloo
sourcelms (entró por tu plataforma), invite (enlace de invitación), member (miembro de LearnFloo)
rolehost, speaker, viewer
joinedAt, leftAtPrimera entrada y última salida. leftAt es null mientras la persona está en la sala
watchSecTiempo real pasado en la sala, sumando todas las conexiones, limitado a la duración del directo
watchPctParte del directo seguida, de 0 a 100. El campo que hay que usar para validar una asistencia (ej.: >= 80)
connectionsNú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.

GET/live/sessions/:id/participants

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.

GET/live/sessions/:id/chat

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).

POST/live/sessions/:id/conversions

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"
}
CampoDescripción
externalIdIdentificador del alumno en tu LMS (el de /users). Se encuentra en el enlace del botón, ver más abajo
userIdO el identificador de LearnFloo (lf_user del enlace)
emailO el email de la cuenta de LearnFloo
nameOpcional, nombre mostrado en el anuncio (si no, el nombre de la cuenta encontrada; si no, anuncio anónimo)
label, amountCents, currencyOpcionales, 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.

GET/live/sessions/:id/replay

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.

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

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).

Escenas: principio

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"
}
CampoDescripción
idLibre 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
jingleCortinilla: 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, nextSceneIdEncadenamiento 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)
layoutsolo (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[].kindhost (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, titleEncuadre cover o contain; vídeo en bucle o sin sonido; título mostrado en la realización
slots[].shape, filter, bgMode, bgBlur, bgImage, volume, mutedCá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, volumeVí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
lockedCandado: 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
audiosSonidos 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, transitionColor 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.
framedEnmarcado: 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.

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

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.

En antena, rótulos, medios

LlamadaEfecto
POST/live/sessions/:id/scenes/:sceneId/activatePone 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).

Stickers, rótulos y huecos de una escena

LlamadaEfecto
POST…/scenes/:sceneId/overlaysAñade un sticker o un rótulo (objeto overlays[] anterior). 201 { "overlay" }
PATCH…/scenes/:sceneId/overlays/:overlayIdModifica 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/:overlayIdLo 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/:overlayIdQuita el elemento, devuelve la escena
POST…/scenes/:sceneId/slotsAñade un hueco (objeto slots[]; position opcional, 0 = principal). 201 { "slot" }
PATCH…/scenes/:sceneId/slots/:slotIdModifica el hueco (otro medio mediante mediaId o url, guestIndex, fit, geometría en free, position para moverlo en el orden)
DELETE…/scenes/:sceneId/slots/:slotIdQuita 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" }

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

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.

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

{ "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": { … } }.

Encuestas y cuestionarios: principio

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.

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

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.

Lanzar, resultados, cerrar

LlamadaEfecto
POST…/polls/:pollId/openAbre 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/closeCierra 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/bandRó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.

Soporte: principio

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.

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 }
}
CampoDescripción
statusopen (esperando al equipo), pending (esperando al usuario), resolved, closed
prioritylow, normal, high, urgent
lastMessageBymember o staff: quién escribió el último

GET/support/tickets

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 }).

POST/support/tickets

{
  "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 } ]
}
CampoDescripción
user.externalId, user.nameObligatorios. 200 y 60 caracteres máx.
subjectObligatorio, 200 caracteres máx.
contentTexto plano (por defecto) o HTML con "format": "html". Obligatorio salvo si se aporta un archivo adjunto
priorityPor defecto normal
categoryLibre, opcional (se muestra al equipo)
attachmentsOpcional, 10 máx., objetos devueltos por /attachments

Respuesta 201: { "ticket": { … } }. Se notifica al equipo del espacio.

GET/support/tickets/:id

?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.

POST/support/tickets/:id/messages

{ "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).

POST/support/tickets/:id/status

{ "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": { … } }.

Soporte del lado del equipo (staff)

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

POST/attachments

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.

GET/space

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).

GET/space/usage 5 tokens

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).

GETPOST/users

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.

GETPATCHDELETE/users/:id

: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.

POST/users/:id/entry

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>).

GET/users/:id/progress 3 tokens

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": "…" }
] }

GETPOST/users/:id/xp

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 }

Clasificación, insignias, retos

LlamadaRespuesta
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/badgesCatá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/:idEl reto con sus participants ({ id, externalId, name, progress, completedAt, joinedAt })

Grupos: principio y audiencia

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…" } }

GETPOST/groups y GETPATCHDELETE/groups/:id

LlamadaCuerpo / 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/:idEl 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)

Miembros de un grupo

LlamadaCuerpo / 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:…

GETPUT/users/:id/groups

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" } ] }

Clasificación, seguimiento y documentos de un grupo

LlamadaRespuesta
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 tokensSeguimiento 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.

GETPOST/courses

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": { … } }

GETPATCHDELETE/courses/:id

: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.

Módulos y lecciones

LlamadaCuerpo / 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 }

GET/courses/:id/progress 5 tokens

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 } ] } }

POST/lessons/:id/complete

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)

Publicaciones y comentarios

LlamadaCuerpo / 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/:idLa 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/:idElimina la publicación, sus comentarios y reacciones
POST/posts/:id/comments{ "content", "format"?, "parentCommentId"?, "attachments"?, "author"? }201 { "comment": { … } }
DELETE/comments/:idElimina 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…" } }

Eventos del calendario

LlamadaCuerpo / 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/:idMismos 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 }

Vídeos del estudio

Grabaciones del estudio LearnFloo (pantalla, cámara, importaciones) y repeticiones de directos importadas, alojadas en Bunny Stream.

LlamadaRespuesta
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.

GETPOST/media y GETPATCHDELETE/media/:id

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.

Correos: principio

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.

GETPOST/emails/campaigns y GETPATCHDELETE/emails/campaigns/:id

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.

Enviar, programar, cancelar, probar, previsualizar

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.

GETPOST/emails/segments y GETPATCHDELETE/emails/segments/:id

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.

GETPOST/emails/templates y PATCHDELETE/emails/templates/:id

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.

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

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)

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

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.

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

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.

GETPUT/users/:id/email-preferences

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.

Webhooks: principio y eventos

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 … } } }
Eventodata
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)
pingEnví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.

Firma y reintentos

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.

Gestión de los webhooks

LlamadaCuerpo / 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/testEnvía un evento ping202 { "sent": true }
GET/webhooks/:id/deliveries?limit=50Registro de envíos (30 días): { "deliveries": [ { "id", "event", "attempt", "status": "pending|success|failed", "responseStatus", "error", "createdAt", "deliveredAt" } ] }

OpenAPI

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.

Servidor MCP (asistentes de IA)

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
PrincipioEl 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 puedePropietario o administrador del espacio, con su propia cuentaCualquiera que tenga la clave
Atribución de las accionesAl usuario que autorizóAl creador de la clave
RiesgoNingún secreto en la URL ni en la configuración del clienteUna clave en la URL puede acabar en registros; si se filtra, revócala
Dónde se gestionaConfiguració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

Configuración de los clientes

ClienteOAuthClave API
claude.ai, aplicación ClaudeConfiguración → Conectores → Añadir un conector personalizado, URL https://api.learnfloo.com/mcp, luego «Conectar»: se abre la página de autorización de LearnFlooLa misma pantalla, URL https://api.learnfloo.com/mcp?key=lf_live_…, sin autenticación
Claude Codeclaude mcp add --transport http learnfloo https://api.learnfloo.com/mcp y luego /mcp para autenticarseclaude 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.

La clave en la URL (?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.

Herramientas

159 herramientas, por familia. Columna «Acceso»: lectura (abierta a las claves de solo lectura) o escritura.

Espacio

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

Miembros y gamificación

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

Directos

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

Escenas y realización

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

Encuestas y cuestionarios

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

Soporte

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

Grupos

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

Cursos

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

Muro de la comunidad

HerramientaDescripciónAccesoRuta
list_postsFeed of the space, newest first, paginated; categories general, announcements, question, wins, resource.lecturaGET /posts
get_postA post with its comments (oldest first, parentId for replies).lecturaGET /posts/:id
create_postCreates a post in the feed (mentions, XP and notifications as in the app). pinned is for moderators.escrituraPOST /posts
update_postChanges title, category, content, pinned state or audience of a post.escrituraPATCH /posts/:id
delete_postDeletes a post with its comments and reactions.escrituraDELETE /posts/:id
create_commentAdds a comment (or a reply with parentCommentId) to a post.escrituraPOST /posts/:id/comments
delete_commentDeletes a comment and its replies.escrituraDELETE /comments/:id

Calendario

HerramientaDescripciónAccesoRuta
list_eventsCalendar events by start date (scheduled lives included with liveSessionId).lecturaGET /events
get_eventOne calendar event.lecturaGET /events/:id
create_eventCreates a calendar event (lives are created with create_live_session).escrituraPOST /events
update_eventChanges an event (an event linked to a live is changed with update_live_session).escrituraPATCH /events/:id
delete_eventDeletes a calendar event.escrituraDELETE /events/:id

Vídeos del estudio

HerramientaDescripciónAccesoRuta
list_videosStudio recordings and imported live replays, newest first, paginated.lecturaGET /videos
get_videoOne video with its playback URLs (ready videos only), duration, thumbnail, linked lesson or live.lecturaGET /videos/:id
update_videoChanges the title or description of a video.escrituraPATCH /videos/:id

Mediateca

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

Webhooks

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

emails

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

GET/public/spaces

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" } ] }

GET/public/spaces/:slug

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.

Errores

{ "error": "message" }
CódigoCaso
400Cuerpo no válido, campo que falta, sesión terminada (Session is over), sesión desconocida, anfitrión no autorizado, regla de negocio no respetada
401Clave ausente, mal formada o revocada
403Acción prohibida al autor elegido (rol insuficiente), propietario del espacio
404Ruta desconocida, recurso desconocido o perteneciente a otro espacio, ticket perteneciente a otro usuario
405Método no admitido en esta ruta
413, 415Adjunto demasiado grande o de un tipo no admitido
429Lí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).

Visualización (iframe, pestaña)

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>
El atributo 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.

Dominio personalizado

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.

Ejemplo en Node

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

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

// 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'] })

Historial

FechaCambio
2026-09-16Cursos 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-15Correos: 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-15Sala 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-15OpenAPI 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-15MCP: 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-13Escenas: campo jingle (cortinilla: se reproduce durante su duración y luego la escena interrumpida se reanuda donde estaba).
2026-09-13Escenas: 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-13Escenas: 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-13Escenas: 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-13Escenas: campo framed (enmarcado: margen alrededor de los huecos, el fondo sigue visible), al crear y al modificar.
2026-09-13Reproductor 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-13Directos: 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-12Tarifa ú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-12MCP: 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-12Servidor 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-12Escenas: 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-11Escenas: widget viewers (contador de espectadores) y PUT /live/sessions/:id/viewers para añadirle espectadores; extraViewers en el objeto sesión.
2026-09-11Escenas: estilo de los huecos poll y message (colores, opacidad del fondo, tamaño del texto, redondeo, borde, sombra, fuente, elementos mostrados).
2026-09-11Escenas: opciones de los vídeos autoplay, once, startSec, durationSec, volume; startSec y durationSec también en el sonido de escena (audio).
2026-09-11Escenas: transiciones fade, black, slide-*, zoom, wipe, blur y duración transitionMs.
2026-09-11Escenas: 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-11Encuestas 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-11Directos: 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-11Escenas: campo locked (candado); una escena o plantilla bloqueada rechaza modificaciones y eliminación hasta { "locked": false }.
2026-09-11Escenas 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-10Oferta ú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-10Monedas: 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-10Inscripciones 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-10Chat 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-10Directos: 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-10Grupos (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-10Espacios 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-09Lí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-07Webinars: 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-07Soporte: /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-07GET /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-06Errores devueltos en JSON limpio { "error" }. Dominio personalizado por espacio para las páginas del directo. API servida en api.learnfloo.com.
2026-09-05Primera versión: /entry, /sessions (POST, GET), /sessions/:id, /sessions/:id/attendance.

Preguntas: equipo de LearnFloo.