Getting Started
Integrate Maktub games into your platform in 3 steps: create a session, embed an iframe, and handle webhooks.
Step 1 — Create a Session (Server-Side)
Your backend calls POST https://server.maktub.bet/session to get a game URL. Webhook-wallet operators authenticate this server-side request with the webhook secret provided during onboarding; never expose that secret in client-side code.
// Your backend endpoint
app.post('/api/game-session', async (req, res) => {
const response = await fetch('https://server.maktub.bet/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-operator-secret': process.env.MAKTUB_WEBHOOK_SECRET,
},
body: JSON.stringify({
clientId: process.env.MAKTUB_CLIENT_ID,
userId: req.user.id,
currency: { code: 'BRL', prefix: 'R$', rate: 5.5 },
language: 'pt',
theme: {
buttonColor: '#22c993',
darkBackground: '#101a18',
darkLightBackground: '#1b2825',
},
showBalance: true,
showRules: true,
}),
})
const { token, gameUrl } = await response.json()
// gameUrl bakes your session theme/lang/currency as query params, e.g.:
// https://play.maktub.bet?token=abc123&bg=%23101a18&accent=%2322c993&lang=pt&cur=BRL&curPrefix=R%24
res.json({ token, gameUrl })
})Session Parameters
| Field | Type | Required | Description |
|---|---|---|---|
clientId | string | ✅ | Your customer identifier provided by Maktub |
userId | string | ✅ | Unique identifier for the end user in your system |
currency | object | — | { code: 'USD', prefix: '$', rate: 1 } — currency display config |
language | string | — | Locale code (e.g. 'en', 'pt', 'es'). Default: 'en' |
theme | object | — | Color overrides (see Theming). Overrides customer defaults. |
logoUrl | string | — | URL to your logo image. Overrides customer default. |
cardLogoUrl | string | — | URL to a logo shown on the back of playing cards (card games only — Blackjack, Baccarat, Hi-Lo, Video Poker). Overrides logoUrl there. Falls back to logoUrl when omitted. |
token | string | — | Opaque per-session token (max 512 chars) echoed on every webhook as sessionToken (see Webhooks). Disambiguates concurrent sessions for the same userId. Also accepted as sessionToken. |
showBalance | boolean | — | Show balance in game UI. Default: false |
showRules | boolean | — | Show game rules button. Default: false |
userName | string | — | Display name for multiplayer games (e.g. Crash) |
Response
{
"token": "abc123...",
"gameUrl": "https://play.maktub.bet?token=abc123…&bg=%23101a18&accent=%2322c993&bgLight=%231b2825&lang=pt&cur=BRL&curPrefix=R%24"
}The gameUrl bakes your session’s theme, language and currency as query
params (bg, accent, bgLight, lang, cur, curPrefix), so an iframe
pointed at it paints themed and localized from the first frame — no pop. It is
game-agnostic: insert the game path to choose a game, e.g.
https://play.maktub.bet/dice?token=…&bg=…&lang=…. The embed options below use
the token (shown as TOKEN_FROM_STEP_1) plus those same params.
Step 2 — Embed the Game
You set the width; you never manage the height. The game measures itself and tells the embed how tall it needs to be, so it grows to fit — switching Manual ↔ Auto/Advanced never clips or scrolls. Pick the path that fits your stack.
Recommended — drop-in script
Paste a placeholder and our one script. You write no JavaScript: the script builds the iframe and keeps its height glued to the game’s content.
<div
data-maktub-game="dice"
data-token="TOKEN_FROM_STEP_1"
data-bg="#101a18"
data-accent="#22c993"
data-bg-light="#1b2825"
data-lang="pt"
data-currency="BRL"
data-currency-prefix="R$"
></div>
<script src="https://play.maktub.bet/embed.js" async></script>-
Swap
data-maktub-gamefor any game:mines,crash,blackjack,plinko,futures, … (see Available Games). -
data-tokenis thetokenfrom your Step 1 session. -
Theme attributes (optional but recommended) — pass your session’s colors so the game paints in your palette from the very first frame:
data-bg— main dark background (the game surface).data-accent— button / highlight color.data-bg-light— secondary (lighter) dark background.
These should match the
themeyou set when creating the session. Omit them and the game falls back to a neutral dark theme, then snaps to your session theme once it loads — so passing them avoids a brief color pop. -
Locale & currency attributes (optional but recommended) — pass your session’s language and currency so the game’s text and money prefix are correct from the very first frame, with no flash:
data-lang—en,pt, ores.data-currency— currency code (e.g.BRL).data-currency-prefix— symbol shown before amounts (e.g.R$).
These should match the session you created. Omit them and the game starts in English /
$, then snaps to your session’s language and currency once it loads — so passing them avoids a brief text/currency pop. -
Optional
data-initial-height="600"sets the placeholder height shown before the game reports its own (avoids a layout jump). -
Multiple games per page and SPAs work out of the box — the script watches for placeholders added later.
The script is served from play.maktub.bet, validates the message origin for
you, and resizes the iframe as the game grows. Nothing else to wire up.
Instant load. No blank or loading wall: when you pass your theme colors the script paints a fully-styled, themed snapshot of the game over the frame while the live game loads behind it, then reveals the live, interactive game in place the moment it’s ready. (Pass no theme colors and you get a neutral loader instead, so a default palette never flashes.) You get this automatically; there’s nothing to configure.
Without our script (manual)
If your CSP forbids third-party scripts — or you’d just rather not load ours —
embed a plain iframe and add the listener yourself. The game posts
{ type: 'maktub:resize', height }; you set the iframe height.
<!-- In your page <head>: open the connection early so the iframe loads faster. -->
<link rel="preconnect" href="https://play.maktub.bet">
<iframe
id="maktub-game"
src="https://play.maktub.bet/dice?token=TOKEN_FROM_STEP_1&bg=%23101a18&accent=%2322c993&bgLight=%231b2825&lang=pt&cur=BRL&curPrefix=R%24"
style="width:100%;height:600px;border:0;display:block"
scrolling="no"
allow="clipboard-write; fullscreen"
></iframe>
<script>
var frame = document.getElementById('maktub-game');
window.addEventListener('message', function (e) {
if (e.origin !== 'https://play.maktub.bet') return; // origin check (security)
if (e.source !== frame.contentWindow) return;
if (e.data && e.data.type === 'maktub:resize' && e.data.height) {
frame.style.height = Math.ceil(e.data.height) + 'px';
}
});
</script>-
Game route — point the
srcathttps://play.maktub.bet/<game>?token=…(e.g./dice,/mines,/crash). The game server-renders its themed UI, so it appears styled in your colors as soon as the document loads, then becomes interactive once it finishes booting — one iframe, no second request, no swap. -
Theme — pass
bg,accentandbgLightas URL-encoded query params (e.g.#101a18→%23101a18) matching your session theme, so the game paints in your colors from the first frame — no color pop. -
Locale & currency — pass
lang(en/pt/es),cur(currency code, e.g.BRL) andcurPrefix(symbol, URL-encoded — e.g.R$→R%24) matching your session, so text and money are correct from the first frame — no language/currency pop. -
Faster first load — add
<link rel="preconnect" href="https://play.maktub.bet">to your page<head>so the browser opens the connection (DNS + TLS) before the iframe needs it. This is the single biggest win for embed load time. -
Fullscreen — the game’s bottom bar has a fullscreen button. Browsers only let an iframe go fullscreen when its tag opts in, so include
fullscreenin theallowattribute (allow="clipboard-write; fullscreen"). Without it the button hides itself automatically.
No listener at all? A plain
<iframe>at a fixed height still works — the game scales to fit the box you give it and any extra controls scroll inside. You just don’t get the auto-grow.
Token not ready yet? — instant shell, then upgrade
If you can’t put the token in the iframe src at embed time — because your
backend is still creating the session (Step 1) when the page renders — point the
iframe at the game without a token. The game server-renders its themed,
disabled loading shell on the first byte (no blank frame, no spinner), firing
no authenticated requests. The moment your token arrives, postMessage it to
the frame and the game boots live in place — same iframe, no reload, no flash.
<link rel="preconnect" href="https://play.maktub.bet">
<iframe
id="maktub-game"
src="https://play.maktub.bet/dice?bg=%23101a18&accent=%2322c993&bgLight=%231b2825&lang=pt&cur=BRL&curPrefix=R%24"
style="width:100%;height:600px;border:0;display:block"
scrolling="no"
allow="clipboard-write; fullscreen"
></iframe>
<script>
var frame = document.getElementById('maktub-game')
// Resize handshake (same as the manual embed above).
window.addEventListener('message', function (e) {
if (e.origin !== 'https://play.maktub.bet') return
if (e.source !== frame.contentWindow) return
if (e.data && e.data.type === 'maktub:resize' && e.data.height) {
frame.style.height = Math.ceil(e.data.height) + 'px'
}
})
// Create the session (Step 1) however you do it, then hand the token to the
// already-painted shell. The game boots live in the SAME frame — no reload.
fetch('/api/game-session').then(function (r) { return r.json() }).then(function (s) {
frame.contentWindow.postMessage(
{ type: 'maktub:upgrade', token: s.token },
'https://play.maktub.bet',
)
})
</script>- Use the same theme / locale / currency params as a normal embed (
bg,accent,bgLight,lang,cur,curPrefix) so the shell already matches your palette and locale — the live game then swaps in over an identical paint. - The upgrade message is
{ type: 'maktub:upgrade', token }. Post it toframe.contentWindowwith the target originhttps://play.maktub.bet. (Asearchstring — e.g.'?token=…'— is accepted in place oftoken.) - Tip: start the iframe
heightclose to the game’s natural height (a fixed600/700is fine). On mobile a box much shorter than the game makes the first paint scale down, then grow once the resize handshake runs. - Works for every game — this is the iframe-only path: one bare
<iframe>, no second frame, noembed.js.
For aggregators — bundle the resize helper
If you ship your own JS bundle to operators (or render games inside your own
app), drop this self-contained helper into your bundle instead of loading our
script. It depends on nothing but the maktub:resize message.
// Attach Maktub auto-resize to an iframe you created. Returns a cleanup fn.
export function attachMaktubResize(iframe, origin = 'https://play.maktub.bet') {
function onMessage(e) {
if (e.origin !== origin) return;
if (e.source !== iframe.contentWindow) return;
const d = e.data;
if (d && d.type === 'maktub:resize' && typeof d.height === 'number' && d.height > 0) {
iframe.style.height = Math.ceil(d.height) + 'px';
}
}
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}const iframe = document.createElement('iframe');
iframe.src = `https://play.maktub.bet/dice?token=${token}`;
iframe.style.cssText = 'width:100%;height:600px;border:0;display:block';
iframe.setAttribute('allow', 'clipboard-write; fullscreen'); // enables the in-game fullscreen button
container.appendChild(iframe);
const detach = attachMaktubResize(iframe); // call detach() on unmountStep 3 — Handle Webhooks
When a bet is placed or resolved, our server calls your webhook endpoint. The easiest way to handle this is with the @maktubbet/webhook helper library:
yarn add @maktubbet/webhookimport { createWebhookHandler, WebhookError } from '@maktubbet/webhook'
export default createWebhookHandler({
secret: process.env.MAKTUB_WEBHOOK_SECRET,
expectedCustomerId: process.env.MAKTUB_CUSTOMER_ID,
onBalanceCheck: async ({ userId, customerId }) => {
return { balance: await db.getBalance(userId, customerId) }
},
onBet: async ({ userId, customerId, amount, betId, event }) => {
const result = await db.atomicDebit({ userId, customerId, amount, betId, event })
if (!result.ok) throw new WebhookError(result.reason, result.status)
return { balance: result.balance }
},
onWin: async ({ userId, customerId, amount, betId, event }) => {
const result = await db.atomicCredit({ userId, customerId, amount, betId, event })
if (!result.ok) throw new WebhookError(result.reason, result.status)
return { balance: result.balance }
},
})See the full Webhooks guide → for event details and manual implementation.
Step 4 (Optional) — Listen for Events
The iframe communicates with your page via postMessage. This is optional but useful for updating your UI in real time:
window.addEventListener('message', (e) => {
if (e.origin !== 'https://play.maktub.bet') return
switch (e.data.type) {
case 'balance_update':
updateBalanceUI(e.data.balance)
break
case 'auth_required':
redirectToLogin()
break
case 'bet_result':
trackAnalytics(e.data)
break
}
})See the full postMessage API → for all event types.
Theming
Theme colors can be set on your customer account (via admin panel) or overridden per-session:
// In your POST /session request body:
{
theme: {
buttonColor: '#E6007A', // Bet / Cashout button
darkBackground: '#1D121F', // Main background
darkLightBackground: '#1A0F1C' // Sidebar background
},
logoUrl: 'https://yoursite.com/logo.svg',
cardLogoUrl: 'https://yoursite.com/card-logo.svg' // optional — overrides logoUrl on card backs (card games only)
}All theme properties are optional. Session values override customer-level defaults.
Accepted color formats: #RGB, #RRGGBB, or #RRGGBBAA (hex only — named colors and rgb() are not accepted). An invalid value is dropped for that key only: the key falls back to its own customer/built-in default and the other keys are unaffected.
Resolution order, per key: session value (if present and valid) → customer-level default → built-in default.
Built-in defaults:
| Key | Default |
|---|---|
buttonColor | #FFFFFF |
darkBackground | #0D1117 |
darkLightBackground | #161B22 |
cardLogoUrl only affects the back of playing cards in card games (Blackjack, Baccarat, Hi-Lo, Video Poker); the bottom-bar logo always uses logoUrl. When cardLogoUrl is omitted, card backs fall back to logoUrl.
Demo Mode
Demo mode is automatically enabled when users place a bet with a $0 amount. This allows players to try games without risking real funds. No special configuration is needed — demo mode works with any valid session.
To run a game entirely client-side without a backend session, pass the isDemo prop:
<DiceGameSDK
isDemo
accessToken={null}
user={{ betCount: 0, isAuthenticated: false }}
updateBalance={(b) => console.log(b)}
onAuthRequired={() => {}}
/>