Conecta Puzzel a tu propia plataforma
Puzzel puede entregar el resultado de un jugador directamente a otro sistema —su puntuación, hasta dónde llegó, qué respondió— sin que ese sistema tenga que ser un LMS completo. Esta página reúne todos los canales que existen: qué sale, cuándo sale y lo mínimo que tienes que construir para recibirlo.
- Los resultados salen mediante
- Un webhook, o un mensaje a la página que rodea la actividad
- Tu página puede
- Iniciar la sesión del jugador y reiniciar la actividad
- Se activa
- Por actividad, en Desarrollador dentro del editor
- Incluido con
- Un plan de pago — estos ajustes están desactivados en una cuenta gratis
¿Qué canal necesitas?
De una actividad pueden salir tres cosas, y puede entrar una. Cuál te conviene depende de una sola pregunta: ¿el jugador está dentro de tu página, o en un sitio completamente distinto?
Cada resultado guardado se envía por POST, como JSON, a una URL de tu propiedad.
- Úsalo cuando
- El jugador puede estar en cualquier sitio —un enlace compartido, un código QR, la página de otra persona— y quieres el resultado en tu propia base de datos.
- Necesitas
- Un endpoint HTTPS que acepte un POST de origen cruzado.
save_puzzle_results_via_webhookEl mismo JSON, enviado a la página que inserta la actividad en lugar de a un servidor.
- Úsalo cuando
- Insertas la actividad en la página de tu propio curso y esa página puede hacer algo con el resultado.
- Necesitas
- Un iframe en tu página y un listener de mensajes. Sin servidor, sin CORS.
save_results_iframe_postmessageUn mensaje cuando el jugador termina, que no lleva más información que ese hecho.
- Úsalo cuando
- Solo quieres saber si ha terminado —para marcar la lección como hecha, desbloquear la siguiente o mostrar tu propia pantalla.
- Necesitas
- Un iframe en tu página y un listener de mensajes.
send_completion_signal_when_embeddedWebhook de resultados
Activa «Enviar los resultados a un webhook» en el editor y dale una URL. A partir de ahí, cada vez que se guarda el progreso del jugador, su navegador envía por POST el resultado completo a esa URL como JSON.
- 1 Abre la actividad en el editor y ve al menú Desarrollador.
- 2 Activa «Enviar los resultados a un webhook» y pega tu endpoint en el campo de debajo. Tiene que ser una URL completa —un dominio sin más se rechaza— y tiene que ser https, porque el navegador bloquea una llamada http sin más hecha desde una página servida por https.
- 3 Juega la actividad una vez tú mismo. El primer POST llega en cuanto respondes algo.
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);
});Sale un resultado cada vez que se guarda la entrada: tras una pausa al escribir, cuando se coloca una carta, cuando el reloj se detiene, y una vez más cuando termina la actividad. Un crucigrama largo son varias decenas de llamadas, no una sola —así que escribe tu handler como un upsert con clave en playerUid y activityKey, no como una inserción. Cada llamada lleva el estado completo, así que la más reciente siempre reemplaza a la anterior, y una llamada que se pierde queda cubierta por la siguiente.
progress es un porcentaje: 100 significa que la actividad está completa. Algunos tipos pueden terminar sin llegar a ese valor —un quiz respondido hasta el final, un campo de solución resuelto— y en su lugar llevan hasAlternateCompletion. Trata cualquiera de los dos como terminado.
El POST llega desde la pestaña donde se está jugando la actividad, no desde un servidor de Puzzel. La mayoría de los endpoints preparados para recibir webhooks ya lo aceptan así. Si el tuyo nunca ve ninguna solicitud, esta es la razón: el navegador pide permiso antes, así que responde a la solicitud OPTIONS previa con una cabecera Access-Control-Allow-Origin, y el POST real llega después.
La solicitud no lleva firma, y viene de un navegador que no controlas, así que cualquiera que mire la página puede enviarte una también. Eso está bien para rellenar una barra de progreso o un panel. Para cualquier cosa que no dejarías fijar a un estudiante por su cuenta —una calificación que cuenta, un certificado, un pago— compáralo con los resultados de tu propio panel de Puzzel, o deja que los conectores de calificación del LMS se encarguen de la puntuación.
Resultados a la página que la rodea
Si insertas la actividad, puedes hacer que ese mismo JSON se envíe a tu propia página en lugar de a un servidor. Activa «Enviar los resultados a la página principal» y escucha el mensaje. Nada sale del navegador, así que no hay ningún endpoint que construir ni CORS del que preocuparse.
<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>Tu listener recibe todos los mensajes enviados a la página, incluidos los de otros marcos y extensiones del navegador. Compara event.origin con https://puzzel.org antes de confiar en su contenido.
Este es el gemelo del webhook: los mismos campos, enviados en los mismos momentos. Todo lo que dice «Qué contiene un resultado» se aplica también aquí.
Señal de finalización
El canal más pequeño, para cuando el resultado en sí no es asunto tuyo: activa «Enviar una señal de finalización» y tu página recibe un mensaje en el momento en que el jugador termina.
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"
}La señal se envía deliberadamente después de que se haya completado el guardado final, así que una página que reaccione leyendo de vuelta el resultado lo encontrará ahí.
Ambos canales de mensajes envían a la página que enmarca la actividad. Abierta en su propia pestaña no hay nadie a quien avisar, así que no se envía nada.
Qué contiene un resultado
Una sola forma, sea cual sea el canal que la lleve. Las respuestas del jugador tienen como clave los ids propios de cada elemento de la actividad, así que esas mismas claves aparecen en correctUids.
| Campo | Tipo | Qué hace |
|---|---|---|
activityKey siempre string | string | La actividad a la que pertenece el resultado. La misma clave que ves en la URL propia de la actividad, después de ?p=. |
playerUid siempre string | string | Quién jugó, como id anónimo. Es estable para este jugador en este dispositivo, así que es lo que debes usar como clave de los resultados —no es una dirección de correo ni una cuenta de Puzzel. |
player a veces object | object | Los campos de registro que pide la actividad, tal como los configuraste: nombre, correo, clase, student_id, etc. Está ausente hasta que el jugador se registra, y ausente por completo en una actividad que no pide nada. |
progress siempre number | number | Hasta dónde ha llegado, como porcentaje. 100 significa terminado. |
timePassed siempre number | number | Tiempo en la actividad, en milisegundos. |
lastPlayedAt siempre number | number | Cuándo se guardó este resultado, como marca de tiempo Unix en milisegundos. |
createdAt a veces number | number | Cuándo empezó el intento, como marca de tiempo Unix en milisegundos. |
playerInput a veces object | object | Lo que el jugador introdujo realmente, indexado por el id del elemento al que pertenece. La forma interna depende del tipo de actividad —una palabra, una lista de cartas colocadas, una opción elegida. |
correctUids a veces object | object | Cuáles de esos elementos son correctos, con la misma clave. Está ausente mientras no se haya respondido nada todavía. |
score a veces number | number | Puntos obtenidos, en los tipos que puntúan una partida. Está ausente en el resto de casos —incluida una partida que realmente obtuvo cero puntos, así que comprueba que la clave existe antes de leerla. |
performance a veces number | number | La medida propia de cada tipo de cómo le fue, cuando la tiene —palabras por minuto en práctica de mecanografía, por ejemplo. |
attempts a veces number | number | Qué número de partida es: 1 la primera vez, uno más en cada reinicio. Solo en los tipos que pueden terminar una partida antes de tiempo y cuentan los reintentos. |
knockedOut a veces boolean | boolean | La partida terminó con una respuesta incorrecta y acabó sin completarse. |
hasAlternateCompletion a veces boolean | boolean | La actividad se terminó de una forma que no llega al 100 % —un quiz respondido hasta el final, un campo de solución resuelto. Trátalo como una finalización. |
missedKeys a veces array | array | Caracteres que el jugador seguía fallando, empezando por los más fallados. Solo en práctica de mecanografía. |
contentVersion a veces number | number | Con qué versión del contenido de la actividad se jugó. Cambia cuando el propietario edita las preguntas, así que un resultado antiguo se puede distinguir de uno actual. |
{
"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 campo que no aplica se omite del JSON en lugar de enviarse como null o cero. Así se distingue un tipo que no puntúa una partida de una partida que obtuvo cero puntos —así que léelo con un valor por defecto y nunca des por hecho que la clave está presente.
Reiniciar la actividad desde tu página
Una instrucción viaja en sentido contrario. Con «Aceptar órdenes de la página principal» activado, la página que hace la inserción puede borrar las respuestas del jugador y volver a poner la actividad al principio —para un botón «intentar de nuevo» propio, fuera del marco.
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');No existe ningún mensaje para enviar un resultado, abrir el mensaje final o saltar a una pregunta. Un mensaje que pida cualquier cosa que no sea un reinicio se ignora.
La orden solo se acepta desde la página que enmarca la actividad y de ningún otro sitio —ni un marco hermano, ni un script de la página. El ajuste está desactivado por defecto, así que actívalo en las actividades que controles tú.
Aportar tu propia identidad de jugador
Si tu plataforma ya sabe quién está jugando, la actividad no tiene que volver a preguntárselo. Existen dos protocolos de enlace, ambos para una actividad dentro de tu página, y ambos los activamos nosotros en lugar del editor —cambian a quién pertenece un resultado, así que se configuran contigo y no desde una casilla.
La actividad se anuncia con 'app-loaded' y espera. Tu página envía de vuelta los datos del jugador, y el resultado queda archivado a su nombre sin que el jugador escriba nada ni vea una pantalla de registro.
El mismo protocolo, pero tu página envía el JWT emitido por tu proveedor de identidad en lugar de los campos en sí. Lo verificamos contra los emisores configurados para tu cuenta antes de dejar entrar al jugador, así que la identidad queda demostrada y no solo declarada —este es el que hay que pedir cuando el resultado tiene que ser 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();
});Dinos cuál de los dos quieres y dónde se insertarán las actividades, y configuraremos tu cuenta y te acompañaremos en el proceso.
Escríbenos sobre identidadCrear actividades desde tu sistema
Todo lo anterior trata de un resultado que sale. En sentido contrario —crear las actividades a partir de contenido que ya tienes— está la Puzzle API: un POST por tipo de actividad, y recibes de vuelta una clave y una URL para insertar.
Leer la referencia de la APICuando un conector ya hecho es la mejor opción
Si la plataforma del otro lado es un LMS de verdad, probablemente no necesites nada de esto. Las calificaciones pueden volver solas a su libro de calificaciones, sin que tengas que alojar nada.
Se lanza desde dentro del LMS, y la puntuación se escribe de vuelta en su libro de calificaciones.
Publica una actividad como tarea y las calificaciones vuelven automáticamente.
Las plataformas de cursos, los sitios de membresía, las intranets y cualquier cosa que hayas construido tú mismo son exactamente para lo que sirven los canales de esta página. Una inserción más la señal de finalización cubre la mayoría de los casos.
Lo que no hay
Para que no lo busques donde no está:
- No hay ningún endpoint para leer los resultados de vuelta. La API crea actividades; los resultados salen por los canales de esta página, o por las exportaciones de tu panel.
- El webhook no lleva firma. No hay nada contra lo que verificar la solicitud, por eso un resultado no debería ser lo único que respalde algo importante.
- No hay ningún webhook a nivel de cuenta. La URL es un ajuste de cada actividad, así que una actividad que copias lo lleva consigo y una nueva empieza sin él.
- Nada en un juego por equipos o una sala en directo. Tanto los canales de mensajes como el webhook son para juego individual, y los ajustes se desactivan solos cuando el juego por equipos está activado.
- No hay ninguna cola de entrega. Nada se almacena ni se reenvía —el siguiente guardado es el reintento, y la última llamada de la partida es la que cuenta.
¿Algo no funciona como debería?
Envíanos la petición que has usado y el error que recibiste. Te responderá la persona que creó la API.
Escribir a soporte