Verbind Puzzel met je eigen platform
Puzzel kan het resultaat van een speler rechtstreeks doorgeven aan een ander systeem — de score, hoe ver iemand kwam, wat er is ingevuld — zonder dat dat systeem een volwaardig LMS hoeft te zijn. Deze pagina laat elk kanaal zien dat er is: wat eruit gaat, wanneer het eruit gaat, en het kleinste stukje dat je moet bouwen om het op te vangen.
- Resultaten gaan naar buiten via
- Een webhook, of een bericht naar de pagina rond de activiteit
- Jouw pagina kan
- De speler inloggen en de activiteit resetten
- Ingeschakeld
- Per activiteit, onder Ontwikkelaar in de editor
- Inbegrepen bij
- Een betaald abonnement — deze instellingen staan uit bij een gratis account
Welk kanaal heb je nodig?
Drie dingen kunnen een activiteit verlaten en één kan erin. Welke bij jou past hangt af van één vraag: zit de speler binnen jouw pagina, of ergens heel anders?
Elk opgeslagen resultaat wordt met een POST als JSON naar een URL van jou gestuurd.
- Gebruik dit wanneer
- De speler kan overal zitten — een gedeelde link, een QR-code, de site van iemand anders — en je wilt het resultaat in je eigen database.
- Je hebt nodig
- Een HTTPS-endpoint dat een cross-origin POST accepteert.
save_puzzle_results_via_webhookDezelfde JSON, gepost naar de pagina die de activiteit insluit in plaats van naar een server.
- Gebruik dit wanneer
- Je sluit de activiteit in op je eigen cursuspagina en die pagina kan zelf iets met het resultaat doen.
- Je hebt nodig
- Een iframe op je pagina en een message listener. Geen server, geen CORS.
save_results_iframe_postmessageEén bericht zodra de speler klaar is, dat verder niets bevat dan dat feit.
- Gebruik dit wanneer
- Je wilt alleen weten of ze klaar zijn — om de les af te vinken, de volgende te ontgrendelen, of je eigen scherm te tonen.
- Je hebt nodig
- Een iframe op je pagina en een message listener.
send_completion_signal_when_embeddedResultaten-webhook
Zet "Resultaten naar een webhook sturen" aan in de editor en geef er een URL bij op. Vanaf dan post de browser van de speler bij elke keer dat de voortgang wordt opgeslagen het hele resultaat als JSON naar die URL.
- 1 Open de activiteit in de editor en ga naar het menu Ontwikkelaar.
- 2 Zet "Resultaten naar een webhook sturen" aan en plak je endpoint in het veld eronder. Het moet een volledige URL zijn — een kaal domein wordt geweigerd — en het moet https zijn, omdat de browser een gewone http-aanroep blokkeert vanaf een pagina die over https wordt geserveerd.
- 3 Speel de activiteit zelf één keer. De eerste POST komt binnen zodra je iets invult.
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);
});Er gaat een resultaat naar buiten elke keer dat de invoer wordt opgeslagen: na een pauze in het typen, wanneer een kaart wordt neergelegd, wanneer de klok stopt, en nog eens wanneer de activiteit is afgerond. Een lange kruiswoordpuzzel is een stuk of twintig aanroepen, niet één — schrijf je handler dus als een upsert op basis van playerUid en activityKey, niet als een insert. Elke aanroep bevat de volledige staat, dus de nieuwste vervangt altijd de vorige, en een aanroep die wegvalt wordt door de volgende weer goedgemaakt.
progress is een percentage: 100 betekent dat de activiteit is afgerond. Een paar types kunnen eindigen zonder dat te bereiken — een quiz die helemaal is doorlopen, een opgelost oplossingsveld — en die krijgen in plaats daarvan hasAlternateCompletion. Behandel beide als afgerond.
De POST komt uit het tabblad waarin de activiteit wordt gespeeld, niet van een server van Puzzel. De meeste endpoints die zijn gebouwd om webhooks te ontvangen accepteren dit al. Komt er bij jou nooit een verzoek binnen, dan is dit de reden: de browser vraagt eerst toestemming, dus beantwoord de OPTIONS-preflight met een Access-Control-Allow-Origin-header en de echte POST volgt vanzelf.
Er zit geen handtekening op het verzoek, en het komt van een browser die jij niet beheert, dus iedereen die naar de pagina kijkt kan er ook een sturen. Dat is prima voor het vullen van een voortgangsbalk of een dashboard. Voor alles wat je een leerling niet zelf zou laten instellen — een cijfer dat meetelt, een certificaat, een betaling — controleer je het tegen de resultaten in je eigen Puzzel-dashboard, of laat je de LMS-koppelingen voor cijfers de score meenemen.
Resultaten naar de omliggende pagina
Sluit je de activiteit in, dan kun je diezelfde JSON naar je eigen pagina laten posten in plaats van naar een server. Zet "Resultaten naar de bovenliggende pagina sturen" aan en luister naar het bericht. Er verlaat niets de browser, dus je hoeft geen endpoint te bouwen en niet aan CORS te denken.
<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>Jouw listener hoort elk bericht dat naar de pagina wordt gepost, ook vanuit andere frames en browserextensies. Vergelijk event.origin met https://puzzel.org voordat je vertrouwt op wat erin staat.
Dit is de tweelingbroer van de webhook: dezelfde velden, verstuurd op dezelfde momenten. Alles onder "Wat een resultaat bevat" geldt hier ook.
Afrondingssignaal
Het kleinste kanaal, voor als het resultaat zelf je niets aangaat: zet "Een afrondingssignaal versturen" aan en je pagina krijgt één bericht op het moment dat de speler klaar is.
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"
}Het signaal wordt bewust pas verstuurd nadat de afrondende save is aangekomen, zodat een pagina die daarop reageert door het resultaat terug te lezen het daar ook aantreft.
Beide berichtkanalen posten naar de pagina die de activiteit omkadert. Geopend in een eigen tabblad is er niemand om het aan te vertellen, dus wordt er niets verstuurd.
Wat een resultaat bevat
Eén vorm, welk kanaal het ook draagt. De antwoorden van de speler zijn gesleuteld op de eigen item-id's van de activiteit, dus dezelfde sleutels komen terug in correctUids.
| Veld | Type | Wat het doet |
|---|---|---|
activityKey altijd string | string | De activiteit waar het resultaat bij hoort. Dezelfde sleutel die je ziet in de eigen URL van de activiteit, na ?p=. |
playerUid altijd string | string | Wie er speelde, als een anonieme id. Stabiel voor deze speler op dit apparaat, dus gebruik dit als sleutel voor je resultaten — het is geen e-mailadres en geen Puzzel-account. |
player soms object | object | De registratievelden die de activiteit uitvraagt, zoals jij ze hebt ingesteld: naam, e-mail, klas, student_id en zo verder. Afwezig totdat de speler zich heeft geregistreerd, en volledig afwezig bij een activiteit die niets uitvraagt. |
progress altijd number | number | Hoe ver iemand is, als percentage. 100 betekent afgerond. |
timePassed altijd number | number | Tijd op de activiteit, in milliseconden. |
lastPlayedAt altijd number | number | Wanneer dit resultaat is opgeslagen, als Unix-timestamp in milliseconden. |
createdAt soms number | number | Wanneer de poging is gestart, als Unix-timestamp in milliseconden. |
playerInput soms object | object | Wat de speler daadwerkelijk heeft ingevoerd, gesleuteld op de id van het item waar het bij hoort. De vorm daarbinnen hangt af van het activiteitstype — een woord, een lijst geplaatste kaarten, een gekozen optie. |
correctUids soms object | object | Welke van die items goed zijn, op dezelfde manier gesleuteld. Afwezig zolang er nog niets is beantwoord. |
score soms number | number | Behaalde punten, bij de types die een run scoren. Overal elders afwezig — ook bij een run die daadwerkelijk nul scoorde, dus controleer of de sleutel bestaat voordat je hem uitleest. |
performance soms number | number | De eigen maatstaf van een type voor hoe goed het ging, waar die wordt bijgehouden — woorden per minuut bij typeoefening, bijvoorbeeld. |
attempts soms number | number | Welke run dit is: 1 de eerste keer, telkens één hoger bij elke herstart. Alleen bij types die een run vroegtijdig kunnen beëindigen en de pogingen tellen. |
knockedOut soms boolean | boolean | De run eindigde op een fout antwoord en is voorbij zonder afgerond te zijn. |
hasAlternateCompletion soms boolean | boolean | De activiteit is afgerond op een manier die geen 100% haalt — een quiz die helemaal is doorlopen, een opgelost oplossingsveld. Behandel dit als een afronding. |
missedKeys soms array | array | Tekens die de speler steeds fout bleef typen, meest gemist eerst. Alleen bij typeoefening. |
contentVersion soms number | number | Tegen welke versie van de inhoud van de activiteit dit is gespeeld. Deze verandert wanneer de eigenaar de vragen bewerkt, zodat een oud resultaat te onderscheiden is van een actueel. |
{
"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
}Een veld dat niet van toepassing is wordt uit de JSON weggelaten in plaats van als null of nul verstuurd. Zo is een type dat een run niet scoort te onderscheiden van een run die nul scoorde — lees dus met een standaardwaarde en ga er nooit van uit dat een sleutel aanwezig is.
De activiteit resetten vanaf jouw pagina
Eén instructie gaat de andere kant op. Met "Triggers van de bovenliggende pagina accepteren" aangezet kan de insluitende pagina de antwoorden van de speler wissen en de activiteit terugzetten naar het begin — voor een eigen "opnieuw proberen"-knop buiten het frame.
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');Er is geen bericht voor het inleveren van een resultaat, het openen van het eindbericht, of het springen naar een vraag. Een bericht dat om iets anders dan een reset vraagt wordt genegeerd.
De trigger wordt alleen geaccepteerd van de pagina die de activiteit omkadert en van nergens anders — geen naburig frame, geen script op de pagina. De instelling staat standaard uit, dus zet hem aan voor de activiteiten die jij aanstuurt.
Je eigen spelersidentiteit meenemen
Als jouw platform al weet wie er speelt, hoeft de activiteit dat niet nog eens te vragen. Er zijn twee handshakes, allebei voor een activiteit binnen jouw pagina, en allebei door ons ingeschakeld in plaats van in de editor — ze bepalen aan wie een resultaat toebehoort, dus die stellen we samen met jou in plaats van via een vinkje.
De activiteit meldt zichzelf met 'app-loaded' en wacht. Jouw pagina post de gegevens van de speler terug, en het resultaat wordt daaronder vastgelegd zonder dat de speler iets hoeft te typen of een registratiescherm te zien.
Dezelfde handshake, maar jouw pagina post het JWT dat je identity provider heeft uitgegeven in plaats van de velden zelf. Wij verifiëren dit tegen de issuers die voor jouw account zijn ingesteld voordat de speler wordt toegelaten, zodat de identiteit bewezen is in plaats van beweerd — dit is degene om te vragen wanneer het resultaat betrouwbaar moet zijn.
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();
});Laat ons weten welke van de twee je wilt en waar de activiteiten worden ingesloten, en wij richten je account in en lopen het samen met je door.
Mail ons over identiteitActiviteiten maken vanuit jouw systeem
Alles hierboven gaat over een resultaat dat naar buiten komt. De andere kant op — de activiteiten zelf maken vanuit inhoud die je al hebt — is de Puzzle API: één POST per activiteitstype, en je krijgt een sleutel en een URL terug om in te sluiten.
Lees de API-referentieWanneer een kant-en-klare koppeling de betere oplossing is
Is het platform aan de andere kant een echt LMS, dan heb je dit allemaal waarschijnlijk niet nodig. Cijfers kunnen vanzelf terugkomen in de cijferlijst, zonder dat jij iets hoeft te hosten.
Gestart vanuit het LMS zelf, waarbij de score wordt teruggeschreven naar de cijferlijst.
Plaats een activiteit als opdracht en laat de cijfers automatisch terugkomen.
Cursusplatforms, ledensites, intranetten en alles wat je zelf hebt gebouwd — daar zijn de kanalen op deze pagina precies voor bedoeld. Een insluiting plus het afrondingssignaal dekt het meeste al.
Wat er niet is
Zodat je er niet naar hoeft te zoeken:
- Geen endpoint om resultaten terug te lezen. De API maakt activiteiten aan; resultaten verlaten het systeem via de kanalen op deze pagina, of via de exports in je dashboard.
- Geen handtekening op de webhook. Er is niets om het verzoek tegen te verifiëren, en dat is waarom een resultaat niet het enige mag zijn dat iets belangrijks onderbouwt.
- Geen accountbrede webhook. De URL is een instelling op een activiteit, dus een activiteit die je kopieert neemt hem mee, en een nieuwe begint zonder.
- Niets bij een teamspel of een live room. Zowel de berichtkanalen als de webhook zijn voor solo spelen, en de instellingen schakelen zichzelf uit zodra teamspel aanstaat.
- Geen verzendwachtrij. Er wordt niets opgeslagen en opnieuw verstuurd — de volgende keer opslaan is de nieuwe poging, en de laatste aanroep van de run is degene die telt.
Doet iets niet wat het moet doen?
Stuur je request en de foutmelding die je terugkreeg. Je krijgt antwoord van de maker van de API.
Mail support