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.
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:
- Your key is active (see the dashboard).
- Your domain is on the allow-list — see Domains.
- Your own
Content-Security-Policypermits 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:
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:
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:
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:
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.
| Attribute | Meaning |
|---|---|
data-key | Required. Your embed key. |
data-target | CSS selector of the container. Omit and the frame is inserted where the script tag sits. |
data-width | Frame width. Default 100%. |
data-height | Desktop height, any CSS length or
calc(). Default 900px. Phones ignore it — they always get
the visible height. |
data-bottom-gap | Height in px of your fixed bottom navigation, subtracted on phones so the two bars do not overlap. |
data-embed | 1 (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-theme | UI language, and
dark or light. |
data-launch | A signed SSO token, if you have one when the page renders — see Signing players in. |
data-launch-url | An 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.
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):
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:
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.
The symptom is a console message and an empty frame:
URL parameters
| Parameter | Meaning |
|---|---|
key | Your 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. |
lang | Starting 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=1 | Skip the book's own top bar and left rail. Use this when your page already has a header, or the chrome is doubled. |
launch | A 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_theme | URL-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.
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.
| Group | Tokens |
|---|---|
| Brand | accent accent-ink brand
green blue |
| Surfaces | bg surface surface-2
surface-3 hover border track
disabled |
| Shell | shell shell-line nav-txt
nav-sub |
| Text | text text-2 text-dim
text-faint |
| Status | live up down
win-green hl-pink |
| Layout | radius 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:
| Field | Meaning |
|---|---|
player_id | Your own id for the player — the same value the wallet calls will quote back to you. Stable per player. |
expiry | Epoch milliseconds. Must be in the future and no more than 10 minutes ahead; mint it per page load, not per session. |
session | Your 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
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
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:
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
| Status | Meaning |
|---|---|
400 | Key is not a well-formed ifr_ key, or the
launch token does not have four fields. |
401 | Expired, dated too far ahead, or the signature does not
match — nearly always the wrong secret, or fields joined with something other than
|. |
404 | Unknown or suspended key. |
409 | No 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
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
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):
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
- Idempotent. Store
transaction_id. A repeat with the same id and the same payload returns the original result. A repeat with the same id and a different payload is a conflict — answer409. - Atomic. Lock the player row and apply the change in one transaction; a network retry must not double-debit.
- Reject overdraft. A debit larger than the balance returns
422and changes nothing. - Verify the signature on every call, and compare the callback token with a constant-time comparison.
- Answer promptly. A slow reply is indistinguishable from a lost one and will be retried — see below.
How we treat your reply
This decides whether a stake is retried, so it is worth getting right.
| Your reply | What we do |
|---|---|
2xx | Applied. 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:
- Only settled tickets count. An open bet is not billed until it resolves, and a refunded one is never billed.
- A ticket is live if any of its selections was placed in-play; otherwise it is prematch. The two are billed at different rates.
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.
| Plan | Unlocked by | Live GGR | Prematch GGR | Account type |
|---|---|---|---|---|
| Starter | — | 7.5% | 4.5% | Manager |
| Business | USDT 899 funded | 6.5% | 3.5% | Super Manager |
| Scale | USDT 2,499 funded | 5.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.
| Pack | You pay | To network float | Stays as credit |
|---|---|---|---|
| Starter | USDT 199 | USDT 100 | USDT 99 |
| Business | USDT 899 | USDT 100 | USDT 799 |
| Scale | USDT 2,499 | USDT 100 | USDT 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
| Symptom | Cause |
|---|---|
| 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.