Integration Documentation

How to embed the WinHouse white-label sportsbook: the snippet, your domains, the theme tokens, sizing and CSP, player accounts and the seamless wallet, billing, and what to check when something does not render.

Quick start

The book is one tag. There is no SDK, no build step and nothing to install on your server.

<!-- where the book should appear --> <div id="winhouse-book"></div> <script src="https://iframe.winhouse.bet/embed.js" data-key="ifr_your_key" data-target="#winhouse-book"></script>

Use the loader rather than a hand-written iframe. A fixed height that is taller than a phone screen hides the book's own tab bar and betslip — see The loader for why, and for the raw iframe if you need one.

Three things have to be true before it will render on your site:

  1. Your key is active (see the dashboard).
  2. Your domain is on the allow-list — see Domains.
  3. Your own Content-Security-Policy permits our origin — see CSP.

The embed key

Your key looks like ifr_ followed by 40 hex characters. It identifies which brand's skin to paint and nothing else.

The key is not a secret. It sits in your page source where anyone can read it, so it is public by construction. It grants no access to data and no ability to place bets. What stops another site embedding your book is the domain allow-list, not the key.

Rotating the key from the dashboard invalidates the old one immediately, so update your page in the same change.

Domains & going live

A new account is in sandbox: the book works and you can preview it, but it refuses to load inside any site until you add a domain.

One domain per key. Enter it as an origin — scheme and host, plus the port if it is not the default. www. and the bare domain count as the same domain and both are allowed whichever you type:

https://example.com

A second domain — a subdomain, a mirror, a staging site — is refused when you save. Wildcards are deliberately not supported. *.example.com would mean an abandoned or hijacked subdomain becomes a valid host for your book.

Running one book on several sites

If the same integration — one player base, one wallet, one network — runs on more than one domain (a second brand, a mirror, a country site), ask us to open your key to the number of domains you need. Once granted, the Domain field in My Book takes them comma-separated:

https://example.com, https://example-mirror.com

Every listed domain gets the same book, the same key and the same players; the frame-ancestors policy names all of them. The limit is set on our side and shows in the field's hint. If instead the sites are separate businesses with their own players and their own wallet, use a separate key for each: create another customer account, or ask us.

The book records which of your sites each bet was placed from, and which site each player first arrived through. Your Dashboard, Tickets and Reports gain a site picker beside the date range as soon as more than one is in play, Reports gets a By site breakdown, and the agent panel's ticket list has a matching Site filter. On model B, your server may send the host in an X-WH-Domain header on POST /tenant/session so a server-side launch is attributed the same way.

The book is served with a Content-Security-Policy: frame-ancestors header built from your domain, so a browser refuses to render it inside any other page — a copied key is useless elsewhere. You can read the policy back:

GET /tenant/frame-policy?key=ifr_... { "header": "frame-ancestors 'self' https://example.com https://www.example.com" }

Enforcement lives on our side, in the response the book is served with. Nothing about it depends on your page; only the domains you register matter. Changing the domain takes effect on the next load — including removing one: a site you drop stops rendering the book immediately.

Server IP

Separate from the domain: the one IP address your server calls our API from. It applies only to server-to-server calls made with your wallet secret (POST /tenant/session); players reach the book from anywhere and are never IP-checked. IPv4, IPv6, or a CIDR block if your servers share a range:

203.0.113.10

Empty means any address. Once set, a call from any other address gets a 403 before the secret is even looked at.

The loader (embed.js)

embed.js builds the iframe for you and, more importantly, keeps it the right height. It is 4 KB, has no dependencies and needs no build step.

Why it matters. On a phone the book pins its bottom tab bar, its betslip button and the betslip sheet with position:fixed — and inside an iframe that means fixed to the bottom of the frame, not of the screen. A 900px frame on an 800px screen therefore parks all three below the fold, and because a swipe scrolls the board inside the frame the page never scrolls down to reach them: your punters get no menu and no betslip. The loader measures where the frame sits and gives it the height that is actually on screen, on every resize, rotation and URL-bar collapse.

AttributeMeaning
data-keyRequired. Your embed key.
data-targetCSS selector of the container. Omit and the frame is inserted where the script tag sits.
data-widthFrame width. Default 100%.
data-heightDesktop height, any CSS length or calc(). Default 900px. Phones ignore it — they always get the visible height.
data-bottom-gapHeight in px of your fixed bottom navigation, subtracted on phones so the two bars do not overlap.
data-embed1 (default) hides the book's own top bar and left rail. 0 keeps them, for a page with no chrome of its own.
data-lang data-themeUI language, and dark or light.
data-launchA signed SSO token, if you have one when the page renders — see Signing players in.
data-launch-urlAn endpoint on your origin that mints one. Called with credentials, expected to answer {"launch":"…"}. If it fails or times out the book loads signed out rather than not at all.

It exposes window.WinHouseEmbed = { frame, refit(), setLaunch(token) } — call refit() if your own layout changes the frame's position, and setLaunch() after a player signs in without a page reload.

The raw iframe is still supported. If you use it, size it as below.

<div class="sportsbook"> <iframe src="https://iframe.winhouse.bet/book?key=ifr_your_key&embed=1" allow="fullscreen"></iframe> </div>

Sizing the frame

The book scrolls internally. Give it a real height — a sportsbook inside a short box becomes a scroll within a scroll and is unusable on a phone.

On a phone the height must be the visible height and no more, or the tab bar and betslip end up below the fold (see The loader, which does this for you):

.sportsbook iframe { display: block; width: 100%; height: calc(100dvh - 70px); /* viewport minus your header */ min-height: 560px; border: 0; } @media (max-width: 900px) { .sportsbook iframe { height: calc(100dvh - 132px); /* your header + your bottom nav */ min-height: 0; /* a floor taller than the screen IS the bug */ } }

Watch a fixed bottom nav. The book puts its own tab bar at the bottom of the frame on phones. If your site has a fixed bottom navigation, subtract its height too or the two overlap and neither is tappable.

If your page wrapper constrains width or adds a top margin, break out of it:

.sportsbook { margin-top: 0; width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); }

Your site's CSP

Allow-listing your domain with us is necessary but not sufficient. If your site sends a Content-Security-Policy, its frame-src must also name our origin, or the browser blocks the frame before it loads.

Content-Security-Policy: frame-src 'self' https://iframe.winhouse.bet;

The symptom is a console message and an empty frame:

Refused to frame 'https://iframe.winhouse.bet/' because it violates the following Content Security Policy directive: "frame-src 'self' ..."

URL parameters

ParameterMeaning
keyYour embed key. If it is missing, the book identifies your brand from the page embedding it, matched against your domain allow-list — so a snippet pasted without the key still paints your skin on a listed domain. Elsewhere it loads unbranded. The launch token always needs the key.
langStarting language of the book (en sq de it fr es el tr pt bg ar fa), e.g. &lang=de. Your page and the book are different origins, so this is the only way to pass your visitor's choice through; the player can still switch inside the book.
embed=1Skip the book's own top bar and left rail. Use this when your page already has a header, or the chrome is doubled.
launchA signed, short-lived token identifying a player already signed in on your site. The book exchanges it for a session so the punter is not asked to log in twice — see Signing players in. Model B only.
preview_themeURL-encoded JSON of token overrides, for previewing a skin before saving. Styling only.

Theme tokens

The book is built on CSS custom properties. Your skin is a set of overrides for them, served with the book and applied to :root at boot. Set one from the dashboard and everything using it repaints — there is no per-page styling to chase.

GET /tenant/config?key=ifr_... { "ok": true, "name": "Acme Bet", "theme": { "accent": "#FF8A00", "bg": "#12100E", "radius": "14px" } }

Values are validated on both sides: a token must be a hex colour or a short length (px, rem, em, %). Anything else — url(...), a closing brace, a whole rule — is dropped rather than escaped, because these values land in a stylesheet.

Token reference

Blank inherits the default. Thirty-one tokens across six groups.

GroupTokens
Brandaccent accent-ink brand green blue
Surfacesbg surface surface-2 surface-3 hover border track disabled
Shellshell shell-line nav-txt nav-sub
Texttext text-2 text-dim text-faint
Statuslive up down win-green hl-pink
Layoutradius row-h odd-w sidebar-w slip-w

up and down are the price-movement flashes; live is the in-play badge; odd-w and row-h control the density of the board.

Account models

There are two ways to run players, and the choice decides how money moves. Pick one before you launch — switching later means migrating balances.

A. Book-side accounts

Players register and sign in inside the book. They sit in your own agent tree, and bets settle against the balance you fund from your operator credit line. Nothing is required from your site: embed the iframe and you are done.

B. Seamless wallet

Your site stays the source of truth for both identity and money. Your existing players sign in on your site, and the book debits and credits your wallet for every bet and settlement. This needs endpoints on your side — see below.

Being signed in on your site does not sign a player into the book on its own. They are separate systems with separate databases, and a third-party iframe cannot read your session. The hand-off that connects them is the launch token — see Signing players in.

Signing players in

Model B only. Your player is signed in on your site; this is how they arrive in the book already signed in, without a second login. Your site mints a short-lived launch token and puts it in the iframe URL — the book redeems it for a session by itself, so there is nothing to call from your page.

1. Mint the token

Four dot-separated fields. The signature is an HMAC-SHA256, hex, keyed with your wallet API key (not the callback token), over the first three fields joined by |. The key is generated the first time you save a wallet URL and callback token, and is shown under Manage Account → General → Seamless wallet, with Copy and Rotate:

launch = <player_id>.<expiry>.<session>.<signature> signature = HMAC_SHA256( "<player_id>|<expiry>|<session>", wallet_api_key ).hex()
FieldMeaning
player_idYour own id for the player — the same value the wallet calls will quote back to you. Stable per player.
expiryEpoch milliseconds. Must be in the future and no more than 10 minutes ahead; mint it per page load, not per session.
sessionYour session id for this login. Stored against the player and quoted back on every wallet call, so ending a session on your side lets you stop bets on ours. May be empty, but then you lose that control.

2. Put it in the iframe URL

<iframe src="https://iframe.winhouse.bet/book?key=ifr_your_key&embed=1&launch=<url-encoded token>"></iframe>

Mint it server-side when you render the page. Never expose the wallet API key to the browser — anyone holding it can sign a token for any of your players.

3. What the book does

POST /tenant/sso { "key": "ifr_…", "launch": "<token>" } → { "ok": true, "token": "<book session>", "username": "acme_5512", "tenant": "acme" }

On first sight we provision a shell player under your tree, named <slug>_<player_id>. It carries no balance of its own — every stake and settlement goes to your wallet endpoints.

A missing, expired or malformed launch is not a hard failure: the book simply loads signed out rather than breaking the page. So if players are being asked to log in, check the token before suspecting the embed.

Server-to-server alternative

If minting an HMAC client-side is awkward, ask for the session directly. Authenticate with the wallet API key as a bearer token — it travels in a header so it never lands in an access log next to the payload. Same provisioning, same response body:

POST /tenant/session Authorization: Bearer <wallet_api_key> { "customer_key": "ifr_…", "player_id": "5512" }

If you have set a server IP, this call must come from that address; otherwise it answers 403 with the address it saw in ip. Optionally add X-WH-Domain: example.com — the site the player is on — so the launch is attributed to that site in your reports, exactly as a browser launch is (see several sites).

Errors

StatusMeaning
400Key is not a well-formed ifr_ key, or the launch token does not have four fields.
401Expired, dated too far ahead, or the signature does not match — nearly always the wrong secret, or fields joined with something other than |.
404Unknown or suspended key.
409No wallet configured for this account. SSO is model B only; on model A players sign in inside the book.

Seamless wallet (model B)

Two endpoints on your side, called server-to-server. You give us one base URL and we append the two paths to it, so https://your.site/api/winhouse becomes …/api/winhouse/user-data and …/api/winhouse/balance-change.

Two different secrets, and mixing them up is the most common integration failure. The callback token travels in the ?token= query string and only proves the call came from us — compare it in constant time. The wallet API key never travels; it is the HMAC key you verify signature with, and the same key that signs launch tokens.

Player lookup

GET <base_url>/user-data?token=<callback_token>&username=<player_id> → { "username": "5512", "balance": "250.00", "currency": "EUR" }

username is your own player_id, the same one you signed into the launch token. A reply we cannot read as a number is treated as "balance unknown" — we never assume zero.

Balance change

POST <base_url>/balance-change?token=<callback_token> { "username": "5512", "session": "<your session id>", // as sent in the launch token "transaction_id": "bet_88213", // idempotency key, stable across retries "amount": -25.00, // negative = stake, positive = payout "action": "bet", // "bet" | "win" "ticket_id": "88213", // may be empty "signature": "<hmac>" } → { "status": "ok", "balance": "225.00" }

Verifying the signature

HMAC-SHA256, hex, keyed with your wallet API key, over five fields concatenated with no separator, in exactly this order. An absent field contributes an empty string, and amount is stringified as sent (-25, not -25.00, when that is what is on the wire):

signature = HMAC_SHA256( username + amount + action + ticket_id + transaction_id, wallet_api_key ).hex()

Reconstruct the base from the parsed body, not the raw JSON text — field order in the document is not part of the contract.

Rules your endpoint must follow

How we treat your reply

This decides whether a stake is retried, so it is worth getting right.

Your replyWhat we do
2xxApplied. We read balance if present.
401 403 404 409 422 A deliberate refusal. Final — the bet is rejected and not retried.
Timeout, 5xx, unreachable In doubt, not failed. The transaction stays pending and is retried later with the same transaction_id. This is precisely why idempotency is not optional.

Retries happen. Treat every write as "may arrive twice, possibly out of order" — the idempotency key is what makes that safe.

Agent panel

Your network lives at winhouse.bet/agent/ with the account issued when your customer was created — a Manager, Super Manager or Boss, depending on your plan: managers, agents, shops and players, credit down the chain, stake limits, market blocks, tickets, settlement and reports. Tickets and players show which of your sites they came from, and the ticket list can be filtered by site.

Your tree is scoped to you. No other operator can see it, and you cannot see theirs.

Languages

This site and your customer portal at iframe.winhouse.bet/portal are available in 26 languages:

en English, de Deutsch, it Italiano, fr Français, es Español, sq Shqip, me Crnogorski, tr Türkçe, el Ελληνικά, sr Српски, zh 中文, pt Português, ru Русский, da Dansk, sv Svenska, bn বাংলা, bg Български, pl Polski, ro Română, cs Čeština, hu Magyar, sk Slovenčina, nl Nederlands, et Eesti, ar العربية, fa فارسی.

ar and fa are laid out right-to-left. The public pages live at /<code>/ (for example /de/) and carry hreflang alternates, so search engines index each one on its own.

In the portal, the globe at the top right switches the language on the spot and remembers the choice in this browser; Account Settings → Preferences saves a default on your account so every device opens in it. ?lang=<code> on the portal URL forces a language for that visit. Translations load on demand, one small file per language, so the choice costs nothing on pages you do not open.

The book your players see is a separate surface with its own dictionary — twelve languages today (en sq de it fr es el tr pt bg ar fa), chosen with the lang parameter or the book's own menu. A player's choice is kept in the same browser key as the portal's, so a WinHouse-hosted page and the book it embeds stay in step.

Billing & credit

There is no setup fee and no licence. You buy prepaid credit in USDT (minimum 50, and it never expires), and once a month we draw a percentage of your GGR out of it.

GGR = stakes taken − payouts made, across your own network only. Two details decide what is counted:

The fee is floored at zero per rate, so a month in which your players came out ahead costs you nothing and does not carry forward.

PlanUnlocked byLive GGRPrematch GGRAccount type
Starter7.5%4.5%Manager
BusinessUSDT 899 funded6.5%3.5%Super Manager
ScaleUSDT 2,499 funded5.5%2.5%Boss

The three account types, lowest to highest: Manager — your players and their tickets; Super Manager — your own agents underneath you, their limits and their tickets; Boss — managers under you as well, plus bet settings, market blocks and bonuses for the whole network.

The plan follows your lifetime funded total, not your current balance, and is upgrade-only — spending the credit down never puts the rate back up. A plan set for you by hand pins it, so a negotiated rate survives your next top-up.

Prepaid packs and network float

Credit can be bought as a pack — Starter USDT 199, Business USDT 899, Scale USDT 2,499 — or as any custom amount from 50. A pack does one extra thing: USDT 100 of it is turned into network float for your agent tree the moment the payment settles, and the rest stays as platform credit. The Starter pack at 199 therefore gives you USDT 100 of float (converted to your book's currency at the live rate) and USDT 99 of credit. A custom top-up is credit only; you can still buy float from it yourself under Wallet → Network float.

PackYou payTo network floatStays as credit
StarterUSDT 199USDT 100USDT 99
BusinessUSDT 899USDT 100USDT 799
ScaleUSDT 2,499USDT 100USDT 2,399

The float is bought with the pack's own credit, never on overdraft: if your credit is negative when a pack settles, the credit is added but the float purchase is refused and shown on the top-up — contact us and we will apply it.

Two monthly add-ons sit outside the ladder and are billed with the month they cover: account type on subscription (Super Manager USDT 100, Boss USDT 200) and the reseller panel at USDT 100. Cancelling a subscription drops you back to whatever your funding alone earns.

Your bills, the ladder and your current rates are all in the dashboard under Credit & Plans.

Suspension

Suspending an account disables the embed and the boss login together, and takes effect immediately — the config endpoint is intentionally uncached, so a suspension is a real kill switch rather than something that expires later.

A suspended key returns 404 from /tenant/config and the book will not render.

Troubleshooting

SymptomCause
Empty frame, console says "Refused to frame" Your site's CSP frame-src does not list our origin.
Book loads, but with the WinHouse look instead of yours The key in your snippet is empty or mistyped and the embedding domain is not on your allow-list. Fix either one.
Frame loads but shows nothing / refuses Your domain is not on the allow-list, or the account is still in sandbox.
Book worked, then stopped on one of your sites after you changed the Domain The Domain field replaces the previous value: the site you removed is no longer allowed to frame the book. Put it back, or ask us to open the key to several domains if you run both.
Book asks players to log in although they are signed in on your site On model A this is expected. On model B the launch token is missing, expired or mis-signed — the book stays signed out rather than erroring. See Signing players in.
Bets rejected, wallet endpoint returns 401 The signature is keyed with the wallet API key, while ?token= carries the callback token. They are different secrets — see Seamless wallet.
The same stake arrives twice A timeout is retried with the same transaction_id. Your endpoint must be idempotent on that key.
Colours did not change A token failed validation and was dropped — it must be a hex colour or a short length.
Chrome appears twice (two headers) Add &embed=1 to the iframe URL.
No bottom tab bar and no betslip on mobile The frame is taller than the screen, so the book's fixed bars sit below the fold. Use embed.js, or give the frame the visible height and drop any min-height on phones.
Bottom tab bar overlapped on mobile Your own fixed bottom nav sits on top of it — set data-bottom-gap to its height (or subtract it yourself).
Old snippet stopped working The key was rotated. Copy the current one from the dashboard.

Something not covered here? sales@winhouse.bet or reach us on Telegram.