Liga o Puzzel à tua própria plataforma
O Puzzel pode entregar o resultado de um jogador diretamente a outro sistema — a pontuação, até onde chegou, o que respondeu — sem que esse sistema seja um LMS completo. Esta página reúne todos os canais que existem: o que sai, quando sai e o mínimo que precisas de construir para o receber.
- Os resultados saem através de
- Um webhook, ou uma mensagem para a página que envolve a atividade
- A tua página pode
- Iniciar sessão do jogador e reiniciar a atividade
- Ativado
- Por atividade, em Programador no editor
- Incluído em
- Um plano pago — estas definições estão desativadas numa conta gratuita
De que canal precisas?
Há três coisas que podem sair de uma atividade e uma que pode entrar. A que serve depende de uma única pergunta: o jogador está dentro da tua página, ou completamente noutro lado?
Cada resultado guardado é enviado por POST, em JSON, para um URL que te pertence.
- Usa-o quando
- O jogador pode estar em qualquer lado — uma ligação partilhada, um código QR, o site de outra pessoa — e queres o resultado na tua própria base de dados.
- Precisas de
- Um endpoint HTTPS que aceite um POST entre origens diferentes.
save_puzzle_results_via_webhookO mesmo JSON, enviado para a página que incorpora a atividade em vez de para um servidor.
- Usa-o quando
- Incorporas a atividade na tua própria página de curso e essa página consegue fazer algo com o resultado.
- Precisas de
- Um iframe na tua página e um recetor de mensagens. Sem servidor, sem CORS.
save_results_iframe_postmessageUma mensagem quando o jogador termina, que não transporta mais nada além desse facto.
- Usa-o quando
- Só precisas de saber se terminaram — para marcar a lição como concluída, desbloquear a seguinte, ou mostrar o teu próprio ecrã.
- Precisas de
- Um iframe na tua página e um recetor de mensagens.
send_completion_signal_when_embeddedWebhook de resultados
Ativa "Enviar os resultados para um webhook" no editor e indica um URL. A partir daí, sempre que o progresso do jogador é guardado, o navegador dele envia o resultado completo por POST, em JSON, para esse URL.
- 1 Abre a atividade no editor e vai ao menu Programador.
- 2 Ativa "Enviar os resultados para um webhook" e cola o teu endpoint no campo por baixo. Tem de ser um URL completo — um domínio isolado é recusado — e tem de ser https, porque o navegador bloqueia uma chamada em http simples feita a partir de uma página servida por https.
- 3 Joga a atividade uma vez, tu próprio. O primeiro POST chega assim que respondes a alguma coisa.
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);
});Um resultado sai sempre que a entrada é guardada: depois de uma pausa a escrever, quando uma carta é colocada, quando o cronómetro para, e mais uma vez quando a atividade termina. Umas palavras cruzadas longas geram umas duas dezenas de chamadas, não uma só — por isso escreve o teu recetor como um upsert indexado por playerUid e activityKey, e não como uma inserção. Cada chamada transporta o estado completo, por isso a mais recente substitui sempre a anterior e uma chamada que se perde é compensada pela seguinte.
progress é uma percentagem: 100 significa que a atividade está completa. Alguns tipos podem terminar sem chegar lá — um quiz respondido até ao fim, um campo de solução resolvido — e esses trazem hasAlternateCompletion em vez disso. Trata qualquer um dos dois casos como concluído.
O POST vem do separador onde a atividade está a ser jogada, não de um servidor do Puzzel. A maioria dos endpoints criados para receber webhooks já aceita isto. Se o teu nunca vê um pedido, é por esta razão: o navegador pede autorização primeiro, por isso responde ao preflight OPTIONS com um cabeçalho Access-Control-Allow-Origin, e o POST verdadeiro segue-se.
Não há assinatura no pedido, e este vem de um navegador que não controlas, por isso qualquer pessoa que veja a página também te pode enviar um. Isso não é problema para preencher uma barra de progresso ou um painel. Para algo que não deixarias um aluno definir por si próprio — uma nota que conta, um certificado, um pagamento — confirma-o com os resultados no teu próprio painel do Puzzel, ou deixa que os conectores de notas do LMS transportem a pontuação em vez disso.
Resultados para a página que a envolve
Se incorporares a atividade, podes ter esse mesmo JSON enviado para a tua própria página em vez de para um servidor. Ativa "Enviar os resultados para a página principal" e fica à escuta da mensagem. Nada sai do navegador, por isso não há endpoint para construir nem CORS a considerar.
<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>O teu recetor ouve todas as mensagens enviadas para a página, incluindo de outras frames e extensões do navegador. Compara event.origin com https://puzzel.org antes de confiares no que traz.
Isto é o gémeo do webhook: os mesmos campos, enviados nos mesmos momentos. Tudo o que está em "O que contém um resultado" também se aplica aqui.
Sinal de conclusão
O canal mais pequeno, para quando o resultado em si não te diz respeito: ativa "Enviar um sinal de conclusão" e a tua página recebe uma mensagem no momento em que o jogador 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"
}O sinal é enviado deliberadamente depois de o guardar de conclusão ter sido concluído, por isso uma página que reaja lendo o resultado de volta vai encontrá-lo lá.
Ambos os canais de mensagens enviam para a página que enquadra a atividade. Aberta no seu próprio separador, não há ninguém para avisar, por isso nada é enviado.
O que contém um resultado
Uma única forma, seja qual for o canal que a transporta. As respostas do jogador são indexadas pelos ids dos próprios itens da atividade, por isso as mesmas chaves aparecem em correctUids.
| Campo | Tipo | O que faz |
|---|---|---|
activityKey sempre string | string | A atividade a que o resultado pertence. A mesma chave que vês no próprio URL da atividade, depois de ?p=. |
playerUid sempre string | string | Quem jogou, como um id anónimo. Estável para este jogador neste dispositivo, por isso é nele que deves indexar os resultados — não é um endereço de e-mail nem uma conta do Puzzel. |
player por vezes object | object | Os campos de registo que a atividade pede, tal como os configuraste: nome, e-mail, turma, student_id, e assim por diante. Ausente até o jogador se registar, e ausente por completo numa atividade que não pede nada. |
progress sempre number | number | Até onde chegou, em percentagem. 100 significa terminado. |
timePassed sempre number | number | Tempo passado na atividade, em milissegundos. |
lastPlayedAt sempre number | number | Quando este resultado foi guardado, como um timestamp Unix em milissegundos. |
createdAt por vezes number | number | Quando a tentativa foi iniciada, como um timestamp Unix em milissegundos. |
playerInput por vezes object | object | O que o jogador realmente introduziu, indexado pelo id do item a que pertence. A forma interna depende do tipo de atividade — uma palavra, uma lista de cartas colocadas, uma opção escolhida. |
correctUids por vezes object | object | Quais desses itens estão certos, indexados da mesma forma. Ausente enquanto ainda não foi respondido nada. |
score por vezes number | number | Pontos obtidos, nos tipos que pontuam uma partida. Ausente em todos os outros casos — incluindo numa partida que genuinamente pontuou zero, por isso confirma que a chave existe antes de a leres. |
performance por vezes number | number | A medida própria de um tipo de como correu, quando existe uma — palavras por minuto no treino de teclado, por exemplo. |
attempts por vezes number | number | Que partida é esta: 1 da primeira vez, mais um a cada recomeço. Só nos tipos que terminam uma partida antecipadamente e contam as repetições. |
knockedOut por vezes boolean | boolean | A partida terminou numa resposta errada e acabou sem estar completa. |
hasAlternateCompletion por vezes boolean | boolean | A atividade foi terminada de uma forma que não chega aos 100% — um quiz respondido até ao fim, um campo de solução resolvido. Trata isto como uma conclusão. |
missedKeys por vezes array | array | Carateres que o jogador continuou a errar, começando pelos mais falhados. Só no treino de teclado. |
contentVersion por vezes number | number | Contra que versão do conteúdo da atividade isto foi jogado. Muda quando o proprietário edita as perguntas, por isso um resultado antigo pode ser distinguido de um atual. |
{
"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
}Um campo que não se aplica é deixado de fora do JSON, em vez de ser enviado como null ou zero. É assim que se distingue um tipo que não pontua uma partida de uma partida que pontuou zero — por isso lê sempre com um valor por omissão e nunca assumas que uma chave está presente.
Reiniciar a atividade a partir da tua página
Uma instrução viaja no sentido contrário. Com "Aceitar comandos da página principal" ativado, a página que faz a incorporação pode limpar as respostas do jogador e voltar a colocar a atividade no início — para um botão "tentar novamente" teu, fora da 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');Não existe mensagem para submeter um resultado, abrir a mensagem final, ou saltar para uma pergunta. Uma mensagem que peça outra coisa que não um reinício é ignorada.
O comando só é aceite a partir da página que enquadra a atividade e de mais lado nenhum — nem uma frame irmã, nem um script na página. A definição está desativada por omissão, por isso ativa-a nas atividades que controlas.
Trazer a tua própria identidade de jogador
Se a tua plataforma já sabe quem está a jogar, a atividade não precisa de perguntar outra vez. Existem dois handshakes, ambos para uma atividade dentro da tua página, e ambos ativados por nós em vez de no editor — mudam a quem pertence um resultado, por isso são configurados contigo em vez de a partir de uma caixa de verificação.
A atividade anuncia-se com 'app-loaded' e espera. A tua página envia de volta os dados do jogador, e o resultado fica associado a esses dados sem que o jogador escreva nada ou veja um ecrã de registo.
O mesmo handshake, mas a tua página envia o JWT emitido pelo teu fornecedor de identidade em vez dos próprios campos. Verificamo-lo em relação aos emissores configurados para a tua conta antes de o jogador ser deixado entrar, por isso a identidade é comprovada e não apenas declarada — este é o que deves pedir quando o resultado tem de ser fiável.
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();
});Diz-nos qual dos dois queres e onde as atividades vão ser incorporadas, e nós configuramos a tua conta e explicamos-te o processo passo a passo.
Envia-nos um e-mail sobre identidadeCriar atividades a partir do teu sistema
Tudo o que foi dito acima é sobre um resultado a sair. No sentido contrário — criar as próprias atividades a partir de conteúdo que já tens — está a Puzzle API: um POST por tipo de atividade, e recebes de volta uma chave e um URL para incorporar.
Ler a referência da APIQuando um conector já feito é a melhor resposta
Se a plataforma do outro lado for um LMS a sério, provavelmente não precisas de nada disto. As notas podem voltar sozinhas para o livro de notas, sem nada que precises de alojar.
Lançada de dentro do LMS, com a pontuação escrita de volta no livro de notas.
Publica uma atividade como trabalho e as notas voltam automaticamente.
Plataformas de cursos, sites de membros, intranets e qualquer coisa que tenhas construído tu próprio são exatamente para isso que servem os canais desta página. Uma incorporação mais o sinal de conclusão cobre a maior parte dos casos.
O que não existe
Para não andares à procura disso:
- Não há endpoint para ler resultados de volta. A API cria atividades; os resultados saem através dos canais desta página, ou através das exportações no teu painel.
- Não há assinatura no webhook. Não há nada contra o qual verificar o pedido, e é por isso que um resultado não deve ser a única coisa a sustentar algo que importa.
- Não há webhook à escala da conta. O URL é uma definição de uma atividade, por isso uma atividade que copias transporta-o consigo e uma nova começa sem ele.
- Nada num jogo em equipa ou numa sala ao vivo. Tanto os canais de mensagens como o webhook são para jogo a solo, e as definições desativam-se sozinhas quando o modo de equipas está ativado.
- Não há fila de entrega. Nada é guardado e reenviado — o próximo guardar é a nova tentativa, e a última chamada da partida é a que conta.
Algo não está a funcionar bem?
Envia o pedido que tentaste e o erro que recebeste e terás uma resposta a sério, de quem escreveu o endpoint.
Enviar e-mail ao suporte