Puzzel in deine eigene Plattform einbinden
Puzzel kann das Ergebnis eines Spielers direkt an ein anderes System übergeben — die Punktzahl, wie weit er gekommen ist, was er geantwortet hat —, ohne dass dieses System ein vollständiges LMS sein muss. Diese Seite zeigt jeden Kanal, den es gibt: was rausgeht, wann es rausgeht, und das Kleinste, was du bauen musst, um es aufzufangen.
- Ergebnisse gehen raus über
- Einen Webhook oder eine Nachricht an die Seite rund um die Aktivität
- Deine Seite kann
- Den Spieler anmelden und die Aktivität zurücksetzen
- Aktiviert
- Pro Aktivität, unter Entwickler im Editor
- Enthalten bei
- Einem bezahlten Tarif — bei einem kostenlosen Konto sind diese Einstellungen deaktiviert
Welchen Kanal brauchst du?
Drei Dinge können eine Aktivität verlassen, eines kann hineinkommen. Was passt, hängt von einer einzigen Frage ab: Sitzt der Spieler auf deiner eigenen Seite oder ganz woanders?
Jedes gespeicherte Ergebnis wird als JSON per POST an eine URL deiner Wahl geschickt.
- Nutze es, wenn
- Der Spieler kann überall sein — ein geteilter Link, ein QR-Code, die Seite von jemand anderem — und du willst das Ergebnis in deiner eigenen Datenbank.
- Du brauchst
- Einen HTTPS-Endpunkt, der einen Cross-Origin-POST annimmt.
save_puzzle_results_via_webhookDasselbe JSON, gesendet an die Seite, die die Aktivität einbettet, statt an einen Server.
- Nutze es, wenn
- Du bettest die Aktivität in deine eigene Kursseite ein, und die Seite selbst kann mit dem Ergebnis etwas anfangen.
- Du brauchst
- Ein iframe auf deiner Seite und ein Message-Listener. Kein Server, kein CORS.
save_results_iframe_postmessageEine Nachricht, wenn der Spieler fertig ist — sie enthält nichts außer genau dieser Tatsache.
- Nutze es, wenn
- Du willst nur wissen, ob er fertig ist — um die Lektion abzuhaken, die nächste freizuschalten oder deinen eigenen Bildschirm zu zeigen.
- Du brauchst
- Ein iframe auf deiner Seite und ein Message-Listener.
send_completion_signal_when_embeddedErgebnis-Webhook
Aktivier "Ergebnisse an einen Webhook senden" im Editor und gib eine URL an. Von da an sendet der Browser des Spielers bei jedem Speichern des Fortschritts das komplette Ergebnis als JSON per POST an diese URL.
- 1 Öffne die Aktivität im Editor und geh zum Menü Entwickler.
- 2 Aktivier "Ergebnisse an einen Webhook senden" und füg deinen Endpunkt in das Feld darunter ein. Es muss eine vollständige URL sein — eine reine Domain wird abgelehnt — und sie muss https sein, weil der Browser einen reinen HTTP-Aufruf von einer über https ausgelieferten Seite aus blockiert.
- 3 Spiel die Aktivität einmal selbst durch. Der erste POST kommt an, sobald du etwas beantwortest.
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);
});Ein Ergebnis geht jedes Mal raus, wenn der Eintrag gespeichert wird: nach einer Tipppause, wenn eine Karte platziert wird, wenn die Uhr stoppt, und noch einmal, wenn die Aktivität abgeschlossen ist. Bei einem langen Kreuzworträtsel sind das ein paar Dutzend Aufrufe, nicht einer — schreib deinen Handler also als Upsert mit playerUid und activityKey als Schlüssel, nicht als Insert. Jeder Aufruf enthält den vollständigen Zustand, sodass der neueste immer den letzten ersetzt und ein verlorener Aufruf vom nächsten wieder ausgeglichen wird.
progress ist ein Prozentwert: 100 bedeutet, die Aktivität ist abgeschlossen. Ein paar Typen können enden, ohne das zu erreichen — ein bis zum Ende beantwortetes Quiz, ein gelöstes Lösungsfeld — und tragen stattdessen hasAlternateCompletion. Behandle beides als abgeschlossen.
Der POST kommt aus dem Tab, in dem die Aktivität gespielt wird, nicht von einem Puzzel-Server. Die meisten Endpunkte, die für den Empfang von Webhooks gebaut wurden, akzeptieren das bereits. Wenn deiner nie eine Anfrage sieht, liegt es daran: Der Browser fragt zuerst um Erlaubnis, also beantworte den OPTIONS-Preflight mit einem Access-Control-Allow-Origin-Header, dann folgt der eigentliche POST.
Die Anfrage trägt keine Signatur und kommt aus einem Browser, den du nicht kontrollierst — jeder, der sich die Seite ansieht, kann dir also auch eine schicken. Das ist völlig in Ordnung, um einen Fortschrittsbalken oder ein Dashboard zu füllen. Für alles, was du einen Schüler nicht selbst festlegen lassen würdest — eine zählende Note, ein Zertifikat, eine Zahlung — gleich es mit den Ergebnissen in deinem eigenen Puzzel-Dashboard ab, oder lass stattdessen die LMS-Notenanbindung die Punktzahl übertragen.
Ergebnisse an die umgebende Seite
Wenn du die Aktivität einbettest, kannst du dasselbe JSON an deine eigene Seite senden lassen statt an einen Server. Aktivier "Ergebnisse an die übergeordnete Seite senden" und lausche auf die Nachricht. Nichts verlässt den Browser, also musst du keinen Endpunkt bauen und dir keine Gedanken über CORS machen.
<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>Dein Listener hört jede Nachricht, die an die Seite gesendet wird — auch von anderen Frames und Browser-Erweiterungen. Vergleich event.origin mit https://puzzel.org, bevor du dem Inhalt vertraust.
Das ist das Gegenstück zum Webhook: dieselben Felder, gesendet zu denselben Zeitpunkten. Alles unter "Was ein Ergebnis enthält" gilt auch hier.
Abschlusssignal
Der kleinste Kanal, für den Fall, dass dich das Ergebnis selbst nichts angeht: Aktivier "Abschlusssignal senden" und deine Seite bekommt eine Nachricht in dem Moment, in dem der Spieler fertig ist.
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"
}Das Signal wird bewusst erst gesendet, nachdem der abschließende Speichervorgang abgeschlossen ist — eine Seite, die daraufhin das Ergebnis abruft, findet es dort bereits vor.
Beide Nachrichtenkanäle senden an die Seite, die die Aktivität einbettet. In einem eigenen Tab geöffnet gibt es niemanden, dem man es mitteilen könnte — also wird nichts gesendet.
Was ein Ergebnis enthält
Eine Form, egal welcher Kanal sie trägt. Die Antworten des Spielers sind nach den eigenen Element-IDs der Aktivität geschlüsselt, sodass dieselben Schlüssel auch in correctUids auftauchen.
| Feld | Typ | Was es macht |
|---|---|---|
activityKey immer string | string | Die Aktivität, zu der das Ergebnis gehört. Derselbe Schlüssel, den du in der URL der Aktivität selbst siehst, nach ?p=. |
playerUid immer string | string | Wer gespielt hat, als anonyme ID. Für diesen Spieler auf diesem Gerät stabil, also der Wert, über den du Ergebnisse zuordnest — keine E-Mail-Adresse und kein Puzzel-Konto. |
player manchmal object | object | Die Registrierungsfelder, nach denen die Aktivität fragt, so wie du sie konfiguriert hast: name, email, class, student_id und so weiter. Fehlt, solange sich der Spieler nicht registriert hat, und fehlt komplett bei einer Aktivität, die nichts abfragt. |
progress immer number | number | Wie weit fortgeschritten, als Prozentwert. 100 bedeutet fertig. |
timePassed immer number | number | Zeit auf der Aktivität, in Millisekunden. |
lastPlayedAt immer number | number | Wann dieses Ergebnis gespeichert wurde, als Unix-Zeitstempel in Millisekunden. |
createdAt manchmal number | number | Wann der Versuch begonnen wurde, als Unix-Zeitstempel in Millisekunden. |
playerInput manchmal object | object | Was der Spieler tatsächlich eingegeben hat, geschlüsselt nach der ID des zugehörigen Elements. Die Form darin hängt vom Aktivitätstyp ab — ein Wort, eine Liste platzierter Karten, eine gewählte Option. |
correctUids manchmal object | object | Welche dieser Elemente richtig sind, auf dieselbe Weise geschlüsselt. Fehlt, solange noch nichts beantwortet wurde. |
score manchmal number | number | Erzielte Punkte, bei den Typen, die einen Durchlauf bewerten. Fehlt überall sonst — auch bei einem Durchlauf, der tatsächlich null Punkte erzielt hat, also prüf, ob der Schlüssel existiert, bevor du ihn ausliest. |
performance manchmal number | number | Das eigene Maß eines Typs dafür, wie gut es lief, sofern er eines führt — zum Beispiel Wörter pro Minute beim Tipptraining. |
attempts manchmal number | number | Der wievielte Durchlauf das ist: 1 beim ersten Mal, jeweils eins mehr bei jedem Neustart. Nur bei Typen, die einen Durchlauf vorzeitig beenden und die Versuche zählen. |
knockedOut manchmal boolean | boolean | Der Durchlauf endete bei einer falschen Antwort und ist vorbei, ohne abgeschlossen zu sein. |
hasAlternateCompletion manchmal boolean | boolean | Die Aktivität wurde auf eine Weise beendet, die nicht 100 % erreicht — ein bis zum Ende beantwortetes Quiz, ein gelöstes Lösungsfeld. Behandle das als Abschluss. |
missedKeys manchmal array | array | Zeichen, die der Spieler immer wieder falsch getippt hat, die häufigsten zuerst. Nur beim Tipptraining. |
contentVersion manchmal number | number | Gegen welche Version des Aktivitätsinhalts das gespielt wurde. Sie ändert sich, wenn der Eigentümer die Fragen bearbeitet, sodass sich ein altes Ergebnis von einem aktuellen unterscheiden lässt. |
{
"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
}Ein Feld, das nicht zutrifft, wird aus dem JSON weggelassen, statt als null oder 0 gesendet zu werden. So unterscheidet sich ein Typ, der einen Durchlauf gar nicht bewertet, von einem Durchlauf, der null Punkte erzielt hat — lies also immer mit einem Standardwert und geh nie davon aus, dass ein Schlüssel vorhanden ist.
Die Aktivität von deiner Seite aus zurücksetzen
Eine Anweisung geht den umgekehrten Weg. Ist "Auslöser von der übergeordneten Seite annehmen" aktiviert, kann die einbettende Seite die Antworten des Spielers löschen und die Aktivität auf den Anfang zurücksetzen — für eine eigene "Nochmal versuchen"-Schaltfläche außerhalb des Frames.
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');Es gibt keine Nachricht zum Einreichen eines Ergebnisses, zum Öffnen der Abschlussnachricht oder zum Springen zu einer Frage. Eine Nachricht, die etwas anderes als ein Zurücksetzen verlangt, wird ignoriert.
Der Auslöser wird nur von der Seite akzeptiert, die die Aktivität einbettet, und von nirgendwo sonst — kein benachbarter Frame, kein Skript auf der Seite. Die Einstellung ist standardmäßig deaktiviert, also aktivier sie für die Aktivitäten, die du steuerst.
Deine eigene Spieleridentität mitbringen
Wenn deine Plattform schon weiß, wer spielt, muss die Aktivität nicht noch einmal danach fragen. Es gibt zwei Handshakes, beide für eine Aktivität innerhalb deiner Seite, und beide werden von uns aktiviert statt im Editor — sie ändern, wem ein Ergebnis zugeordnet wird, deshalb werden sie gemeinsam mit dir eingerichtet und nicht über eine Checkbox.
Die Aktivität meldet sich mit 'app-loaded' und wartet. Deine Seite sendet die Angaben des Spielers zurück, und das Ergebnis wird unter ihnen abgelegt, ohne dass der Spieler etwas eintippen oder einen Registrierungsbildschirm sehen muss.
Derselbe Handshake, aber deine Seite sendet statt der einzelnen Felder das JWT, das dein Identity-Provider ausgestellt hat. Wir prüfen es gegen die für dein Konto eingerichteten Aussteller, bevor der Spieler hereingelassen wird — die Identität ist also nachgewiesen und nicht nur behauptet. Das ist die richtige Wahl, wenn das Ergebnis vertrauenswürdig sein muss.
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();
});Sag uns, welche der beiden Varianten du möchtest und wo die Aktivitäten eingebettet werden, und wir richten dein Konto ein und gehen es gemeinsam mit dir durch.
E-Mail zum Thema Identität sendenAktivitäten aus deinem eigenen System erstellen
Alles oben dreht sich darum, dass ein Ergebnis rauskommt. Der umgekehrte Weg — die Aktivitäten selbst aus Inhalten zu erstellen, die du schon hast — ist die Puzzle-API: ein POST pro Aktivitätstyp, und du bekommst einen Schlüssel und eine URL zum Einbetten zurück.
API-Referenz lesenWenn ein fertiger Connector die bessere Antwort ist
Wenn die Plattform auf der anderen Seite ein echtes LMS ist, brauchst du wahrscheinlich nichts von alldem. Noten können von allein ins Notenbuch zurückfließen, ohne dass du etwas hosten musst.
Gestartet aus dem LMS heraus, mit der Punktzahl, die zurück ins Notenbuch geschrieben wird.
Eine Aktivität als Aufgabe posten und die Noten automatisch zurückbekommen.
Kursplattformen, Mitgliederseiten, Intranets und alles, was du selbst gebaut hast, sind genau das, wofür die Kanäle auf dieser Seite da sind. Eine Einbettung plus Abschlusssignal deckt das meiste davon ab.
Was es nicht gibt
Damit du nicht danach suchst:
- Kein Endpunkt zum Auslesen von Ergebnissen. Die API erstellt Aktivitäten; Ergebnisse verlassen das System über die Kanäle auf dieser Seite oder über die Exporte in deinem Dashboard.
- Keine Signatur beim Webhook. Es gibt nichts, wogegen du die Anfrage prüfen könntest — deshalb sollte ein Ergebnis nie das Einzige sein, was hinter etwas Wichtigem steht.
- Kein kontoweiter Webhook. Die URL ist eine Einstellung an einer Aktivität — eine kopierte Aktivität übernimmt sie, eine neue startet ohne sie.
- Nichts bei einem Team-Spiel oder einem Live-Raum. Beide Nachrichtenkanäle und der Webhook sind für Einzelspiel gedacht, und die Einstellungen schalten sich selbst aus, sobald der Team-Modus aktiv ist.
- Keine Zustellwarteschlange. Nichts wird gespeichert und erneut gesendet — das nächste Speichern ist der Wiederholungsversuch, und der letzte Aufruf des Durchlaufs zählt.
Etwas macht nicht, was es soll?
Schick deine Anfrage und die erhaltene Fehlermeldung. Du bekommst persönlich Hilfe von der Person, die den Endpunkt entwickelt hat.
Support per E-Mail