Naar de inhoud
Integraties voor ontwikkelaars

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?

Uit Puzzel
Naar Puzzel

Resultaten-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.

Zo stel je het in
  1. 1 Open de activiteit in de editor en ga naar het menu Ontwikkelaar.
  2. 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. 3 Speel de activiteit zelf één keer. De eerste POST komt binnen zodra je iets invult.
Een ontvanger, van begin tot eind
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);
});
Het vuurt terwijl je speelt, niet pas aan het einde

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.

Een afronding herkennen tussen de tussentijdse opslagmomenten

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.

Het wordt verstuurd door de browser van de speler

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.

Behandel de payload als een bewering, geen bewijs

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.

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>
Controleer waar het bericht vandaan komt

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.

Dezelfde payload, dezelfde timing

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.

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);
});
Wat er binnenkomt
{
  "completed": true,
  "activityKey": "-Nq8sample_activity_key"
}
Het resultaat is al opgeslagen wanneer het binnenkomt

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.

Alleen binnen een iframe

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.

VeldTypeWat het doet
activityKey
altijd
string
stringDe activiteit waar het resultaat bij hoort. Dezelfde sleutel die je ziet in de eigen URL van de activiteit, na ?p=.
playerUid
altijd
string
stringWie 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
objectDe 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
numberHoe ver iemand is, als percentage. 100 betekent afgerond.
timePassed
altijd
number
numberTijd op de activiteit, in milliseconden.
lastPlayedAt
altijd
number
numberWanneer dit resultaat is opgeslagen, als Unix-timestamp in milliseconden.
createdAt
soms
number
numberWanneer de poging is gestart, als Unix-timestamp in milliseconden.
playerInput
soms
object
objectWat 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
objectWelke van die items goed zijn, op dezelfde manier gesleuteld. Afwezig zolang er nog niets is beantwoord.
score
soms
number
numberBehaalde 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
numberDe eigen maatstaf van een type voor hoe goed het ging, waar die wordt bijgehouden — woorden per minuut bij typeoefening, bijvoorbeeld.
attempts
soms
number
numberWelke 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
booleanDe run eindigde op een fout antwoord en is voorbij zonder afgerond te zijn.
hasAlternateCompletion
soms
boolean
booleanDe 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
arrayTekens die de speler steeds fout bleef typen, meest gemist eerst. Alleen bij typeoefening.
contentVersion
soms
number
numberTegen 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.
Een afgeronde quiz, zoals die binnenkomt
{
  "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
}
Afwezig, niet leeg

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.

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');
Resetten is de enige trigger

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.

Alleen de insluitende pagina wordt gehoord

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.

Post ons de speler

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.

Post ons een token

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.

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();
});
Vraag ons het aan te zetten

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 identiteit

Activiteiten 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-referentie

Wanneer 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.

Geen van deze?

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