Conecte o Puzzel à sua própria plataforma
O Puzzel pode enviar o resultado de um jogador diretamente para outro sistema — a pontuação, até onde ele chegou, o que respondeu — sem que esse sistema precise ser um LMS completo. Esta página reúne todos os canais que existem: o que sai, quando sai e o mínimo que você precisa construir para recebê-lo.
- Os resultados saem via
- Um webhook ou uma mensagem para a página que envolve a atividade
- Sua página pode
- Fazer login do jogador e reiniciar a atividade
- Ativado
- Por atividade, em Desenvolvedor, no editor
- Incluído em
- Um plano pago — essas configurações ficam desativadas em uma conta gratuita
De qual canal você precisa?
Três coisas podem sair de uma atividade e uma pode entrar. Qual delas serve depende de uma única pergunta: o jogador está dentro da sua página ou em outro lugar completamente diferente?
Cada resultado salvo é enviado por POST, como JSON, para uma URL que é sua.
- Use quando
- O jogador pode estar em qualquer lugar — um link compartilhado, um código QR, o site de outra pessoa — e você quer o resultado no seu próprio banco de dados.
- Você precisa de
- Um endpoint HTTPS que aceite um POST de origem cruzada.
save_puzzle_results_via_webhookO mesmo JSON, enviado para a página que incorpora a atividade em vez de para um servidor.
- Use quando
- Você incorpora a atividade na sua própria página de curso e a página em si pode fazer algo com o resultado.
- Você precisa de
- Um iframe na sua página e um listener de mensagens. Sem servidor, sem CORS.
save_results_iframe_postmessageUma mensagem quando o jogador termina, sem carregar nada além desse fato.
- Use quando
- Tudo que você quer saber é se ele terminou — para marcar a lição como concluída, liberar a próxima ou mostrar sua própria tela.
- Você precisa de
- Um iframe na sua página e um listener de mensagens.
send_completion_signal_when_embeddedWebhook de resultados
Ative "Enviar resultados para um webhook" no editor e informe uma URL. A partir daí, toda vez que o progresso do jogador for salvo, o navegador dele envia por POST o resultado inteiro para essa URL, como JSON.
- 1 Abra a atividade no editor e vá até o menu Desenvolvedor.
- 2 Ative "Enviar resultados para um webhook" e cole seu endpoint no campo logo abaixo. Precisa ser uma URL completa — um domínio isolado é recusado — e precisa ser https, porque o navegador bloqueia uma chamada http simples feita a partir de uma página servida via https.
- 3 Jogue a atividade uma vez você mesmo. O primeiro POST chega assim que você responder 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 toda vez que a entrada é salva: depois de uma pausa na digitação, quando uma carta é posicionada, quando o cronômetro para, e mais uma vez quando a atividade termina. Uma grade de palavras cruzadas longa gera algumas dezenas de chamadas, não uma só — por isso escreva seu handler como um upsert com chave em playerUid e activityKey, em vez de um insert. Cada chamada carrega o estado completo, então a mais recente sempre substitui a anterior e uma chamada perdida é compensada pela próxima.
progress é uma porcentagem: 100 significa que a atividade está completa. Alguns tipos podem terminar sem chegar lá — um quiz respondido até o fim, um campo de solução resolvido — e esses trazem hasAlternateCompletion em vez disso. Trate qualquer um dos dois como finalizado.
O POST vem da aba em que a atividade está sendo jogada, não de um servidor do Puzzel. A maioria dos endpoints criados para receber webhooks já aceita isso. Se o seu nunca recebe uma requisição, é por isso: o navegador pede permissão primeiro, então responda ao preflight OPTIONS com um cabeçalho Access-Control-Allow-Origin e o POST de verdade vem em seguida.
Não há assinatura na requisição, e ela vem de um navegador que você não controla, então qualquer pessoa que olhar a página também pode te enviar uma. Isso é tranquilo para preencher uma barra de progresso ou um painel. Para qualquer coisa que você não deixaria um aluno definir sozinho — uma nota que conta, um certificado, um pagamento — confira com os resultados no seu próprio painel do Puzzel, ou deixe que os conectores de nota do LMS carreguem a pontuação em vez disso.
Resultados para a página ao redor
Se você incorporar a atividade, pode fazer com que esse mesmo JSON seja enviado para a sua própria página em vez de para um servidor. Ative "Enviar resultados para a página pai" e escute a mensagem. Nada sai do navegador, então não há endpoint para construir nem CORS para se preocupar.
<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>Seu listener escuta toda mensagem enviada para a página, incluindo de outros frames e extensões do navegador. Compare event.origin com https://puzzel.org antes de confiar no que está nela.
Este é o gêmeo do webhook: os mesmos campos, enviados nos mesmos momentos. Tudo o que está em "O que um resultado contém" também vale aqui.
Sinal de conclusão
O menor canal, para quando o resultado em si não é da sua conta: ative "Enviar um sinal de conclusão" e sua página recebe uma mensagem no exato 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 propositalmente depois que o salvamento de conclusão já foi concretizado, então uma página que reage lendo o resultado de volta vai encontrá-lo lá.
Os dois canais de mensagem enviam para a página que enquadra a atividade. Aberta em sua própria aba, não há ninguém para avisar, então nada é enviado.
O que um resultado contém
Um único formato, seja qual for o canal que o carrega. As respostas do jogador são indexadas pelos próprios ids dos itens da atividade, então as mesmas chaves aparecem em correctUids.
| Campo | Tipo | O que faz |
|---|---|---|
activityKey sempre string | string | A atividade à qual o resultado pertence. A mesma chave que você vê na própria URL da atividade, depois de ?p=. |
playerUid sempre string | string | Quem jogou, como um id anônimo. Estável para esse jogador nesse dispositivo, então é nele que você deve basear a chave dos resultados — não é um endereço de e-mail nem uma conta do Puzzel. |
player às vezes object | object | Os campos de cadastro que a atividade pede, conforme você os configurou: name, email, class, student_id e assim por diante. Ausente até o jogador se cadastrar, e totalmente ausente em uma atividade que não pede nada. |
progress sempre number | number | Até onde chegou, em porcentagem. 100 significa finalizado. |
timePassed sempre number | number | Tempo na atividade, em milissegundos. |
lastPlayedAt sempre number | number | Quando esse resultado foi salvo, como um timestamp Unix em milissegundos. |
createdAt às vezes number | number | Quando a tentativa foi iniciada, como um timestamp Unix em milissegundos. |
playerInput às vezes object | object | O que o jogador realmente digitou, indexado pelo id do item ao qual pertence. O formato interno depende do tipo de atividade — uma palavra, uma lista de cartas posicionadas, uma opção escolhida. |
correctUids às vezes object | object | Quais desses itens estão certos, indexados da mesma forma. Ausente enquanto nada foi respondido ainda. |
score às vezes number | number | Pontos obtidos, nos tipos que pontuam uma tentativa. Ausente em todos os outros — inclusive em uma tentativa que realmente pontuou zero, então verifique se a chave existe antes de lê-la. |
performance às vezes number | number | A própria medida de desempenho de um tipo, quando ele mantém uma — palavras por minuto no exercício de digitação, por exemplo. |
attempts às vezes number | number | Qual tentativa é essa: 1 na primeira vez, mais uma a cada novo começo. Só nos tipos que encerram uma tentativa antes da hora e contam as repetições. |
knockedOut às vezes boolean | boolean | A tentativa terminou em uma resposta errada e acabou sem estar completa. |
hasAlternateCompletion às vezes boolean | boolean | A atividade foi finalizada de um jeito que não chega a 100% — um quiz respondido até o fim, um campo de solução resolvido. Trate isso como uma conclusão. |
missedKeys às vezes array | array | Caracteres que o jogador continuou errando, do mais errado para o menos errado. Somente no exercício de digitação. |
contentVersion às vezes number | number | Contra qual versão do conteúdo da atividade isso foi jogado. Ela muda quando o proprietário edita as perguntas, então um resultado antigo pode ser diferenciado 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 um tipo que não pontua uma tentativa se diferencia de uma tentativa que pontuou zero — então leia com um valor padrão e nunca presuma que uma chave está presente.
Reiniciando a atividade a partir da sua página
Uma instrução viaja no sentido contrário. Com "Aceitar gatilhos da página pai" ativado, a página que faz a incorporação pode limpar as respostas do jogador e colocar a atividade de volta no início — para um botão "tentar de novo" seu, fora do 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 enviar um resultado, abrir a mensagem final ou pular para uma pergunta. Uma mensagem pedindo qualquer coisa além de um reinício é ignorada.
O gatilho só é aceito vindo da página que enquadra a atividade e de nenhum outro lugar — nem um frame irmão, nem um script na página. A configuração vem desativada por padrão, então ative-a para as atividades que você controla.
Trazendo sua própria identidade de jogador
Se sua plataforma já sabe quem está jogando, a atividade não precisa perguntar de novo. Existem dois handshakes, ambos para uma atividade dentro da sua página, e ambos ativados por nós em vez de no editor — eles mudam a quem um resultado pertence, então são configurados junto com você, e não por uma caixa de seleção.
A atividade se anuncia com 'app-loaded' e espera. Sua página envia de volta os dados do jogador, e o resultado é registrado sob eles sem que o jogador digite nada ou veja uma tela de cadastro.
O mesmo handshake, mas sua página envia o JWT emitido pelo seu provedor de identidade em vez dos próprios campos. Nós o verificamos contra os emissores configurados para sua conta antes de deixar o jogador entrar, então a identidade é comprovada em vez de apenas declarada — esse é o handshake certo para pedir quando o resultado precisa ser confiá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();
});Diga qual dos dois você quer e onde as atividades serão incorporadas, e nós configuramos sua conta e explicamos tudo com você.
Enviar e-mail sobre identidadeCriando atividades a partir do seu sistema
Tudo acima é sobre um resultado saindo. Na direção oposta — criar as próprias atividades a partir de um conteúdo que você já tem — está a Puzzle API: um POST por tipo de atividade, e você recebe de volta uma chave e uma URL para incorporar.
Ler a referência da APIQuando um conector pronto é a resposta melhor
Se a plataforma do outro lado é um LMS de verdade, provavelmente você não precisa de nada disso. As notas podem voltar sozinhas para o diário de classe dele, sem nada para você hospedar.
Iniciada de dentro do LMS, com a pontuação registrada de volta no diário de classe dele.
Publique uma atividade como uma tarefa e receba as notas de volta automaticamente.
Plataformas de curso, sites de associados, intranets e qualquer coisa que você mesmo construiu são exatamente para o que servem os canais desta página. Uma incorporação mais o sinal de conclusão cobre a maior parte disso.
O que não existe
Para você não sair procurando por isso:
- Nenhum endpoint para ler resultados de volta. A API cria atividades; os resultados saem pelos canais desta página, ou pelas exportações no seu painel.
- Nenhuma assinatura no webhook. Não há nada contra o que verificar a requisição, por isso um resultado não deveria ser a única coisa sustentando algo importante.
- Nenhum webhook para a conta inteira. A URL é uma configuração da atividade, então uma atividade que você copia leva a configuração junto, e uma nova começa sem ela.
- Nada em um jogo em equipe ou em uma sala ao vivo. Tanto os canais de mensagem quanto o webhook são para o jogo individual, e as configurações se desativam sozinhas quando o modo em equipe está ativado.
- Nenhuma fila de entrega. Nada é armazenado e reenviado — o próximo salvamento é a nova tentativa, e a última chamada da tentativa é a que importa.
Algo não está se comportando como deveria?
Envie a requisição que você tentou e o erro que recebeu de volta, e você terá uma resposta de verdade, da pessoa que escreveu o endpoint.
E-mail para suporte