Embed SDK
The embed SDK is a small script (under 5 KB compressed) that puts a puzzle on your page in an iframe, resizes it to the puzzle, renews embed tokens through your backend, passes your visitor's analytics consent and tells your page what happens in the game.
Add the script
Load one fixed version of the script. Each version's file never changes, so you can pin it with Subresource Integrity.
<script
src="https://dev.pluzzento.com/static/embed/1.0.0/puzzle-embed.js"
integrity="sha384-SzL9yUHrS1PHWED62EylnMzS8jbZIHN/GhKbQeurDF1a6GP++tZu4HluyjVIwqwP"
crossorigin="anonymous"
></script>Version 1.0.0. New versions get a new address; the old ones stay available, so your page only changes when you update it.
Create an embed
Call PuzzleEmbed.create with the element to fill and the publication's ID. Give the iframe an accessible name in your page's language with title.
<div id="puzzle"></div>
<script>
const embed = PuzzleEmbed.create({
container: "#puzzle",
publicationId: "PUBLICATION_ID",
playOrigin: "https://dev-play.pluzzento.com",
title: "Daily crossword",
consent: { analytics: false },
on: {
ready: (detail) => console.log("ready", detail),
complete: (detail) => console.log("complete", detail),
},
});
embed.on("error", (detail) => console.warn("puzzle error", detail));
</script>The play address is worked out from the script's address; pass playOrigin when your page loads the script from somewhere else.
Embed tokens
When a publication requires an embed token, your backend asks the API for one with your API key and gives it to your page. Never put the API key in the browser.
On your backend
app.get("/puzzle-token", async (req, res) => {
const response = await fetch(
"https://dev-api.pluzzento.com/v1/publications/PUBLICATION_ID/embed-tokens",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PUZZLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
origin: "https://news.example.com",
path: "/puzzles/today",
expiresInSeconds: 300,
}),
}
);
if (!response.ok) return res.status(502).end();
const { token } = await response.json();
res.set("Cache-Control", "no-store").json({ token });
});On your page
PuzzleEmbed.create({
container: "#puzzle",
publicationId: "PUBLICATION_ID",
playOrigin: "https://dev-play.pluzzento.com",
title: "Daily crossword",
getToken: async () => {
const response = await fetch("/puzzle-token", { credentials: "same-origin" });
if (!response.ok) throw new Error("No embed token");
return (await response.json()).token;
},
});Tokens are short-lived. When one expires during play, the SDK calls your getToken function again and hands the new token to the puzzle; nobody has to reload the page.
Analytics consent
Pass your visitor's choice as consent each time you create the embed, and update it with setConsent when it changes. It applies only to this puzzle on your site, for the current page view: each page view must state it again, and without it no consent is assumed, so the puzzle sends no analytics identifier. If the visitor has declined analytics on the puzzle platform itself, that choice always wins over yours.
const embed = PuzzleEmbed.create({
container: "#puzzle",
publicationId: "PUBLICATION_ID",
consent: { analytics: visitorAcceptedAnalytics },
});
onConsentChange((accepted) => embed.setConsent({ analytics: accepted }));Events
Listen with on (or the on option). Events are informative: they never prove that a puzzle was solved, and they never contain answers, checks or the player's grid.
| Event | When | Detail |
|---|---|---|
| ready | The puzzle loaded and can be played. | { gameType, mode, title } |
| start | The player's first action in an attempt. | { mode, resumed } |
| complete | The puzzle was solved on this device. | { mode, elapsedMs, hintsUsed, revealsUsed, assisted } |
| pause | The game was paused. | {} |
| resume | The game was resumed. | {} |
| resize | The puzzle's height changed (the iframe follows it unless autoResize is off). | { height } |
| error | The puzzle couldn't load (source puzzle) or the SDK couldn't continue (source sdk: token_unavailable, token_expired, handshake_timeout). | { code, source: "puzzle" | "sdk" } |
Methods
- embed.pause()
- Pauses the game, when the publication allows pausing.
- embed.resume()
- Resumes a paused game.
- embed.setConsent({ analytics })
- Updates the visitor's analytics consent.
- embed.on(name, handler)
- Adds an event listener; returns a function that removes it.
- embed.destroy()
- Removes the iframe and stops listening.
Security
- The SDK accepts messages only from the play address and its own iframe, and sends messages only to the play address, never to every origin.
- The puzzle accepts commands only from the page that embeds it, after checking its origin against the embed token and the browser.
- Every message carries the protocol version, a unique ID and a timestamp; repeated or stale messages are ignored.
- Only the domains set in the publication's embed settings can show the puzzle at all.
The postMessage protocol
The SDK wraps protocol version 1. If you write your own integration, post the handshake puzzle:init to the play address after the iframe loads, accept messages only from that address and that iframe, and validate every field.
{
"protocol": 1,
"id": "6f1c2a9e-4b7d-4c1e-9a52-3d8e0f6b1c27",
"type": "puzzle:completed",
"timestamp": "2026-10-24T10:00:00.000Z",
"publicationId": "PUBLICATION_ID",
"payload": {
"mode": "casual",
"elapsedMs": 312000,
"hintsUsed": 1,
"revealsUsed": 0,
"assisted": false
}
}Commands from your page: puzzle:init, puzzle:set_token, puzzle:set_consent, puzzle:pause, puzzle:resume.