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 ?
Chaque résultat enregistré est envoyé en POST au format JSON vers une URL que tu possèdes.
- À utiliser quand
- Le joueur peut être n'importe où — un lien partagé, un QR code, le site de quelqu'un d'autre — et tu veux le résultat dans ta propre base de données.
- Il te faut
- Un point de terminaison HTTPS qui accepte un POST cross-origin.
save_puzzle_results_via_webhookLe même JSON, envoyé à la page qui intègre l'activité plutôt qu'à un serveur.
- À utiliser quand
- Tu intègres l'activité dans la page de ton propre cours et cette page peut elle-même exploiter le résultat.
- Il te faut
- Un iframe sur ta page et un écouteur de messages. Pas de serveur, pas de CORS.
save_results_iframe_postmessageUn seul message quand le joueur termine, qui ne porte que cette information.
- À utiliser quand
- Tu veux seulement savoir s'il a terminé — pour cocher la leçon, débloquer la suivante, ou afficher ton propre écran.
- Il te faut
- Un iframe sur ta page et un écouteur de messages.
send_completion_signal_when_embeddedWebhook 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.
- 1 Ouvre l'activité dans l'éditeur et va dans le menu Développeur.
- 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 Joue toi-même une fois à l'activité. Le premier POST arrive dès que tu réponds à quelque chose.
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);
});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.
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.
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.
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.
<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>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.
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.
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);
});{
"completed": true,
"activityKey": "-Nq8sample_activity_key"
}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à.
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.
| Champ | Type | Rôle |
|---|---|---|
activityKey toujours string | string | L'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 | string | Qui 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 | object | Les 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 | number | La progression, en pourcentage. 100 signifie terminé. |
timePassed toujours number | number | Temps passé sur l'activité, en millisecondes. |
lastPlayedAt toujours number | number | Quand ce résultat a été enregistré, sous forme d'horodatage Unix en millisecondes. |
createdAt parfois number | number | Quand la tentative a commencé, sous forme d'horodatage Unix en millisecondes. |
playerInput parfois object | object | Ce 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 | object | Lesquels 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 | number | Points 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 | number | La 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 | number | Le 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 | boolean | La partie s'est terminée sur une mauvaise réponse et est finie sans être complète. |
hasAlternateCompletion parfois boolean | boolean | L'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 | array | Les 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 | number | La 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. |
{
"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
}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.
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');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é.
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.
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.
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.
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();
});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'APIQuand 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é.
Lancée depuis le LMS, avec le score renvoyé dans son carnet de notes.
Publie une activité comme un devoir et récupère les notes automatiquement.
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