Xamle API /Documentation technique
API v1
Accueil · Documentation · Technique

API Xamle

L'API REST Xamle expose contenus, programmes, quiz, progression et événements de vos applications. Authentification JWT, réponses JSON enveloppées, collecte analytics par lots et SDK mobile Flutter. Une même API sert le backoffice, les applications mobiles et les intégrations.

URL de base & en-têtes

Toutes les routes sont préfixées par /api/v1. Une documentation Swagger interactive est exposée par le serveur sur /api/docs.

EnvironnementURL de base
Productionhttps://<domaine-api>/api/v1
Développement localhttp://localhost:3001/api/v1

Identifier l'application : X-App-Key

La plateforme est multi-applications. Chaque requête est rattachée à une application via un en-tête :

En-têteQui l'envoieRôle
X-App-KeyApps mobiles & intégrationsClé d'application (générée dans la console, rotation possible). Clé invalide ou révoquée → 401.
X-App-IdBackoffice webIdentifiant de l'application active, combiné au JWT admin.

Sans en-tête, la requête est rattachée à l'application par défaut de la plateforme (rétrocompatibilité). Les routes strictement mobiles comme GET /mobile/config exigent X-App-Key.

GET/api/v1/mobile/configVoir la réponse
200 OK
config.json
{
  "success": true,
  "data": {
    "app": { "id": "…", "slug": "xamle", "name": "Xamle" },
    "capabilities": { "managesUsers": true, "managesAnalytics": true, "hasFeed": true },
    "features": ["engagement.feed", "engagement.agents", "…"],
    "branding": { /* thème Design Manager ou null */ },
    "profileFields": [ /* champs du profil déclarés */ ]
  }
}

Authentification

Deux populations, deux flux — dans les deux cas l'API délivre un couple access token + refresh token (JWT signés RS256) à passer en Authorization: Bearer <accessToken>. L'access token expire après 15 minutes ; le refresh token (7 jours) permet de renouveler la session avec rotation.

Comptes équipe (backoffice)

Connexion par e-mail et mot de passe. Si la 2FA est activée sur le compte, la réponse contient requires2FA et un tempToken à échanger avec le code à 6 chiffres.

login.sh
# Connexion d'un compte équipe
curl -X POST https://<domaine-api>/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "vous@exemple.sn", "password": "…" }'

# → { "success": true, "data": { "accessToken": "…", "refreshToken": "…", "expiresIn": 900 } }
# ou, si 2FA active : { "requires2FA": true, "tempToken": "…" } → POST /auth/2fa/verify
login.js
const res = await fetch("https://<domaine-api>/api/v1/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email, password })
});
const { data } = await res.json();  // { accessToken, refreshToken, expiresIn }
const me = await fetch("…/api/v1/auth/me", {
  headers: { Authorization: "Bearer " + data.accessToken }
});
RouteDescription
POST /auth/loginConnexion (limité à 5 tentatives / 15 min ; compte verrouillé 15 min après 5 échecs).
POST /auth/2fa/verifyÉchange tempToken + code à 6 chiffres contre les tokens.
POST /auth/refreshRenouvelle les tokens (rotation du refresh token).
GET /auth/meProfil, rôles et permissions effectives du compte connecté.
GET /auth/invitation/:token · POST /auth/set-passwordFlux d'invitation : vérification du lien puis définition du mot de passe.
POST /auth/logout · POST /auth/logout/allRévoque la session courante, ou toutes les sessions du compte.

Apprenants (mobile) — OTP par SMS

Pas de mot de passe côté apprenant : la connexion se fait par numéro de téléphone + code SMS à 4 chiffres (valable 5 minutes). Un mode invité anonyme permet d'explorer l'app ; la session invité est fusionnée au compte lors de la vérification.

RouteDescription
POST /mobile/auth/guestCrée une session invité anonyme.
POST /mobile/auth/otp/request{ "phoneNumber": "+221771234567" } — envoie le code SMS (3 demandes / min max).
POST /mobile/auth/otp/verify{ phoneNumber, code, guestToken? } → { user, accessToken, refreshToken, expiresIn, isNew, missingFields }.
POST /mobile/registrationComplète l'inscription avec les champs du profil requis.
POST /mobile/auth/refreshRenouvelle les tokens apprenant.
GET /mobile/auth/meProfil de l'apprenant + champs de profil manquants.
Un token, une application

Le JWT apprenant embarque l'identifiant de son application : un token émis pour une app est refusé (401) si la requête est faite avec la clé d'une autre app.

Réponses & erreurs

Toutes les réponses sont enveloppées de façon uniforme.

Succès

success.json
{ "success": true, "data": { /* payload */ }, "timestamp": "2026-09-07T10:00:00.000Z" }

Listes paginées

Les listes acceptent ?page= et ?limit= et renvoient un bloc meta :

paginated.json
{
  "success": true,
  "data": [ /* éléments */ ],
  "meta": { "page": 1, "limit": 20, "total": 137, "totalPages": 7,
            "hasNextPage": true, "hasPrevPage": false }
}

Erreurs

error.json
{
  "success": false,
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [ /* détails éventuels */ ],
  "requestId": "…",
  "timestamp": "…",
  "path": "/api/v1/…"
}
CodeSignification
400Validation stricte : toute propriété non prévue par le contrat est refusée (whitelist). Vérifiez les noms de champs.
401Token absent/expiré, clé d'application invalide, ou token émis pour une autre application.
403Permission manquante ou application hors de votre périmètre.
409Conflit d'unicité (enregistrement déjà existant).
429Limite de débit atteinte — réessayez plus tard.

Chaque réponse porte un requestId (repris de l'en-tête X-Request-ID si fourni) : joignez-le à vos signalements de bug.

Endpoints mobiles

Référence des routes consommées par les applications apprenantes (et le SDK). Sauf mention public, elles exigent un JWT apprenant.

Contenus & programmes

RouteDescription
GET /contents · /contents/:idBibliothèque publiée de l'application (vidéos, audios, documents).
GET /programs · /programs/:idProgrammes et leur structure. Public (progression incluse si connecté).
GET /modules/:idDétail d'un module et de ses leçons.
GET /glossaryGlossaire de l'application (public).
POST /glossary/:id/viewsSuivi : fiche ouverte par l'apprenant ({ "audio": true } = explication audio écoutée).

Progression & évaluations

RouteDescription
POST /progress/lessons/:lessonIdMarque la progression d'une leçon.
GET /me/programs/:id/progress · /me/modules/:id/progressProgression de l'apprenant.
GET /me/stats · /me/rewardsStatistiques et récompenses ; attestation PDF via /me/certificates/:id/pdf.
GET /quizzes/dailyQuiz du jour (public).
GET /quizzes/:id · POST /quizzes/:id/attemptsDétail d'un quiz ; soumission d'une tentative, notée côté serveur. Suivi : timeTaken, context (STANDARD · DAILY · EXAM) et examAttemptId optionnels.
GET /exam-sets · /exam-sets/:idSéries d'examens blancs (public).
POST /exam-sets/:id/attempts · /exam-sets/attempts/:attemptId/completeSuivi d'un passage d'examen blanc : ouverture (renvoie attemptId, à joindre aux tentatives de quiz), puis clôture avec le résultat agrégé calculé côté serveur.

Feed, profil & codes

RouteDescription
GET /feed · /feed/:idFil d'actualité (si la feature est active) ; interactions POST /feed/:id/like|comment|share|view.
GET /mobile/profile-fields · POST /mobile/profile-fields/valuesChamps du profil déclarés et soumission des valeurs (dont le code agent, validé côté serveur).
GET /me/notifications · POST /me/devicesNotifications in-app, marquage lu, enregistrement de l'appareil pour le push.
POST /me/codes/validate · /me/codes/redeemValidation puis activation d'un code d'accès. Idempotent : un code déjà consommé par le même apprenant confirme (ou rétablit) l'accès au lieu d'échouer ; les refus portent un motif machine reason.
GET /me/entitlementsDroits d'accès de l'apprenant (programmes débloqués, origine, échéance) : la source de vérité du cadenas côté app. Servi avec un ETag (If-None-Match → 304) ; un push silencieux entitlements_updated signale qu'il faut relire.
POST /progress/batchRejeu d'un lot de remontées de progression accumulées hors ligne (clé d'idempotence par item, progression jamais rétrogradée). POST /quizzes/:id/attempts accepte de même un en-tête Idempotency-Key.
GET /offline/manifest · POST /offline/download/completeManifeste hors ligne (fichiers avec taille, version, disponibilité ; readiness: preparing tant qu'un MP4 n'est pas généré) et résultat d'un téléchargement (réussi / échoué / annulé, cause, octets, réseau).
Contrat OpenAPI

Le contrat exact des routes mobiles est exporté en OpenAPI (docs/api/mobile-openapi.json dans le repo) et consultable en interactif sur /api/docs.

API d'administration (backoffice)

Tout ce que fait la console passe par l'API d'administration, sous /api/v1/admin/*. Chaque requête porte le JWT d'un compte équipe (Authorization: Bearer …) et l'en-tête X-App-Id pour désigner l'application de travail. Chaque route exige une permission précise (format module:action) : sans elle → 403. Quelques opérations destructives sont réservées au rôle Admin.

Référence exhaustive

La liste complète, avec les schémas de requête/réponse, est dans Swagger (/api/docs, tri par méthode, recherche intégrée). Ci-dessous : la carte des domaines et leurs routes principales.

Équipe, rôles & applications

RouteDescription · permission
GET /admin/users · POST /admin/users/inviteListe de l'équipe · invitation par e-mail — team:read / team:manage
PUT /admin/users/:id/roles · /applications · /permissionsRemplace rôles cumulables, applications et grants à la carte — team:manage
GET /admin/users/:id/permissionsVue {rôles, héritées, additionnelles, effectives} — team:manage
GET /admin/roles · /admin/permissionsMatrice réelle des rôles · catalogue des permissions — team:read
GET/POST /admin/applications · POST /admin/applications/:id/rotate-keyPortefeuille d'apps, création (apps:create), rotation de la clé X-App-Key (apps:config)
GET /admin/applications/mineApps du membre courant (alimente le sélecteur) — authentification seule

Contenus (bibliothèque)

RouteDescription · permission
GET/POST /admin/contents · PATCH /:idCRUD des contenus (les « moments » sont un champ du contenu) — contents:read / contents:write
POST /admin/contents/upload/videoSession d'upload Cloudflare Stream (tus résumable) → {uploadUrl, streamId}
POST /admin/contents/:id/upload-url → /upload-completeURL présignée PUT (audio/PDF/image → R2), puis confirmation
GET /admin/contents/gdrive/files · POST /gdrive/importImport direct Google Drive → Stream, suivi via GET /:id/import/status
POST /:id/submit-review · /:id/approve · /:id/publish · /:id/unpublish · /:id/archiveCycle de vie (approve = contents:moderate)
/:id/captions/:language · /:id/audio · /:id/sharesSous-titres, pistes audio multilingues, partage inter-apps
POST /admin/contents/stream/webhookWebhook entrant Cloudflare Stream (public, signé webhook-signature)

Programmes, modules & évaluations

RouteDescription · permission
GET/POST /admin/programs · /:id/publish · /:id/unpublishProgrammes et leur cycle de vie — programs:read / programs:write
POST /admin/programs/:id/modules · PATCH /modules/reorderRattacher/créer des modules, ordre par glisser-déposer
GET/POST /admin/modules · /:moduleId/lessons · /lessons/reorderModules réutilisables et leçons (contenu ou quiz)
POST /admin/programs/:id/rules · /admin/modules/:id/rulesRègles de complétion → récompenses (points, badge, attestation)
POST …/:id/quizzes (programs & modules)Rattachement N-N des quiz aux modules/programmes
GET/POST /admin/quizzes · /:id/attempts · /:id/statsÉditeur de quiz (quizzes:write), tentatives & stats (analytics:read)
/admin/quiz-schedules · /admin/exam-setsPlanning du quiz du jour · séries d'examens blancs — quizzes:write
/admin/glossary (+ /categories, /:id/audio)Glossaire illustré avec audios multilingues — glossary:write
/admin/rewardsCatalogue des récompenses — programs:write

Diffusion & engagement

RouteDescription · permission
GET/POST /admin/feed · PATCH /reorder · /configFil d'actualité : ajout, ordre manuel/algo, thèmes — contents:write / feed:config
POST /admin/notifications/send · GET /admin/notifications · /stats · /devicesEnvoi (push FCM, SMS, in-app), historique, livraison — notifications:send / notifications:read
/admin/campaigns · /:id/activate · /:id/pause · /:id/testCampagnes : segments, déclencheurs, planification, test — campaigns:manage
/admin/segments · /presetsSegments d'audience système et personnalisés
/admin/codes/generate · /admin/codes · /:id/revoke · /batch/:batchId/revokeCodes d'accès par lot, suivi d'usage, révocation — access-codes:manage
/admin/agents · /cohorts (+ /:id/zones) · /overview · /export · /:id/send-recapAgents de terrain : cohortes à objectifs, zones datées, KPIs, export paie, récap SMS — agents:read / agents:manage ; page publique GET /public/agents/:token/recap
/admin/partners (validate / suspend / code / programs / ledger / payouts)Fiches partenaires, code, registre financier, versements — partners:manage

Utilisateurs mobiles, analytics & système

RouteDescription · permission
/admin/app-users · /:id/activity · /:id/quiz-attempts/:attemptId · /:id/suspend · /:id/reset · /:id/export · /:id/request-deletionApprenants : fiche, journal d'activité (cours, quiz, quiz du jour, examens blancs, panneaux), détail d'une tentative, suspension, reset, export & suppression RGPD — app-users:read / app-users:manage
/admin/profile-fields (+ /reorder)Champs du profil mobile (types, étapes, ordre) — settings:technical
/admin/analytics/*Agrégats : overview, users (rétention D1/D7/D30, acquisition), usage (sessions, heatmap, top écrans), feed, learning (funnel, quiz par module), practice (quiz du jour, examens blancs, glossaire, availability), notifications, revenue, offline, schema-errors — analytics:read / reporting:read
/admin/dashboard/* · /admin/reporting/exports/*Widgets du tableau de bord · exports CSV asynchrones — reporting:read
/admin/design-manager (draft / publish / rollback / preview)Thème de l'app mobile par versions — design:read / design:manage
/admin/moderation/reports · /admin/offline/resourcesSignalements · ressources téléchargeables hors-ligne — moderation:manage / offline:manage
/admin/settings/mail · /admin/settings/driveSMTP et compte de service Google Drive, avec test — settings:technical
/admin/auditJournal d'audit append-only, filtrable par utilisateur — audit:read
Bon à savoir

Les suppressions d'application, de programme, de quiz, de membre et les remboursements sont réservés au rôle Admin. Les lectures de certains modules d'édition (quiz, glossaire) exigent la permission d'écriture correspondante — un rôle purement « lecture » ne les voit pas.

Collecte analytics

Les événements produit sont envoyés par lots (1 à 500 événements) et acceptés en 202 Accepted : la validation métier et l'agrégation se font ensuite en file de traitement. L'authentification est optionnelle — les événements anonymes sont acceptés, et si un JWT apprenant est présent, l'identité de confiance est résolue côté serveur.

POST/api/v1/analytics/events202 Accepted · max 500 événements
events.json
{
  "events": [
    {
      "event_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",  // unique — sert à la déduplication
      "event_name": "lesson_completed",
      "timestamp_occurred": "2026-09-07T14:22:05.120Z",
      "anonymous_id": "device_3f9a2b1c",               // requis
      "session_id": "sess_2cb9e8a1",
      "source": "client",                                // "client" | "server"
      "context": { "app_version": "1.4.0", "platform": "android", "language": "fr" },
      "properties": { "lesson_id": "…", "program_id": "…" }
    }
  ]
}
// → { "accepted": 1, "rejected": 0, "rejected_ids": [] }

Règles

RègleDétail
Champs requisevent_id, event_name, timestamp_occurred (ISO 8601), anonymous_id, source.
DéduplicationUn event_id déjà reçu est ignoré : renvoyez un lot entier sans risque de doublon.
ApplicationRésolue côté serveur via X-App-Key — n'envoyez pas d'app_id (il serait refusé par la validation stricte).
IdentitéSi la requête porte un JWT apprenant, l'identifiant utilisateur de confiance remplace le distinct_id déclaré.
Tracking planLes event_name sont validés contre le tracking plan de la plateforme (docs/analytics/tracking-plan.md) ; un nom inconnu est rejeté au traitement.
Horodatage double

Envoyez timestamp_occurred (moment réel) et, si l'envoi est différé (file hors-ligne), timestamp_sent : la latence d'acheminement reste mesurable.

SDK mobile Flutter

Le SDK officiel est un monorepo de 7 packages Flutter/Dart (repo xamle-mobile-sdk, releases coordonnées — tous les packages portent la même version, actuellement v0.9.1). Chaque package couvre un domaine ; vous n'embarquez que ce dont votre app a besoin. Prérequis : Dart ≥ 3.8, Flutter ≥ 3.24.

PackageRôle
xamle_sdk_coreInitialisation, client HTTP, tokens, capacitésrequis
xamle_sdk_authInvité, OTP SMS, profil apprenantmode managed
xamle_sdk_contentProgrammes, quiz, progression, feed, codes, glossaire, examensrecommandé
xamle_sdk_analyticsCollecte d'événements fiabilisée (file SQLite, batchs gzip)recommandé
xamle_sdk_offlineTéléchargement hors-ligne des programmes/modulesoptionnel
xamle_ui_playerLecteurs vidéo & audio prêts à l'emploi (moments, vitesse)optionnel
xamle_sdk_partnerApp « Pro » partenaire : élèves, paiements, gainsoptionnel

Installation (dépendance git)

Le repo est privé et rien n'est publié sur pub.dev : chaque package se consomme en dépendance git, tous épinglés sur la même ref. Les dépendances entre packages sont résolues automatiquement dans le clone.

pubspec.yaml
dependencies:
  xamle_sdk_core:
    git: { url: https://github.com/Xamle-tech/xamle-mobile-sdk.git, ref: v0.9.1, path: packages/xamle_sdk_core }
  xamle_sdk_auth:
    git: { url: https://github.com/Xamle-tech/xamle-mobile-sdk.git, ref: v0.9.1, path: packages/xamle_sdk_auth }
  xamle_sdk_content:
    git: { url: https://github.com/Xamle-tech/xamle-mobile-sdk.git, ref: v0.9.1, path: packages/xamle_sdk_content }
  xamle_sdk_analytics:
    git: { url: https://github.com/Xamle-tech/xamle-mobile-sdk.git, ref: v0.9.1, path: packages/xamle_sdk_analytics }
  # optionnels : xamle_sdk_offline, xamle_ui_player, xamle_sdk_partner (même url/ref)
Si la résolution échoue avec un tag

Selon la version de Flutter, pub peut buter sur les dépendances de chemin internes : remplacez alors ref: v0.9.1 par le SHA du commit tagué (git rev-list -n 1 v0.9.1). Gardez impérativement la même ref sur tous les packages.

Initialisation

XamleSdk.init est le point d'entrée unique. Il charge GET /mobile/config (avec cache disque : l'app démarre même hors-ligne dès le second lancement), fige les capacités de l'application et choisit le mode :

main.dart
final sdk = await XamleSdk.init(
  appKey: 'xk_live_…',                       // clé de l'application (console → Réglages)
  baseUrl: 'https://<domaine-api>/api/v1',   // requis
  // externalUserId: 'user-42',              // requis en mode contentOnly uniquement
  deepLinks: [XamleDeepLink(key: 'program', label: 'Programme')], // écrans notifiables
);

sdk.config.name;              // nom de l'app
sdk.mode;                     // XamleMode.managed | contentOnly
sdk.capabilities.hasFeed;     // capacités figées à l'init
sdk.features.contentPrograms; // modules activés dans la console
ModeQuandComportement
managedL'app gère ses utilisateurs via Xamle (cas normal)Auth OTP, progression, notifications, analytics complet — routes mobiles.
contentOnlyVotre système d'utilisateurs existe déjàLecture des programmes via /delivery/* avec votre externalUserId ; analytics restreint à 5 événements d'apprentissage.

Les gates de fonctionnalités sont intégrés : chaque repository vérifie que le module correspondant est activé pour l'app et lève une XamleCapabilityError explicite sinon — vos écrans peuvent aussi tester sdk.features.has(…) pour se masquer.

xamle_sdk_core — le socle

Un client HTTP unique (dio) que tous les packages partagent, avec quatre interceptors : en-tête X-App-Key systématique, Authorization: Bearer automatique, refresh sur 401 protégé par mutex (une seule requête de refresh même si dix appels échouent en même temps), et retry exponentiel sur erreurs 5xx/réseau (300 ms → 800 ms → 2 s).

Les tokens vivent dans un TokenStore chiffré (flutter_secure_storage — Keychain iOS, EncryptedSharedPreferences Android, avec récupération automatique si le stockage est corrompu). Les erreurs sont typées : XamleAuthError, XamleValidationError, XamleNetworkError, XamleCapabilityError, XamleConfigError — un simple on XamleAuthError suffit pour router vers l'écran de connexion.

xamle_sdk_auth — connexion apprenant (managed)

auth_flow.dart
final auth = AuthRepository();  // ⚠️ à créer dès le boot : c'est elle qui branche
                                //    le refresh automatique des tokens sur le client

// Mode invité (exploration sans compte)
final guest = await auth.signInAsGuest();

// Connexion par OTP
final req = await auth.requestOtp('+221771234567');
final res = await auth.verifyOtp(phoneNumber: '+221771234567', code: '1234',
                                guestToken: guest.accessToken); // fusionne la session invité

if (res.needsProfileCompletion) {
  // Champs du profil configurés dans la console (dont « Code agent »)
  final schema = await ProfileRepository().getFields();
  await ProfileRepository().submitValues({'full_name': 'Awa Diop', 'code_agent': 'AWA-763'});
}

Autres méthodes : me() (profil + champs manquants), currentAccessToken(), logout() (efface les tokens locaux), updateProfile(…). Les repositories sont sans état : la session persistée vit dans le TokenStore ; la gestion d'état UI (Riverpod, Bloc…) reste celle de votre app.

xamle_sdk_content — contenus & progression

Huit repositories, un par domaine — tous branchés sur le client du core :

RepositoryMéthodes principales
ProgramsRepositorylist(page, limit, search, language, level) · getById(id) — renvoie l'arbre complet Programme → Modules → Leçons (avec URLs de lecture HLS et moments).
ProgressRepositoryreportLesson(lessonId, watchedPercent, completed, positionSeconds) (renvoie les récompenses fraîchement gagnées) · getProgramProgress · getMyStats · getMyRewards · downloadCertificate(earnedId) (PDF).
QuizzesRepositorygetById · getDaily() (quiz du jour) · submitAttempt(quizId, answers, timeTaken) — la correction et les bonnes réponses ne reviennent qu'avec le résultat.
ExamsRepositorylist() · getById(id) — séries d'examens blancs (notation via submitAttempt quiz par quiz).
FeedRepositorygetFeed(cursor, language, limit) (pagination par curseur) · like · comment · share · view(id, watchTime, completed).
AccessCodesRepositoryvalidate(code) (aperçu avant activation) · redeem(code) · partnerLink() (rattachement partenaire).
NotificationsRepositorylist · unreadCount · markRead/markAllRead · registerDevice(fcmToken, platform) pour le push.
GlossaryRepositorylist(category) — fiches avec média et audios multilingues.

Les titres sont des I18nString : title.resolve('wo') renvoie la traduction demandée avec repli sur le français. Chaque leçon expose playbackUrl (HLS adaptatif) et moments (puces horodatées) — à passer tels quels au player.

xamle_sdk_analytics — collecte fiabilisée

analytics.dart
await XamleAnalytics.instance.start();   // après XamleSdk.init

MaterialApp(
  navigatorObservers: [XamleAnalytics.instance.navigatorObserver], // screen_viewed auto
  ...
);

await XamleAnalytics.instance.track('lesson_completed',
    properties: {'lesson_id': id, 'program_id': programId});

Sous le capot : file SQLite locale (cap 2 000 événements), envoi par lots gzip toutes les 15 s vers POST /analytics/events, reprise avec backoff exponentiel après coupure, sessions automatiques (timeout 30 min), événements de cycle de vie (app_opened, session_started…) et contexte device injecté (modèle, OS, réseau, version d'app via AnalyticsConfig(appVersion: …)).

Deux pièges

1) Le suivi d'écran automatique ne capte que les routes nommées (RouteSettings.name) — nommez vos routes, y compris avec go_router. 2) Utilisez les noms d'événements du tracking plan : un lot contenant un nom inconnu est rejeté au traitement.

xamle_ui_player — lecteurs prêts à l'emploi

Deux widgets autonomes (aucune dépendance au reste du SDK), thémables et localisables :

player.dart
XamleVideoPlayer(
  source: XamleVideoSource.network(lesson.playbackUrl!),
  moments: lesson.moments.map(XamlePlayerMoment.fromJson).toList(),
  startAt: Duration(seconds: lastPosition),         // reprise de lecture
  onProgress: (position, duration, completed) =>
      progress.reportLesson(lessonId: lesson.id,     // toutes les 5 s + pause + fin
        positionSeconds: position.inSeconds, completed: completed),
  onMomentTap: (m) => m.isQuiz ? openQuiz(m.targetId) : openGlossary(m.targetId),
)

XamleAudioPlayer(
  source: XamleAudioSource.network(url),
  title: lesson.title.resolve(),
  speeds: [1.0, 1.25, 1.5, 2.0],                     // vitesse, avance/recul 10 s
)

XamleVideoPlayer affiche les moments comme marqueurs sur la barre et puces cliquables au bon instant — l'ouverture du quiz ou de la fiche glossaire reste à la main de votre app (onMomentTap). onProgress est calibré pour alimenter reportLesson sans réglage.

xamle_sdk_offline — hors-ligne

OfflineRepository parle au serveur (catalogue des ressources téléchargeables, manifeste des fichiers, journalisation) ; OfflineStore gère l'appareil : download(type, targetId, onProgress) télécharge tous les fichiers d'un programme/module avec progression globale, downloaded() liste le local, localPath(…) donne le chemin à passer à XamleVideoSource.file(…), syncAll() met à jour ce qui a changé, delete(…) libère l'espace.

xamle_sdk_partner — app « Pro » partenaire

Pour l'application des partenaires (auto-écoles) : candidature et suivi d'agrément (me(), apply(…), upload du justificatif), tableau de bord (hub()), liste des élèves filtrable/triable, enregistrement des paiements par tranche (recordPayment — espèces, Wave, Orange Money…, annulable 48 h), encouragements et relances, gains (commissions + primes par période) et outils de partage (affiche PDF avec QR code). L'authentification est celle de xamle_sdk_auth (OTP) sur l'application Pro et sa propre clé.

Ordre d'intégration recommandé (managed)

XamleSdk.init(appKey, baseUrl)
Au démarrage, avant runApp — le cache de config permet un démarrage hors-ligne.
AuthRepository() une fois au boot
Active le refresh automatique des tokens ; puis invité ou OTP selon votre parcours.
Champs du profil
Si needsProfileCompletion, affichez les champs de getFields() et soumettez via submitValues.
XamleAnalytics.instance.start() + navigatorObserver
La collecte tourne ensuite toute seule.
Repositories de contenu + players
Programmes → leçons → XamleVideoPlayer avec onProgress → reportLesson.

Points d'attention

SujetÀ savoir
VersionsTous les packages sur la même ref. flutter_secure_storage accepté de 9.2.2 à 10.x depuis la v0.9.1 — montez à 0.9.1 si votre app utilise secure_storage 10.
AndroidStockage chiffré ⇒ minSdkVersion ≥ 23. Permission INTERNET requise.
Audio en arrière-planXamleAudioPlayer s'appuie sur video_player : la lecture écran verrouillé demande la configuration native habituelle (background modes iOS / service Android).
Accès au repoRepo git privé : chaque poste et la CI doivent avoir un accès (clé SSH ou token).
XamleSdk.reset()Vide le singleton mais pas les tokens ni le cache : pour une déconnexion complète, appelez aussi auth.logout().

Sécurité & limites de débit

Permissions

Les comptes équipe portent des permissions au format module:action (ex. contents:write, notifications:send, agents:read), issues de l'union de leurs rôles. Une route protégée sans la permission requise répond 403 — de même qu'une requête sur une application hors de votre périmètre.

Limites de débit

PérimètreLimite
Global (par client)10 req/s · 50 req/10 s · 100 req/min
POST /auth/login5 tentatives / 15 min (puis verrouillage du compte 15 min).
POST /mobile/auth/otp/request3 SMS / minute par client.

Une réponse 429 indique un dépassement : espacez vos requêtes et privilégiez les envois par lots (analytics) plutôt que les appels unitaires.

Bonnes pratiques

FaireNe pas faire
Passer par le SDK côté mobile (refresh, retry et files gérés).Embarquer des identifiants de compte équipe dans une app cliente.
Régénérer la clé d'application en cas de fuite (console → Réglages).Committer une clé X-App-Key de production dans un repo public.
Transmettre X-Request-ID pour tracer vos appels.Rejouer un lot analytics en changeant les event_id (doublons garantis).