Aller au contenu
Intégrations développeur

Connecte Puzzel à ta propre plateforme

Puzzel peut transmettre directement le résultat d'un joueur à un autre système — son score, sa progression, ce qu'il a répondu — sans que ce système soit un LMS complet. Cette page recense tous les canaux disponibles : ce qui sort, quand ça sort, et le minimum à construire pour le récupérer.

Les résultats sortent via
Un webhook, ou un message vers la page qui héberge l'activité
Ta page peut
Connecter le joueur et réinitialiser l'activité
Activé
Par activité, dans le menu Développeur de l'éditeur
Inclus avec
Une formule payante — ces paramètres sont désactivés sur un compte gratuit

De quel canal as-tu besoin ?

Trois choses peuvent sortir d'une activité et une seule peut y entrer. Le bon choix dépend d'une seule question : le joueur se trouve-t-il dans ta page, ou complètement ailleurs ?

Hors de Puzzel
Vers Puzzel

Webhook de résultats

Active "Envoyer les résultats à un webhook" dans l'éditeur et indique une URL. À partir de là, chaque fois que la progression du joueur est enregistrée, son navigateur envoie l'ensemble du résultat en POST vers cette URL, au format JSON.

Mise en place
  1. 1 Ouvre l'activité dans l'éditeur et va dans le menu Développeur.
  2. 2 Active "Envoyer les résultats à un webhook" et colle ton point de terminaison dans le champ juste en dessous. Ce doit être une URL complète — un simple domaine est refusé — et elle doit être en https, car le navigateur bloque un appel en http simple depuis une page servie en https.
  3. 3 Joue toi-même une fois à l'activité. Le premier POST arrive dès que tu réponds à quelque chose.
Un récepteur, de bout en bout
import express from 'express';

const app = express();

// The POST is made by the player's browser, so the browser asks permission
// first. Answer the preflight and the real request can land.
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', 'https://puzzel.org');
  res.set('Access-Control-Allow-Headers', 'Content-Type');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

app.post('/puzzel/results', express.json(), async (req, res) => {
  const result = req.body;

  // One run posts several times as it goes. Key on the pair, not on an
  // insert: each call carries the whole state, so the newest simply wins.
  await db.results.upsert(
    { playerUid: result.playerUid, activityKey: result.activityKey },
    result
  );

  // Only act on the finish once.
  if (result.progress >= 100 || result.hasAlternateCompletion) {
    await markCourseStepComplete(result);
  }

  res.sendStatus(200);
});
Ça se déclenche pendant que tu joues, pas une seule fois à la fin

Un résultat sort à chaque enregistrement de la saisie : après une pause dans la frappe, quand une carte est posée, quand le chronomètre s'arrête, et encore une fois quand l'activité est terminée. Une longue grille de mots croisés génère une vingtaine d'appels, pas un seul — écris donc ton gestionnaire comme un upsert indexé sur playerUid et activityKey plutôt que comme une insertion. Chaque appel transporte l'état complet, si bien que le plus récent remplace toujours le précédent et qu'un appel perdu est rattrapé par le suivant.

Distinguer une fin d'un simple enregistrement

progress est un pourcentage : 100 signifie que l'activité est terminée. Quelques types peuvent se terminer sans l'atteindre — un quiz entièrement répondu, un champ solution résolu — et portent alors hasAlternateCompletion à la place. Considère les deux cas comme terminés.

C'est le navigateur du joueur qui l'envoie

Le POST provient de l'onglet où l'activité est jouée, pas d'un serveur Puzzel. La plupart des points de terminaison conçus pour recevoir des webhooks l'acceptent déjà. Si le tien ne voit jamais de requête, voici pourquoi : le navigateur demande d'abord la permission, donc réponds au préflight OPTIONS avec un en-tête Access-Control-Allow-Origin et le vrai POST suivra.

Traite le contenu comme une déclaration, pas comme une preuve

La requête n'est pas signée et provient d'un navigateur que tu ne contrôles pas, donc n'importe qui regardant la page peut t'en envoyer une aussi. C'est sans risque pour remplir une barre de progression ou un tableau de bord. Pour tout ce que tu ne laisserais pas un élève régler lui-même — une note qui compte, un certificat, un paiement — vérifie-la par rapport aux résultats de ton propre tableau de bord Puzzel, ou laisse les connecteurs de notation LMS transmettre le score à ta place.

Résultats vers la page qui l'héberge

Si tu intègres l'activité, tu peux faire envoyer ce même JSON à ta propre page plutôt qu'à un serveur. Active "Envoyer les résultats à la page parente" et écoute le message. Rien ne sort du navigateur, donc aucun point de terminaison à construire et aucun CORS à gérer.

index.html
<iframe
  src="https://puzzel.org/en/quiz/embed?p=-Nq8sample_activity_key"
  width="100%"
  height="700"
  frameborder="0"
  allowfullscreen
></iframe>

<script>
  window.addEventListener('message', (event) => {
    if (event.origin !== 'https://puzzel.org') return;

    const result = event.data;
    if (!result || !result.activityKey) return;

    // Same shape the webhook posts, same advice: it arrives repeatedly.
    saveProgress(result);
  });
</script>
Vérifie la provenance du message

Ton écouteur reçoit tous les messages envoyés à la page, y compris depuis d'autres frames et des extensions de navigateur. Compare event.origin à https://puzzel.org avant de faire confiance à son contenu.

Le même contenu, le même timing

C'est le jumeau du webhook : les mêmes champs, envoyés aux mêmes moments. Tout ce qui est dit dans "Ce que contient un résultat" s'applique aussi ici.

Signal de fin

Le plus petit canal, pour quand le résultat lui-même ne te regarde pas : active "Envoyer un signal de fin" et ta page reçoit un message dès que le joueur termine.

index.html
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://puzzel.org') return;
  if (event.data?.completed !== true) return;

  // { completed: true, activityKey: '-Nq8sample_activity_key' }
  unlockNextLesson(event.data.activityKey);
});
Ce qui arrive
{
  "completed": true,
  "activityKey": "-Nq8sample_activity_key"
}
Le résultat est déjà enregistré quand il arrive

Le signal est volontairement envoyé après que l'enregistrement final a eu lieu, donc une page qui réagit en relisant le résultat le trouvera bien là.

Uniquement dans un iframe

Les deux canaux de messages envoient à la page qui encadre l'activité. Ouverte dans son propre onglet, il n'y a personne à prévenir, donc rien n'est envoyé.

Ce que contient un résultat

Une seule structure, quel que soit le canal qui la transporte. Les réponses du joueur sont indexées par les identifiants propres des éléments de l'activité, donc les mêmes clés se retrouvent dans correctUids.

ChampTypeRôle
activityKey
toujours
string
stringL'activité à laquelle appartient le résultat. La même clé que celle affichée dans l'URL de l'activité, après ?p=.
playerUid
toujours
string
stringQui a joué, sous forme d'identifiant anonyme. Stable pour ce joueur sur cet appareil, c'est donc sur cette valeur qu'il faut indexer les résultats — ce n'est ni une adresse e-mail ni un compte Puzzel.
player
parfois
object
objectLes champs d'inscription demandés par l'activité, tels que tu les as configurés : name, email, class, student_id, etc. Absent tant que le joueur ne s'est pas inscrit, et absent entièrement sur une activité qui ne demande rien.
progress
toujours
number
numberLa progression, en pourcentage. 100 signifie terminé.
timePassed
toujours
number
numberTemps passé sur l'activité, en millisecondes.
lastPlayedAt
toujours
number
numberQuand ce résultat a été enregistré, sous forme d'horodatage Unix en millisecondes.
createdAt
parfois
number
numberQuand la tentative a commencé, sous forme d'horodatage Unix en millisecondes.
playerInput
parfois
object
objectCe que le joueur a réellement saisi, indexé par l'identifiant de l'élément auquel ça appartient. La structure interne dépend du type d'activité — un mot, une liste de cartes placées, une option choisie.
correctUids
parfois
object
objectLesquels de ces éléments sont corrects, indexés de la même façon. Absent tant qu'aucune réponse n'a encore été donnée.
score
parfois
number
numberPoints obtenus, sur les types qui notent une partie. Absent partout ailleurs — y compris sur une partie qui a réellement obtenu un score nul, donc vérifie que la clé existe avant de la lire.
performance
parfois
number
numberLa mesure propre à un type pour évaluer sa performance, quand il en garde une — le nombre de mots par minute pour l'entraînement au clavier, par exemple.
attempts
parfois
number
numberLe numéro de cette partie : 1 la première fois, un de plus à chaque nouveau départ. Uniquement sur les types qui peuvent arrêter une partie avant la fin et qui comptent les tentatives.
knockedOut
parfois
boolean
booleanLa partie s'est terminée sur une mauvaise réponse et est finie sans être complète.
hasAlternateCompletion
parfois
boolean
booleanL'activité a été terminée d'une façon qui n'atteint pas 100 % — un quiz entièrement répondu, un champ solution résolu. Considère cela comme une fin d'activité.
missedKeys
parfois
array
arrayLes caractères que le joueur a le plus souvent mal saisis, du plus manqué au moins manqué. Uniquement pour l'entraînement au clavier.
contentVersion
parfois
number
numberLa version du contenu de l'activité sur laquelle cela a été joué. Elle change quand le propriétaire modifie les questions, ce qui permet de distinguer un ancien résultat d'un résultat actuel.
Un quiz terminé, tel qu'il arrive
{
  "activityKey": "-Nq8sample_activity_key",
  "playerUid": "kK3r9TzSampleAnonymousUid",
  "player": {
    "name": "Ava Ortega",
    "email": "ava@school.example",
    "class": "5B"
  },
  "progress": 100,
  "timePassed": 243120,
  "lastPlayedAt": 1758297843120,
  "createdAt": 1758297600000,
  "playerInput": {
    "-Nq8item_one": "Lisbon",
    "-Nq8item_two": "1969"
  },
  "correctUids": {
    "-Nq8item_one": true,
    "-Nq8item_two": false
  },
  "score": 800,
  "contentVersion": 1757923200000
}
Absent, pas vide

Un champ qui ne s'applique pas est omis du JSON plutôt qu'envoyé comme null ou zéro. C'est ainsi qu'on distingue un type qui ne note pas une partie d'une partie qui a obtenu un score nul — lis donc avec une valeur par défaut et ne suppose jamais qu'une clé est présente.

Réinitialiser l'activité depuis ta page

Une instruction circule dans l'autre sens. Avec "Accepter les commandes de la page parente" activé, la page qui fait l'intégration peut effacer les réponses du joueur et remettre l'activité à son point de départ — pour ton propre bouton "réessayer", en dehors du cadre.

reset
const frame = document.querySelector('iframe').contentWindow;

// Sent to the frame, from the page that embeds it — no other sender is
// accepted, and the activity has to have the trigger setting switched on.
frame.postMessage({ trigger: { type: 'reset' } }, 'https://puzzel.org');
La réinitialisation est la seule commande possible

Il n'existe aucun message pour soumettre un résultat, ouvrir le message de fin ou sauter à une question. Un message demandant autre chose qu'une réinitialisation est ignoré.

Seule la page qui intègre l'activité est écoutée

La commande n'est acceptée que depuis la page qui encadre l'activité, et de nulle part ailleurs — ni une frame voisine, ni un script sur la page. Le paramètre est désactivé par défaut, active-le donc pour les activités que tu pilotes.

Utiliser ta propre identité de joueur

Si ta plateforme sait déjà qui joue, l'activité n'a pas besoin de le redemander. Deux échanges existent, tous deux pour une activité intégrée dans ta page, et tous deux activés par nous plutôt que dans l'éditeur — ils déterminent à qui appartient un résultat, donc ils se mettent en place avec toi plutôt que depuis une case à cocher.

Poste-nous le joueur

L'activité s'annonce avec 'app-loaded' et attend. Ta page renvoie les informations du joueur, et le résultat est classé sous son nom sans qu'il ait à saisir quoi que ce soit ni à voir un écran d'inscription.

Poste-nous un jeton

Le même échange, mais ta page poste le JWT émis par ton fournisseur d'identité plutôt que les champs eux-mêmes. Nous le vérifions par rapport aux émetteurs configurés pour ton compte avant de laisser entrer le joueur, ce qui prouve l'identité au lieu de la déclarer simplement — c'est celui-ci qu'il faut demander quand le résultat doit être fiable.

index.html
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://puzzel.org') return;

  // The activity says it is ready for a player.
  if (event.data === 'app-loaded') {
    frame.postMessage(
      {
        player: { name: 'Ava Ortega', email: 'ava@school.example', class: '5B' },
        // true starts a fresh attempt instead of resuming this player's last one
        should_reset: false
      },
      'https://puzzel.org'
    );
  }

  // Signed in; the board is coming.
  if (event.data?.success === true) hideYourOwnSpinner();
});
Demande-nous de l'activer

Dis-nous lequel des deux tu souhaites et où les activités seront intégrées, et nous configurerons ton compte et t'accompagnerons pas à pas.

Nous écrire à propos de l'identité

Créer des activités depuis ton système

Tout ce qui précède concerne un résultat qui sort. Faire l'inverse — créer les activités elles-mêmes à partir de contenu que tu possèdes déjà — c'est le rôle de la Puzzle API : un POST par type d'activité, et tu récupères une clé et une URL à intégrer.

Lire la référence de l'API

Quand un connecteur tout prêt est la meilleure solution

Si la plateforme en face est un vrai LMS, tu n'as probablement besoin de rien de tout ça. Les notes peuvent remonter d'elles-mêmes dans son carnet de notes, sans rien à héberger de ton côté.

Aucun de ceux-là ?

Les plateformes de cours, les sites à abonnement, les intranets et tout ce que tu as construit toi-même sont exactement ce à quoi servent les canaux de cette page. Une intégration associée au signal de fin couvre la plupart des cas.

Ce qu'il n'y a pas

Pour que tu ne le cherches pas en vain :

  • Aucun point de terminaison pour relire les résultats. L'API crée des activités ; les résultats sortent par les canaux de cette page, ou par les exports de ton tableau de bord.
  • Aucune signature sur le webhook. Il n'y a rien pour vérifier la requête, c'est pourquoi un résultat ne devrait jamais être la seule chose garantissant quelque chose d'important.
  • Aucun webhook valable pour tout le compte. L'URL est un paramètre de l'activité, donc une activité copiée l'emporte avec elle et une nouvelle activité démarre sans.
  • Rien dans un jeu en équipes ou une salle en direct. Les deux canaux de messages et le webhook sont réservés au jeu en solo, et les paramètres se désactivent d'eux-mêmes quand le mode équipes est actif.
  • Aucune file d'attente de livraison. Rien n'est stocké puis renvoyé — le prochain enregistrement fait office de nouvelle tentative, et c'est le dernier appel de la partie qui compte.

Quelque chose ne se comporte pas comme prévu ?

Envoie ta requête et le message d'erreur reçu. La personne qui a développé le point de terminaison te répondra directement.

Écrire au support