Brancher Liri sur votre site
Liri est le moteur direct, rendez-vous et formation de Cimolace. Votre serveur crée et pilote les sessions par une API REST authentifiée par clé tenant ; le direct lui-même s'affiche dans votre page par une iframe dont vous demandez le jeton. Cette page ne documente que des routes vérifiées dans les contrôleurs.
Vue d'ensemble
Vous avez déjà un site. Liri lui ajoute des sessions en direct sans que vous ayez à gérer un serveur média, des jetons de salle ou un back-office. Le partage des rôles :
- Votre serveur appelle l'API Liri avec une clé tenant (
lk_live_…) pour créer, démarrer et terminer les sessions. - Votre serveur demande un jeton d'embed et reçoit une
iframe_urlprête à poser dans la page. - Le navigateur du visiteur n'affiche que cette iframe. Il ne voit jamais votre clé.
- Liri notifie votre serveur par webhook quand une session démarre ou se termine.
Base de l'API : https://api.cimolace.space/v1/liri — 29 routes, toutes gardées par la même clé. Depuis le 19/09/2026, une clé lk_ est bornée à /v1/liri : elle n'ouvre plus /v1/crm. Pour le CRM il faut une clé lkr_ (lecture) ou lka_ (écriture), émise par POST /v1/liri/api-keys avec une session owner/admin.
Quickstart
1. Obtenir une clé
Une clé Liri a la forme lk_live_ suivie de 48 caractères hexadécimaux. Aucun écran ne permet aujourd'hui de créer votre première clé vous-même : la seule route qui en fabrique une est POST /v1/liri/api-keys, elle-même protégée par une clé. Demandez la première à Cimolace ; une fois en main, elle vous sert à en créer et en révoquer d'autres.
2. Créer une session
curl -X POST https://api.cimolace.space/v1/liri/sessions \
-H "X-Liri-Api-Key: lk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Cours du mardi",
"session_type": "class",
"scheduled_at": "2026-09-08T18:00:00Z",
"capacity": 40
}'3. Afficher le direct dans votre page
Demandez un jeton d'embed depuis votre serveur, puis posez l'iframe_url renvoyée. N'essayez pas de deviner cette URL : elle porte un jeton signé et expire.
curl -X POST https://api.cimolace.space/v1/liri/sessions/<id>/embed-token \
-H "X-Liri-Api-Key: lk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "role": "viewer", "display_name": "Awa N." }'
# → { "data": { "iframe_url": "https://…/embed/live/<id>?et=…&tenant=…&role=viewer",
# "expires_in": 1800, ... } }4. Démarrer, puis terminer
curl -X POST https://api.cimolace.space/v1/liri/sessions/<id>/start -H "X-Liri-Api-Key: lk_live_xxxxxxxx"
curl -X POST https://api.cimolace.space/v1/liri/sessions/<id>/end -H "X-Liri-Api-Key: lk_live_xxxxxxxx"Ces deux appels sont les seuls qui déclenchent un webhook (session.started, session.ended).
Authentification
Toutes les routes /v1/liri exigent la clé tenant dans l'en-tête X-Liri-Api-Key. Il n'y a qu'un seul type de clé : pas de clé « publique » utilisable côté navigateur.
X-Liri-Api-Key: lk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| Règle | Détail |
|---|---|
| Format | lk_live_ + 48 caractères hexadécimaux. Une clé qui ne commence pas par lk_ est refusée avant même la base. |
| Stockage | La clé n'est jamais stockée en clair : seul son hash SHA-256 est comparé. |
| Isolation | La clé résout le tenant. Chaque requête est filtrée sur ce tenant — vous ne voyez jamais les sessions d'une autre organisation. |
| Révocation | Une clé révoquée est refusée immédiatement (401), sans délai de cache. |
| Organisation suspendue | Si le tenant n'est pas active, toutes ses clés sont refusées (401). |
| Audit | last_used_at est mis à jour à chaque appel, sans bloquer la réponse. |
Serveur uniquement — ce n'est pas un choix de style
La politique CORS de l'API n'autorise que trois en-têtes : Content-Type, Authorization et X-Tenant-Slug. X-Liri-Api-Key n'en fait pas partie : un appel depuis un navigateur échoue au contrôle préalable (préflight), quelle que soit votre origine. Appelez l'API depuis votre backend et exposez à votre page uniquement l'iframe_url obtenue.
⛔ Une clé Liri crée des sessions et lit vos enregistrements. Elle ne gère plus vos webhooks et ne crée plus d'autres clés — une clé qui peut reconfigurer l'espace est une clé que la révoquer ne suffit plus à refermer. Ne la mettez jamais dans le JavaScript envoyé au navigateur.
Réponses et erreurs
Toutes les réponses de l'API sont enveloppées dans un objet data. Prévoyez-le dans votre client : la donnée utile est toujours d'un cran plus bas que ce que renvoie l'endpoint.
// GET /v1/liri/sessions
{
"data": {
"sessions": [
{
"id": "…", "title": "Cours du mardi", "status": "scheduled",
"session_type": "class", "scheduled_at": "2026-09-08T18:00:00Z",
"capacity": 40, "price_cents": 0, "currency": "EUR",
"started_at": null, "ended_at": null, "replay_enabled": true
}
],
"total": 1
}
}Les erreurs suivent une forme unique :
{ "error": { "code": "UNAUTHORIZED", "message": "Clé API invalide ou révoquée" } }Les 29 routes
La totalité de la surface /v1/liri. Rien de plus n'existe.
| Méthode | Chemin | Rôle |
|---|---|---|
GET | /sessions | Lister (filtres status, type, limit, offset) |
POST | /sessions | Créer |
GET | /sessions/:id | Détail complet |
PATCH | /sessions/:id | Modifier |
DELETE | /sessions/:id | Supprimer (refusé si en cours) |
POST | /sessions/:id/start | Démarrer → webhook |
POST | /sessions/:id/end | Terminer → webhook |
POST | /sessions/:id/embed-token | Jeton + iframe_url |
GET | /sessions/:id/participants | État de la salle |
GET | /sessions/:id/recordings | Enregistrements de la session |
GET | /sessions/:id/waiting-room | File d'attente |
POST | /sessions/:id/waiting-room/:wid/admit | Admettre |
POST | /sessions/:id/waiting-room/:wid/reject | Refuser |
GET | /recordings | Tous les enregistrements du tenant |
POST | /sessions/:id/transcribe | Transcrire |
POST | /sessions/:id/summary | Résumer |
POST | /sessions/:id/translate | Traduire |
POST | /sessions/:id/neuro-recall-deck | Cartes de révision |
POST | /masterclasses | Générer depuis un texte |
GET | /masterclasses | Lister |
GET | /masterclasses/:id | Détail |
GET | /api-keys | Lister vos clés |
POST | /api-keys | Créer une clé |
DELETE | /api-keys/:id | Révoquer |
GET | /webhooks | Lister — session owner/admin, plus par clé |
POST | /webhooks | Créer — session owner/admin |
DELETE | /webhooks/:id | Supprimer — session owner/admin |
PATCH | /webhooks/:id/toggle | Activer / désactiver — session owner/admin |
GET | /me | Tenant et identifiant de la clé |
Sessions
POST /sessions
Seul title est obligatoire. Tout le reste a une valeur par défaut.
| Champ | Défaut | Détail |
|---|---|---|
title | — | Requis. |
description | null | Texte libre. |
session_type | webinar | class, webinar, workshop, consultation, debate, commercial, masterclass. |
scheduled_at | maintenant | ISO 8601. |
capacity | null | Illimité si absent. |
price_cents | 0 | Entier, en centimes. |
currency | EUR | Devise affichée. |
replay_enabled | true | Rediffusion. |
config | {} | Objet libre conservé tel quel. |
L'hôte est le propriétaire de l'organisation, sauf si vous passez un host_user_id. La session naît en scheduled.
Cycle de vie
POST /start— passe enlive. Rejouer l'appel sur une session déjà en direct la renvoie telle quelle ; sur une session terminée, il échoue en 400.POST /end— passe enendedet horodateended_at.DELETE— refusé en 400 tant que la session estlive. Terminez-la d'abord.
Salle d'attente
GET /sessions/:id/waiting-room renvoie les personnes en attente, les plus anciennes en premier. Vous les admettez ou les refusez une par une. Cette file n'émet aucun webhook : interrogez-la pendant la session.
Jeton d'embed
POST /sessions/:id/embed-token renvoie un jeton signé et l'URL d'iframe correspondante.
// requête
{ "role": "viewer", "display_name": "Awa N." }
// réponse
{
"data": {
"embed_token": "eyJhbGciOi…",
"iframe_url": "https://…/embed/live/<id>?et=…&tenant=<slug>&role=viewer&display=Awa%20N.",
"expires_in": 1800,
"session_title": "Cours du mardi",
"session_status": "live",
"role": "viewer"
}
}| Rôle | Droits | Durée du jeton |
|---|---|---|
viewer (défaut) | Regarde. | 30 minutes |
co_host | Caméra et micro. | 30 minutes |
host | Contrôle complet de la salle. | 4 heures |
Posez l'iframe_url telle quelle. Elle est construite à partir de l'origine de l'application, qui est une configuration serveur : ne la codez pas en dur, et regénérez le jeton plutôt que de le mettre en cache au-delà de son expiration.
<iframe
src="<iframe_url renvoyée par l'API>"
style="width:100%;height:560px;border:none;border-radius:12px"
allow="camera; microphone; autoplay; fullscreen; display-capture"
allowfullscreen
></iframe>Transcription et IA
Ces routes travaillent sur l'enregistrement d'une session terminée. Elles s'enchaînent dans cet ordre : sans transcription, le résumé et la traduction n'ont pas de matière.
| Route | Corps | Effet |
|---|---|---|
POST /sessions/:id/transcribe | language, force | Transcrit l'enregistrement (Whisper). |
POST /sessions/:id/summary | length (short/medium/long), format (paragraph/bullets) | Résume le transcript. Un résumé déjà calculé est renvoyé tel quel avec cached: true. |
POST /sessions/:id/translate | target_langs (défaut ["en"]), mode (live/video) | Traduit le transcript. |
POST /sessions/:id/neuro-recall-deck | count (1–50), difficulty, save, user_id | Fabrique des cartes question / réponse et les enregistre, sauf save: false. |
curl -X POST https://api.cimolace.space/v1/liri/sessions/<id>/transcribe \
-H "X-Liri-Api-Key: lk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "language": "fr" }'Appeler /translate avant /transcribe ne lève pas d'erreur HTTP : la réponse contient { "error": "NOT_TRANSCRIBED" }. Testez ce cas, il arrive en 200.
Masterclasses
POST /masterclasses transforme un texte source en cours structuré. source_text est obligatoire — vide, la réponse est un 400.
curl -X POST https://api.cimolace.space/v1/liri/masterclasses \
-H "X-Liri-Api-Key: lk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Introduction à la comptabilité",
"source_text": "…votre texte source…"
}'GET /masterclasses liste celles de votre organisation, GET /masterclasses/:id en donne le détail.
Clés API
POST /api-keys attend un label et renvoie la clé en clair une seule fois. Elle n'est stockée que hachée : personne, y compris Cimolace, ne peut vous la relire ensuite.
{
"data": {
"id": "…",
"label": "Serveur production",
"key_prefix": "lk_live_a1b2c3d4e5",
"key": "lk_live_a1b2c3…",
"_warning": "Copiez cette clé maintenant — elle ne sera plus affichée."
}
}GET /api-keys ne renvoie que les préfixes et les dates de dernier usage, jamais les clés. DELETE /api-keys/:id révoque immédiatement.
Pour changer de clé sans coupure : créez la nouvelle, déployez-la, vérifiez sur GET /me qu'elle répond, puis révoquez l'ancienne.
SDK JavaScript
Le SDK est servi par l'application Cimolace à /liri-sdk.js et expose le global LiriSDK. Il déduit tout seul les adresses de l'API et de l'application à partir de l'URL du script — inutile de les configurer.
<div id="live"></div>
<script src="https://app.cimolace.space/liri-sdk.js"></script>
<script>
const liri = LiriSDK.init({ tenant: "mon-ecole" });
liri.viewer("#live", { session: "SESSION_ID" });
</script>Sans clé, le SDK passe par le flux navigateur décrit ci-dessous : l'origine de votre site doit être déclarée. Les méthodes d'affichage sont viewer(), host(), coHost() et embed() (rôle explicite).
Montage automatique
Utile pour un CMS où vous ne pouvez coller que du HTML : tout élément de classe liri-live est monté au chargement.
<div class="liri-live"
data-tenant="mon-ecole"
data-session="SESSION_ID"
data-role="viewer"
data-height="560"
data-theme="dark"></div>
<script src="https://app.cimolace.space/liri-sdk.js" async></script>Événements
L'iframe communique avec votre page. Cinq événements sont disponibles :
const live = liri.viewer("#live", { session: "SESSION_ID" });
live.on("session:joined", (i) => console.log("connecté", i.sessionId, i.room));
live.on("session:ended", (i) => console.log("terminé", i.sessionId));
live.on("participant:joined", (p) => console.log("arrivée", p));
live.on("participant:left", (p) => console.log("départ", p));
live.on("error", (e) => console.error(e.message));
live.resize(720); // change la hauteur
live.unmount(); // retire l'iframe et les écouteurs⚠️ Le SDK accepte une apiKey, et sa documentation interne le dit clairement : ne l'utilisez que dans un contexte serveur (Node), jamais dans une page publique. Depuis un navigateur, la politique CORS de l'API rejette de toute façon l'en-tête — l'appel échouera au lieu de fuiter, mais ne comptez pas là-dessus comme protection.
Rendez-vous publics
Le moteur de rendez-vous expose une entrée entièrement publique, sans clé ni compte : un visiteur anonyme consulte les disponibilités et demande un rendez-vous. Votre organisation est désignée par son slug dans le chemin. La demande arrive dans la file du secrétariat.
| Méthode | Chemin | Détail |
|---|---|---|
GET | /booking-public/:slug/availability | Paramètres windowStart, windowEnd, timezone, country. |
POST | /booking-public/:slug/appointment-request | subject, description, email, whatsapp, preferredIso. |
GET | /booking-public/:slug/services | Catalogue des prestations réservables. |
GET | /booking-public/:slug/vitrine-nav | Liens de navigation publiés par l'organisation. |
GET | /booking-public/reschedule/:token | Contexte d'un report par lien. |
POST | /booking-public/reschedule/:token | Applique le nouveau créneau. |
curl -X POST https://api.cimolace.space/booking-public/mon-ecole/appointment-request \
-H "Content-Type: application/json" \
-d '{
"subject": "Première consultation",
"email": "visiteur@example.com",
"whatsapp": "+241060000000",
"preferredIso": "2026-09-10T14:00:00Z"
}'La création de demande est limitée à 4 requêtes par adresse IP par tranche de 10 minutes (et 50 toutes origines confondues). Au-delà, l'API répond 429. Ces routes étant ouvertes, mettez un contrôle anti-robot devant votre formulaire.
Les routes /booking/* (sans -public) sont réservées au back-office : elles exigent une session utilisateur et ne répondent pas à une clé API.
Webhooks — configuration
⚠️ Depuis le 19/09/2026, les webhooks ne se configurent plus avec une clé API : il faut une session owner/admin. Une clé qui peut détourner le flux de vos contacts vers une autre adresse est une clé dont la révocation ne suffit plus à réparer les dégâts. Utilisez le jeton de session (Authorization: Bearer), pas X-Liri-Api-Key.
curl -X POST https://api.cimolace.space/v1/liri/webhooks \
-H "Authorization: Bearer <jeton de session owner/admin>" \
-H "Content-Type: application/json" \
-d '{
"label": "Mon backend",
"url": "https://mon-site.example/hooks/liri",
"events": ["session.started", "session.ended"]
}'La réponse contient signing_secret : c'est le secret de signature. Conservez-le, il ne réapparaît pas. Vous pouvez aussi fournir le vôtre via secret. La valeur "*" dans events souscrit à tout.
| Contrainte | Détail |
|---|---|
| HTTPS obligatoire | Une URL en http:// est refusée à la création. |
| Pas d'adresse interne | Les adresses privées, locales et non résolubles sont refusées, à la création comme à chaque envoi. La résolution DNS utilisée pour la vérification est celle utilisée pour la connexion. |
| Pas de redirection | Une réponse 3xx n'est pas suivie et compte comme un échec. |
| Délai | 8 secondes. Répondez vite et traitez en tâche de fond. |
| Succès | Seul un code 2xx remet failure_count à zéro. |
| Pas de réessai | Une livraison échouée n'est pas rejouée. Si vous ne pouvez pas perdre un événement, réconciliez périodiquement avec GET /sessions. |
Vérifier la signature
Chaque livraison porte un HMAC-SHA256 du corps brut, calculé avec votre secret et encodé en hexadécimal.
| En-tête | Contenu |
|---|---|
X-Cimolace-Signature | sha256=<hex> — en-tête canonique. |
X-Cimolace-Delivery | Identifiant unique de la livraison. |
X-Liri-Signature | Même valeur que X-Cimolace-Signature — alias historique. |
X-Liri-Delivery | Alias historique de X-Cimolace-Delivery. |
La signature ne couvre que le corps : elle ne contient pas d'horodatage et il n'y a donc pas de fenêtre anti-rejeu. Si le rejeu vous expose, dédupliquez sur liri_delivery_id, que le corps transporte.
import { createHmac, timingSafeEqual } from "crypto";
// Attention : il faut le corps BRUT, pas l'objet déjà analysé.
export function verifyLiriSignature(rawBody, header, secret) {
if (typeof header !== "string" || !header.startsWith("sha256=")) return false;
const received = Buffer.from(header.slice(7), "hex");
const expected = createHmac("sha256", secret).update(rawBody).digest();
if (received.length !== expected.length) return false;
return timingSafeEqual(received, expected);
}Corps reçu
{
"event": "session.ended",
"tenant_id": "…",
"session_id": "…",
"data": { "session_id": "…" },
"liri_delivery_id": "3f2a…",
"timestamp": "2026-08-28T18:42:11.004Z"
}Le contenu de data est volontairement mince : session.started porte session_id et title ; session.ended ne porte que session_id. Pour la durée, le nombre de participants ou l'enregistrement, relisez la session avec GET /sessions/:id.
Événements réellement émis
L'API accepte de vous abonner à toute la liste ci-dessous. Douze événements sont réellement envoyés aujourd'hui ; six sont déclarés mais aucun code ne les déclenche. Ils sont signalés pour que vous ne construisiez rien dessus.
| Événement | Émis ? | Déclencheur |
|---|---|---|
session.started | ✅ | POST /sessions/:id/start |
session.ended | ✅ | POST /sessions/:id/end |
session.cancelled | ❌ | Aucun émetteur. |
participant.joined | ❌ | Aucun émetteur. |
participant.left | ❌ | Aucun émetteur. |
recording.started | ❌ | Aucun émetteur. |
recording.completed | ❌ | Aucun émetteur. |
waiting_room.knock | ❌ | Aucun émetteur — interrogez /waiting-room. |
billing.subscription.activated | ✅ | Abonnement activé. |
billing.invoice.paid | ✅ | Facture payée. |
billing.subscription.past_due | ✅ | Paiement en retard. |
billing.subscription.canceled | ✅ | Abonnement résilié. |
crm.contact.created | ✅ | Contact créé. |
crm.deal.created | ✅ | Affaire créée. |
crm.deal.won | ✅ | Affaire gagnée. |
crm.deal.lost | ✅ | Affaire perdue. |
crm.deal.stage_moved | ✅ | Changement d'étape. |
crm.task.created | ✅ | Tâche créée. |
Les événements de facturation et de CRM arrivent sur le même endpoint : filtrez sur le champ event.
Sécurité
- Clé côté serveur uniquement — la politique CORS n'autorise pas
X-Liri-Api-Keydepuis un navigateur ; exposez l'iframe_url, jamais la clé. - Isolation par organisation — la clé résout le tenant et chaque requête est filtrée dessus.
- Révocation immédiate — une clé compromise se coupe en un appel, sans délai de cache.
- Jetons d'embed courts — 30 minutes, 4 heures pour un hôte. Regénérez plutôt que de prolonger.
- Pas d'escalade depuis le navigateur — le flux sans clé ne délivre que le rôle
viewer. - Webhooks sortants bridés — HTTPS obligatoire, adresses internes refusées, redirections non suivies.
- Portée réelle de la clé — une clé
lk_ouvre/v1/liriet rien d'autre : ni le CRM, ni les webhooks, ni l'émission de clés. Traitez-la tout de même comme un identifiant de service, pas comme un jeton public.
Codes d'erreur
| Symptôme | Cause probable |
|---|---|
401 « Header X-Liri-Api-Key manquant ou invalide » | En-tête absent, ou clé qui ne commence pas par lk_. Vérifiez que vous n'envoyez pas Authorization: Bearer — c'est l'en-tête de Vimbo et MedOS, pas de Liri. |
401 « Clé API invalide ou révoquée » | Clé inconnue ou révoquée. |
401 « Tenant inactif » | L'organisation n'est plus active. |
| Échec CORS avant tout appel | Vous appelez /v1/liri depuis un navigateur. Passez par votre serveur. |
403 sur /lives/embed/token | L'origine du site n'est pas déclarée pour cette organisation. |
404 « Session introuvable » | Identifiant erroné, ou session appartenant à une autre organisation. |
400 « La session est déjà terminée » | /start sur une session ended. |
400 « Impossible de supprimer une session en cours » | Appelez /end avant DELETE. |
200 avec NOT_TRANSCRIBED | /translate appelé avant /transcribe. |
| Transcription entre crochets | Le service de transcription n'est pas configuré côté serveur ; un texte de remplacement est renvoyé au lieu d'une erreur. |
429 sur /booking-public | Plus de 4 demandes de rendez-vous depuis la même IP en 10 minutes. |
| Webhook jamais reçu | URL en http://, hôte non public, redirection, ou réponse au-delà de 8 secondes. Aucune de ces livraisons n'est rejouée. |