A sportsbook iframe integration has two halves: your server opens a player session with an API key and gets a single-use launch URL, and your page loads that URL in an iframe, either through a small JavaScript SDK or as a raw <iframe> that you wire up with postMessage. This guide covers both halves for The Sportsbook, with the real code, the ten events the embed sends, auto height, session renewal and a security checklist. It is a play-for-fun sportsbook: players bet free tokens, never real money.
Key takeaways
- The API opens sessions and moves token balances; the iframe is the player interface. You need both, and neither replaces the other.
- Launch tokens are single use and expire after 60 seconds. The player session they start lasts 12 hours and can be swapped in place with
updateToken(). - The SDK gives you auto height, ten typed events and session renewal in a few lines. A raw iframe works too, if you check
event.originandevent.sourceyourself. - Never post to the iframe with
"*", never accept auser_reffrom the browser, and never move tokens because of apostMessageevent. - Try the embed in the live sportsbook demo (opens in a new tab) before you write any code.
On this page12 sections
- iframe vs SDK vs API: what each does here
- The launch token flow
- Mounting with the SDK
- Options
- Methods
- The ten events
- The raw iframe: postMessage events, origin and source checks
- Auto height and mobile layouts
- Renewing a session without a reload
- Handling maintenance, suspension and blocked players
- Rate limits
- Security checklist
- What sportsbook iframe vendors usually leave out
- Where to go next
- Frequently asked questions
iframe vs SDK vs API: what each does here
Vendor pages usually answer "iframe vs API" in one paragraph: the iframe is faster, the API more flexible. Here the three pieces simply have separate jobs.
| Piece | Runs on | What it does |
|---|---|---|
API (https://api.thesportsbook.app/b2b/v1/*) | Your server, with your API key | Opens player sessions, credits and debits tokens in managed mode, reads rounds and reports |
SDK (https://play.thesportsbook.app/sdk.js) | Your page | Creates the iframe, sizes it, turns embed messages into callbacks, renews sessions |
iframe (https://play.thesportsbook.app) | The player's browser | The sportsbook itself: markets, live matches, the betslip, bet history |
The embed places bets with the player's session token, never with your API key. Your server doesn't call the bet endpoints at all. If you're still deciding whether you need a hosted sportsbook or raw prices, the comparison of sportsbook APIs, odds APIs and widgets covers that first.
The launch token flow
Every page view that shows the sportsbook starts on your server, which knows who is logged in and asks for a session:
curl -X POST https://api.thesportsbook.app/b2b/v1/sessions \ -H "Authorization: Bearer $SPORTSBOOK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"user_ref": "viewer-42", "display_name": "Viewer 42", "locale": "en"}'{ "launch_token": "eyJhbGciOi...", "launch_url": "https://play.thesportsbook.app/embed?token=eyJhbGciOi...", "expires_in": 60}user_ref is your own stable, pseudonymous id for the player (1 to 128 characters). Don't use an email address, a real name or anything that changes, such as a display name. display_name should be a nickname the player chose.
Why single use and 60 seconds? The launch URL ends up in the page and the iframe's src. A token that works once and dies a minute later is worthless to anyone who copies it. The embed exchanges it straight away for a player session that lasts 12 hours, and removes it from the URL.
So:
- Create the session right before you render the page.
- Never cache or log the launch URL, on your server, a CDN or a page cache.
- If the player is blocked in your console, the call returns
403 {"code": "USER_BLOCKED"}, so render a message instead of the iframe.
Mounting with the SDK
The SDK is the recommended path. Load it once and call TheSportsbook.init:
<div id="sportsbook"></div><script src="https://play.thesportsbook.app/sdk.js"></script><script> const sportsbook = TheSportsbook.init({ container: "#sportsbook", launchUrl: "LAUNCH_URL_FROM_YOUR_SERVER", height: "auto", onBetPlaced(bet) { refreshBalance(); }, onTokensRequested() { openFreeRefill(); }, async onSessionExpired(e) { if (e.reason === "user_blocked") return; const { launch_url } = await fetch("/api/sportsbook/session", { method: "POST" }).then((r) => r.json()); sportsbook.updateToken(launch_url); }, });</script>refreshBalance, openFreeRefill and the /api/sportsbook/session route are your own code. If you don't need callbacks at all, two lines are enough: <div data-thesportsbook data-launch-url="LAUNCH_URL_FROM_YOUR_SERVER"></div> plus the script tag. The SDK mounts every element with data-thesportsbook on page load.
Options
| Option | What it does |
|---|---|
container | Element or CSS selector the iframe is appended to. Required. |
launchUrl | The launch_url from POST /b2b/v1/sessions. |
token | The launch_token instead, combined with baseUrl. launchUrl wins if both are set. |
baseUrl | Embed origin. Defaults to the origin sdk.js was loaded from. |
height | "auto" (default) or a fixed height in px. |
locale | UI language override: en or es. |
title | The iframe's title for assistive technology. Default Sportsbook. |
on… callbacks | One per event, such as onBetPlaced or onSessionExpired. |
Methods
init returns an instance with four methods: updateToken() swaps the session without a reload, setHeight() switches between auto and fixed height at runtime, on() and off() add and remove listeners (pass "*" for every event), and destroy() removes the iframe and every listener. The full list, including the static helpers, is in the SDK reference (opens in a new tab). TypeScript types are served at https://play.thesportsbook.app/sdk.d.ts.
The ten events
| Event | Payload | Use it to |
|---|---|---|
loaded | { status: "booting" } | Know the embed document is up and exchanging the token |
ready | { tenant } | Hide your own loading state; fires once per iframe load |
resize | { height } | Size a raw iframe (the SDK does it for you) |
bet-placed | { bet_id, stake, potential_win } | Refresh the balance widget on your page from your server |
session-expired | { reason } | Renew on expired; stop on user_blocked |
session-updated | { tenant, user_id } | Confirm that updateToken() worked |
tokens-requested | { balance, currency } | Open your free refill when the player presses "Get more tokens" |
maintenance | { active, scope, message } | Hide your own bet prompts while betting is paused |
tenant-suspended | { active } | Show a notice while the sportsbook is unavailable |
error | { code, message? } | Log failed updateToken() calls or rejected options |
Events are informational. The source of truth for balances is the API in managed mode, or the calls to your own wallet in seamless mode. Never credit or debit tokens because a message arrived in the browser.
The raw iframe: postMessage events, origin and source checks
If you can't load a third-party script, or you want full control, use the iframe directly:
<iframe id="sportsbook" src="LAUNCH_URL_FROM_YOUR_SERVER" style="width:100%;height:720px;border:0" allow="clipboard-write" title="Sportsbook"></iframe><script> const iframe = document.getElementById("sportsbook"); window.addEventListener("message", (e) => { if (e.origin !== "https://play.thesportsbook.app" || e.source !== iframe.contentWindow) return; const msg = e.data; if (!msg || msg.source !== "sportsbook") return; switch (msg.type) { case "resize": iframe.style.height = msg.payload.height + "px"; break; case "bet-placed": refreshBalance(); break; case "tokens-requested": showHowToGetTokens(); break; case "session-expired": renewSession(msg.payload.reason); break; case "maintenance": setBetPromptsPaused(msg.payload.active); break; case "tenant-suspended": showUnavailableNotice(msg.payload.active); break; } });</script>The helper functions are your code. Three checks matter, in this order:
event.originmust be exactlyhttps://play.thesportsbook.app. Any page can post messages to your window.event.sourcemust be this iframe'scontentWindow. With two embeds or other frames on the page, the origin alone doesn't tell you which one spoke.msg.sourcemust be"sportsbook".
When you post to the iframe, always pass the embed origin as the target, never "*". A wildcard target sends your message to whatever document happens to be in the frame. Details are in the raw iframe section of the docs (opens in a new tab).
Auto height and mobile layouts
Nested scrollbars ruin embedded apps on phones. With height: "auto" the SDK resizes the iframe to its content, so the page scrolls as one document. It also tells the sportsbook which part of the iframe is on screen, so the betslip and notifications stay in view even when the iframe is taller than the viewport. If your layout needs a fixed box, pass a number of pixels, or switch at runtime with setHeight().
The layout follows the width of the iframe, not the window, so give it your full content column. On a phone-sized frame the sidebar becomes a drawer and the betslip a bottom sheet; on a wide one you get the sidebar or top bar and the list or grid cards you picked in the console. Test at the widths your theme really renders.
Renewing a session without a reload
A player session lasts 12 hours. Long matches, a laptop left open overnight or a browser tab restored the next day will outlive it. When that happens the embed sends session-expired with reason: "expired". The same event arrives when the player presses "Start a new session" on one of the embed's session screens.
The fix is the same in every case: ask your server for a new session and hand it to the running iframe.
async onSessionExpired(e) { if (e.reason === "user_blocked") return; const { launch_url } = await fetch("/api/sportsbook/session", { method: "POST" }).then((r) => r.json()); sportsbook.updateToken(launch_url);}updateToken() accepts a launch token or the full launch URL, and the result arrives as session-updated or error. The iframe isn't reloaded, so the player stays where they were. With a raw iframe, post { source: "sportsbook-sdk", type: "session-token", payload: { token } } to the iframe with the embed origin as target, or reload the iframe with a new URL.
Your /api/sportsbook/session route must identify the player from your own session cookie. It must not read a user_ref from the request body.
Handling maintenance, suspension and blocked players
Three states can interrupt play. The embed already shows each one to the player; the events let your page react too.
| State | Who triggers it | What the player sees | What your page should do |
|---|---|---|---|
Maintenance, scope: "tenant" | You, in the console, with your own message | Betting paused, everything else works | Hide your own "place a bet" prompts until active: false |
Maintenance, scope: "platform" | The platform | A maintenance screen in the player's language (message is always null) | Same; don't retry in a loop |
| Suspension | The platform | "Temporarily unavailable" | Show a notice; the same session works again after active: false |
| Blocked player | You, in the console | The account is on hold | Stop. Don't renew the session |
Blocking takes effect quickly: an open embed is told within about a second and emits session-expired with reason: "user_blocked". Unblocking doesn't revive old sessions, so open a new one. Open bets still settle normally. If a player asks you to stop them playing, blocking is how you do it.
Rate limits
Two budgets apply, and they are sized so that normal use never reaches them:
- Your server: 600 requests per minute per API key on
/b2b/v1/*. Above it you get429with aRetry-Afterheader. - Players: 100 requests per minute per player on each endpoint inside the embed, counted by session, not by IP. A watch party or an office behind one public IP doesn't share one budget.
The rate limit section of the docs (opens in a new tab) has the details.
Security checklist
Adapted from the security checklist in the docs (opens in a new tab):
- The API key (
sbk_<prefix>_<secret>) lives only on your server, in an environment variable or secret manager. It is shown once and stored by us as a hash. - One key per environment. Rotate by creating a new key, deploying it, then revoking the old one; a revoked key stops on the next request.
- Your server creates sessions only for the player logged in on your site. Never take a
user_reffrom the browser. - Launch tokens are created right before rendering and never logged, cached or reused.
- Every site that embeds the sportsbook is listed in Settings → Allowed origins, as exact origins with no paths or wildcards.
- Your
messagelistener checksevent.originandevent.source, and you post with the embed origin as target. - You never move tokens because of a
postMessageevent.
A word on allowed origins: they govern browser API access. They don't stop another site from framing your sportsbook, because framing isn't restricted per operator yet. What protects a sportsbook is that only your server, with your API key, can open player sessions. Without a fresh launch URL from you, a frame on someone else's site has no player to show.
If you run your own token balances, the seamless wallet adds its own list (signature checks, idempotency, retries), covered in the seamless wallet integration guide.
What sportsbook iframe vendors usually leave out
Most sportsbook iframe pages we found describe the benefits and show no code. Whichever provider you evaluate, these are the questions worth asking before you sign anything:
| Ask | Why it matters | The answer here |
|---|---|---|
| How is a player session created, and who can create one? | Decides whether a copied link can impersonate a player | Your server, with your API key; single-use launch token, 60 seconds |
| How long does a session last, and can it renew without a reload? | Long matches outlive short sessions | 12 hours; updateToken() swaps it in place |
| Which events does the frame send, with which payloads? | You need them for balances, prompts and analytics | Ten documented events, typed in sdk.d.ts |
| Does the frame size itself? | Nested scrollbars on phones | height: "auto" |
| What happens during maintenance or when a player is blocked? | You need to handle it on your page too | Events with active, scope and reason |
| What are the rate limits, and are players limited per IP? | Shared IPs at venues and on mobile carriers | 600/min per key; 100/min per player per endpoint |
| Can you try it without a sales call? | Evaluation time | A public demo and a Free plan |
Some answers will differ for real-money providers, because a real-money wallet has requirements a play-for-fun one doesn't. If you're building on a site that needs real-money betting, talk to those providers.
Where to go next
For the step-by-step version from sign-up to a branded sportsbook, read how to embed a sportsbook on your website. On React, the React and Next.js embed guide turns this into a route handler and a component. The complete guide to embeddable sportsbooks covers sports, leagues, branding, plans and the play-for-fun rules.
The quickest way to see the events, the auto height and the mobile layout is the live sportsbook demo (opens in a new tab): the real embed with free demo tokens and no sign-up.
Frequently asked questions
What is a sportsbook iframe?
It is a complete sportsbook served from the provider's domain and shown inside a frame on your page. Your site keeps its own login and layout, and the frame handles markets, the betslip and bet history. With The Sportsbook, your server opens a player session first and the frame loads with a single-use launch URL.
Is iframe or API integration better for a sportsbook?
They do different jobs. The API is server-to-server: opening player sessions and managing token balances. The iframe is the player interface. With The Sportsbook you use both: one API call per session, then the iframe or the SDK to show it. The API doesn't hand you odds to build your own betting interface.
How long does a sportsbook iframe integration take?
The technical part is small: one server endpoint that opens a session and a few lines of HTML or JavaScript. Most of the time goes into your own login, how players get free tokens, and testing on phones. The Free plan lets you try the whole flow before you commit to anything.
Can I make the sportsbook iframe resize to its content?
Yes. With the SDK, set height to 'auto' (the default) and the iframe follows its content. With a raw iframe, listen for the resize message, check its origin and source, and set the iframe height from payload.height in pixels.
Can other websites embed my sportsbook iframe?
Framing isn't restricted per operator yet, so don't rely on that. What protects your sportsbook is that only your server, holding your API key, can open player sessions. The launch URL is single use and expires after 60 seconds, so a copied link is useless.
Written by
The Sportsbook team
The people who build The Sportsbook. We have spent seven years building sportsbook software, and we write these guides from that work.
Last updated .