MINIGAMES · EMBED KIT

Minigames Lobby

A self-contained minigames lobby — game catalog plus a live leaderboard — that you drop into any site with a single <iframe>. No SDK, no build step. The UI adapts to the width of the frame you give it, so it looks right in a phone-width sidebar or a full-width page.

Container-aware

Lays out from the width of the iframe, not the visitor's screen. Single column when narrow, a two-column dashboard past ~790px — powered by CSS container queries.

One iframe, one token

Point an <iframe> at the /lobby/ path with a session token on the URL. No SDK, no bundler, no build step on your side.

Static & portable

Prerendered static export — plain HTML/CSS/JS. Host it on S3 or any CDN. No server, no runtime dependencies.

Live preview

Both frames below are real embeds of the deployed /lobby/ route, each sized only by its wrapper. Resize your browser to watch the fluid one reflow — the layout follows the frame's width, not the screen.

They carry no session token — this is a public page, and a token on it would be a token anyone could take. So they show the layout, not live data: the catalog and the leaderboard both ask for a session. Add #session=… and both populate.

Fluid — fills its containertwo columns ≥ 790px
Phone390px

In a narrow frame

Same component, same code — the layout responds to the frame's own width:

  • Between 448px and 790px the leaderboard moves below the game catalog, so the two stack into a single column.
  • Below 448px the phone layout takes over: the catalog becomes a one-card carousel.
  • LOBBY and LEADERBOARD tabs then switch between the two.
  • Nothing is cropped — the frame scrolls when the layout needs more height than you gave it.

Embed it

Point the iframe at your deployed lobby URL. That's the whole integration.

HTML
<iframe
  src="https://your-host.example.com/lobby/#session=SESSION_TOKEN"
  title="Minigames Lobby"
  width="100%"
  height="720"
  loading="lazy"
  style="border: 0; display: block;"
></iframe>
React / JSX
export const MinigamesLobby = ({ sessionToken }) => (
  <iframe
    src={`https://your-host.example.com/lobby/#session=${sessionToken}`}
    title="Minigames Lobby"
    loading="lazy"
    className="block h-[720px] w-full border-0"
  />
);
HTML — in a narrow slot
<!-- Narrow slot? The lobby detects its own width and
     switches to the phone layout — no config needed. -->
<div style="width: 380px">
  <iframe
    src="https://your-host.example.com/lobby/#session=SESSION_TOKEN"
    title="Minigames Lobby"
    width="100%"
    height="720"
    loading="lazy"
    style="border: 0"
  ></iframe>
</div>

The session token

The one thing you have to supply. The lobby fetches its games and its standings for a specific player, so it needs a token that says who that is — passed as #session= on the iframe URL.

Where it comes from
Your own backend mints it per player with StartSession, before it renders the page holding the iframe. The lobby has no login of its own and cannot mint one.
Where it goes
On the iframe URL as #session=… — nowhere else. A fragment, not a query string. The lobby reads it there and sends it to the minigames API as a Bearer token; games opened from the lobby get their own copy from the API.
Keep it short-lived
A fragment is never sent to a server, so the token stays out of the lobby host's access logs and out of any Referer header. It does sit in the iframe URL in your own page, where your own scripts and browser history can read it — so still mint it per player, and still mint it short-lived.
Without a token
The lobby still renders — the catalog says games need a session, and the leaderboard says standings do. Nothing errors, and no request is made. This is what the previews above are showing.
When it expires
You get one session-expired message. Mint a new token and give the frame a URL carrying it — but changing only the fragment does not reload an iframe, it just moves the hash. Force the load: blank the src first, or replace the element, or add a cache-busting query parameter alongside the new fragment.

Events

You cannot see inside the frame, so the lobby tells you when a player starts a game, when they come back out, and when the session you gave it has lapsed. Three postMessage events on window.parent — no SDK, nothing to opt into. Only the two game events carry a game, so narrow on type before reaching for a title.

game-opened
A player tapped PLAY NOW and the game is taking over the frame. Carries game.title.
game-closed
They left the game and the lobby is back. Fires however they got out — the close control, or Escape in the windowed layout.
session-expired
The token you supplied has lapsed, and the lobby has stopped loading games and standings. Carries no game. Mint a fresh token and re-point the iframe, forcing the load — the lobby cannot do it for you. Fires once per frame, not once per failed request.
JavaScript — listen on your page
window.addEventListener("message", async (event) => {
  // Only trust messages coming from the host you embedded.
  if (event.origin !== "https://your-host.example.com") {
    return;
  }

  const message = event.data;
  if (message?.source !== "oddin-minigames") {
    return;
  }

  if (message.type === "game-opened") {
    // A player started message.game.title: pause your own carousel,
    // hide a sticky header, hold off a live refresh …
    return;
  }

  if (message.type === "game-closed") {
    // They are back in the lobby — undo whatever you did above.
    return;
  }

  if (message.type === "session-expired") {
    // Only game events carry message.game — narrow on type before
    // reaching for a title. This one carries nothing but the news.
    // The lobby cannot renew itself: mint a fresh token and point the
    // iframe at a new URL. Blank the src first — a URL differing only
    // in its fragment moves the hash without reloading the frame.
    iframe.src = "about:blank";
    iframe.src = withSession(await mintSessionToken());
  }
});

Every message carries source: "oddin-minigames", so it is safe to filter on that and ignore the rest of your page's postMessage traffic. The target origin is "*" — the lobby is a static export and does not know who embeds it — so check event.origin against your lobby host, as the snippet does.

Sizing & behavior

Width
Set it to the width of your slot — width="100%" is usually what you want. From about 790px the catalog and the leaderboard sit side by side; below that they stack into one column, and below 448px you get the phone layout.
Height
An iframe can't grow to its content on its own, so give it a height: 720 is a good default, and the lobby fills whatever it gets. The two-column layout needs about 700px to fit; the stacked one about 1240px.
Scrolling
A frame shorter than the layout needs scrolls inside itself — your page never grows to fit the embed. Nothing else is required: the lobby has no chrome of its own, so no permissions to grant.
Path
Reference the deployed /lobby/ URL — mind the trailing slash, before the #: the static export serves /lobby/index.html. A browser carries the fragment across the redirect from the slashless /lobby#session=…, so the token survives it, but the redirect is a round trip you can skip by writing the slash.
Language
Add ?lang= after the trailing slash — /lobby/?lang=ru — and the lobby, the game descriptions, the games' loading screens and the games themselves follow it. en (the default) and ru are supported; anything else falls back to en. Game titles are artwork and stay as they are.
Session
Put the player's session token in the URL's hash — /lobby/#session=<token> — and the lobby passes it on to the game it starts in that game's own hash. Keeping it out of the query means it is never sent to a server, so it stays out of access logs and referrers on both hops; combine it with ?lang= as /lobby/?lang=ru#session=<token>. Without it the lobby and the games still run, unauthenticated. The token is read once when the lobby loads, and changing only the hash does not reload a frame — to hand over a new one, reload the iframe itself (remount it, or call contentWindow.location.reload()).
Accessibility
Always set a title. Add loading="lazy" so the embed never blocks your page load.

Deployment. The lobby ships as a static export (plain HTML/CSS/JS). Host it on S3 static website hosting or any CDN — index document index.html, error document 404.html. Serve /lobby/ from the same host you embed from (or reference its absolute URL) and you're done.